Task 2: Anti-debug
- Debugging (dynamic analysis) là một kĩ thuật quan trọng trong dịch ngược, giúp phân tích hành vi chương trình bên cạnh static analysis. Để bảo vệ chương trình hoặc che giấu hành vi của mã độc, người viết code có thể sử dụng các phương pháp chống debug (anti-debug).
- Task 2 mình được giao nghiên cứu và tìm hiểu một số loại anti-debug được ghi chép lại trong Antidebug checkpoint. Hiện đang có 8 kiểu anti-debug tất cả, trong đó, hầu như dạng nào mình cũng đã từng gặp qua, thường gặp debug flags, timing, exceptions. Hôm nay mình sẽ báo cáo lại các dạng anti-debug và code lại chúng, cũng như đề xuất hướng bypass (nếu có thể).
- Theo mình hiểu, kĩ thuật này sẽ kiểm tra các cờ debug từ WinAPI (ví dụ IsDebuggerPresent, Remote…), hoặc từ process memory (tuy nhiên phần này hơi chuyên sâu nên mình chỉ tập trung vào WinAPI).
1.1. IsDebuggerPresent and Manual check
- Hàm API này thực chất sẽ kiểm tra cờ
BeingDebugged
trong Process Environment Block
(PEB). Về cơ bản, code sẽ như sau:
- Bên cạnh đó, chương trình cũng có thể thực hiện kiểm tra thủ công như sau:
- Trong cả hai cách, nếu thực hiện run bằng debugger (mình sử dụng Visual Studio để debug cho nhanh) thì đều trả ra:
Image Not Showing
Possible Reasons
- The image was uploaded to a note which you don't have access to
- The note which the image was originally uploaded to has been deleted
Learn More →
Image Not Showing
Possible Reasons
- The image was uploaded to a note which you don't have access to
- The note which the image was originally uploaded to has been deleted
Learn More →
- Và tất nhiên, nếu mình run .exe không dùng debugger thì không có gì xảy ra:
Image Not Showing
Possible Reasons
- The image was uploaded to a note which you don't have access to
- The note which the image was originally uploaded to has been deleted
Learn More →
- Bên cạnh
IsDebuggerPresent
còn có CheckRemoteDebuggerPresent
tuy nhiên mình chưa biết cách để mô phỏng nên sẽ tạm thời bỏ qua. Về cơ bản là sẽ kiểm tra xem remote debugger có debug chương trình hay không. Mình cũng thường sử dụng file linux_server64
để tạo một Remote Linux Debugger
debug file ELF trên Windows nhưng cũng chưa gặp con anti-debug trên bao giờ.
- Một hàm nữa mình cũng thường thấy trong các bài CTF là
NtQueryInformationProcess
. Theo mình đọc được từ tài liệu, hàm này sẽ lấy ra các thông tin về tiến trình được handled:
- Tại đây, class thông tin sẽ được khai báo tại tham số thứ 2, và trong kĩ thuật anti-debug, các cổng được kiểm tra thường sẽ là
ProcessDebugPort(0x7)
,
ProcessDebugObjectHandle(0x1E)
,
ProcessDebugFlags(0x1F)
(mình tham khảo từ đây)
- Sau khi khai báo class, thông tin lấy được sẽ được return trong
ProcessInformation
, và tùy theo từng class, giá trị trả về sẽ giúp ta xác định chương trình có bị debug hay không. Code mình viết dưới đây (có tham khảo) đã gộp cả ba trường hợp này vào thành 1, vì đều gọi chung hàm NtQueryInformationProcess
, chỉ khác class nên mình rút ngắn gọn nó lại:
- Nếu run với debugger:

- Nếu không:

