To display an example program, we will use the example given on the pwntools entry for ret2dlresolve:
pwntools contains a fancy Ret2dlresolvePayload
that can automate the majority of our exploit:
Let's use rop.dump()
to break down what's happening.
As we expected - it's a read
followed by a call to plt_init
with the parameter 0x0804ce24
. Our fake structures are being read in at 0x804ce00
. The logging at the top tells us where all the structures are placed.
Now we know where the fake structures are placed. Since I ran the script with the DEBUG
parameter, I'll check what gets sent.
system
is being written to 0x804ce00
- as the debug said the Symbol name addr
would be placed
After that, at 0x804ce0c
, the Elf32_Sym
struct starts. First it contains the table index of that string, which in this case is 0x4ba4
as it is a very long way off the actual table. Next it contains the other values on the struct, but they are irrelevant and so zeroed out.
At 0x804ce1c
that Elf32_Rel
struct starts; first it contains the address of the system
string, 0x0804ce00
, then the r_info
variable - if you remember this specifies the R_SYM
, which is used to link the SYMTAB
and the STRTAB
.
After all the structures we place the string /bin/sh
at 0x804ce24
- which, if you remember, was the argument passed to system
when we printed the rop.dump()
:
Resolving our own libc functions
During a ret2dlresolve, the attacker tricks the binary into resolving a function of its choice (such as system
) into the PLT. This then means the attacker can use the PLT function as if it was originally part of the binary, bypassing ASLR (if present) and requiring no libc leaks.
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.
For all these screenshots, I broke at read@plt
. I'm using GDB with the pwndbg
plugin as it shows it a bit better.
The PLT jumps to wherever the GOT points. Originally, before the GOT is updated, it points back to the instruction after the jmp
in the PLT to resolve it.
In order to resolve the functions, there are 3 structures that need to exist within the binary. Faking these 3 structures could enable us to trick the linker into resolving a function of our choice, and we can also pass parameters in (such as /bin/sh
) once resolved.
There are 3 structures we need to fake.
The JMPREL
segment (.rel.plt
) stores the Relocation Table, which maps each entry to a symbol.
These entries are of type Elf32_Rel
:
The column name
coresponds to our symbol name. The offset
is the GOT entry for our symbol. info
stores additional metadata.
Note the due to this the R_SYM
of gets
is 1
as 0x107 >> 8 = 1
.
Much simpler - just a table of strings for the names.
Symbol information is stores here in an Elf32_Sym
struct:
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.
We now know we can get the STRTAB
offset of the symbol's string using the R_SYM
value we got from the JMPREL
, combined with SYMTAB
:
Here we're reading SYMTAB + R_SYM * size (16)
, and it appears that the offset (the SYMTAB
st_name
variable) is 0x10
.
And if we read that offset on STRTAB
, we get the symbol's name!
Let's hop back to the GOT and PLT for a slightly more in-depth look.
If the GOT entry is unpopulated, we push the reloc_offset
value and 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.