Link challange: https://cdn.discordapp.com/attachments/1025117429556330640/1044155088681312296/ctf_challenge.zip
pass: infected
Mở đầu chương trình thì ta sẽ thấy chương trình define 1 dãy data, ban đầu thì ta không hiểu đó sẽ là data gì và đi tiếp xem nhưng dòng code tiếp theo để hiểu được ý nghĩa của đoạn khai báo này là gì

Sau khi define thì chương trình đọc file flag.txt và lưu vào biến buffer , sau đó sẽ đưa vào hàm sub_951040 cùng với 1 địa chỉ của biến lpAddress (biến lpAddress lúc này vẫn chưa biết cụ thể nó là gì)

Phân tích sâu vào hàm sub_951040, ban đầu tôi nhìn hàm này thấy khá là lộn xộn nên sẽ tiến hành tạo struct và ép kiểu để cho code clean hơn và dễ reverse hơn
Trước khi clean code:

Sau khi clean code:

Đọc qua đoạn code sau khi clean trên thì tôi thấy hàm này sẽ tạo 1 class kế thừa từ class có những function này (những function này đã được rename cho tiện đọc)

và sử dụng design pattern singleton cho class đó. Sau đấy alloc 1 số đoạn buffer và zeroMemory cho những đoạn buffer đó, riêng lpStartAddress sẽ set toàn là 0x90 (144).
this->bufferFile sẽ lấy data của file (nôm na là flag) theo kiểu truyền và ép kiểu. Còn this->buffer3 thì sẽ lưu chuỗi “1234567890123456”

Đọc đoạn tiếp theo thì tôi thấy chương trình sẽ tạo 1 thread và sử dụng lpStartAddress , lúc này đã hiểu được vì sao tác giả lại set 1 dãy 0x90 cho địa chỉ này (0x90 = nop ), sau đấy thì lại set 0xEB và 0xFE (đó là tạo 1 vòng lặp vô hạn để tránh chương trình chạy tuột đi và sinh ra crash chương trình). Sau khi tạo thread, chương trình sẽ suspend thread đó và tạo 1 context cho thread và cung cấp thông tin cho context đó như hình bên dưới 😐

Sau khi phân tích xong, quay lại hàm main và ép struct vào để clean code. Tổng quát thì như hình nên sẽ tiến hành đi sâu vào từng function để biết chi tiết.

Hàm func1 class sẽ sử dụng class, dãy data đã được khai báo ở đầu chương trình và biến v12. Bên trong thì sẽ lấy giá trị tại vị trí đầu tiên của dãy data đó tính toán và đi theo switch case.

Đi sâu vào từng function, đầu tiên sẽ là func4, thì tôi thấy cũng là switch case khá là lộn xộn bởi các phép tính toán trong từng case. Nhưng hãy để ý nó sẽ tính toán và cuối cùng sẽ truyền vào this->PTHREAD_START_ROUTINE (đây là địa chỉ code của thread được tạo trước đó) → các case này là tính toán để tạo shellcode cho chương trình chạy.

Sau khi chiêm nghiệm xong func4, tôi tiếp tục xem những function khác và thấy nó tương tự là tạo ra shellcode. Chẳng hạn func5

Riêng ở func9 thì nó không tạo shellcode, thay vào đó là sẽ lấy context.Eflags & 0x40 và switch case

Đọc trong link này https://en.wikipedia.org/wiki/FLAGS_register, thì tôi hiểu được cái context.Eflags & 0x40 sẽ lấy giá trị của cờ ZF.
Sau khi phân tích sơ qua xong toàn bộ func1, thì tiếp tục đi tiếp các function của class tiếp theo:
- func2: ResumeThread

