DownUnderCTF 2021
Overview
- Nguồn : https://play.duc.tf/
- Nhận xét : Ngoài việc khó ra thì hết rồi :))
Write ups
1. No strings - Reversing
Bài này khá đơn giản, follow theo đề xem qua các chuỗi có trong file đã cho (có thể dùng strings của linux thay vì trâu bò IDA :) )
flag: DUCTF{stringent_strings_string}
2. Flag loader - Reversing
Nhận xét chung về bài này thì khá là tốn thời gian cho việc nghĩ làm sao cho nó chạy >>> chạy xong thì hết giờ :) Cũng là lần đầu đụng tới cái z3 để giải mấy bài toán tìm nghiệm, mất đâu đó hết nửa ngày để debug…
Sau khi bỏ vào IDA, ta có hàm main sau khi đã disassemble như sau:
Tổng quan những thứ trong hàm này là chạy 3 hàm check (check1
, check2
, check3
), lấy giá trị trả về của từng hàm (v4
, v5
, v6
) nhân lại để sleep
(sleep khá lâu đây <(“)) sau đó là trả về flag cho người dùng.
Ngó qua hàm init()
Chương trình đã được set alarm
ở 0x3C = 60s >>> Phiên hoạt động phải xong trước 60s, hay nói cách khác là làm sao vừa chạy được hết 3 cái check kia và vừa in ra flag kịp giờ, v4 * v5 * v6 < 60
:))
Xem qua hàm check1()
:
Tổng quan về hàm này là sẽ đưa cho chương trình 5 ký tự, xor
chuỗi vừa nhập với mảng X
cộng tổng vào biến v1, lấy tích với biến đếm i vào biến v2. Nếu sau vòng lặp mà v1 khác 0 hoặc v2 bằng 0 thì sẽ kết thúc chương trình >>> phải tìm cách né cái này.
Sau khi follow mảng X
này ta được giá trị như sauX[] = {'D', 'U', 'C', 'T', 'F'}
Theo phép xor thì chỉ cần xor với chính nó = 0 >>> chuỗi nhập vào là “DUCTF” thì có thể pass được ?
┌──(meka㉿meka)-[~/ductf] |
Tất nhiên không dễ như vậy rồi :)
Sau khi debug chương trình thì mình thấy được cái v2 sẽ = 0 nếu nhập y nguyên, nên ta sẽ sửa 1 tí bằng cách lấy “nhẹ” 1 chữ và bù “nhẹ” 1 chữ…0 + 0 + 0 + 0 + 0 = 0 + -1 + 0 + 1 + 0 = 0
Ở đây thì mình sẽ bớt chữ U đi 1 và cộng thêm 1 cho chữ T (range 8 bit)
1 | 'U' = 0x55 |
Kết quả:
Script cho màn 1 này
1 | p.recvuntil(b"Give me five letters: ") |
Vậy là xong được check1
và ghi điểm cho mình 128s vào hàm sleep
<(“), thực ra lúc đầu mình nghĩ là phải làm sao cho số này càng nhỏ càng tốt, nhớ mang máng cỡ 40s nhưng tới 2 check sau bất khả thi quá nên mới phải nghĩ cách sau:
Như đã thấy ở hàm main, v4
, v5
, v6
đều là biến int (32 bit), giá trị trả về của check1
tối đa có 8 bit, của check2
, check3
là 16 bit (sẽ nói ở phần sau) nên ta sẽ có ý tưởng overflow 32 bit như sau:
Ta đã biết v4 = 128 = 2 ^ 7
Và mục tiêu là giảm thiểu thời gian sleep tối đa (ở đây là 0 luôn cho tiện)
Nên mục tiêu sẽ là v4 * v5 * v6 = 2 ^ 32 = 0
Suy ra v5 * v6 = 2^25
, chọn v5 = 2 ^ 12
, v6 = 2 ^ 13
Hàm check2
Tổng quan về hàm này sẽ là đưa cho 1 số bất kỳ, tìm cặp (x,y) sao cho thoả các điều kiện
Nhét vào z3 và giải thôi :))
1 | p.recvuntil(b"Solve this: x + y = ") |
Hàm check3
Tương tự, ta cũng có:
1 | a, b, c, d, e = BitVecs("a b c d e", 32) |
Kết quả:
[+] Opening connection to pwn-2021.duc.tf on port 31919: Done |
flag:DUCTF{y0u_sur3_kn0w_y0ur_int3gr4l_d4t4_typ3s!}
3. Connect the dots - Reversing
Bài này thì tốn sương sương có một ngày <(“) nguyên nhân chính là sài cái extract data IDA ko quen nên mảng lỗi chỗ nọ chỗ kia >>> giải sai hướng phải quay xe mấy lần.
Sau khi bỏ vào IDA, hàm main như sau:
Tổng quan ở hàm này là sẽ nhập 1 chuỗi từ người dùng, độ dài tối đa là 0xDEF = 3567
, chuỗi sau đó sẽ qua hàm sub_11E0
xử lý và đem xor lại với một mảng khác và in ra màn hình. Dự đoán ban đầu của mình là chuỗi kết quả sẽ được in ra nên không cần quan tâm khúc xor kia.
Hàm sub_11E0
1 | __int64 __fastcall sub_11E0(char *a1) |
Sau khi kiểm thử hàm sub_1196
thì đây chỉ là hàm báo sai và kết thúc chương trình
Hàm sub_1196
:
1 | __int64 __fastcall sub_1196(int *a1, int a2, int a3) |
Ở hàm này sẽ chỉ check xem phần tử a3 có trong mảng không, nếu đúng thì trả về giá trị âm
Quay lại với hàm sub_11E0
, trong block while, ta thấy v1 = v4++
nên từng ký tự của chuỗi ban đầu nhập vào sẽ được duyệt đến hết, mỗi ký tự sẽ là một trong số "hjklx"
.
Trong 4 cases đầu, ta thấy biến v6 được thêm bớt đi 1 hoặc 60, bằng biện pháp nghiệp vụ của mình thì chắc chắn đây là điều hướng trong mảng 2 chiều có 60 cột :))
Kiểm tra số lượng phần tử của mảng thì có 3600 >>> Ma trận vuông 60 x 60
Với ‘h’ là qua trái, ‘j’ xuống, ‘k’ lên, ‘l’ phải. Các hướng di chuyển bị cấm thì phụ thuộc vào 4 bit cuối của dword_4080[v6] (&1, &2, &4, &8).
Ở trường hợp ‘x’ thì sẽ kiểm tra bit thứ 7 (&0x80)
1 | v8 = (dword_4080[v6] >> 4) & 7; // Tương đương (dword_4080[v6] >> 4) % 8 |
Sau đó kiểm tra v8
có trong v9
chưa với sub_1196
sau đó gán vào v9
, mỗi lần thành công tăng biến v7 lên và kiểm đến khi v7 == 8
Ta rút ra được kết luận là sẽ có một map 60x60 giá trị, cần phải đi qua 8 điểm đặc biệt để có thể hoàn thành challenge.
Dump mảng và bắt đầu code thôi :))
Ở đoạn code sau thì mình sẽ lấy thông tin quan trọng về các hướng có thể đi được ở từng ô + những điểm đặc biệt lưu ra file:
1 |
|
Kết quả (đã lược bớt khúc trên)
Vậy là có 8 điểm {11, 15}, {20, 35}, {24, 18}, {28, 54}, {35, 11}, {39, 34}, {48, 57}, {59, 45}. Tính giá trị của từng ô với (dword_4080[v6] >> 4) & 7;
thì có giá trị lần lượt là 0, 1, 2, 3, 4, 5, 6, 7 -> đặt tên đỉnh theo thứ tự này luôn.
Bình thường thì chỉ cần tìm đường đi qua 8 đỉnh này là xong, nhưng vấn đề là qua theo thứ tự nào… Ở hàm sub_11E0
:
1 | v5 = (unsigned __int8)dword_4060[v8] ^ (dword_4060[v8] >> 8) & v5; |
Như vậy v5 quyết định thứ tự cho việc này. Ta có dword_4060[] = {65407, 60999, 31620, 5074, 27425, 53858, 20942, 3721};
Nhờ được ông @hungt1 brute force cho đoạn này nên có thứ tự các đỉnh là 6 -> 3 -> 7 -> 5 -> 1 -> 4 -> 0 -> 2
Code thôi :))
1 |
|
Kết quả:
1 | 3566 lljhjjljljll... |
Độ dài vừa khít với buffer :)) bỏ vào chương trình và lấy flag thôi
DUCTF{bfs_dfs_ffs_no_more_mazes_a8fb66c12cd}
4. OutBackdoor - Pwn
Ta có hàm main như sau:
1 | int __cdecl main(int argc, const char **argv, const char **envp) |
Hàm gets không kiểm tra số lượng đọc vào >>> buffer overflow. Biến v4 ở vị trí $rbp-10h, suy ra offset là 0x10 + 8 = 24
Kiểm tra sơ bộ file:
┌──(meka㉿meka)-[~/ductf] |
Bit NX đã được bật nên không thể áp shellcode vào được, xem qua danh sách function thì thấy có hàm outBackdoor:
1 | int outBackdoor() |
Do PIE không được bật nên này là điển hình của dạng bài ret2ret luôn rồi :))
Dùng objdump để tìm địa chỉ của hàm này:
➜ objdump -d outBackdoor| grep outBackdoor |
Có địa chỉ 0x4011d7, overwrite thử $rip:
1 | from pwn import * |
Kết quả:
[+] Starting local process './outBackdoor': pid 1948 |
Vậy là chạy local được nhưng server thì gặp lỗi EOF, xem lại code ở hàm outBackdoor có:
1 | return system("/bin/sh"); |
Có thể hàm system bị return quá sớm >>> ngắt ở server. Dùng ROPgadget, tìm được gadget 0x401016 : ret
phù hợp cho việc này.
Quá trình attack diễn ra như sau:
1 | from pwn import * |
[+] Starting local process './outBackdoor': pid 2404 |
flag: DUCTF{https://www.youtube.com/watch?v=XfR9iY5y94s}
5. babygame - Pwn
Trước tiên là decompile hàm main
1 | int __cdecl __noreturn main(int argc, const char **argv, const char **envp) |
Kiểm tra thử biến NAME
1 | .bss:00000000000040A0 ; char NAME[32] |
Ta thấy có vấn đề như sau: read(0, NAME, 0x20uLL);
đọc vào tối đa 32 bytes, nhưng nếu đọc hết 32 bytes thì NAME có thể không kết thúc bằng \0
, dẫn đến việc lấn sang con trỏ RANDBUF trong memory.
1 | unsigned __int64 game() |
Tổng quan thì hàm này sẽ đọc 4 bytes từ file mở từ đường dẫn RANDBUF (Được gán “/dev/urandom” ở hàm main) và so với một con số được đọc từ bàn phím, nếu đoán đúng thì sẽ chạy system(“/bin/sh”) để lấy flag.
1 | size_t set_username() |
Ở hàm này có một vấn đề là nó sẽ đọc một string có độ dài nhỏ hơn hoặc bằng string ban đầu với strlen thay vì read hẳn 32 bytes, cộng với vấn đề đã nói ở trên thì biến RANDBUF sẽ là mục tiêu bị đổi :)).
Ở hàm print_username() chỉ đơn giản là in biến NAME ra màn hình.
Vậy nếu thay đổi được RANDBUF thì sẽ thay đổi thành gì ? ở hàm game sẽ đọc file và lấy 4 bytes đầu >>> để đoán được thì RANDBUF cần trỏ tới đường dẫn tới một file static (“/bin/sh”) do header của elf luôn giữ nguyên
Kiểm tra địa chỉ của string /bin/sh và /dev/urandom
1 | .rodata:0000000000002024 aDevUrandom db '/dev/urandom',0 ; DATA XREF: main+3A↑o |
Như vậy thì 2 biến này chỉ khác nhau ở byte đầu tiên (0x24 và 0xA3), nằm trong 0x100 nên bypass aslr được.
Vậy quá trình attack sẽ như sau:
Fill biến NAME với 32 ký tự bất kỳ >>> Print name để leak địa chỉ của chuỗi “/dev/urandom” >>> set name để sửa lại 1 byte >> gọi hàm game và điền số đã biết vào.
Header của file elf sẽ là 7F 45 4c 46
, đọc theo little endian sẽ là 46 4c 45 7f
, chuyển về số nguyên: 1179403647.
1 | from pwn import * |
[+] Opening connection to pwn-2021.duc.tf on port 31907: Done |
flag: DUCTF{whats_in_a_name?_5aacfc58}