Summary
This a UPX packed Golang Binary emulating a polling station. This challenge few steps, which are outlined below
This Binary’s Packed Pretty Tight…
Immediate analysis of the binary should indicate that it’s packed, entropy is off the charts, and the size is fairly small for a binary not written in c. Running strings will give a few notable results:
jzVOTE
...
PROT_EXEC|PROT_WRITE failed.
_j<X
$Info: This file is packed with the UPX executable packer http://upx.sf.net $
$Id: UPX 4.24 Copyright (C) 1996-2024 the UPX Team. All Rights Reserved. $
_RPWQM)
j"AZR^j
PZS^
/proc/self/exe
...
6)cL
VOTE
VOTE
The first notable string indicates that the binary was packed with UPX 4.24. This is great news, since UPX allows us to decompress binaries with upx -d. Let’s give that a try:
$ upx -d -o decompressed_polling_station polling_station
Ultimate Packer for eXecutables
Copyright (C) 1996 - 2024
UPX 4.2.4 Markus Oberhumer, Laszlo Molnar & John Reiser May 9th 2024
File size Ratio Format Name
-------------------- ------ ----------- -----------
upx: polling_station: NotPackedException: not packed by UPX
Unpacked 0 files.
Well, that’s not reassuring.
Clearly we either A: Are packed with UPX, or B: the UPX string is a red herring. It’s far more likely the first, so let’s see if we can figure out why it’s not decompressing.
Bypassing The Anti-Unpacking
Looking into UPX Anti-Unpacking methods, you’ll come across this article that covers a method used by Mirai, simply overwriting the UPX!
magic bytes in the file with a different string. Looking at a hexdump of our binary does appear to match the format in the article…
$ xxd polling_station | tail -n 4
000731e0: e280 566d b0b9 879c 0000 0000 0056 4f54 ..Vm.........VOT
000731f0: 4500 0000 0000 0000 564f 5445 0e16 0e0a E.......VOTE....
00073200: 9c74 c016 c104 ab61 401c 1600 f831 0700 .t.....a@....1..
00073210: 401c 1600 4919 0087 f400 0000 @...I.......
Simply changing all the instances of VOTE within the binary with the original magic bytes UPX! allows the file to be properly decompressed!
$ xxd polling_station_fixed | tail -n 4
000731e0: e280 566d b0b9 879c 0000 0000 0055 5058 ..Vm.........UPX
000731f0: 2100 0000 0000 0000 5550 5821 0e16 0e0a !.......UPX!....
00073200: 9c74 c016 c104 ab61 401c 1600 f831 0700 .t.....a@....1..
00073210: 401c 1600 4919 0087 f400 0000 @...I.......
$ upx -d -o decompressed_polling_station polling_station_fixed
Ultimate Packer for eXecutables
Copyright (C) 1996 - 2024
UPX 4.2.4 Markus Oberhumer, Laszlo Molnar & John Reiser May 9th 2024
File size Ratio Format Name
-------------------- ------ ----------- -----------
1452976 <- 471580 32.46% linux/amd64 decompressed_polling_station
Unpacked 1 file.
And Into The Depths We Golang
Reviewing simple strings for the binary reveals a few things:
- We’re dealing with a golang binary
- It appears to have some of the information stripped, we don’t have all the expected segments
To start reversing this, we could open the binary in Ghidra, but as much as we love Ghidra, it really doesn’t handle Golang very well. Instead, let’s use Ida Free and give it a look.
Well Ida Like To Reverse This
Opening the functions list in Ida reveals some key elements of the program:
This tells us the main beats of the program. There appears to be a voting menu, some authentication function, a secure authentication function, a function that closes voting(!), an encryption function, and a couple of helper functions for clearing the screen and making the program beep. Reviewing the call graph for the main function gives us an even better idea of what is going on.
We can see with this that the password that allows us to cast a vote is gotv2024
, and the password for voiding the last vote in the case of human error, is gotvsupervisor123
, great password security as always. There’s still one more function that ends voting though, and that password isn’t quite as obvious:
The Password In The (En)Crypts
The final menu item to close voting and tally the votes is locked behind the “secureAuthentication” function, so this is likely the key to solving the challenge. Reviewing that call graph shows us that our work is not quite so simple this time:
secureAuthenticate takes a byte array of some sort as a parameter to the function, then takes a password in stdin, encrypts the password, and compares the results with the byte array passed to the function.
For the sake of staying readable, we’ll skip over some specifics, but eventually you should find that the bytes passed to the function are as follows: 0x95, 0xed, 0xbb, 0x79, 0x1b, 0x09, 0xfb, 0x12, 0x6c, 0x49, 0xbe, 0x5e, 0xdb, 0xbb, 0x1a, 0x3e, 0xb6, 0xb3, 0x58, 0x8d, 0x89, 0x3c, 0x83, 0x3e, 0xbf, 0xfc, 0xc9, 0x8a, 0x86, 0x98, 0x3a, 0xc4, 0x70, 0xfb, 0x51, 0xda, 0x66, 0x01, 0xcc
Reviewing the encrypt function reveals the following block:
push rbp
mov rbp, rsp
sub rsp, 0B8h
mov [rsp+0C0h+arg_8], rbx
mov [rsp+0C0h+arg_0], rax
mov rdx, 7620797265762061h
mov [rsp+0C0h+var_78], rdx
mov rdx, 7972657620797265h
mov [rsp+0C0h+var_70], rdx
mov rdx, 6573207972657620h
mov [rsp+0C0h+var_68], rdx
mov rdx, 79656B2074657263h
mov [rsp+0C0h+var_60], rdx
mov ecx, 20h ; ' '
lea rax, [rsp+0C0h+var_78]
mov rbx, rcx
call crypto_aes_NewCipher
test rcx, rcx
jz short loc_4A20AD
This is hard to read, but it effectively looks to be creating an AES cipher, loading the key as four integers. Decoding the four integers and correcting for order and endian-ness gives us the key a very very very very secret key
!
Going further, we find the call to encrypt, which passes the password along with a static IV to the AES CFB cipher.
loc_4A20AD:
mov [rsp+0C0h+var_40], rbx
mov [rsp+0C0h+var_50], rax
mov rcx, [rsp+0C0h+arg_8]
lea rbx, [rcx+10h]
lea rax, RTYPE_uint8
mov rcx, rbx
call runtime_makeslice
mov [rsp+0C0h+var_30], rax
movups [rsp+0C0h+var_88], xmm15
mov rdx, 3837363534333231h
mov qword ptr [rsp+0C0h+var_88], rdx
mov rdx, 3635343332313039h
mov qword ptr [rsp+0C0h+var_88+8], rdx
mov rbx, [rsp+0C0h+var_40]
lea rcx, [rsp+0C0h+var_88]
mov edi, 10h
mov rsi, rdi
mov rax, [rsp+0C0h+var_50]
nop dword ptr [rax+rax+00h]
call crypto_cipher_NewCFBEncrypter
mov [rsp+0C0h+var_58], rax
mov [rsp+0C0h+var_48], rbx
mov rcx, [rsp+0C0h+arg_8]
xor eax, eax
mov rbx, [rsp+0C0h+arg_0]
call runtime_stringtoslicebyte
mov rdx, [rsp+0C0h+var_58]
mov rdx, [rdx+18h]
mov rdi, [rsp+0C0h+arg_8]
mov rsi, rdi
neg rsi
sar rsi, 3Fh
and esi, 10h
mov r8, [rsp+0C0h+var_30]
add rsi, r8
mov [rsp+0C0h+var_38], rsi
mov r8, rbx
mov r9, rcx
mov rbx, rsi
mov rcx, rdi
mov rsi, rax
mov rax, [rsp+0C0h+var_48]
call rdx
mov rax, [rsp+0C0h+var_38]
mov rbx, [rsp+0C0h+arg_8]
mov rcx, rbx
xor edi, edi
add rsp, 0B8h
pop rbp
retn
Doing the same dance with the integers gives us the static IV of 1234567890123456
(ASCII, not hex)
With this information, everything needed to solve the challenge is in hand! We simply decrypt the intial encrypted password with the key and IV, and get the flag!
Flag
MetaCTF{sh0uld_h4v3_u53d_p4p3r_b4ll0ts}