1.3. NtGlobalFlag
- Đây là một trường có trong PEB, tùy theo phiên bản 64 bit hay 32 bit thì trường này sẽ nằm ở offset (0x68 hoặc 0xBC), giá trị của trường mặc định là 0. Việc attach một debugger không làm thay đổi trường này, nhưng nếu chạy một tiến trình, các cờ sau sẽ được bật:
FLG_HEAP_ENABLE_TAIL_CHECK (0x10)
FLG_HEAP_ENABLE_FREE_CHECK (0x20)
FLG_HEAP_VALIDATE_PARAMETERS (0x40)
- Thực hiện
OR
các giá trị cờ này, chương trình có thể phát hiện ra debugger, code như sau (nếu không muốn phải include các header dài dòng khác):
- Và nếu muốn include header:
- Tùy từng trường hợp mà debugger có thể bị detected, ví dụ trong phần
NtGlobalFlag
này, mình run debugger bằng VS tím mà không bị detected
Image Not Showing
Possible Reasons
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
Learn More →
. Tuy nhiên với IDA thì:
Image Not Showing
Possible Reasons
- The image was uploaded to a note which you don't have access to
- The note which the image was originally uploaded to has been deleted
Learn More →
Image Not Showing
Possible Reasons
- The image was uploaded to a note which you don't have access to
- The note which the image was originally uploaded to has been deleted
Learn More →
1.4. Heap Protection
- Một vài hệ quả của việc set flag trong NtGlobalFlag có thể sử dụng để phát hiện debugger, ví dụ:
- Nếu cờ
TAIL_CHECKING
được bật, DWORD 0xABABABAB
sẽ được thêm vào cuối của khối heap được cấp phát (2 lần nếu là win 32-bit và 4 lần nếu 64-bit)
- Nếu cờ
FREE_CHECKING
được bật, DWORD 0xFEEEFEEE
cũng sẽ được thêm để fill vào dải bytes còn trống cho tới ô nhớ sau đó.
- Code để detect như sau:
- Khi debug bằng IDA:

và kết quả:

1.5. Bypass
- Để bypass các anti-debug này, chúng ta chỉ đơn giản là sửa lại logic chương trình. Ví dụ, code asm
IsDebuggerPresent
như sau:
- Thì đơn giản, mình chỉ cần sửa opcode
jne
ở dòng 3 thành je
là có thể bypass được nó. Tương tự với các con anti-debug ở trên, mình đều làm theo hướng này để bypass và tiếp tục reverse.
- Phần này mình chưa được gặp quá nhiều trong thực tế, tuy nhiên có tài liệu nên mình sẽ tham khảo và cố gắng mô phỏng nó.
2.1. OpenProcess()
- Code của phần này mình đã có test nhưng với máy mình thì con debugger lại không bị phát hiện. Về cơ bản, chương trình sẽ call tới
csrss.exe
, và điều đặc biệt là chỉ thành viên trong admin group với quyền debug mới có thể open process này. Nếu gặp lỗi thì chương trình có khả năng đang bị debug. Hàm check như sau:
2.2. CreateFile()
- Kĩ thuật này lợi dụng cơ chế đọc file của debugger. Khi event
CREATE_PROCESS_DEBUG_EVENT
xảy ra, handle của file bị debug sẽ được lưu trong CREATE_PROCESS_DEBUG_INFO
, các thông tin này giúp debugger có thể đọc thông tin debug từ file. Nếu debugger không đóng handle, file sẽ không được mở với quyền truy cập đặc biệt. Bằng cách sử dụng kernel32!CreateFileW/A()
để truy cập đặc biệt một file nào đó và kiểm tra call, chúng ta có thể phát hiện debugger. Kĩ thuật khá đặc biệt, nhưng áp dụng trên máy mình thì không thành công, có thể do kĩ thuật đã lạc hậu chăng?. Hàm check như sau:
2.3. CloseHandle()
- Trong tất cả các kĩ thuật của phần này, đây là kĩ thuật duy nhất mình có thể mô phỏng thành công. Dựa trên việc raise exception, chương trình có thể nhận biết được debugger. Khi bị debug, nếu một handle không hợp lệ được truyền vào
ntdll!NtClose()
hay kernel32!CloseHandle()
thì EXCEPTION_INVALID_HANDLE (0xC0000008)
sẽ được raise, ngoại lệ này có thể nhận biết bởi exception handler. Khi đó, việc control được chuyển sang handler cũng đồng nghĩa rằng đang có debugger. Hàm check như sau:
- Khi run bằng debugger, một ngoại lệ sẽ được catch:

- Nếu run tiếp thì:

- Trong trường hợp run bình thường:

2.4. Bypass
- Do mình mới chỉ mô phỏng được anti-debug bằng
CloseHandle()
nên cách bypass của mình cũng chỉ áp dụng với nó. Giống như các exception khác, mình đều set breakpoint trước khi exception được raised, sau đó set IP vào luồng chuẩn, hoặc patch NOP phần đó luôn.
- Các kĩ thuật dưới đây đều raise lên exception để nhận biết debugger bằng cách kiểm tra hành vi của chương trình có phù hợp như là một tiến trình run bình thường hay không. Phần này mình gặp lần đầu tiên trong giải TTV2025.
3.1. UnhandledExceptionFilter()
- Kĩ thuật này khá đơn giản, lí thuyết rằng nếu có ngoại lệ xảy ra và chương trình chưa đăng kí bất kì exception handler nào để xử lí, thì hàm
kernel32!UnhandledExceptionFilter()
sẽ được gọi tới. Chúng ta cũng có thể đăng kí một handler xử lí ngoại lệ bằng hàm kernel32!SetUnhandledExceptionFilter()
. Tuy nhiên, nếu chương trình đang chạy bị debug, exception sẽ được chuyển control sang cho debugger chứ không phải hai hàm trên, đây chính là anti-debug.

- Code check như sau:
- Nếu run bằng debug VS tím:

- Output nếu continue:

- Nếu run bình thường:

3.2. RaiseException()
- Một số ngoại lệ như
DBC_CONTROL_C
hay DBG_RIPEVENT
không được truyền vào handlers để xử lí mà phải thông qua debugger. Từ đây, chúng ta có thể đăng kí một handler (giả sử handler1) kiểm tra xem control có được chuyển hướng sang handler1 hay không. Nếu không, vậy thì khả năng chương trình đang được run với debugger.
- Code như sau:
- Control được chuyển qua debugger:

- Nếu run bình thường:

3.3. Control Flow Hiding
- Đây không phải kĩ thuật giúp nhận biết debugger mà là kĩ thuật giúp ẩn giấu hành vi chương trình dưới những exception handlers. Được biết, chúng ta có thể đăng kí các ngoại lệ bằng SEH hoặc VEH. Sau khi ngoại lệ xảy ra, chương trình nếu không bị debug, sẽ điều hướng luồng tới hàm xử lí ngoại lệ. Ngược lại, nếu debug thì control sẽ được chuyển cho debugger. Điều này giúp quá trình debug trở nên khó khăn và phần nào đó giúp che giấu hành vi của chương trình. Mình gặp kĩ thuật này lần đầu tiên khi làm chall
Mixture
của noobmannn
. Rất cảm ơn anh đã cho em trải nghiệm kiến thức mới mẻ này
Image Not Showing
Possible Reasons
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
Learn More →
.
3.4. Bypass
- Với các thể loại exceptions này, mình đều làm theo hướng NOP hết mọi code cản trở. Ngoài ra, với kĩ thuật che giấu hành vi ở trên, mình thường trace và set IP để thực hiện phân tích.
- Một trong những kĩ thuật anti-debug mình gặp nhiều nhất trong khoảng thời gian đầu tự học RE, đặc biệt là khi làm bài của
kcbowhunter
. Được biết, thời gian chương trình xử lí các câu lệnh là cực kì nhanh, khi đó, ta có thể lợi dụng sự chênh lệch thời gian giữa hai câu lệnh để kiểm tra có debugger hay không. Kĩ thuật này cơ bản, đơn giản và khá tốt với những newbie single-step reverser, nhưng cũng rất dễ để bypass.
4.1. RDTSC
- Nếu có debugger:

- Nếu không:

4.2. GetLocalTime()
- Phần này mình lại không detect được debugger, code như sau:
4.3. So on and bypass
- Các kĩ thuật timing sau cũng đơn giản cấu trúc như trên nên mình sẽ không đề cập nữa vì nó khá đơn giản. Thay vào đó, mình sẽ đề xuất cách bypass. Với timing, mình thường sẽ NOP các hàm lấy thời gian như trên, hoặc một cách hay hơn đó là hạn chế single-step qua các bước kiểm tra chênh lệch mốc thời gian và chỉ đặt breakpoint sau các hàm này.
- Kĩ thuật anti-debug trong mục này dựa trên việc một process có thể tự kiểm tra memory của chính nó để nhận biết debugger, có thể thông qua thread contexts, breakpoints hoặc function patching.
5.1 Software Breakpoints (INT 3)
- Kĩ thuật này sẽ kiểm tra sự xuất hiện của byte 0xCC tương đương với instruction
INT 3
trong chương trình. Trên thực tế, khi debug chương trình, tại nơi breakpoints, debugger sẽ thêm opcode 0xCC vào để dừng chương trình tại đó. Cụ thể có thể xem tại đây, mình sẽ tóm gọn lại: set breakpoint tương ứng với việc thay thế opcode tại đó bằng 0xCC, debugger sẽ nhớ lại byte bị thay thế đó, cho khi debugger reach 0xCC, nó sẽ điền lại byte ban đầu vào vị trí cũ, set IP - 1 và tiếp tục debug.
- Đó là cách mà software bp được sử dụng trong phân tích, và lợi dụng điều này, anti-debug sẽ examine toàn bộ mem của nó để nhận biết có anti-debug hay không. Tuy nhiên cách này hơi có phần thiếu căn cứ và cần được sử dụng đúng cách. Code như sau:
- Một phương pháp nữa mình cũng gặp khá nhiều đó là sử dụng
Toolhelp32
để đọc mem từ các process. Kĩ thuật này có thể được sử dụng để anti-step-over, cũng dựa trên opcode 0xCC:
5.3. Bypass
- Một cách hữu hiệu để bypass tất cả các anti-debug trong phần này là NOP. Mình sẽ tìm tất cả các code check mem và NOP chúng, hoặc chỉnh sửa logic/giá trị return để phân tích.
- Các kĩ thuật trong phần này sẽ nhận biết debugger thông qua hành vi của debugger khi CPU thực thi các instruction nhất định.
6.1. INT 3/2D
- Theo lí thuyết,
int 3
hay 0xCC
là opcode giúp debugger dừng lại tại breakpoints. Tuy nhiên, coder có thể sử dụng opcode này để nhận anti-debug, bởi khi CPU gặp int 3
có sẵn trong code, EXCEPTION_BREAKPOINT (0x80000003)
sẽ được raise. Control sẽ được chuyển cho debugger nếu chương trình đang bị debug, đây chính là kĩ thuật anti, tương tự với int 2D
. Đây là code:
6.2. DebugBreak
- Bên cạnh cách trên, chúng ta cũng có thể dùng
DebugBreak
để anti-debug. Đây là kĩ thuật mình đã gặp trong bài steal
của giải KCSC REcruitment 2025. Code khá đơn giản như sau:
6.3. Stack Segment Register
- Đây là một cách khá hay giúp set Trap Flag để kiểm tra xem chương trình có đang bị traced hay không. Vì
Trap Flag
được clear bởi debuggers, ta có thể phát hiện debugger bằng kĩ thuật này, có thể đọc thêm ở đây. Code như sau:
6.4. POPF
- Giống với
ss
ở trên, đây cũng là kĩ thuật giúp nhận biết chương trình có đang bị traced hay không dựa vào Trap Flag
. Code như sau:
6.5. Bypass
- Cách tốt nhất để bypass các kĩ thuật trên đều là NOP bởi chúng đều là assembly instructions không dài, mình có thể patch các instruction đó mà không làm thay đổi logic chương trình. Bên cạnh đó, mình có thể set bp ngay trước các lệnh, và set IP pass qua lệnh đó.
- Các kĩ thuật trong này mình chưa gặp bao giờ, chỉ biết qua tài liệu. Tuy nhiên, mình sẽ note down hai kĩ thuật mình thấy gần gũi nhất.
- Hàm này giúp chúng ta ẩn thread khỏi debugger. Sau khi thread được ẩn, debugger sẽ không thể nhận biết events liên quan đến thread này, sau đó, thread có thể thực hiện các cách anti-debug nói trên để counter.
- Nếu có bp trong hidden thread hoặc main thread bị ẩn, process sẽ bị crash và không thể debug. Code thực hiện kĩ thuật như sau:
7.2. OutputDebugString()
- Đây là kĩ thuật cũ rất nổi tiếng mà chỉ thực hiện được với các phiên bản Vista trở xuống. Idea của kĩ thuật khá đơn giản, nếu chương trình không bị debug, khi gọi tới
kernel32!OutputDebugString
thì lỗi sẽ xảy ra. Vậy không có lỗi đồng nghĩa với việc có debugger. Code như sau:
- Do là kĩ thuật cũ nên mình không thể mô phỏng được
7.3. Bypass
- Tiếp tục là NOP các hàm khả nghi hoặc bypass bằng cách đặt breakpoint trước khi hàm được gọi và set IP nhảy qua nó.
- No comment, các kĩ thuật trong này mình cũng chỉ gặp 1, 2 lần vì nó quá đa dạng
8.1. Parent Process Check
- Thông thường, nếu người dùng mở ứng dụng lên bằng cách kích đúp chuột, parent process của ứng dụng sẽ là
explorer.exe
, khi đó, chương trình chỉ cần lấy PID của parent process và so sánh với explorer.exe
là có thể phát hiện được debugger.
- Đầu tiên, chương trình sẽ lấy shell process handle với
user32!GetShellWindow()
và lấy ID của process bằng cách gọi tới user32!GetWindowThreadProcessId()
.
- PID có thể được lấy từ struct
PROCESS_BASIC_INFORMATION
khi gọi tới ntdll!NtQueryInformationProcess()
. Code như sau:
- Một kĩ thuật mình cũng gặp khá nhiều trong các bài CTF. ID và tên của tiến trình cha có thể được lấy bằng cách gọi hàm
kernel32!CreateToolhelp32Snapshot()
và kernel32!Process32Next()
. Code như sau:
8.2. FindWindow()
- Kĩ thuật này sẽ duyệt qua danh sách các window classes trong system và so sánh với các classes debuggers đã khai báo. Các hàm có thể được sử dụng là
user32!FindWindowW/A/ExW/ExA()
. Code như sau:
8.3. DbgPrint()
- Kĩ thuật cũng gần giống như
OutputDebugString()
ở trên. Do ntdll!DbgPrint()
sẽ gây ra ngoại lệ DBG_PRINTEXCEPTION_C (0x40010006)
, ta có thể sử dụng ngoại lệ này để kiểm tra xem exception handler hay debugger xử lí nó. Code như sau:
8.4. Bypass
- Cách hợp lí và hiệu quả nhất vẫn là NOP các hàm check. Đối với kĩ thuật lấy PID ở trên, mình thường làm thay đổi giá trị trả về của
isDebugging
và từ đó phân tích chương trình bình thường.
Subtask 2: Anti1 & Anti3
- Hai bài khá lạ đối với mình, một bài là của anh Tuna, một bài là của VCS training, mình sẽ đi sâu vào phân tích các kĩ thuật là chủ yếu vì mục đích học hỏi, còn
flag
thì tạm thời không chú trọng.
Anti3
- Một bài lạ,
flag
có len 100 và sử dụng đến 6 kĩ thuật anti-debug. Ngay đầu vào, mình đã bắt gặp hàm xử lí ngoại lệ SetUnhandledExceptionFilter
:

- Sau khi run thử thì:

- Dính lỗi ngoại lệ chia cho 0 ngay tại đây:

- Vậy sau khi chương trình dính exception, do mình sử dụng debugger nên phần xử lí ngoại lệ sẽ được giao cho debugger và mình sẽ bị mất luồng thực thi chính.
- Với
SetUnhandledExceptionFilter
, hàm truyền vào sẽ là hàm được thực thi sau khi xảy ra ngoại lệ. Lợi dụng điều này, mình patch code để chương trình thực thi thẳng vào luồng chính bằng cách sửa call SetUnhandledExceptionFilter
thành:

- Và nop hết toàn bộ những gì không liên quan (do trong hàm thực thi khi ngoại lệ có calling convention rồi nên mình cũng nop luôn ở ngoài)

- Patch & rename:

và một đống NOPs ở dưới. Như vậy, sau khi bật debug thì mình có thể nhảy vào luồng chính. Tuy nhiên, khi vào được main_exception
thì mình lại gặp phải chút code rác:


- Nguyên do là tác giả đã chèn vào vài byte rác khiến việc phân tích khó hơn. Kĩ thuật để làm đẹp lại code cũng đơn giản chỉ là NOP nên mình sẽ không nói chi tiết.
- Sau khi sửa, mình có được code như sau:


- Các hàm trong
main_exception
mình sẽ phân tích khi đi vào từng parts.
Part 1
- Trong phần này có kĩ thuật cần chú ý là examine
BeingDebugged Flag
trong PEB, nếu có debugger, các debugByte
sẽ bị thay đổi (đây là các byte sử dụng cho mã hóa các part tiếp theo). Bên cạnh đó, hàm isBreakpointThere
(mình đã đổi tên) sử dụng để kiểm tra opcode 0xCC
trong khoảng memory nhất định (lát nữa sẽ phân tích sau) và thay đổi debugDword
.
- Mã hóa ở đây khá đơn giản, mình chỉ cần nhặt
flag_enc
nằm trong hàm lastCheck
là có thể giải, trong hai bài anti
này thì mình tiện học hỏi cách sử dụng code ida python luôn:

Part 2, 3, 4

- Phần này có sử dụng đến
debugDword
mà mình nói ở phần trước. debugDword
bị thay đổi do hàm isBreakpointThere()
:

- Hàm này sẽ kiểm tra opcode
0x99 ^ 0x55 = 0xCC
trong vùng .text của phase3
, nếu có thì khả năng cao là có debugger can thiệp vào. Đây chính là kĩ thuật detect debugger trong phần Process Memory - Software Breakpoints
ở trên. Để bypass thì chúng ta chỉ cần không đặt bp trong hàm phase3
là được. Bên cạnh đó, debugDword
nếu chạy bình thường thì sẽ luôn có giá trị 48879
hay 0xBEEF
.
- Tại
phase3
này thì part2
và part3
của flag được enc như sau:

- Đây là code để giải mã (sau khi test thì mình nhận ra giá trị đúng của
debugByte2 = 0xAB
và debugByte3 = 0xCD
):

- Tiếp theo, chương trình cũng thực hiện mã hóa
part4
bằng cách:
- Để giải, mình chỉ cần lấy được giá trị của
debugDword = 0xBEEF
và xor
ngược lại là xong:

Part 5, 6, 7


- Kĩ thuật phần này khá rõ ràng, bao gồm:
- int 2D
- int 3 - debugbreak()
- nên mình sẽ không đi sâu vào nữa, để bypass thì chỉ cần chạy đến instruction int 2D hoặc int 3 và setIP là được. Mình không khuyến khích NOP đi hai lệnh đó lắm vì mình làm thế thì các offset bị thay đổi dẫn đến sai data. Các
part5
, part6
, part7
cũng khá dễ dàng để giải mã:

- Sau khi ghép tất cả các phần lại, mình được
flag
.
kcsc{unh4Ndl33xC3pTi0n_pebDebU9_nt9lob4Lfl49_s0F7w4r38r34Kp01n7_int2d_int3_YXV0aG9ydHVuYTk5ZnJvbWtjc2M===}
Dài vcl

Anti1
- Chall này cũng dị, mình phải run bằng quyền admin, các kĩ thuật được sử dụng khá mới với mình. Đây là
main - WinMain
:
- Trong đây có thể thấy chương trình sử dụng khá nhiều API để build app, và lòi ra được một hàm khá sú
sub_401260
ở dòng 30. Bước vào hàm thì thấy được:

- Đây là hàm khởi tạo cho GUI của chương trình, mình sẽ phân tích tập trung vào
sub_401350
:
- Có thể thấy, đây là code chính trong core chương trình của mình. Mình sẽ chỉ tập trung vào hàm
checking
mà thôi:
cheking()
- Hàm rất dài, và được chia thành rất nhiều case, và sau khi phân tích thì mình hiểu luồng như sau: chương trình nhận
input
rồi truyền vào hàm, mỗi input[index]
sẽ được lựa chọn một trong 7 case tương ứng với 7 loại anti-debug, sau đó detect debugger và xor
input[index]
với giá trị nào đó.
Resolving API
- Tuy nhiên, dấu hiệu của anti-debug không được rõ ràng, vì chương trình đã sử dụng kĩ thuật
API Hashing
(xem thêm tại đây) để resolve các hàm.
- Trong đó, hai hàm được sử dụng để resolve là:
sub_401DF0
và sub_401F10
(tùy theo từng phiên phân tích mà tên có thể khác nhau nhưng 4 hex cuối sẽ luôn không đổi). Mình sẽ debug và xref setIP để comment vào các case antidebug. Mình cũng có viết code idc để phục vụ phân tích nhanh hơn:
- Một số hình ảnh hàm được resolve





- Có thể thấy, tác giả sử dụng hàm
BlockInput
khá nhiều, hàm này chặn chúng ta sử dụng chuột hay bàn phím để tương tác với ứng dụng, nếu tham số truyền vào là 0
thì user được unblock và ngược lại. Mình sẽ cố gắng phân tích đủ các phase check antidebug (các phase được phân tích theo code trong hàm checking
).
Phase 0: TlsCallback - Debug Flag

