Writeups MatesCTF Round 3
Protected
Bài này là một dạng virtual machine. Tuy nhiên có một điều thú vị là nó sử dụng 2 process, một process dùng để lưu vmcode và một dùng để thực thi vmcode.
Hàm main
Chương trình mới chạy thì không tồn tại mutex MATESCTF_2019 rồi nên chắc chắn hàm main sẽ gọi sub_405290
sub_405290
tiến hành tạo mutex MATESCTF_2019 ở dòng số 5 rồi tạo một process con với tham số Filename
là đường dẫn của process cha và CommandLine
là process ID của process cha ở dòng số 18. Sau khi tạo xong process con thì process cha ngồi chờ đến khi nào có debug event ở dòng 24 thì mới có thể chạy tiếp.
Khi process con chạy thì nó sẽ chạy lại hàm main, lúc này mutex MATESCTF_2019 đã tồn tại do sub_405290
của process cha tạo ra nên process con sẽ thực thi hàm sub_4053B0
. Hàm này sẽ tạo ra giao diện dialog với callback function
Đoạn code trong nhánh điều kiện a2 == WM_COMMAND
sẽ được chạy mỗi khi ấn nút Unlock ở trên giao diện. Nó sẽ lấy input ở text field và đưa vào hàm check_flag
. Kiểm tra hàm check_flag
Khi process con chạy đến địa chỉ 004013B3
thì process con sẽ tạm dừng và process cha được đánh thức do lệnh int 3 tạo ra debug exception.
Quay lại process cha, sau khi được đánh thức thì nó sẽ thực hiện hàm sub_405160
và gọi API ContinueDebugEvent để cho process con tiếp tục chạy.
Tập trung vào nhánh code a1->dwDebugEventCode == EXCEPTION_DEBUG_EVENT
, process cha sẽ lấy context, là kiểu cấu trúc lưu giá trị của các thanh ghi của process con tại thời điểm process con tạm dừng do gặp phải lệnh int 3
. Giá trị của các thanh ghi được truyền vào hàm sub_404630
Hàm cũng hơi dài tí nhưng hiểu đơn giản là nó sẽ đọc 5 bytes bắt đầu từ địa chỉ EIP - 1
của process con (chính là địa chỉ chứa lệnh int 3
) lưu vào biến Buffer
. Lệnh switch case sẽ dựa vào giá trị của Buffer[0]
để thực hiện đoạn code tương ứng. Thực ra phần lệnh switch case chính là phần xử lý vmcode, ví dụ như Buffer[0] == 0
thì đoạn code bên trong thực hiện giống với lệnh mov
2 thanh ghi trong assembly, tương tự với những case 1, 2, 3,... còn lại.
Sau khi xử lý xong vmcode thì process cha sẽ lưu 1 byte giá trị tại địa chỉ của thanh ghi EIP
của process con và ghi đè giá trị mới là 0xCC, 0xCC chính là opcode của lênh int 3
. Điều này làm cho process con sau khi được process cha gọi dậy bằng hàm ContinueDebugEvent
thì lại tiếp tục ngủ do lại gặp phải lệnh int 3
ở lệnh tiếp theo. Cách làm này giống như cách debugger làm khi đặt software breakpoint vậy, mỗi khi bạn đặt breakpoint tại một địa chỉ nào đó thì debugger sẽ lưu lại 1 byte giá trị tại địa chỉ đấy và ghi đè bằng 0xCC.
Và cuối cùng là bài giải của mình. Đầu tiên mình sử dụng IDAPython để dump đống vmcode
1 2 3 4 |
from idaapi import * vmcode = GetManyBytes(0x4013b4, 0x10000) open('vmcode.bin', 'wb').write(vmcode) |
Tiếp theo mình viết một chương trình bằng python convert vmcode thành mã assembly x86
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 |
import sys VMCODE = open('vmcode.bin', 'rb').read()[5:] def get_register(op): regs = ['eax', 'ebx', 'ecx', 'edx', 'esi', 'edi', 'esp', 'eip', 'ebp'] return regs[op - 1] def set_register(op, value): regs = ['eax', 'ebx', 'ecx', 'edx', 'esi', 'edi', 'esp', 'eip', 'ebp'] if isinstance(value, int): x = hex(value) else: x = str(value) return 'mov ' + regs[op - 1] + ', ' + x def main(): ip = 0 while (ip < len(VMCODE)): buffer = VMCODE[ip: ip + 5] # print ip + 5, # print instruction address if ord(buffer[0]) == 0: v3 = get_register(ord(buffer[2])) ins = set_register(ord(buffer[1]), v3) ip += 3 elif ord(buffer[0]) == 1: ins = set_register(ord(buffer[1]), ord(buffer[2])) ip += 3 elif ord(buffer[0]) == 2: v32 = get_register(ord(buffer[1])) v33 = get_register(ord(buffer[2])) ins = 'xor ' + v32 + ', ' + v33 ip += 3 elif ord(buffer[0]) == 3: v32 = get_register(ord(buffer[1])) v33 = get_register(ord(buffer[2])) ins = 'sub ' + v32 + ', ' + v33 ip += 3 elif ord(buffer[0]) == 4: v32 = get_register(ord(buffer[1])) v33 = get_register(ord(buffer[2])) ins = 'add ' + v32 + ', ' + v33 ip += 3 elif ord(buffer[0]) == 5: v27 = get_register(ord(buffer[1])) ins = 'push ' + v27 ip += 2 elif ord(buffer[0]) == 6: reg = get_register(ord(buffer[1])) ins = 'pop ' + reg ip += 2 elif ord(buffer[0]) == 7: v23 = get_register(ord(buffer[2])) v24 = get_register(ord(buffer[3])) ins = set_register(ord(buffer[1]), 'byte [' + v23 + ' + ' + v24 + ']') ip += 4 elif ord(buffer[0]) == 8: ins = 'jmp $+' + str(ord(buffer[1]) + 1) ip += ord(buffer[1]) + 2 elif ord(buffer[0]) == 9: ins = 'jz $+' + str(ord(buffer[1]) - 1) ip += 2 elif ord(buffer[0]) == 10: v18 = get_register(ord(buffer[1])) v19 = get_register(ord(buffer[2])) ins = 'cmp ' + v18 + ', ' + v19 ip += 3 elif ord(buffer[0]) == 11: v32 = get_register(ord(buffer[1])) v33 = get_register(ord(buffer[2])) ins = 'and ' + v32 + ', ' + v33 ip += 3 elif ord(buffer[0]) == 0xC: v32 = get_register(ord(buffer[1])) v33 = get_register(ord(buffer[2])) ins = 'shr ' + v32 + ', ' + v33 ip += 3 elif ord(buffer[0]) == 0xD: v32 = get_register(ord(buffer[1])) v33 = get_register(ord(buffer[2])) ins = 'shr ' + v32 + ', ' + v33 ip += 3 elif ord(buffer[0]) == 0xE: ip += 2 v10 = get_register(ord(buffer[1])) ins = 'call ' + v10 elif ord(buffer[0]) == 0xF: v9 = get_register(buffer[1]) ins = 'jmp ' + v9 print 'EXIT' sys.exit() elif ord(buffer[0]) == 0x10: v7 = get_register(ord(buffer[2])) v8 = get_register(ord(buffer[3])) ins = set_register(ord(buffer[1]), '[' + v8 + ' + ' + v7 + ']') ip += 4 elif ord(buffer[0]) == 0x11: ip += 2 ins = 'jnz $+' + str(ord(buffer[1]) - 1) else: print 'EXIT' sys.exit() print ins if __name__ == '__main__': main() |
Sau khi chạy script ở trên thì kết quả sẽ cho ra tầm 4k dòng assembly =)) Mình không phải là một thằng có khả năng tính toán tốt cũng như sự kiên trì nên mình tìm cách để rút gọn 4k dòng code đó lại. Mình sử dụng NASM để compile đống mã assembly, tất nhiên là mình có sửa lại code assembly một chút để nó phù hợp với NASM rồi quăng vào IDA =)) Chức năng mã giả của IDA có 1 tính năng ẩn mà ít người để ý đến đó là rút gọn các phép tính hằng số, ví dụ như x = a + 1 + 2 + 3
thì mã giả chỉ hiện là x = a + 6
, đây chính là lý do mình quyết định sử dụng NASM compile lại rồi đưa vào IDA :v
Và đây là mã giảTừ input[0] đến input[14] tính khả đươn giản, input[19] mình dùng Z3 để giải, còn input[15] đến input[18] được tính đựa vào
sub_401000
vì biến a5
chính là sub_401000
. Tuy nhiên, có biến retaddr
là mình không xác định được giá trị tại nó là giá trị tại chỉ địa của ESP
nên mình đành phải debug process cha để lấy giá trị của ESP
và dùng Cheat Engine để đọc memory của process con.
Và cuối cùng key để unlock là gAISzUSwJl4i6BITLOp8
Hello
Mình có la liếm câu này một chút tại vì nó là nền tảng của IoT nên mình cũng muốn vọc chơi thử.
Mình có đọc bài của một bạn Reverse Arduino Uno, Hello thấy nể quá, cách làm của mình cũng tương tự nhưng mình sử dụng linux để debug và mình không giải ra được flag =)))) tại vì hàm tính flag dài dòng quá, mình không đủ nhẫn nại để ngồi tính. Đúng là sự khác biệt giữa con nhà người ta và con nhà mình 🙁
Mình chỉ muốn bổ sung vài điều là trong quá trình cài simavr thì sẽ thiếu một số file C header, chỉ cần search "tên_file.h ubuntu" là sẽ biết cần cài package nào để có được file C header đó, hoặc là package manager của ubuntu có chức năng tìm file C header nằm trong package nào.
avr-gdb mặc định của ubuntu sẽ không hỗ trợ python, cần phải tải source code gdb (nên tải gdb phiên bản giống với trong máy) về cấu hình lại phần compile với tham số sau ./configure --with-python --target=avr
, nếu compile lỗi thì có thể nên cài thêm gói python-dev. Cài đặt xong là có thể quẩy được câu này rồi 😀
Ngoài ra mình bị vướng 1 điều là mình đã đọc data sheet của atmega328p rồi mà vẫn không thể nào biết cách tự xác định đâu là hàm main cả 🙁 Với cả địa chỉ jump của một số hàm khi mình dùng radare2 và IDA đều không giống với trong data sheet 🙁 đến giờ vẫn còn hoang mang không hiểu mình hiểu sai chỗ nào 🙁