[WannaOne UIT 2021- Rev] Dad And Son

 Giải UIT qua cũng lâu nhưng giờ thi xong mới có dịp làm lại, tiện kiếm 20k tiền viết bài.

Bài này lại sử dụng anti debug của linux bằng cách dùng fork vào ptrace để giao tiếp tiến trình cha con. 


Tiến trình con thực thi 1 đoạn shellcode lưu ở loc_40a0, xem thử nó là gì


Còn tiến trình cha sẽ là đoạn switch với các case là các signals mà tiến trình con gửi cho cha.


Mình có comment trong Ida rồi, tý mình sẽ gửi file idb của mình.

Sau một lúc debug thì tóm tắt luồng như sau:

1. Nhập input với 64 ký tự.

2. Ở địa chỉ 0x40c3 có phép idiv ecx, mà ecx bằng 0 nên sinh ra Signal SIGFPE là case 8.

Trong case 8 nó gọi PTRACE_GETREGS rồi lưu vào mảng v7, suy ra được v7[0] đang chứa thanh ghi r15.

Sao mình đoán dc r15, thì mọi người lên search cách sử dụng PTRACE_GETREGS, trong đó v7 là struct user_regs_struct, struct đó như sau


 


Mọi người có thể dùng kỹ thuật hook hoặc debug bằng gdb thì sẽ thấy lúc này r13 của thằng con đang trỏ tới input và r15 đang chứa 4 byte đầu của input. Suy ra v7[0] đang chứa 4 byte đầu của input, được đưa qua hàm sub_12D3 để mã hóa xong gửi lại cho con bằng cách thay đổi r15 thông qua PTRACE_SETREGS.

3. Tiếp theo thằng con chạy tiếp câu lệnh sau idiv ecx, nó lại gọi syscall kill với cờ 0x12 ở địa chỉ 0x40db, lúc này thằng cha sẽ nhảy vào case 0x12

nhìn có vẻ phức tạp hơn case 8 nhưng nó giống hệt case 8 :D. v7[0] là r15 của thằng con, mà r15 đang chứa output sau khi mã hóa ở case 8. Lúc này lại mã hóa thêm thông qua hàm sub_1300.

Ok xong nó ghi đè giá trị lên dword_4300 của thằng con bằng PTRACE_PEEKTEXT. Cái này tự nhẩm được, nếu không chắc thì mọi người debug bằng gdb nhé. Thế là 4 byte đầu của dword_4300 chứa kết quả của 4 byte đầu input đi qua 2 hàm mã hóa. Sau đó thằng con tiếp tục chạy cho đến hết 64 byte.
Kết luận: input 64 ký tự được chia thành nhóm 4 byte rồi thông qua 2 hàm mã hóa sub_12D3 và sub_1300 rồi lưu vào dword_4300

4. Sau khi thoát hết 64 ký tự nó thoát khỏi vòng lặp và ăn ngay cái int 3 ở địa chỉ 0x40DF, gây ra SIGTRAP. Lúc này thằng cha nhảy vào case 5

v7[2] là thanh ghi r13 của thằng con, cái case 5 này chỉ để set r13 về dword_4300 sau khi nó vừa bị tăng lên do lặp hết 64 ký tự.

5. Sau khi reset r13 thì nó bắt đầu vòng lặp mới

ud2 sẽ gây ra cái SIGILL, invalid instruction hay sao ấy :D. Lúc này thằng cha nhảy vào case 4.

Case này chả khác gì case 0x12, mọi người debug hoặc tự nhìn code assembly của thằng con sẽ thấy v7[0] đang chứa 4 byte output của 2 hàm mã hóa, rồi lại mã hóa nó bằng hàm sub_1331.
Kết luận: input 64 byte chia thành 4 byte và mã hóa thông qua 3 hàm.

6. Sau một hồi mệt mỏi thì đã đến đoạn nó kiểm tra


Nhưng nó phải đi qua 2 cái syscall to đùng kia đã mới đến đoạn kiểm tra.
Syscall ở địa chỉ 0x412e là syscall kill với flag 0x1c. Lúc này thằng cha nhảy tới case 0x1c

Mình có đổi tên và comment rồi. v7[0] chứa r15, v[1] chứa r14 của con


r13 đang trỏ tới dword_4300, suy ra r15 chứa 8 byte đầu của dword_4300, r14 chứa 8 byte tiếp theo.
Sau đó qua hàm sub_126D và sub_1297 nó thay đổi r10 và r8 của thằng con bằng  PTRACE_SETREGS. À mọi người thấy thằng index không, xref nó thì chả thấy thằng nào write cho nó mà chỉ có read. Ông tác giả có đoạn lừa ở đây, do PTRACE_SETREGS được ghi và v7, mà user_regs_struct to như thế mà v7 nó chỉ là mảng 5 phần tử, nên nó sẽ bị đè lên index. theo vị trí trong struct thì index chứa giá trị của r9, đọc code của thằng con thì r9 thì tăng từ 0-3 sau mỗi lần loop nên index cũng tăng từ 0-3

