Flash CTF – Go Vote!

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:

  1. We’re dealing with a golang binary
  2. 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:

image showing Ida function list output. Output contains functions main_main, main_encrypt, main_secureAuthenticate, main_authenticate, main_initiateVotingMenu, main_voidLastVote, main_closeVotingAndTallyVotes, main_beep, and main_clear

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.

graph showing the passwords gotv2024 and gotvsupervisor123

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:

graph of secureAuthenticate

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!

Here’s a Cyberchef solution

Flag

MetaCTF{sh0uld_h4v3_u53d_p4p3r_b4ll0ts}