## VEH
*Lại một bài Window khá hay từ sau flare, lần này cũng như TetCTF mình sẽ không thiên quá nhiều về làm thế nào để hoàn thành được bài. Mình sẽ chủ yếu nói về kỹ thuật mà bài đã sử dụng cũng như những đoạn code nào và cơ chế nào có thể lại gây ra như thế.*
Đầu tiên ta thử chạy chương trình để xem chương trình này đang làm gì.


Cơ bản chương trình này sẽ kiểm tra các argv và sẽ có tất cả 4 argv cần phải truyền vào (không tính tên chương trình).
Tiến hành phân tích bằng IDA và đây là hàm main của chương trình.

Ta sẽ thấy đầu tiên đoạn code sau:

Ở đây sau khi chương trình lấy những tham số truyền vào sẽ tiến hành chuyển những tham số vừa nhập về hexa và lưu vào `dword_407470` nên mình sẽ tạm đặt biến global của chương trình này sẽ là `input`. Vậy qua đây ta sẽ xác định input sẽ bao gồm các ký tự số và A,B,C,D,E,F.
Tiếp đến chương trình sẽ cấp phát một vùng nhớ:

> Tham khảo thêm [tại đây.](https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualalloc)
Để hiểu rõ hơn tại sao trong VirtualAlloc lại có được những tham số đó ta sẽ xét qua ví dụ sau:
```cpp
#include <windows.h>
#include <stdio.h>
int main(){
LPVOID lpAddress;
SIZE_T dwSize;
DWORD flAllocationType;
DWORD flProtect;
lpAddress = VirtualAlloc(NULL, 0x1000, MEM_COMMIT|MEM_RESERVE, PAGE_GUARD|PAGE_READONLY);
if(lpAddress == NULL) {
printf("Error");
exit(-1);
}
else{
printf("Nicee");
exit(-1);
}
}
```
Phân tích đoạn code trên với IDA:

Ta sẽ nhìn với thông số ở `flProtect` sẽ bao gồm Guard và Read. Điều này sẽ có nghĩa là vùng nhớ vừa được cấp phát nếu truy cập vào thì việc đầu tiên chương trình sẽ raise ra một exception `STATUS_GUARD_PAGE_VIOLATION` nếu cố gắng truy cập lần tiếp theo và thao tác như ghi vào vùng nhớ này sẽ raise ra một exception.
Bây giờ ta sẽ sang chương trình chính debug xem có đúng thật là như thế không.

Nhưng ở đây ta thấy chỉ có một exception của `READONLY` vậy của Guard ở đâu ??
Mình đã hỏi anh Mochi tại sao lại có chuyện này xảy ra thì anh đã nói với mình như thế này:

Có vẻ như debugger ~~khá thông minh~~ loại bỏ exception.
> Một điều khá thú vị đối với **xử lý những bài exception đó là phải đặt hardware bp ở exception thì debugger mới tiến hành nhảy tới**, còn không đặt thì nó sẽ tự động bỏ qua và nhảy đến phần code tiếp theo.
Có thể dễ dàng thấy được đây chính là phần code xử lý exception:

Ta sẽ xem thử những địa chỉ này được gọi ở những đâu:

Và ta thấy được một struct SEH như sau:

> Tham khảo thêm giải thích chi tiết [ở đây](https://github.com/Speedi13/ManualMapped_SEH_32bit)
Bây giờ, như đã nói ở trên ta sẽ đặt hardware bp ở mỗi exception xem chương trình đang làm gì.
Có thể thấy được rằng khi vừa đặt bp tại đoạn exception đầu tiên thì chương trình đã raise ra được như sau:

Vậy đã đúng như ban đầu, chương trình gọi lần đầu tiên và sẽ nhảy vào exception của guard.
Sau khi nhảy được vào exception thì chương trình sẽ gọi một hàm như sau:

Ở đây ta sẽ thấy được kết quả của input sau khi xor sẽ mang đi xor tiếp tục với một global array.
Sau khi xor hoàn tất chương trình sẽ thay đổi quyền tại địa chỉ vùng nhớ mà lúc đầu vừa cấp sẽ có quyền đọc và ghi:

Tiếp đến chương trình sẽ tiến hành gọi một shellcode. Shellcode này chính là kết quả từ việc xor với global array. Tại sao biết được chương trình call shellcode mà shellcode được lấy từ kết quả vừa xor?? Ta sẽ nhìn vào ngay sau khi chương trình chuyển xong quyền đọc ghi và memset như sau:

Đây chính là đoạn shellcode đó:

Ta sẽ so sánh với giá trị vừa xor xong. Vì giá trị sau khi xor sẽ lưu lại ở global nên ta sẽ vào đó check:


Thấy được hai đoạn này khá là tương tự nhau. Vậy bây giờ ta sẽ tìm một shellcode đúng với chương trình.
*Vì bài này chỉ tập trung vào kỹ thuật mà bài sử dụng nên đoạn này mình sẽ chỉ nói ý tưởng đó là tìm sao cho sau khi xor input sẽ có kết quả là 0x18. Và cách để tìm ra được 0x18 đó chính là bf và in ra thử xem đoạn shellcode nào là hợp lí nhất*
```python
enc = bytes.fromhex("D44D91FD7CB92818181893581493580C931893580845DB00")
test = []
for i in enc:
test.append(i^0x18)
test = bytearray(test)
md = Cs(CS_ARCH_X86, CS_MODE_32)
for i in md.disasm(test, 0x1000):
print("0x%x:\t%s\t%s" %(i.address, i.mnemonic, i.op_str))
# 0x1000: int3
# 0x1001: push ebp
# 0x1002: mov ebp, esp
# 0x1004: mov eax, dword ptr fs:[0x30]
# 0x100a: mov eax, dword ptr [eax + 0xc]
# 0x100d: mov eax, dword ptr [eax + 0x14]
# 0x1010: mov eax, dword ptr [eax]
# 0x1012: mov eax, dword ptr [eax + 0x10]
# 0x1015: pop ebp
# 0x1016: ret
```
Khi tìm thành công và vào trong đoạn shellcode này thì tiếp tục raise một exception:

Lần này tiếp tục lại đặt bp tại exception thứ 2:

Sau đó, chương trình lại tiếp tục nhảy vào đây và so sánh tiếp đối số tiếp theo.
Vậy qua đó có thể hình dung được cách chương trình chạy đó là thông qua các exception. Tức là sẽ có những đoạn code cố tình làm rối ở `try` để bắt buộc nhảy vào `except`.
Quay trở lại với exception thứ 2, câu hỏi được đặt ra ở đây là **tại sao lại xảy ra exception??** Nếu như lần đầu tiên thì do cơ chế bảo vệ khi cấp phát, nhưng ở đây rõ ràng chương trình đã cấp toàn quyền vậy tại sao còn xảy ra exception?
Ta sẽ nhìn lại đoạn shellcode vừa giải mã ra ins `int 3 ;Trap to Debugger`. Câu trả lời chính nằm ở đây, chương trình đã cố tình đặt một software breakpoint ở shellcode và cách đặt sẽ được thông qua ví dụ sau:
```cpp
void myFunction() {
int x = 10;
int y = 20;
__debugbreak(); // This line creates a breakpoint
int sum = x + y;
std::cout << "Sum: " << sum << std::endl;
}
int main() {
std::cout << "Starting the program..." << std::endl;
myFunction();
std::cout << "Program execution continues after the breakpoint." << std::endl;
return 0;
}
```

> Cách để bypass loại này thì khi IDA hiển thị lên cảnh báo ta sẽ chọn vào No.
Sau khi nhảy vào exception kế tiếp thì tương tự đối số thứ nhất, tiếp tục chương trình sẽ xor và lấy kết quả đó để decode một shellcode thứ 2.
*Tương tự đối số đầu tiên, đối số lần này sau khi xor phải là 0x0A*
Sau khi có được đối số thứ 2 thì một shellcode vẫn tiếp tục được gọi và cách để nhảy vào exception tương tự lần đầu tiên với `int 3`.

Tiếp đến tương tự như lần 1,2 chương trình tiếp tục lấy đối số thứ 3 và mang xor với `0xD1` sau đó lại tiếp tục nhảy đến try:

Lần này mình thật sự cũng không hiểu thao tác xor với `0xd1` để làm gì, vì kết quả sau cùng không thấy được sử dụng ở bất cứ nơi nào. Đây chính là kết quả được lưu lại sau khi xor `0xd1`

Nếu như những lần trước thì kết quả sau khi xor sẽ là một đoạn shellcode, nhưng ở đây lại không phải. Bởi vì đoạn shellcode được gọi sau khi xor với đối số thứ 3 là kết quả của xor với đối số thứ 2.


```python
enc1 = bytes.fromhex("C65F83EF89E6163BCA834FF6834FF2834FFE834FFA834FE6834FE2834FEE626F780A0A626B646E66626365644262696F7A7E626F6E4F7262697E6578626E6E5C6F62587E664B836FE66EAB3A0A0A0A814A06814A1E810A814A1A83C98149360BD2814A720BD281421E8347F68142160BD38347F281422A0BD38347FE81422E0BD38347FA3BCA3BC3817FE68177FEF681368D0BD56CB3150AF9AC7E0C4A314FF67FEC8147FA815FF26C810E4B810E880BD2E10A3BD88147025B600BF5DA89CE1689CE2A57C9000000")[:0xc5]
test = []
for i in enc1:
test.append(i^0xa)
test = bytearray(test)
md = Cs(CS_ARCH_X86, CS_MODE_32)
for i in md.disasm(test, 0x1000):
print("0x%x:\t%s\t%s" %(i.address, i.mnemonic, i.op_str))
```
Sau một hồi tìm kiếm thì ta sẽ bỏ qua đoạn này vì có thể nó không liên quan gì đến chương trình.
Ở đoạn try thứ 3 có những đoạn code như sau:

Tại đây, thấy được nó sẽ push một hàm lên stack sau đó gọi shellcode *(kết quả đoạn code xor thứ 2*). Câu hỏi được đặt ra ở đây đó là việc **push hàm lên stack để làm gì ??** Và **tại sao lại push rồi mới call shellcode ??**
Ta sẽ thử xem hàm `sub_4015D0` là hàm gì ở đây:

Lúc đầu vào hàm thì check với những số khá lạ. Ta thử check xem những số này có ý nghĩa gì.

Kết hợp với lúc đầu ta đã thấy một struct SEH. Ta có thể đoán được đây chính là check exception.
Bây giờ ta sẽ tạm gác giải quyết tiếp bài này sang một bên. Ta sẽ nhìn vào đoạn tài liệu [**Structured Exception Handling Functions**](https://learn.microsoft.com/en-us/windows/win32/debug/structured-exception-handling-functions) và [**Vectored Exception Handling**](https://github.com/MicrosoftDocs/win32/blob/docs/desktop-src/Debug/vectored-exception-handling.md)
Cứ trace theo tài liệu ta sẽ thấy được quy trình thêm một vector để handle exception. Đầu tiên sẽ add `AddVectoredExceptionHandler` sau khi add vào thành công sẽ có một con trỏ trỏ đến `VectoredHandler` ở đây sẽ chứa những thông tin cụ thể của exception bao gồm `context` *(chứa thông tin của một processor tại lúc xử lý exception)* và `ExceptionRecord` *(chứa thông tin cụ thể của exception)*. Để hình dung làm thế nào có thể code ra được như thế ta sẽ nhìn qua ví dụ sau:
```cpp
#include <windows.h>
#include <stdio.h>
LONG PvectoredExceptionHandler(
struct _EXCEPTION_POINTERS *ExceptionInfo
) {
DWORD64 regis = ExceptionInfo->ContextRecord->Rip;
DWORD exceptionRecord = ExceptionInfo->ExceptionRecord->ExceptionCode;
printf("Exception caught in Vectored Exception Handler\n");
printf("%x\n", regis);
printf("%x\n", exceptionRecord);
return EXCEPTION_CONTINUE_SEARCH;
}
int main() {
PVOID h = AddVectoredExceptionHandler(0, PvectoredExceptionHandler);
if (h == NULL) {
printf("Failed to install Vectored Exception Handler\n");
return 1;
}
int *p = NULL;
*p = 0;
printf("This message won't be printed\n");
return 0;
}
```
Và đây là cách IDA trả về:

> Như github đã nói ở trên Vector handle exception sẽ không phải là frame-baseed *(tức mỗi hàm được gọi sẽ có một frame call nhưng vector handle thì không có)* nên có thể gọi bất cứ khi nào ngay sau khi vectỏ được add vào.
Ta sẽ phân tích một xíu. Đầu tiên chương trình sẽ đưa địa chỉ của `handler` vào stack. Sau đó chương trình sẽ gọi `AddVectoredExceptionHandler` để thêm vector vào. Đến lúc này chương trình vẫn chưa xuất hiện exception. Ta có thể kiểm chứng bằng cách debug như sau:

Có thể thấy rất rõ sau khi call để add vector thì chương trình vẫn tiếp tục chạy *(trong code ví dụ thì nó sẽ chạy xuống `int *p = NULL`)*. Sau đó chạy đến `*p = 0` thì lúc này mới raise ra exception:

Vậy là ta đã đi qua cách vector handle exception hoạt động như thế nào. Tiếp đến vào hàm `handler`.

Rõ ràng trên đoạn code ví dụ nó là một struct nhưng IDA đã không dectect được. Vậy công việc của mình bây giờ sẽ đổi struct `a1` lại và đây chính là kết quả:

Bây giờ mọi thứ khá rõ ràng và đây là kết quả của chương trình:

Vô cùng chính xác với những gì ta muốn. Chương trình đã ghi lại info của `RIP` lúc vừa raise exception và mã của exception.
Quay trở lại với bài chính, qua ví dụ trên cũng đã có câu trả lời và có thể rename một số thứ lại như sau:


> Có thể sẽ thắc mắc document lúc đầu có rất nhiều nhưng tại sao lại chọn `AddVectoredExceptionHandler`?? Đó là bởi vì debug lúc call shellcode nhìn dưới hex-view sẽ thấy một chuỗi `handlhionHhcepthedExhctorhddVehRtlA` đã bị xáo trộn nhưng có thể thấy nó là add vector.
Bây giờ đặt bp ở `Handler_VEH` để xem sau khi check exception xong thì chương trình làm gì ở đây. Sở dĩ chọn đặt bp ở đây bởi vì trong handler gọi tới một hàm có liên quan tới input:

Thấy được ở đây chỉ thực hiện vài thao tác rồi sau đó check với dữ liệu đã được set sẵn:

Đơn giản chỉ việc dùng z3 và tìm ra để cho phù hợp.
```python
global_check = [0x252d0d17, 0x253f1d15, 0xbea57768, 0xbaa5756e]
s = z3.Solver()
arg0 = z3.BitVec('arg0', 32)
arg1 = z3.BitVec('arg1', 32)
arg2 = z3.BitVec('arg2', 32)
arg3 = z3.BitVec('arg3', 32)
def xor_as_bytes(d):
return ((d >> 24) & 0xFF) ^ ((d >> 16) & 0xFF) ^ ((d >> 8) & 0xFF) ^ ((d >> 0) & 0xFF)
s.add((arg1 ^ ((arg0 << 16) | ((arg0 >> 8) & 0xFF00) | ((arg0 >> 24) & 0xFF))) == global_check[0])
s.add((arg0 ^ ((arg1 << 16) | ((arg1 >> 8) & 0xFF00) | ((arg1 >> 24) & 0xFF))) == global_check[1])
s.add((arg3 ^ ((arg2 << 16) | ((arg2 >> 8) & 0xFF00) | ((arg2 >> 24) & 0xFF))) == global_check[2])
s.add((arg2 ^ ((arg3 << 16) | ((arg3 >> 8) & 0xFF00) | ((arg3 >> 24) & 0xFF))) == global_check[3])
s.add(xor_as_bytes(arg0) == 0x18)
s.add(xor_as_bytes(arg1) == 0x0a)
```
Vẫn chưa xong chương trình, tiếp đến có một kỹ thuật khá hay có thể tham khảo thêm [tại đây.](https://anti-debug.checkpoint.com/techniques/assembly.html#instruction-counting)
Ta để ý rằng sau khi thực hiện kiểm tra với dữ liệu được set sẵn thì chương trình liên tục raise ra exception `Single step exception`. Vậy **do đâu mà chương trình sinh ra như vậy ??** Liệu sinh ra như thế là đúng hay sai ??
Tiếp tục tạm bỏ bài sang một bên, ta sẽ nhìn qua một xíu về resgister như sau:

Nhìn vào bit thứ 8 sẽ thấy mô tả `Trap Flag`. Bây giờ sẽ tạo một chương trình đơn giản và chỉnh xem nó có điều gì xảy ra ở đây.
```cpp
LONG PvectoredExceptionHandler(
struct _EXCEPTION_POINTERS *ExceptionInfo
) {
DWORD contextflag = ExceptionInfo->ContextRecord->ContextFlags;
if (ExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_SINGLE_STEP){
printf("Step here\n");
return EXCEPTION_CONTINUE_SEARCH;
}
printf("Exception caught in Vectored Exception Handler\n");
printf("%d", contextflag);
return EXCEPTION_CONTINUE_SEARCH;
}
int main() {
PVOID h = AddVectoredExceptionHandler(0, PvectoredExceptionHandler);
if (h == NULL) {
printf("Failed to install Vectored Exception Handler\n");
return 1;
}
// Set the trap flag for the current thread
CONTEXT context;
context.ContextFlags = CONTEXT_CONTROL;
HANDLE hThread = GetCurrentThread();
GetThreadContext(hThread, &context);
context.EFlags |= 0x100;
SetThreadContext(hThread, &context);
printf("Single step test\n");
return 0;
}
```
Đây là IDA trả về:

Tạm thời không để ý nhiều đến nội dung của chương trình, ta sẽ test ở những đoạn code lúc đầu chương trình. Đây là đoạn code lúc đầu khởi tạo:

Để ý kỹ ở `TF` được set là `0`. Bây giờ ta sẽ chuyển nó sang `1` và đây là kết quả:

Có thể thấy một exception được raise ra và có nội dung `Single step exception`. Có nghĩa là khi cờ này được bật lên thì đồng nghĩa với việc chương trình phát hiện đang thực hiện debug.
Nhìn lại đoạn code ví dụ, ở đây đã cố tình lấy thông tin của thread hiện tại và set bit thứ 8 là 1. Thì lúc này qua đoạn code này chương trình sẽ phát hiện ra đang debug và in ra `Step here`. Đây chính là kết quả khi chạy với debug:

Quay lại chương trình ban đầu, khi đã set lại được struct cho handler ta sẽ nhìn thấy một đoạn như sau:

Giống hệt với ví dụ, thì đây cũng chính là nguyên nhân gây ra exception.
Tiếp đến ta sẽ nhìn vào đoạn code sau:

Ở đây chương trình đang cố điều khiển `EIP` đến một nơi nào đó. Và `(_DWORD *)((char *)&unk_404160 + 56 * dword_407454);` có vẻ như là một mảng global của chương trình. Ta sẽ thử convert struct ở đây một lần nữa. Và như này là hợp lí nhất:

> Tham khảo thêm [dr7](https://en.wikipedia.org/wiki/X86_debug_register)
Vậy chương trình đang chạy từ đầu đến cuối một global array và chuyển `EIP` đến một hàm khác. Và đó chính là hàm này:

Đặt bp ở hàm này và sẽ thấy được có thêm những tham số truyền vào, việc của ta bây giờ chỉ việc viết lại z3 và thêm điều kiện là hoàn tất.
```python
acsc = u32(b"ACSC")
year = u32(b"2024")
s.add(arg0 ^ arg1 == acsc)
s.add(arg3 ^ arg2 == year)
s.add((arg1 ^ acsc) & 0xff == 0x99)
s.add((arg3 ^ year) & 0xff == 79)
```
=>Qua bài này đã khá nhiều trick cũng như hiểu hơn SEH và cách xử lí đối với một exception. Không thể software bp đối với những exception.