7. Tiếp theo là syscall 0x12c ở địa chỉ 0x4137, nhảy tới case 0x1f. Cái này cũng chỉ mã hóa thôi nên mình không nhắc lại nhé, bài dài quá rồi mà mình thì lười.


r13 đang trỏ tới dword_4300, rsp đang trỏ tới unk_4260, unk_4260 chứa đoạn mã hóa là kết quả cần so sánh.
Nhìn đoạn so sánh trên thì biết dc cần đảm bảo 

dword_4300[i * 4 + 1] == unk_4260[i * 5]
dword_4300[i * 4 + 2] == unk_4260[i * 5 + 1]
r10d == unk_4260[i * 5 + 2] 
ebp == unk_4260[i * 5 + 3]
r8d == unk_4260[i * 5 + 4]

r10d, r8d, ebp bị thay đổi do case 0x1c và 0x1f. dword_4300 là mảng chứa 64 byte, mỗi 4 byte là kết quả của 4 byte từ input bị thay đổi qua 3 hàm mã hóa.

Thôi dài quá, :(( mình show code z3 luôn
from z3 import *

def sub_12D3(a1):
    u2 = a1 ^ (((a1 >> 3) & 0x20000000) + (a1 << 5))
    return u2 ^ (u2 << 7)

def sub_1300(a1):
    u2 = ((a1 >> 1) & 0xff) + a1
    return (((u2 >> 3) & 0x20000000) + (u2 << 5)) ^ u2

def sub_1331(a1):
    u2 = ((a1 << 7) ^ a1) & 0xffffffff
    return (((u2 >> 1) & 0xff) + u2) & 0xffffffff

def sub_126D(a0, a2, cal_const):
    return ((a2 >> cal_const[0]) + a0) & 0xffffffff

def sub_1297(a0, a3, cal_const):
    return ((a3 * cal_const[1]) + (cal_const[2] * a0)) & 0xffffffff

unk_4260 = [0xf8702841, 0xce47caca, 0xbcb79d5c, 0x72d5a7a1,
          0x3171de4a, 0xe7caf8b5, 0xd390fced, 0x31ba728e,
          0xf99f30c2, 0x1f20769c, 0xa5e300b5, 0xdd24b3ad,
          0xe01b0c5f, 0x14484392, 0x660a7ce6, 0xeff044be,
          0x39cfb467, 0xaf6fdb3b, 0x7acf5264, 0x57474bb1]

unk_41c0 = [[3, 9, 4, 8, 6, 7], [3, 2, 4, 6, 7, 6],
             [2, 1, 3, 9, 5, 6], [8, 4, 9, 8, 3, 3]]

flag = b''
for i in range(4):
    solver = Solver()
    input_n = [BitVec('input_%d' % i, 32) for i in range(4)]
    array_0 = sub_1331(sub_1300(sub_12D3(input_n[0])))
    array_1 = sub_1331(sub_1300(sub_12D3(input_n[1])))
    array_2 = sub_1331(sub_1300(sub_12D3(input_n[2])))
    array_3 = sub_1331(sub_1300(sub_12D3(input_n[3])))
    for j in range(4):
        solver.add((input_n[j] & 0xff) >= 32, (input_n[j] & 0xff) <= 126)
        solver.add(((input_n[j] >> 8) & 0xff) >= 32, ((input_n[j] >> 8) & 0xff) <= 126)
        solver.add(((input_n[j] >> 16) & 0xff) >= 32, ((input_n[j] >> 16) & 0xff) <= 126)
        solver.add(((input_n[j] >> 24) & 0xff) >= 32, ((input_n[j] >> 24) & 0xff) <= 126)
   
    solver.add(array_1 == unk_4260[i * 5])
    solver.add(array_2 == unk_4260[i * 5 + 1])
    r10d = sub_126D(array_0, array_2, unk_41c0[i])
    r8d = sub_1297(array_0, array_3, unk_41c0[i])
    r10d = ((r10d * unk_41c0[i][3]) + (r8d * unk_41c0[i][4])) & 0xffffffff
    ebp = ((r8d * unk_41c0[i][5]) + array_0) & 0xffffffff
    solver.add(r10d == unk_4260[i * 5 + 2])
    solver.add(ebp == unk_4260[i * 5 + 3])
    solver.add(r8d == unk_4260[i * 5 + 4])
    assert(solver.check() == sat)
    m = solver.model()
    flag += m[input_n[0]].as_long().to_bytes(4, 'little')
    flag += m[input_n[1]].as_long().to_bytes(4, 'little')
    flag += m[input_n[2]].as_long().to_bytes(4, 'little')
    flag += m[input_n[3]].as_long().to_bytes(4, 'little')
   
print(flag)

Mọi người nếu không muốn chịu khổ đau khi làm mấy bài này thì nên tập lập trình nhiều hơn thì lúc dịch ngược mình có thể tự tính được luồng chạy chương trình.

Nhận xét