UIUCTF 2021 - speakeasy
Categories: re
2021-08-03
by not_really
The local speakeasy has a password to get in. Can you guess it?
Author: 2much4u
Files: speakeasy.exe
>speakeasy
The year is 1923.
You recently moved to Chicago and your friend told you about the local speakeasy.
Great! But, this friend left out one key detail...
The password to get in!
Good evening. Welcome to the juice joint.
My apologies, but I can't let you in without the password.
I know you want to get zozzled just like everyone else, but I can't just let you walk in here.
You fumble your words and muster out...
uiuctf{lol pls tell me the flag}
Go chase yourself, bull!
Come back with a warrant
The local speakeasy has a password to get in. Can you guess it?
Author: 2much4u
Files: speakeasy.exe
>speakeasy
The year is 1923.
You recently moved to Chicago and your friend told you about the local speakeasy.
Great! But, this friend left out one key detail...
The password to get in!
Good evening. Welcome to the juice joint.
My apologies, but I can't let you in without the password.
I know you want to get zozzled just like everyone else, but I can't just let you walk in here.
You fumble your words and muster out...
uiuctf{lol pls tell me the flag}
Go chase yourself, bull!
Come back with a warrant
The exe given is a ginormous 1 MB file that appears to be VmProtect because of the large .vmp0 section. While searching around, I couldn’t find anything to disassemble or decompile the VmProtect instructions, so I tried figuring out how it worked on my own. I spent a good thirty minutes before I gave up and resorted to debugging, though.
Quick Look at the Code
Here’s the main function:
void main() {
char inp[112];
thunk_FUN_14012b2bb();
printf("\x1b[1m");
slowPrint("The year is 1923.\n");
slowPrint("You recently moved to Chicago and your friend told you about the local speakeasy.\n");
slowPrint("Great! But, this friend left out one key detail");
slowPrint(".");
slowPrint(".");
slowPrint(".\n");
slowPrint("The password to get in!\n\n");
printf("\x1b[22m");
printf("\x1b[33m");
slowPrint("Good evening. Welcome to the juice joint.\n");
slowPrint("My apologies, but I can't let you in without the password.\n");
slowPrint("I know you want to get zozzled just like everyone else, but I"
"can't just let you walk in here.\n\n");
printf("\x1b[1m");
slowPrint("You fumble your words and muster out");
printf("\x1b[37m");
slowPrint(".");
slowPrint(".");
slowPrint(".\n");
printf("\x1b[0m");
FUN_140005820(inp); //read input?
FUN_140001630(inp); //check input?
thunk_FUN_14012b29a();
printf("\x1b[0m");
return;
}
The output prints really slowly, so I’ll do the same thing as last year and change the print function to sleep for 0 milliseconds.
void slowPrint(char* param_1) {
size_t sVar1;
FILE* _File;
int local_18 = 0;
while (true) {
sVar1 = strlen(param_1);
if (sVar1 <= (ulonglong)(longlong)local_18)
break;
thunk_FUN_140003e0c(param_1[local_18]);
_File = (FILE*)__acrt_iob_func(1);
fflush(_File);
Sleep(40);
local_18++;
}
Sleep(200);
return;
}
As usual, I just search for the bytes in a hex editor and replace it by hand since I can’t trust Ghidra’s instruction patcher.
FUN_140005820
and FUN_140001630
seem complicated, so let’s check what they do in a debugger first.
Debugging
If we type something into the input, we can see that FUN_140005820
is checking the input. Not bad so far.
After running FUN_140001630
, we see that the flag is checked as incorrect. It looks like we can ignore whatever magic is happening in thunk_FUN_14012b29a
.
Here’s the checkInp
function:
void checkInp(char* inp) {
byte local_38 [40];
for (byte local_48 = 0; local_48 < 40; local_48++) {
ulonglong in_RDX = (ulonglong)(byte)inp[local_48];
byte bVar1 = thunk_FUN_140028270((ulonglong)local_48, in_RDX);
local_38[local_48] = bVar1;
}
byte local_47 = 0;
while (true) {
ulonglong local_40 = (ulonglong)local_47;
size_t sVar2 = strlen(inp);
if (sVar2 <= local_40)
break;
ulonglong in_RDX = (ulonglong)local_38[local_47];
byte bVar1 = thunk_FUN_1400262c9((ulonglong)local_47, in_RDX);
local_38[local_47] = bVar1;
local_47++;
}
thunk_FUN_14012b360(local_38, in_RDX);
return;
}
The thunk
functions all seem to be calls to VmProtect functions, so we probably don’t want to get into those. One thing that’s really nice about this is that it seems to read each character of the input individually for 40 bytes (?) and do two operations on it: thunk_FUN_140028270
, and thunk_FUN_1400262c9
.
After those two first functions, the input becomes encrypted to this:
What are we trying to match with? We can set a breakpoint on the encrypted input to find out.
Looks like it’s just comparing with some other bytes and the 8th byte is different, so it looks like uiuctf{
is right. Maybe these bytes can be found in the file?
Sure enough, it’s reading from the DOS header, so we can guess that the twice encrypted input is compared against these bytes.
Omega Dumb Solution to this Challenge
I was solving this challenge at 2 AM and my brain was not at high capacity, so I took a rather strange approach to this problem: brute forcing input and comparing output against the 3D 44
bit from the DOS header. Easy enough – GDB Python has us covered; IDA freeware does not have Python built in.
There were a few issues I ran into.
First is that I didn’t know how to use GDB without symbols, and for whatever reason, trying to set a breakpoint at an address and running GDB relocates the code but doesn’t relocate the breakpoint; I had to turn off ASLR in Windows (disable ASLR for easier malware debugging – thanks, Seraphin) so that I could set breakpoints on actual addresses.
Second is that for whatever reason, run <<< somestring
(run with STDIN) does not work on Windows. I reused a GDB Python script from a Linux challenge that worked well, but for whatever reason, it didn’t work on Windows. That was sort of a big deal because there was no good way to script input into the program. If I was on Linux, I could use pwntools and give the process STDIN that way. There was most likely some other kind of Windows program that can do what pwntools does, but I decided to try and generate a bunch of GDB lines to copy and paste into the terminal.
genstr = list("uiuctf{aaaaaaaaaaaaaaaaaaaaaaaaaa}")
alpa = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()_+-"
for i in range(7, 34):
for j in alpa:
gsc = list(genstr)
gsc[i] = j
gscs = "".join(gsc)
print("echo " + str(ord(j)) + " >> gdbout.txt")
print("gdb -q -x gdbsolvese.py >> gdbout.txt")
print(gscs)
The issue with this was that the GDB script can’t tell if the input was correct or not, so I just had to later go into the gdbout.txt log and piece it together manually. Not only that, but for whatever reason, CMD and Windows terminal both mess up when you try to paste big clipboards.
I grabbed a random AHK script from the internet to paste clipboard lines with a delay. Once all of that was done, I could finally run the script. It simply checks for the return value from thunk_FUN_1400262c9
and compares it with the corr array values. If it was right, it would say, “is correct!” and I can CTRL+F for it in the output.
import gdb
#dos header bytes
corr = [61, 68, 113, 137, 213, 193, 54, 166, 131, 131, 223, 198, 150, 169, 32, 87, 116, 228, 222, 180, 215, 166, 70, 51, 66, 138, 219, 118, 30, 11, 174, 250, 118, 105, 109, 111, 100, 101, 46, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
def readReg(reg):
gdbValue = gdb.selected_frame().read_register(reg)
return int(gdbValue.cast(gdb.lookup_type("long")))
def setBreakEvent(callback, addr):
class GdbPyBreakpoint(gdb.Breakpoint):
def __init__(self, callback, addr):
super(GdbPyBreakpoint, self).__init__("*" + hex(addr), type=gdb.BP_BREAKPOINT, internal=False)
self.callback = callback
def stop(self):
pos = readReg("rip")
res = self.callback(pos)
return False
GdbPyBreakpoint(callback, addr)
i = 0
def byteSetEvent(addr):
global i
c = readReg("rax")&0xff
if corr[i] == c and i > 6:
print(f"{i} is correct!")
i += 1
def endEvent(addr):
gdb.execute("quit")
#from ida
setBreakEvent(byteSetEvent, 0x7FF6B57B16D1) #right after thunk_FUN_1400262c9
setBreakEvent(endEvent, 0x7FF6B57B1837) #end of main
gdb.execute("set pagination off")
gdb.execute("file speakeasy-faster.exe")
gdb.execute(f"run")
Output:
uiuctf{Daaaaaaaaaaaaaaaaaaaaaaaaa}
7 is correct!
32 is correct!
uiuctf{a0aaaaaaaaaaaaaaaaaaaaaaaa}
8 is correct!
32 is correct!
uiuctf{aanaaaaaaaaaaaaaaaaaaaaaaa}
9 is correct!
32 is correct!
uiuctf{aaataaaaaaaaaaaaaaaaaaaaaa}
10 is correct!
32 is correct!
uiuctf{aaaa_aaaaaaaaaaaaaaaaaaaaa}
11 is correct!
32 is correct!
uiuctf{aaaaabaaaaaaaaaaaaaaaaaaaa}
12 is correct!
32 is correct!
uiuctf{aaaaaa3aaaaaaaaaaaaaaaaaaa}
13 is correct!
32 is correct!
uiuctf{aaaaaaa_aaaaaaaaaaaaaaaaaa}
14 is correct!
32 is correct!
uiuctf{aaaaaaaa@aaaaaaaaaaaaaaaaa}
15 is correct!
32 is correct!
uiuctf{aaaaaaaaa_aaaaaaaaaaaaaaaa}
16 is correct!
32 is correct!
uiuctf{aaaaaaaaaaWaaaaaaaaaaaaaaa}
17 is correct!
32 is correct!
uiuctf{aaaaaaaaaaa3aaaaaaaaaaaaaa}
18 is correct!
32 is correct!
uiuctf{aaaaaaaaaaaaTaaaaaaaaaaaaa}
19 is correct!
32 is correct!
uiuctf{aaaaaaaaaaaaa_aaaaaaaaaaaa}
20 is correct!
32 is correct!
uiuctf{aaaaaaaaaaaaaabaaaaaaaaaaa}
21 is correct!
32 is correct!
uiuctf{aaaaaaaaaaaaaaalaaaaaaaaaa}
22 is correct!
32 is correct!
uiuctf{aaaaaaaaaaaaaaaa4aaaaaaaaa}
23 is correct!
32 is correct!
uiuctf{aaaaaaaaaaaaaaaaanaaaaaaaa}
24 is correct!
32 is correct!
uiuctf{aaaaaaaaaaaaaaaaaaKaaaaaaa}
25 is correct!
32 is correct!
uiuctf{aaaaaaaaaaaaaaaaaaa3aaaaaa}
26 is correct!
32 is correct!
uiuctf{aaaaaaaaaaaaaaaaaaaataaaaa}
27 is correct!
32 is correct!
uiuctf{aaaaaaaaaaaaaaaaaaaaa_aaaa}
28 is correct!
32 is correct!
uiuctf{aaaaaaaaaaaaaaaaaaaaaa6aaa}
29 is correct!
32 is correct!
uiuctf{aaaaaaaaaaaaaaaaaaaaaaanaa}
30 is correct!
32 is correct!
uiuctf{aaaaaaaaaaaaaaaaaaaaaaaa7a}
31 is correct!
32 is correct!
uiuctf{aaaaaaaaaaaaaaaaaaaaaaaaaa}
32 is correct!
It was a bad idea to use a
, but we got the flag anyway: uiuctf{D0nt_b3_@_W3T_bl4nK3t_6n7a}
!
Slightly Better Solution
Instead of brute forcing every single character and using weird hacks like running AHK to paste the clipboard into a CMD window, we can run the function on multiple inputs to see if it’s a simple XOR. GDB wasn’t correctly calling the function, but IDA was able to do it just fine.
thunk_FUN_140028270(0, alphabetChar) //tested w/ abcdef
36,39,38,33,32,35
thunk_FUN_140028270(1, alphabetChar)
82,81,80,87,86,85
thunk_FUN_140028270(2, alphabetChar)
20,23,22,17,16,19
thunk_FUN_140028270(3, alphabetChar)
198,197,196,195,194,193
Wow, it really does just look like a simple XOR. If we XOR ‘a’ with 36, 82, 20, and 198, then we get 45 33 75 A7
; and surprise, we can find these bytes and more in the executable.
And for the other function:
thunk_FUN_1400262c9(0, alphabetChar) //abcdef
108,111,110,105,104,107
thunk_FUN_1400262c9(1, alphabetChar) //abcdef
127,124,125,122,123,120
If we XOR ‘a’ with 108 and 127, then we get 0D 1E ...
, which sadly doesn’t exist in the file. There must be some shenanigans going on here, so let’s set breakpoints on all of the strings around the one we found in thunk_FUN_140028270
.
Bingo, looks like we found the other array. Also, it looks like the values are shifted once to the right (0x1b » 1 = 0x0d).
xor = [
0x45, 0x33, 0x75, 0xA7, 0xA2, 0xD9, 0x64, 0xE5, 0xE2, 0xC6, 0x88, 0xF9,
0xD2, 0xFA, 0x0F, 0x15, 0x7C, 0xBA, 0xD0, 0xC4, 0xF4, 0xB4, 0x2D, 0x42,
0x79, 0xF5, 0xFB, 0x03, 0x56, 0x54, 0xC0, 0xC8, 0x0F, 0x04
]
xor2 = [
0x1B, 0x3D, 0xE2, 0x9B, 0x07, 0xFD, 0x52, 0x0F, 0xA3, 0x57, 0x46, 0xC1,
0x4C, 0xC1, 0xE0, 0x05, 0xAF, 0x12, 0x7A, 0x48, 0xF8, 0xE0, 0x0E, 0x8B,
0xAA, 0x68, 0x27, 0x03, 0x2F, 0xD2, 0x01, 0x0B, 0x30, 0x21
]
match = [
0x3D, 0x44, 0x71, 0x89, 0xD5, 0xC1, 0x36, 0xA6, 0x83, 0x83, 0xDF, 0xC6,
0x96, 0xA9, 0x20, 0x57, 0x74, 0xE4, 0xDE, 0xB4, 0xD7, 0xA6, 0x46, 0x33,
0x42, 0x8A, 0xDB, 0x76, 0x1E, 0x0B, 0xAE, 0xFA, 0x76, 0x69
]
res = ""
for i in range(len(xor2)):
xor2[i] >>= 1
for i in range(len(match)):
res += chr(xor[i] ^ xor2[i] ^ match[i])
print(res)
If the vm code was any more complicated, then this probably wouldn’t have worked.
Intended Solution
Despite my Googling efforts, I did not find any tools to make the VM code readable. However, it was pointed out at the end that the intended solution is to use something: NoVmp. After running it on the executable, it dumps some vtil files which can be opened in VTIL-Utils.
//thunk_FUN_140028270
| | Entry point VIP: 0x2180e
| | Stack pointer: 0x8
| | Already visited?: N
| | ------------------------
| | 0000: [ PSEUDO ] +0x8 lddq t120 $sp 0x0
| | 0001: [ PSEUDO ] +0x8 movq t126 &&base
| | 0002: [ PSEUDO ] +0x8 addq t126 0x14378
| | 0003: [ PSEUDO ] +0x8 movb t124 rcx:8
| | 0004: [ PSEUDO ] +0x8 addq t126 t124
| | 0005: [ PSEUDO ] +0x8 lddb t127:8 t126 0x0
| | 0006: [ PSEUDO ] +0x8 movb rax t127:8
| | 0007: [ PSEUDO ] +0x8 movb t128 rdx:8
| | 0008: [ PSEUDO ] +0x8 xorq rax t128
| | 0009: [ PSEUDO ] +0x8 vexitq t120
//thunk_FUN_1400262c9
| | Entry point VIP: 0x26c2b
| | Stack pointer: 0x8
| | Already visited?: N
| | ------------------------
| | 0000: [ PSEUDO ] +0x8 lddq t144 $sp 0x0
| | 0001: [ PSEUDO ] +0x8 movq t150 &&base
| | 0002: [ PSEUDO ] +0x8 addq t150 0x143c8
| | 0003: [ PSEUDO ] +0x8 movb t148 rcx:8
| | 0004: [ PSEUDO ] +0x8 addq t150 t148
| | 0005: [ PSEUDO ] +0x8 lddb t151:8 t150 0x0
| | 0006: [ PSEUDO ] +0x8 movb rax t151:8
| | 0007: [ PSEUDO ] +0x8 shrq rax 0x1
| | 0008: [ PSEUDO ] +0x8 movb t153 rdx:8
| | 0009: [ PSEUDO ] +0x8 xorq rax t153
| | 0010: [ PSEUDO ] +0x8 vexitq t144
From here, we can see the very obvious XOR with values from 0x14378 and 0x143c8.
The year is 1923. You recently moved to Chicago and your friend told you about the local speakeasy. Great! But, this friend left out one key detail... The password to get in! Good evening. Welcome to the juice joint. My apologies, but I can't let you in without the password. I know you want to get zozzled just like everyone else, but I can't just let you walk in here. You fumble your words and muster out... uiuctf{D0nt_b3_@_W3T_bl4nK3t_6n7a} Welcome! Have a good time and don't go half-seas over