Page cover

Ret2Dlresolve

✅ Kĩ thuật này được sử dụng khi trong chương trình không có function PLT nào có thể sử dụng để leak memory và address. Thì kĩ thuật này được sử dụng để lấy luôn shell (thay vì phải leak memory sau đó

Trong bài phân tích này tôi sẽ sử dụng glibc 2-35 trên Ubuntu 22:04. Build 1 file đơn giản có mã như sau:

#include<stdio.h>

int main(){
    puts("Symbol resolving");
    puts("Symbol resolved");
}
// gcc prog.c -o prog -no-pie -fno-stack-protector

Điều kiện để technique này active cần 2 yếu tố chính:

  • Ta có quyền ghi vào các vùng nhớ tùy ý.

  • Cần có các Gadget để có thể control parameter khi gọi hàm.

Overview

Tại sao lại là Ret2Dlresolve

Dissect

Trước tiên ta cần phân biệt 3 section: got, plt, got.plt

  • .GOT: (Global Offset Table) - chứa địa chỉ của hàm __libc_start_main_impl (đây là hàm khởi động chính từ libc, khi bắt đầu thực thi nó sẽ được gọi để thiết lập mội trường và chạy main). Hoặc có thể gọi là cơ chế eager binding (Đây là cơ chế trong đó tất cả các hàm hoặc biểu tượng cần thiết được liên kết ngay trong quá trình khởi động chương trình (trước khi chương trình thực thi)). Ngoài ra còn chứa các biến đã được gán giá trị (bound - hàm hoặc biến trong chương trình được liên kết với địa chỉ cụ thể trong bộ nhớ -> say that: bound, Ex: (void*)puts linked to địa chỉ của symbol __GI__IO_puts trong bộ nhớ -> đã bound(1)) trước khi chương trình thực thi -> GOT được đánh dấu là Read-Only để prevent overwrite.

  • .PLT: (Procedure Linkage Table) là nơi chứa địa chỉ của các stubs (đoạn mã nhỏ) của hàm tương ứng để tìm kiếm địa chỉ trong .got.plt để nhảy đến đúng địa chỉ hàm đó và kích hoạt các hàm được chứa trong section .got.plt

  • .GOT.PLT: là một phần con của GOT và nó chứa các địa chỉ của các hàm được liên kết động (Dinamic binding entries) được gọi thông qua PLT (trong đó bao gồm cả các hàm chưa được resolve:puts@plt). Sau khi resolve thì địa chỉ mới sẽ được ghi đè (thay đổi thành địa chỉ của Libc tương ứng). Cơ chế này được gọi là Lazy Binding nên section này sẽ có quyền write.

Dưới đây là ảnh dissection 3 section trên:

Focus vào .got.plt section:

  • GOT[0]: địa chỉ của .dynamic

  • GOT[1]: địa chỉ của link_map

  • GOT[2]: địa chỉ của dynamic linker (_dl_runtime_resolve())

  • GOT[3]: #puts

resolve là gì?

Resolve là quá trình phân rã địa chỉ chuyển từ địa chỉ thuộc chương trình elf thành địa chỉ libc trong lần thực hiện đầu tiên

Hiểu nôm na quá trình này sẽ xảy ra khi lần đầu tiên bạn sử dụng hàm đó (1) (Ex: lần đầu tiên hàm puts được thực thi). Lần đầu tiên đó, địa chỉ hàm puts trong global offset table(GOT) sẽ là 0x401030. Đầu tiên, trỏ đến 1 đoạn stubs nhỏ (puts@plt) (2) thực hiện việc tìm kiếm địa chỉ trong GOT table để nhảy (3) đến sau đó call dl_runtime_resolve_xsavec để resolve và nhảy (4) đến puts thực sự trong libc (5). Sau khi thực thi xong thì quá trình resolve đã được hoàn tất và bây giờ địa chỉ của nó sẽ là 0x7ffff7c87be0 (6) và địa chỉ sau khi resolve nó sẽ trỏ thẳng đến function (trong shared library - libc) mà không cần gọi qua dl_runtime_resolve_xsavec nữa (7).

Tại sao là ret2resolve?

ret2resolve có thể được hiểu là return to dl_runtime_resolve (hoặc dl_runtime_resolve_xsavec phụ thuộc vào các tính năng của CPU)

Kĩ thuật này lợi dụng việc lazy binding, từ đó khiên nó overwrite GOT của 1 địa chỉ nào đó trong GOT table thành system. Tại sao lại overwrite được mà không xảy ra lỗi? Đó là vì nó không có hàm nào kiểm tra rằng nó có đang resolve có đúng symbol không hay 1 symbol đã được sửa đổi để call.

DEEP DIVE

Đi sau vào sau bên trong, cách nó tính toán và link đến các dynamic symbols của hàm _dl_runtime_resolve_xsavec()

link_map (not linkmap)

Và các đối số được truyền vào trong hàm _dl_runtime_resolve có thể được biểu diễn như sau (thực ra không hẳn là truyền vào, đây tôi minh họa bằng code cho dễ hiểu thôi :>>):

link_map(1): trỏ đến phần .dynamic của một shared object, mà phần .dynamic đó là nơi chứa các dynamic entry type như: NEEDED, INIT, FINI, GNU_HASH, STRTAB, v.v..

reloc_arg: dùng để tính offset trong lần gọi hàm đầu tiên, nó sẽ pointer đến mục relocation tương ứng trong GOT table.

(1)

P/s: Bạn có thể sử dụng command pwndbg sau để hiển thị cấu trúc của link_map:

Dissect hàm _dl_runtime_resolve_xsavec ta có:

Step into vào bên trong _dl_fixup() ta sẽ thấy có 1 hàm là _dl_lookup_symbol_x Nó sẽ lấy 8 tham số:

Trong đó có 3 arg cần chú ý sau:

  • arg[0]: chứa pointer đến chuỗi được cho là tên của hàm

  • arg[1]: chứa pointer đến link_map

Relocates Process Detail

Qua phần kiến thức bên trên, bạn có thể thấy rằng để tấn công theo kĩ thuật ret2dlresolve, bạn sẽ cần yếu tố chính sau:

  1. Fake chuỗi được cho là tên của hàm (arg[0])

Nhưng mà để tính toán được arg[0] ta cần rất nhiều các tham số khác liên quan:

  1. STRTAB

  2. SYMTAB

  3. JMPREL

STRTAB

Có địa chỉ là: 0x400430

Đây là nơi lưu trữ tên của các hàm, version của libc dưới dạng chuỗi (string)

Phân tích vào phép toán liên quan đến DT_STRTAB:

l_info: nằm tại struct &link_map+0x40 và nó trỏ đến các địa chỉ nằm trong .dynamic section. DT_STRTAB có value là 5 -> l_info[5] = 0x403e88(1)

D_PTR: được define như sau:

Nếu dynamic section được set là Read-Only (2) thì nó sẽ là:

Còn không thì là:

(1)
(2)

Vậy tổn kết lại:

Ngoài ra, đây là structure của Elf64_Dyn:

DT_JMPREL

Trỏ vào biến reloc bạn có thể nó sử dụng Elf64_Rela:

Hàm reloc_offset:

Hoặc có thể viết là:

Biến đổi tương tự phần STRTAB ở trước đó ta sẽ được 1 biểu thức mới:

ElfW(Rela) là gì?

Đây là định nghĩa của nó:

Và có thể được hiểu trong trường hợp này là:

Như vậy sizeof(Elf64_Rela) = 0x18 = 24

Chốt lại ta sẽ có công thức tính reloc là:

DT_SYMTAB

Biến đổi nhanh giai đoạn thứ 1:

ElfW(R_SYM) = Elf64_R_SYM

Dựa theo đinh nghĩa của ELF64_R_SYM ta có:

Chốt lại biểu thức của ta bây giờ sẽ là:

Giai đoạn thứ 2:

Ta có thể hiểu là:

Như vậy nếu kết hợp 2 giai đoạn trên ta có thể kết luận rằng:

reloc->r_info >> 32 là index để tìm structure Elf64_Sym tương ứng trong SYMTAB section

Other

Ta sẽ phân tích thêm các đoạn code trong hàm _dl_fixup để hiểu hơn về chương trình này thay vì chỉ tập trung vào 3 section chính bên trên.

Ta có 1 dòng:

rel_addr là pointer trỏ đến vùng lưu trữ của các resolved symbol.

Tiếp đến là đoạn code kiểm tra reloc->r_info có phải là 1 JUMP_SLOT hợp lệ:

Trong đó:

ELFW(R_TYPE) (reloc->r_info) = (reloc->r_info) & 0xffffffff

ELF_MACHINE_JMP_SLOT được defined là 7 (R_X86_64_JUMP_SLOT hoặc R_AMD64_JUMP_SLOT)

Từ đó dòng 63 có thể được hiểu là:

Tiếp tục 1 điều kiện khác ở dòng 67:

Biến đổi nhanh:

Nếu symbol chưa được resolved thì điều kiện này sẽ đúng (tức = 0 <-> default). Nếu chưa thì điều kiện bên trong if lại tiếp tục kiểm tra:

Và có thể hiểu như sau (ở đây tôi đã viết tắt để bài không bị dài, nếu bạn muốn hiểu cặn kẽ hay đi sâu vào thực hành trên dòng 71):

Rút gọn hơn nữa ta sẽ được công thức sau:

Và nó sẽ tương ứng trong instruction sau:

Và nếu điều kiện này thỏa mãn nó sẽ thực thi tiếp tệp code sau:

Tóm tắt thì:

Cuối cùng thì version tôi đoán nó sẽ là: &link_map + 0x2e8dựa vào instruction dưới đây

_dl_lookup_symbol_x()

Dòng 95, điểm mà ta đã chú ý đến khi disass _dl_fixup, đó là _dl_lookup_symbol_x

Hàm này sẽ phụ trách: Search loaded objects' symbol tables for a definition of the symbol strtab + sym->st_name, perhaps with a requested version for the symbol (1). Và nó sẽ trả về địa chỉ libc base address(4) hoặc địa chỉ link_map chứa data của file libc(3)

(1)
(2)
(3)
(4)

2 hàm tiếp theo sử dụng kết quả trả về của _dl_lookup_symbol_x đó là:

DL_FIXUP_MAKE_VALUE sẽ tìm kiếm offset của symbol trong libc, relocates sau đó lưu nó trong value

dựa vào hàm SYMBOL_ADDRESS để Calculate the address of symbol REF using the base address(1)

(1)

Và để có thể dễ hình dung ta có thể rút gọn (tương tự như trên) như sau:

Và trong GDB, sau khi bạn thực hiện xong hàm _dl_lookup_symbol_x nó thực hiện 2 instruction sau đó để tìm ra chính xác định chỉ của puts() trong libc.

Và cuối cùng hàm elf_machine_fixup_plt() sẽ ghi địa chỉ symbol vừa resolved được (trong địa chỉ mà được trỏ bởi rel_addr)

Summary (tổng kết)

1

has called

2

l->l_info[23] là địa chỉ của JMPREL. (Bạn có thể so sánh giữa 0x17 trong .dynamic và objdump) và tham chiếu ->d.un.d_ptr chỉ đơn giản là lấy địa chỉ của JMPREL thôi. Đoạn này nó lấy pointer đến Elf64_Rela

3

0x18 is size of Elf64_Sym structure

giá trị được trả về là 1 index để tìm structure Elf64_Sym tương ứng trong SYMTAB section. Lấy pointer đến Elf64_Sym

4

Sử dụng r_info trong Elf64_Rela và kiểm tra rằng là 1 JUMP_SLOT hợp lệ. Hoặc đơn giản hơn là xem r_info của Elf64_Rela có bằng 7 hay không.

5

sử dụng st_other trong Elf64_Sym. Và nó sẽ kiểm tra symbol này đã được resolved hay chưa (resolved != 0) (not resolve = 0 (default))

6

Nếu điều kiện này thỏa mãn nó sẽ thực hiện tính ndx như sau:

Và sau đó tính tiếp version number:

7

looks for loaded objects’ symbol tables for a definition of the symbol in strtab + sym->st_name and returns the libc_base or link_map address.

8

Tìm ra offset của symbol từ libc_base address và relocates nó.

9

elf_machine_fixup_plt() sẽ ghi địa chỉ symbol vừa resolved được (trong địa chỉ mà được trỏ bởi rel_addr)

Đọc đến đây mà có chút lú thì meme này dành cho bạn 😀và đọc lại lần nữa.

Ret2Dlresolve technique

Từ phần trước ta có quy trình resolved symbol như sau:

Trong đó

  • phần đỏ là phần đã biết trước thông qua objdump hoặc elf (pwndbg)

  • phần cam là phần ta sẽ cần phải fake để có thể resolved symbol tùy ý mà ta muốn

1

Đầu tiên Push Fake reloc_arg lên stack sau đó jump đến PLT stub. Mục đích cuối cùng là để kết quả const PLTREL *const reloc trỏ đến vùng nhớ mà ta có quyền controllable. Theo công thức sau:

reloc_arg=(fake_relocJMPREL)0x18reloc\_arg = \frac {(fake\_reloc-\texttt{JMPREL})} {\texttt{0x18}}
2

Sau khi fake được reloc_arg ta sẽ có thể fake được luôn pointer đến Elf64_Rela/Elf32_Rela. Mục tiêu là để kết quả cuối cùng của const ElfW(Sym) *sym trỏ đến vùng mà ta có quyền controllable

r_info=((fake_symSYMTAB)0x18<<32)r\_info = \left(\frac {(fake\_sym - \texttt{SYMTAB}) } {\texttt{0x18}} << 32\right)
3

Sau khi có pointer đến fake Elf64_Rela structu re, ta có thể thay đổi giá trị r_info để đảm bảo rằng nó được kết thúc bằng 0x7.

0((fake_symSYMTAB)0x18<<32)0x70 \not= \left(\frac {(fake\_sym - \texttt{SYMTAB}) } {\texttt{0x18}} << 32\right) | \texttt{0x7}
4

Tương tự, ta sẽ thay đổi giá trị của st_other thành 0x00 để chương trình hiểu rằng symbol cần đi resolve.

5

Sau đó là fake st_name trong pointer trỏ đến Elf64_Sym để nó trỏ đến vùng lưu trữ địa chỉ chứa chuỗi của symbol.

st_name=fake_symstrSTRTABst\_name = fake\_symstr - \texttt{STRTAB}
6

Cuối cùng tạo 1 section fake STRTAB chứa tên hàm mà ta muốn resolve như ("system\x00")

Demo

Link file:

2MB
Open

Tôi sẽ dùng glibc2.35 trên Ubuntu 22:04 và source code sau để practice.

Đầu tiên ta cần xác định các yếu tố biết trước trong phép toán, đó là STRTAB, SYMTAB, JMPREL.

Và 1 vùng mem mà ta có quyền controllable:

(1)

Tôi sẽ lấy giá trị: 0x404d30 để nó không ảnh hưởng đến các section memory. Lí do tôi chọn giá trị này là vì trong quá trình debug thì tôi nhận ra trong quá trình lấy địa chỉ này đi tính toán đề di chuyển data trong memory nó có 1 phép tính đè vào vùng read-only -> Segment Fault

Giả sử nếu stack của bạn chọn quá thấp như là 0x404600 thì sau khi trừ nó sẽ nằm trong read-only memory 0x404600 - 0x9c0 = 0x403c40(1)

Và lí do tôi chọn giá trị lẻ đằng sau là vì ta cần căn chỉnh để có thể có 1 struct phù hợp và vài lần segment fault trong quá trình resolve - để giải quyết được vấn đề đó hãy mạnh dạn tằng địa chỉ chọn làm stack lên (Phần này có thể khó hiểu, hãy debug để hiểu rõ hơn 😀)

Tiếp đến, ta cần tính toán và chọn ra 3 pointer cần fake: fake_reloc, fake_sym, fake_symstr để control reloc_arg, st_name, r_info

Việc chọn phải đảm bảo yếu tố phù hợp với bối cảnh. Như ảnh sau là được nhé!

Sau khi xác định được các yếu tố cần thiết, bạn cần khai thác vuln Buffer Overflow để có quyền controllable (read, write, execute) memory mà ta đã xác định trước đó.

Stage 1: Stack Pivoting vie leave ; ret gadget

Ta sẽ dùng leave ; ret gadget để stack pivot. Tôi sẽ không đi sâu vào kĩ thuật này (tránh bài blog này dài)

Stack bây giờ đã nằm trong vùng memory mà ta có quyền kiểm soát. Nhìn vào <read+15> ta có thể input thêm 1 lần nữa. Lần này tôi sẽ gọi là stage 2: Input payload exploit dl_resolve.

Stage 2: Ret2Dlresolve

Stage này sẽ bắt đầu 1 quá trình cầm symbol đi resolve tính từ lúc nhảy đến stub push link_map lên stack kèm theo đó tôi sẽ push luôn tham số /bin/sh lên RDI (ghi đè sau) để khi resolve xong system thì nó sẽ gọi luôn tham số này -> tránh lỗi

Giải thích phần padding, đó là do ta cần sự phù hợp của dữ liệu nạp vào structure kể trên

Như bạn có thể thấy strtab + sym->st_name = địa chỉ của "system".

Sau khi _dl_fixup hoàn thành thì ta sẽ có bảng GOT như sau:

Và kết quả cuối cùng của việc exploit thành công là:

P/s: Nếu bạn không hiểu phần exploit này thì hãy đọc lại phần lí thuyết trên 1 lần nữa nhé :)).

Reference:

Lưu ý:

Phần trước other và sau other là 2 file khác nhau (Khác glibc) nên để thực sự hiểu thì bạn hãy practice theo tôi - Xin lỗi vì sự bất tiện này :<

Ngoài ra nếu có bất kì vấn đề nào liên quan đến bài post. Hãy gửi tin nhắn cho tôi qua Message Box, tôi sẽ trả lời sớm nhất. Cảm ơn sự đóng góp của bạn!

Góc Donate

Nếu bạn thích bài viết này, hãy ủng hộ tác giả cốc coffee/tiền mạng😀.

Last updated

Was this helpful?