- belikeAntiDebug: Hàm này khá thú vị, vì tôi đã phân tích hàm này và phát hiện nó là antidebug nên mới đổi tên hàm chứ mặc định tôi đặt tên như mấy hàm kia là func + số thứ tự, hàm này sẽ sử dụng thanh ghi Dr0, Dr1, Dr2, Dr3 để detect debug (chi tiết https://unprotect.it/technique/debug-registers-hardware-breakpoints/ )

có nhiều cách để bypass đoạn này nhưng cách tôi chọn là sẽ patch 2 giá trị trong if là 0xEB và 0xFE như trước khi if

- func10: in ra giá trị của data file ra màn hình theo từng kí tự ở dạng hex

Sau khi đã hiểu rõ sơ quát chương trình, tôi sẽ đặt breakpoint tại 1 vài nơi tạo ra shellcode (ret 4 hoặc 1 nơi khác để có thể lấy hoàn chỉnh không thừa không thiếu) để lấy log shellcode. Chẳng hạn như ở func4:

Sau khi hoàn thành static, tôi chuyển sang chế độ debug để lấy log, nhưng trước đó tôi sẽ cho input bất kì vào file flag.txt để chương trình có thể đọc và lấy làm input

Và tiến hành chạy debug, tôi thu được:

Decompile đoạn shellcode:

Hmmm, đọc hiểu đoạn assembly này, thì tôi thấy chương trình sẽ chạy và tìm byte NULL của data tại edi, nếu không có byte NULL trong đoạn data đó thì ecx sẽ +=4 . Cuối cùng thì sẽ so sánh với ecx và 0x24. Mà edi là bufferFile (data của flag.txt).

Lưu ý là việc xử lý các kết quả của cmp sẽ nằm tại func9 khi nãy bằng cách lấy cờ ZF

⇒ Đoán được flag có độ dài tối thiểu là 0x24–0x4 = 0x20 kí tự
Thay đổi input và chạy debug lại để lấy log tiếp

Sau khi cho chương trình chạy xong, tôi thu được shellcode đầy đủ

Full shellcode: https://cdn.discordapp.com/attachments/782606355730661416/1046623850412126360/shellcode.txt
Decompile đoạn shellcode thì thấy rằng khi check đúng độ dài thì chương trình sẽ tiếp tục làm gì đó

Đọc hiểu toàn bộ assembly đã decompile từ shellcode và tóm tắt được nó bằng code c:
#include <stdio.h>
int main()
{
char text[] = "1234567890123456";
char input[] = "11112222333344445555666677778888";
unsigned int esi = 0, edi = 0, eax, ebx, ecx = 0, edx =0;
unsigned int tmp =0;
for (int i =0; i<4 ; i++)
{
ecx = 0x9e3779b9;
*(unsigned int *)&esi = *(unsigned int *)(input + 8*i);
*(unsigned int *)&edi = *(unsigned int *)(input + (8*i + 4));
for (int j=0; j<8; j++)
{
ebx = edi;
ebx <<= 4;
ebx += *(unsigned int *)(text + 0x0);
eax = ecx + edi;
ebx ^= eax;
eax = edi;
eax >>= 5;
eax += *(unsigned int *)(text + 0x4);
ebx ^= eax;
esi += ebx;
ebx = esi;
ebx <<= 4;
ebx += *(unsigned int *)(text + 0x8);
eax = ecx + esi;
ebx ^= eax;
eax = esi;
eax >>= 5;
eax += *(unsigned int *)(text + 0xc);
ebx ^= eax;
edi += ebx;
// printf("%x\n", edi);
ecx += 0x9e3779b9;
if (ecx == 0x8ff34781)
{
ecx = 0;
break;
}
}
*(unsigned int *)(input + (8*i)) = esi;
*(unsigned int *)(input + (8*i+4)) = edi;
ebx = 0;
esi = (unsigned int) text;
edi = (unsigned int) input;
if (i == 0)
{
edx = *(unsigned int *)(text + 0x0) ;
ebx = 0xefcdab89;
edx ^= ebx;
*(unsigned int *)(text + 0x4) = edx;
}
else if (i==1)
{
edx = *(unsigned int *)(text + 0x4) ;
ebx = 0xefbeadde;
edx ^= ebx;
*(unsigned int *)(text + 0x8) = edx;
}
else if (i==2)
{
edx = *(unsigned int *)(text + 0x4) ;
ebx = 0xfedcba98;
edx += ebx;
*(unsigned int *)(text + 0x0) = edx;
}
}
}
// cộng thêm 0x2e8946e3 vào cuối chuỗi input
Vì nó biến đổi theo 4 byte 1 lần nên tôi tách thành từng đoạn number của file output.txt mà đề cho sẵn, và lưu ý rằng nó sẽ có 4 byte cuối cùng không liên quan được cộng vào chuỗi nên đừng quan tâm đến nó.

Script lấy flag:
def int32ToString(number):
while number != 0:
a = number & 0xff
number >>= 8
print(chr(a), end="")
def decrypt(esi, edi, arr):
for i in range(8,0, -1):
ecx = 0x9e3779b9 * i & 0xffffffff
ebx = esi
ebx <<= 4
ebx &= 0xffffffff
ebx += arr[2]
ebx &= 0xffffffff
eax = ecx + esi
eax &= 0xffffffff
ebx ^= eax
ebx &= 0xffffffff
eax = esi
eax >>= 5
eax &= 0xffffffff
eax += arr[3]
eax &= 0xffffffff
ebx ^= eax
ebx &= 0xffffffff
edi -= ebx
edi &= 0xffffffff
ebx = edi
ebx <<= 4
ebx += arr[0]
ebx &= 0xffffffff
eax = ecx + edi
eax &= 0xffffffff
ebx ^= eax
ebx &= 0xffffffff
eax = edi
eax >>= 5
eax &= 0xffffffff
eax += arr[1]
eax &= 0xffffffff
ebx ^= eax
esi -= ebx
esi &= 0xffffffff
return esi, edi
esi = 0x32a86394
edi = 0xaea320ce
# ecx = 0xf1bbcdc8
arr = [0x34333231, 0x38373635, 0x32313039, 0x36353433]
esi, edi = decrypt(esi,edi, arr)
int32ToString(esi)
int32ToString(edi)
esi = 0x8d1cbc04
edi = 0xb1228e7a
arr[1] = arr[0]^ 0xefcdab89
esi, edi = decrypt(esi,edi, arr)
int32ToString(esi)
int32ToString(edi)
esi = 0x11b1318a
edi = 0xb70ad3aa
arr[2] = arr[1] ^0xefbeadde
esi, edi = decrypt(esi,edi, arr)
int32ToString(esi)
int32ToString(edi)
esi = 0xa2708b62
edi = 0x820c8b81
arr[0] = arr[1] + 0xfedcba98 & 0xffffffff
esi, edi = decrypt(esi,edi, arr)
int32ToString(esi)
int32ToString(edi)
⇒ Flag: ASCIS{M@sT3r_0f_V1rtu4l_m4Ch1n3}