POSTS
MRMCDCTF2019: KonradVC
Solution to KonradVC (medium - hard) from MRMCDCTF 2019
KonradVC is the second Windows challenge I wrote for the 2019 MRMCDCTF. Even though it’s a Windows challenge, it runs fine under Wine.
KonradVC was intended to be harder than Slicer, the other Windows challenge. It employs a very simple crypter to make analysis of the actual challenge code more difficult.
When loading it in Ghidra, the first thing we notice is that there seem to be only two functions: entry
and FUN_00406039
.
And with both functions it’s quite clear that the decompiler has problems making sense out of them.
The memory map (Window->Memory Map) shows two other interesting things:
First, there is a section named .ntext
in the binary that is not present in normal, compiler generated binaries.
And both functions recognised by Ghidra are in this strange section.
Second, the the .text
section has read/write/execute permissions.
This is most unusual, since memory with such permissions is basically every exploit writers best friend.
No compiler from this decade should produce a binary with these permissions.
Here is the executable again, this time in PE-bear, a tool specially for analysing PE files. The red line in the lower section marks the file’s entry point, the position in the executable were execution starts. In this view it’s quite obvious that there was an additional section appended to the file, that now gets executed first.
If you wish you can also further explore the .text
and .data
sections, but you will not find anything useful there:
these sections only contain garbage.
That all should give you enough indication that this file is protected by some sort of crypter or protector.
The simplest design of these encrypts various parts of the main executable. Than it adds some special code to the file and makes sure that this code gets executed at first (before the now encrypted original code). This piece of code (often called a decryptor stub) then decrypts the original executable in memory, before jumping to the original entry point (OEP) of the original executable. The original code (now in plain text) then executes normally.
There are several ways to defeat such crypters:
One way is to analyse and understand the decryption stub. By doing so, you learn how it decrypts the file, which parts of the file are decrypted and where the keys are stored or generated. With this knowledge, you can create your own decrypter, to decrypt the interesting portions of the file yourself.
Another approach is to simply let decryptor stub to its work, and then acquire the plain executable from memory. I will concentrate on this way here.
For this approach we need a way to get control of the process (via a debugger) after the stub has completed its work. A debugger is a tool to control, examine and manipulate a running program. As a Windows debugger I will here use x64dbg. A complete tutorial (or even an introduction) to its usage would go way beyond the scope of this text, but you can find its documentation here.
The “let the decrypter run and take control” approach can be executed in many ways:
You can run the program in the debugger and wait for the password prompt, then interrupt it (‘F12’). The process will stop somewhere in
kernel32.dll
. In the memory view you can then select the executable and examine its.text
or.data
sections.You can run the program (without a debugger), and attach the debugger while it waits for the password. This will stop the process in a different part of
kernel32.dll
, but you can navigate with the memory view just as seen above.You can locate the
jmp
(orcall
, or evenret
) in the decryption stub that transfers control to the original entry point. This can require some digging through the stub’s code, but here thejmp ebx
seems like a hot candidate. Especially in the graph view (‘G’) thejmp ebx
(lower left branch) sticks out: You can set a breakpoint there and examine the memory once it hits.
There are many more methods possible, but one way or the other you will end up with the (now decrypted) .text
section.
And there is some interesting code right at its start.
00401000 | 55 | push ebp |
00401001 | 8BEC | mov ebp,esp |
00401003 | 81EC 04040000 | sub esp,404 |
00401009 | A1 00304000 | mov eax,dword ptr ds:[403000] |
0040100E | 33C5 | xor eax,ebp |
00401010 | 8945 FC | mov dword ptr ss:[ebp-4],eax |
00401013 | 56 | push esi |
00401014 | 68 00040000 | push 400 |
00401019 | 8D85 FCFBFFFF | lea eax,dword ptr ss:[ebp-404] |
0040101F | 6A 00 | push 0 |
00401021 | 50 | push eax |
00401022 | E8 DD080000 | call <JMP.&memset> |
00401027 | 8B35 98204000 | mov esi,dword ptr ds:[<&printf>] |
0040102D | 68 E0304000 | push konrad.4030E0 | 4030E0:"The password is the flag. What is the password?\n"
00401032 | FFD6 | call esi |
00401034 | 8D85 FCFBFFFF | lea eax,dword ptr ss:[ebp-404] |
0040103A | 68 00040000 | push 400 |
0040103F | 50 | push eax |
00401040 | FF15 90204000 | call dword ptr ds:[<&gets_s>] |
00401046 | 83C4 18 | add esp,18 |
00401049 | 81BD FCFBFFFF 4D524D43 | cmp dword ptr ss:[ebp-404],434D524D |
00401053 | 75 4F | jne konrad.4010A4 |
00401055 | 81BD 00FCFFFF 44435446 | cmp dword ptr ss:[ebp-400],46544344 |
0040105F | 75 43 | jne konrad.4010A4 |
00401061 | 81BD 04FCFFFF 7B66696E | cmp dword ptr ss:[ebp-3FC],6E69667B |
0040106B | 75 37 | jne konrad.4010A4 |
0040106D | 81BD 08FCFFFF 616C6C79 | cmp dword ptr ss:[ebp-3F8],796C6C61 |
00401077 | 75 2B | jne konrad.4010A4 |
00401079 | 81BD 0CFCFFFF 5F686572 | cmp dword ptr ss:[ebp-3F4],7265685F |
00401083 | 75 1F | jne konrad.4010A4 |
00401085 | 81BD 10FCFFFF 655F695F | cmp dword ptr ss:[ebp-3F0],5F695F65 |
0040108F | 75 13 | jne konrad.4010A4 |
00401091 | 81BD 14FCFFFF 616D7D00 | cmp dword ptr ss:[ebp-3EC],7D6D61 |
0040109B | 75 07 | jne konrad.4010A4 |
0040109D | 68 50304000 | push konrad.403050 | 403050:"\n##############################################\n### Congratulations! This is the flag! ###\n##############################################\n\n"
004010A2 | EB 05 | jmp konrad.4010A9 |
004010A4 | 68 38304000 | push konrad.403038 | 403038:"Wrong. Try again.\n"
004010A9 | FFD6 | call esi |
004010AB | 83C4 04 | add esp,4 |
004010AE | 68 18304000 | push konrad.403018 | 403018:"<press any key to continue>\n"
004010B3 | FFD6 | call esi |
004010B5 | 83C4 04 | add esp,4 |
004010B8 | FF15 8C204000 | call dword ptr ds:[<&getchar>] |
004010BE | 8B4D FC | mov ecx,dword ptr ss:[ebp-4] |
004010C1 | 33CD | xor ecx,ebp |
004010C3 | 33C0 | xor eax,eax |
004010C5 | 5E | pop esi |
004010C6 | E8 04000000 | call konrad.4010CF |
004010CB | 8BE5 | mov esp,ebp |
004010CD | 5D | pop ebp |
004010CE | C3 | ret |
The “What is the password?” is printed at 00401032
, then the password is read into the local variable [ebp-404]
at 00401040
via gets_s
.
After that there are a lot of compares, starting at 00401049
.
Each one of them compares a different part of the input (saved at [ebp-404]
) with a with static value.
And if one of these turns out not equal
(ne), the code jumps to 004010A4
- where the “Wrong. Try again.” gets printed.
So 0040109D
, where the “Congratulations” string gets pushed (and later printed), will only be reached if all the values match -
and this will only happen if the input, represented as 32-bit integers, is 434D524D 46544344 6E69667B 796C6C61 7265685F 5F695F65 007D6D61
.
When translating these numbers into a string take care of the Endianness: The lowest value byte of a integer stands at the lowest address in memory (“comes first”), so the byte order must be reversed to transform the numbers above into a string.
When taking this into account, the password/flag (as a hexstring) must be
4d524d43444354467b66696e616c6c795f686572655f695f616d7d00
All that’s left for you to do is print this as ASCII.