Challenge Info
If you have solved WinAntiDbg0x100, you’ll discover something new in this one. Debug the executable and find the flag! This challenge executable is a Windows console application, and you can start by running it using Command Prompt on Windows. This executable requires admin privileges. You might want to start Command Prompt or your debugger using the ‘Run as administrator’ option. Challenge can be downloaded here. Unzip the archive with the password picoctf
This challenge is #2 of a 3 part series
prev: winantidbg0x100
next: winantidbg0x300
Understanding the user-code
Like WinAntiDbg0x100, I started by analyzing the program in Ghidra. And just like last time, I ran a search on the word “flag” through program text.
We should find this user code:
1 | undefined4 __cdecl FUN_004016e0(int param_1,int param_2) |
Right away, we notice more checks. I will concisely explain the user-code before moving to the assembly.
#1. Admin privileges check
1 | iVar2 = FUN_004012f0(); |
- Ensures the program runs with admin privileges
- Exits with error if check fails
- For good practice, this will be the first check we bypass
#2. Mutex check
1 | hObject = CreateMutexW((LPSECURITY_ATTRIBUTES)0x0, 0, L"WinAntiDbg0x200"); |
- Creates a mutex to ensure only one instance runs
- If mutex already exists (error 0xB7), checks for command-line arguments
- If present, tries to debug the process with PID from arguments
- Don’t worry about bypassing this, it’s just to make sure only one instance is running
#3. Initial debugger check
1 | uVar5 = FUN_00401600(); |
- Ironically requires a debugger to be present initially
- If no debugger is detected, displays message to run with debugger
- Don’t worry about bypassing this either, since we’ll have a debugger active anyways
#4. Config file check
1 | iVar2 = FUN_00401450(); |
- Verifies if “config.bin” file can be read
- Aborts if file cannot be accessed
- Again, don’t worry about bypassing this check
#5. Custom check
1 | cVar1 = FUN_004011d0(); |
- Must return ‘\0’ to continue
- This will be the second check we bypass
#6. Standard debugger check
1 | BVar4 = IsDebuggerPresent(); |
- Standard Windows API to detect debuggers
- Must return 0 (no debugger detected) to reach the flag
- Third check we will bypass
#7. Flag decryption
1 | FUN_00401090(1); |
- Only runs if all previous checks pass
- Decrypts and displays the flag
- We want to reach this
HEAVY WORK IN PROGRESS BEYOND THIS POINT
Understanding the assembly
Like in WinAntiDbg0x100, we will find the corresponding TEST
calls for each crucial check. KEEP IN MIND: As discussed in my WinAntiDbg0x100 writeup, the memory addresses in Ghidra and x32dbg might not line up exactly, but the last 4 digits will, so we’ll just use those (easier anyways).
Unlike WinAntiDbg0x100, this challenge uses both the EAX and EDX register, as well as both the JE and JNE jump instructions. Before explaining the assembly, let’s quickly refresh on the difference between JE & JNE:
- JE (Jump if Equal): Jumps to a specified address if the comparison result is equal (zero flag is set). Also called JZ (Jump if Zero).
- JNE (Jump if Not Equal): Jumps to a specified address if the comparison result is not equal (zero flag is clear). Also called JNZ (Jump if Not Zero).
First Check (Admin Privileges Check) - 16eb
1 | 004016eb 85 c0 TEST EAX,EAX |
This check tests if the program has sufficient privileges:
TEST EAX,EAX
performs a bitwise AND on EAX with itself (common way to check if a value is zero)JNZ LAB_00401714
jumps if the result is not zero (meaning privileges are present), and we continueIf EAX = 0
(no privileges), it continues to the error message and exits, and we lose
Second Check (if (cVar1 == '\0')
condition) - 1824
1 | 00401824 85 d2 TEST EDX,EDX |
This corresponds to the cVar1 = FUN_004011d0();
check
TEST EDX,EDX
checks if EDX is zeroJNZ LAB_00401832
jumps if debugger is detected (EDX != 0)- If EDX = 0 (no debugger detected by custom function), it continues to the next check
Third Check (IsDebuggerPresent Check) - 182e
1 | 0040182e 85 c0 TEST EAX,EAX |
This corresponds to the BVar4 = IsDebuggerPresent();
check
TEST EAX,EAX
checks if EAX is zeroJZ LAB_00401847
jumps if EAX = 0 (no debugger detected by Windows API)- This is the opposite logic from the previous checks - it jumps if condition is met (debugger not present)
Bypassing all 3 checks
Knowing this, all you have to do is manually set breakpoints at the corresponding instructions (16eb, 1824, 182e) and edit the EAX/EDX values accordingly.
- 16eb - JNE call, set EAX to 1 to take the jump (bypassing admin check)
- 1824 - JNE call, set EDX to 0 to avoid taking jump, as taking jump will skip over the 3rd check, subsequently denying the flag!)
- 1830 - JE call, set EDX to 0 to take the jump, giving us our flag
flag: picoCTF{0x200_debug_f0r_Win_e6b68f6e}