So I’ve passed the first episode (and got a token with my name in reward!). I watched the chapter’s video (very well produced, by the way), and I expect some reverse engineering efforts. Anyway, let’s dig in.
Clicking on the challenge downloads a file with a really long name cec5317acaa111092eef6da3df8e260dccd69ce8b17aa445a26a7a6771f972301ac3ff20108cf86aa868da1463e486347114e0456ba5b5ca2a3a399f69391e76
.
As should be the norm for unknown files, running the file
utility on them almost always helps.
tal@Tal:~$ file cec5317acaa111092eef6da3df8e260dccd69ce8b17aa445a26a7a6771f972301ac3ff20108cf86aa868da1463e486347114e0456ba5b5ca2a3a399f69391e76
cec5317acaa111092eef6da3df8e260dccd69ce8b17aa445a26a7a6771f972301ac3ff20108cf86aa868da1463e486347114e0456ba5b5ca2a3a399f69391e76: Zip archive data, at least v2.0 to extract, compression method=store
Alright. Let’s open it. Within the zip is another tar-gzip file which I also decompressed (tar -xvf challenge.tar.gz
) which resulted in two files, flag
and wannacry
. Running the file
utility on them both yielded additional info:
tal@Tal:~$ file wannacry
wannacry: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, Go BuildID=IGPSbKhPf45BQqlR84-9/XWC3eVS4fozNp9uK4nDp/_Styn3U-Z8S6ExnY6QOR/RTzNS5QnFmUHeSBeyHIu, with debug_info, not stripped
While the flag
file wasn’t detected as anything meaningful. Cool, so it’s time to jump into the binary.
I opened it in IDA 8.3 (which is given as freeware with cloud-based decompiling - recommended!), and told IDA to freely use the debug information that was found, embedded into the file. It should help greatly since I’ll have function names and whatnot.
IDA threw me into a main_main
function. I happened to reverse a compiled Go program in the past, and it’s not too much fun - IDA completely fails on understanding the structures of objects like the built-in string, and it messes up the decompiler. Also, even the disassembly can confuse IDA in some cases - the stack cookie check is notorious for making IDA unsuccessfully parse functions.
Either way, at the beginning of the function, there’s a call to a main_impossible
. Without reversing it, I noticed that if it returns a non-zero output, the code calls println
to stdout. But what does it print?
.text:0000000000509498 call main_impossible ; Call Procedure .text:0000000000509498 .text:000000000050949D nop dword ptr [rax] ; No Operation .text:00000000005094A0 test al, al ; Logical Compare .text:00000000005094A2 jz short loc_5094EE ; Jump if Zero (ZF=1) .text:00000000005094AA mov rax, cs:main_site ; val ... .text:00000000005094B8 call runtime_convTstring ; Call Procedure ... .text:00000000005094E9 call fmt_Fprintln ; Call Procedure
Looks like a string object named main_site
, which points to offset 0x53A7A4
and of length 0x4F. The string at that address turned out to be Keys are here:\nhttps://wannacry-keys-dot-gweb-h4ck1ng-g00gl3.uc.r.appspot.com/\n
.
Going to this URL, I was met with a list of 200 .pem
files. Clicking on one of them results in a valid PEM-formatted private key, okay…
Back to the binray, after the call to main_impossible
that prints the aforementioend URL, I noticed that the code accesses some global variables from the .bss section of the executable, like os_Args
, flag_CommandLine
, main_keyFile
and main_encryptedFile
.
.text:0000000000509534 mov rdx, cs:main_keyFile .text:000000000050953B mov rbx, [rdx+8] .text:000000000050953F mov rax, [rdx] .text:0000000000509542 test rbx, rbx ; Logical Compare .text:0000000000509545 jz short loc_509555 ; Jump if Zero (ZF=1) .text:0000000000509547 mov rcx, cs:main_encryptedFile .text:000000000050954E cmp qword ptr [rcx+8], 0 ; Compare Two Operands .text:0000000000509553 jnz short loc_50956C ; Jump if Not Zero (ZF=0)
I wanted to know where they’re initialized and looked at their cross references. Looks like there are 3 init functions os_init
, flag_init
and main_init
that initialize them and are probably called before main_main
.
I looked at main_init
specifically, finding that the objects main_keyFile
and main_encryptedFile
contain name
, usage
and value
members behind the scenes.
Looking at the initialization flow, main_keyFile
is initalized with the name "key_file"
, usage "File name of the private key"
and an empty value, while main_encryptedFile
is initialzied with the name "encrypted_file"
, usage "File name to decrypt"
and an empty value too.
.text:00000000005096EC lea rbx, aEncryptedFile ; name="encrypted_file" .text:00000000005096F3 mov ecx, 14 ; name_length=14 .text:00000000005096F8 xor edi, edi ; value=null .text:00000000005096FA xor esi, esi ; value_length=0 .text:00000000005096FC lea r8, aFileNameToDecr ; usage="File name to decrypt." .text:0000000000509703 mov r9d, 21 ; usage_length=21 .text:0000000000509709 call flag__ptr_FlagSet_String ; creates object, return in eax .text:0000000000509717 mov cs:main_encryptedFile, rax
Alright, now that I know that, back to main_main
I go. As seen in the code above, I realized that the check on offset 8 of main_encryptedFile
(and later also at the same offset of main_keyFile
) verifies that its value isn’t empty. If any of them is, the code calls a function taking from the global variable flag_Usage
.
Seeing this it’s enough for me to simply go and run the executable - since seems like it’s designed for command-line usage.
tal@Tal:~$ chmod +x wannacry & ./wannacry
Usage of ./wannacry:
-encrypted_file string
File name to decrypt.
-key_file string
File name of the private key.
Ah, very nice, seems like the objects I looked at above are a special kind of objects used to take input from the command line. The value of the objects above are probably the string supplied by the user.
Skimming forwards in the main_main
function, I noticed that there’s a call to main_readKey
using the main_keyFile
object’s value, and then a os_ReadFile
call using the main_encryptedFile
object’s value. The output of these calls are saved into local variables key
and data
respectively. Shortly after, a call to main_decrypt
is made with key
and data
being two paramteres passed into it.
.text:00000000005095FE mov rbx, [rsp+data] ; data .text:0000000000509603 mov rdi, [rsp+key] ; key .text:0000000000509608 call main_decrypt ; Call Procedure
Peeking at the function list, a lot of crypto-related functions stand out. Looks like a cryptographic library was compiled into this binary. The functions imported are mostly related to elliptic-curve cryptography, which main_decrypt
probably utilizes.
At this point it’s pretty clear the flag was encrypted using one version or another of ECDSA. I have those 200 keys given at the website. Well, it’s not too many - I thought and decided to simply try them all.
import requests
DOMAIN = 'https://wannacry-keys-dot-gweb-h4ck1ng-g00gl3.uc.r.appspot.com'
KEYS = ['01087458-4d66-4677-af0d-da2024cc2111.pem', '02bdbf0d-48c6-4fb5-b5d2-71be3f4f071f.pem', # ...
]
for key in KEYS:
res = requests.get(DOMAIN + "/" + key)
assert(res.status_code == 200)
with open("keys/" + key, "wb") as f:
f.write(res.content)
Grabbing a drink and a minute later, I had all the keys saved to the keys
folder. Time to loop over all of them and run the wannacry
binary on each, searching for an output that looks like a flag.
for key in keys/*; do
output=$(./wannacry -encrypted_file=flag -key_file="$key");
if echo "$output" | grep -q "google"; then
echo "$output" > "decrypted_flag";
break
fi;
done
And there’s a decrypted_flag
in the directory :)