- Một kĩ thuật quen thuộc, với class truyền vào là
0x7
thì chương trình đang examine cờ ProcessDebugPort
, kết quả trả về là -1
đồng nghĩa với việc chương trình bị debug. Mã giả ở đây hơi trôn chút nên mình phải đọc bằng mã asm, luồng đúng sẽ không thay đổi unk_E05018
. Mình sẽ cần patch để debug cho đúng luồng.
Phase 1: NtGlobalFlag - Heap Flag

- Đây là kĩ thuật examine 3 cờ trong
NtGlobalFlag
, cụ thể như sau:

- Giá trị tổng 3 cờ đó đúng bằng
0x70
chứng tỏ có debugger đang attached. Tuy nhiên bài khá ảo khi sử dụng opcode jnz
thay vì jz
để đổi luồng, cụ thể như sau:
- Sau đó, giá trị check debugger trả về sẽ được đưa vào hàm
sub_E02050
để tính toán giá trị xor
.
Phase 2: ProcessHeap - Heap Flag1

- Kĩ thuật tiếp tục là examine Debug Flag, và trong phần này là Heap Flag, cụ thể như sau

- Vì phiên bản của máy mình là 0x64 nên:

- Khi debug, các cờ sẽ được set hết và có tổng là
0x40000062
.
Phase 3: ProcessHeap - Heap Flag2

- Again, examine Heap Flag. Nếu debug, giá trị tại
eax
sẽ là:

- Tuy nhiên, tại sao lại có sự chênh lệch của giá trị tổng cờ khi đều sử dụng kĩ thuật này?

- Lí do là vì trong
Phase 2
, cờ HEAP_GROWABLE (0x2)
cũng được check (+12) nên tổng sẽ là 0x40000062, còn trong Phase 3
này, chương trình chỉ kiểm tra từ (+16) nên bỏ qua HEAP_GROWABLE
.
Phase 4: Heap Protection

- Đây là kĩ thuật được sử dụng:

- Nôm na là chương trình sẽ kiểm tra chuỗi
0xAB
có được appended vào cuối của heap block hay không:



- Do file thực thi là 32 bit nên chuỗi
0xAB
được append 8 lần. Đây chính là cách phát hiện debugger bằng cách check số lần 0xAB
xuất hiện, cách bypass là thay đổi giá trị trả về mà thôi.

- Kĩ thuật được sử dụng ở đây là examine các parent process của chương trình đang chạy. Mình thường thấy các author so sánh tên process với một chuỗi nào đó kiểu
ida.exe
hoặc cmd.exe
, nhưng mình lại không tìm được chuỗi nào như thế. Ban đầu mình nghĩ author sẽ encrypt các chuỗi của process rồi so sánh ở trong phần này:

nhưng lại chưa chứng minh được chương trình sẽ phát hiện anti-debug ở đâu.

- Đây cũng là một kĩ thuật được documented lại trong phần này
- Điều cần lưu ý là:

- Cụ thể, chương trình đã thực hiện
BlockInput
hai lần. Lần đầu tiên nếu thành công thì trong lần thứ hai, giá trị trả về sẽ là 0
. Trong trường hợp giá trị trả về tiếp tục là True
thì khả năng cao là chương trình đang bị hooked (khái niệm mình chưa tìm hiểu kĩ nên chưa giải thích ở đây)

- Ảnh này là mình đang bị blocked nên phải sử dụng trackpad. Luồng đúng sẽ là luồng sao cho hai lần
BlockInput
trả về hai giá trị khác nhau.

