SekaiCTF 2022 - Forensic Symbolic Needs writeup
I've been complaining a lot lately about me not doing enough technical stuff - that's why when @haqpl invited me to play with @justCatTheFish as the guest, I've instantly accepted the offer. SekaiCTF 2022 was the first one when I had a chance to play with the team, and I have to say - it was a great experience (we got 1st place)! So great I thought, that I will create a few writeups of challenges in which I took part. Let's start with the first one!
Symbolic Needs 1
Description
We recently got hold of a cryptocurrency scammer and confiscated his laptop. Analyze the memdump. Submit the string you find wrapped with SEKAI{}.
Attachment md5sum: 4be69c88e6f19dd9c9f8e6c52bc93c28
Author: BattleMonger
https://storage.googleapis.com/sekaictf/Forensics/memdump.zip
The beginning
After the download and unzipping, we are presented with the dump.mem
file:
What we can see from binwalk, is that we are having Linux memdump. This output of course is a lot longer, but it doesn't matter.
Volatility
In my opinion, the best (and the only solution I know) to analyze memdumps is volatility.
Everyone who tried to use it, probably know that proper installation process is challenge by itself. I will ommit this part - if you're looking for the guide how to do it, I can recommend this one (in case of problems with version 3, try to install 2 and check 3 again - it worked for me suprisingly).
Volatility works great with Windows out of the box, but to run with Linux, we have to get proper Profile (vol2) or Intermediate Symbol File (vol3):
Getting volatility to work
How to start then? First, we have to obtain two essentials pieces of information:
- Kernel version,
- Linux distribution and version.
There are two ways of achieving these information - first one is our good friend grep (with a lot of rubbish in the output):
|
|
Second way - volatility3 has a nice plugin called banners.Banners which also works form the beginning:
Now we know, that we've got:
- Kernel: Linux version 5.15.0-43-generic
- OS: Ubuntu 22.04
Kernel is obvious one, but how we know which version of OS? Googling Ubuntu 11.2.0-19ubuntu1
shows that this is connected to the Ubuntu 22.04 :)
Was it so simple?
It wasn't so obvious at first (for me), as I downloaded wrong image at the beginning - 20.04 :(Let's then download Ubuntu 22.04 from official distribution and create a virtual machine.
Building proper ISF
First problem? Wrong version of the kernel, as downloaded one had linux-image-5.15.0-48-generic, not 0.43, but changing it wasn't a problem.
We will try to create ISF for volatility3 - to do this, we need two things:
- debug symbols of the kernel,
- dwarf2json.
I've installed debug symbols using this Ubuntu wiki guide - better be prepared to have some free space for it:
|
|
Then I've installed dwarf2json:
|
|
And created ISF:
|
|
Protip - have some RAM for this operation, as 1024 and 2048MB was not enough, I've ended with 8192MB - hovewer this was probably overkill, but I didn't want to waste more time reseting the VM.
Now let's move created vmlinux-5.15.0-43-generic.json file to proper location, for mine installation of volatility3 it was ~/.local/lib/python3.10/site-packages/volatility3/framework/symbols/linux
.
Test volatility:
Profile for volatility 2?
But what about volatility 2? I've tried building a profile for it and running it, but well… It couldn't properly read it:
Getting the flag
We've started analyzing the dump, and one of the first things to check are processes and bash history:
Bash history actually showed weird string composed of numbers and dots. @gregxsunday was the first one of us who noticed, that they are in the ASCII range, so let's find out what is it - I've used CyberChef to decode it:
The flag was: SEKAI{H0u5T0n_w3_4r3_1n!!!}
Symbolic Needs 2
Description
Recover the private key of the wallet address 0xACa5872e497F0Cc626d1E9bA28bAEC149315266e. Submit the key wrapped with SEKAI{}.
Attachment md5sum: 4be69c88e6f19dd9c9f8e6c52bc93c28
Author: BattleMonger
Continue forensics
Our eyes catched this suspicious output from the process list:
|
|
Some Python bytecode? That's cannot be here by accident. Let's try to decompile it using the newest uncompyle6:
Ohhh… Okay. I've tried searching for another decompiler and I've found Decompyle++, which theoretically is able to decompile Python3.10. I've downloaded it, built and tried to run:
It didn't work - so let's just disassemble the code:
|
|
I got some overall look into what happening here, but to be honest I hadn't enough patience to go instruction by instruction to recreate what's exactly this code does. I've decided I will decompile it somehow.
Make decompilation of Python3.10 great again
Let's read the error from Decompyle++ again:
The problem is unsupported opcode WITH_EXCEPT_START, which following Python documentation:
Calls the function in position 7 on the stack with the top three items on the stack as arguments. Used to implement the call context_manager.exit(*exc_info()) when an exception has occurred in a with statement.
So we thought, that we don't have to decompile an exception from with statement. I've patched the Decompyle++ code - ASTree.cpp is the one which handles what to do with the opcodes. Before default I've added few lines to just pass the WITH_EXCEPT_START opcode:
|
|
Recompiled and tried to run it:
Another unsupported opcode:
Re-raises the exception currently on top of the stack. If oparg is non-zero, restores f_lasti of the current frame to its value when the exception was raised.
This one we also didn't need - but the same approach of just passing the opcode didn't work. So we thought - maybe let's just edit the file.pyc?
Decompyle++ has this nice opcodes map. I've searched for RERAISE - 119, so in hex that would be 0x77 - then tried searching it in the file.pyc:
I've decided to edit the first one from the line 00000080 - it was a lucky shot & rest of the 0x77 looked like part of the strings.
I've used vim - command :%!xxd
changes the file to the hex, and later we can go back to binary using :%!xxd -r
. I've changed 77 to 09 - 0x09 is the NOP opcode:
And rerun Decompyle++:
Success! We've got few None instructions, but they could be ommitted.
Copy of the code:
|
|
Reversing the code
Now we can clearly see the code flow:
- Check if argument (called password) was passed,
- but - it's never used, so it could be anything, we don't need to worry about it.
- Print and exit instructions are probably wrongly decompiled, as they should be a part of finally block code.
- Creation of empty list words.
- Open bip39list.txt and read them into the words list.
- Transform code into binary and do some operations on it.
- By basing on the code choose which words will be a part of a mnemonic.
- Code always ends with printing Wrong.
Before going further, a little introduction to what is even BIP39.
BIP39
BIP - Bitcoin Improvement Proposal - is a design document for introducing new features or information to Bitcoin. BIP39 to which reference we can see in the code, is:
A design implementation that lays out how cryptocurrency wallets create the set of words (or “mnemonic codes”) that make up a mnemonic sentence, and how the wallet turns them into a binary “seed” that is used to create necryption keys […]
Mnemonic senteces are created from the publicly available wordlists. It is possible to recreate private and public keys of the cryptocurrency wallet by having a seed or a mnemonic sentence.
Back to the reversing - the flag
We can simply recreate mnemonics by using default (recommended) english BIP39 wordlist and reusing the code we obtained after decompilation:
|
|
Before generating the keys, I also verified to which cryptocurrency points given public address using this site:
To generate public and private key from mnemonic sentence, we can use publicly available calculator:
The flag is SEKAI{0x81c458e9fae445de18385a3379513acc8e191e4c2667c85aa0a52a32ec4e6d55}
Non-technical part - have some fun from my mistakes :)
By reading writeups like this, it is possible that we would think something like “that was so easy and logical, step by step” or “how they did all of that” - but in reality it took me really a lot of hours to get both flags and one sleepless night - but in the end it was worth it!
I hope this paragraph will show you, that solving challenges like this is a process during which we cannot give up if we want to get better! Or at least will make you smile ;)
Volatility 3 > Volatility 2 (in this case)
I spend at least an hour to make volatility cooperate with me, and the next few searching how to run volatility on the Linux memory dump. My biggest mistake there was trying to run it using vol2 - building the profiles, trying to debug why it doesn't work, trying to use created profiles, trying to debug why they don't work. Only after that I happened to find some information that it's possible to create ISF for vol3 - and this went smoothly.
So which flag we should do first?
The funniest one from the perspective of the time - you need to know, that the second challenge on the SEKAICTF mainpage unlocked only after completing the first one. And I didn't pay attention to the ASCII string in the bash history at the beginning.
I found the PYC file, had some fun decompiling it (also few hours here, probably going opcode by opcode from dissasembly would be faster), learned about BIP39 and recreated the mnemonic sentence… And then what? I didn't know about the second part at this moment - I thought that maybe I need to find some information in generated wallets, so I went key by key… And nothing.
Then @gregxsunday looked with me at the memdump, and noticed ASCII string in the bash history - after submitting this one, it took me basically few moments to also submit a second one.
Summary
That's all for this challenge - thank you for your time you spent to read it all! I will be also writing a writeup for another forensic challenge from SEKAICTF - Blind Infection 1 & 2 - remember to follow me on Twitter and/or LinkedIn to get the info about it!