- Tiếp tục là examine
Debug Flag
, nhưng lần này thay vì sử dụng ProcessDebugPort(0x7)
thì chương trình dùng ProcessDebugFlags(0x1f)
. Nếu giá trị trả về là 0
thì chương trình đang bị debugged.
Script to solve:
- Bài thì có tận 8 phase để anti-debug nên mình cũng rén để bypass cả 8 cái, mình sẽ đi vào từng phase và nhặt giá trị ra để tìm lại
flag
. Trong đó, cần lưu ý mảng để check sẽ là:
validationTable = [
0x0E, 0xEB, 0xF3, 0xF6, 0xD1, 0x6B, 0xA7, 0x8F, 0x3D,
0x91, 0x85, 0x2B, 0x86, 0xA7, 0x6B, 0xDB, 0x7B, 0x6E,
0x89, 0x89, 0x18, 0x95, 0x67, 0xCA, 0x5F, 0xE2, 0x54,
0x0E, 0xD3, 0x3E, 0x20, 0x5A, 0x7E, 0xD4, 0xB8, 0x10,
0xC2, 0xB7]
idxTable = [
0x9, 0x12, 0xf, 0x3, 0x4, 0x17, 0x6,
0x7, 0x8, 0x16, 0xa, 0xb, 0x21, 0xd,
0xe, 0x1b, 0x10, 0x25, 0x11, 0x13, 0x14,
0x15, 0x5, 0x22, 0x18, 0x19, 0x1a, 0x2,
0xc, 0x1d, 0x1e, 0x1f, 0x20, 0x1c, 0x0,
0x23, 0x24, 0x1]
- Để tìm lại các giá trị
xor
đúng, mình sẽ cần nhập chuỗi đầu vào có 38 kí tự, mình chọn "?" * 38
, và cứ thế đi theo luồng, tới đâu thì bypass, ví dụ trong trường hợp đầu tiên rơi vào case 6:

- Mình tìm lại được giá trị
xor
đầu tiên là 0x5B và tìm được kí tự thứ 9 của flag
. Lặp lại 38 lần thì tìm được flag
. Note thêm là mình viết script ida python để giải từng kí tự trong khi debug, mình đặt bp tại hàm tính toán kí tự xor
: sub_xx2050
và tại dòng cmp
với validationTable
để thay đổi cờ ZF
thành 1, sau đó ghi vào trong mảng xorTable
để in ra flag:

- Script:
xorTable = [0x5B, 0xDB, 0x9D, 0xC6, 0xA7, 0x5A, 0x8A, 0xF6, 0x0D, 0xA5, 0xDA, 0x74, 0xE9, 0xCF, 0x58, 0x96, 0x5B, 0x5A, 0xD0, 0xFC, 0x25, 0xF6, 0x54, 0xB8, 0x6E, 0xCC, 0x7A, 0x3F, 0xA4, 0x1E, 0x73, 0x3F, 0x10, 0xE7, 0xF1, 0x21, 0xB6, 0xE8]
validationTable = [
0x0E, 0xEB, 0xF3, 0xF6, 0xD1, 0x6B, 0xA7, 0x8F, 0x3D,
0x91, 0x85, 0x2B, 0x86, 0xA7, 0x6B, 0xDB, 0x7B, 0x6E,
0x89, 0x89, 0x18, 0x95, 0x67, 0xCA, 0x5F, 0xE2, 0x54,
0x0E, 0xD3, 0x3E, 0x20, 0x5A, 0x7E, 0xD4, 0xB8, 0x10,
0xC2, 0xB7]
idxTable = [
0x9, 0x12, 0xf, 0x3, 0x4, 0x17, 0x6,
0x7, 0x8, 0x16, 0xa, 0xb, 0x21, 0xd,
0xe, 0x1b, 0x10, 0x25, 0x11, 0x13, 0x14,
0x15, 0x5, 0x22, 0x18, 0x19, 0x1a, 0x2,
0xc, 0x1d, 0x1e, 0x1f, 0x20, 0x1c, 0x0,
0x23, 0x24, 0x1]
caseTable = [
0x6, 0x1, 0x7, 0x1, 0x3, 0x2,
0x4, 0x3, 0x6, 0x3, 0x7, 0x6,
0x1, 0x4, 0x7, 0x4, 0x1, 0x5,
0x7, 0x6, 0x7, 0x5, 0x6, 0x4,
0x5, 0x1, 0x7, 0x5, 0x2, 0x3,
0x1, 0x2, 0x3, 0x2, 0x1, 0x6,
0x2, 0x4]
assert len(validationTable) == len(idxTable) and len(validationTable) == len(caseTable)
flag = ["?"] * 38
for idx in range(len(xorTable)):
index = idxTable[idx]
flag[index] = chr(validationTable[idx] ^ xorTable[idx])
print("".join(flag))
print(len(xorTable) == len(validationTable))
I_10v3-y0U__wh3n Y0u=c411..M3 Senor1t4
Image Not Showing
Possible Reasons
- The image was uploaded to a note which you don't have access to
- The note which the image was originally uploaded to has been deleted
Learn More →
Bài viết có tham khảo wu của anh Thắng
Image Not Showing
Possible Reasons
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
Learn More →