# SekaiCTF 2022 - Forensic Symbolic Needs writeup

Contents

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

### 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):

 1 2 3 4 5 6 7  $grep -a "BOOT_IMAGE" dump.mem [...] BOOT_IMAGE=/boot/vmlinuz-5.15.0-43-generic$ grep -a "Linux version" dump.mem [...] Linux version 5.15.0-43-generic (buildd@lcy02-amd64-076) (gcc (Ubuntu 11.2.0-19ubuntu1) 11.2.0, GNU ld (GNU Binutils for Ubuntu) 2.38) #46-Ubuntu SMP Tue Jul 12 10:30:17 UTC 2022 (Ubuntu 5.15.0-43.46-generic 5.15.39) 

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:

 1 2 3 4 5 6 7  echo "deb http://ddebs.ubuntu.com $(lsb_release -cs) main restricted universe multiverse deb http://ddebs.ubuntu.com$(lsb_release -cs)-updates main restricted universe multiverse deb http://ddebs.ubuntu.com $(lsb_release -cs)-proposed main restricted universe multiverse" | \ sudo tee -a /etc/apt/sources.list.d/ddebs.list sudo apt install ubuntu-dbgsym-keyring sudo apt-get update sudo apt-get install linux-image-$(uname -r)-dbgsym 

Then I've installed dwarf2json:

 1 2 3 4  git clone https://github.com/volatilityfoundation/dwarf2json cd dwarf2json/ go mod download github.com/spf13/pflag go build 

And created ISF:

 1  sudo ./dwarf2json linux --elf /usr/lib/debug/boot/vmlinux-5.15.0-43-generic --system-map /boot/System.map-5.15.0-43-generic > vmlinux-5.15.0-43-generic.json 

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:

 1 2 3 4 5 6 7  $vol -f dump.mem linux.psaux.PsAux Volatility 3 Framework 2.4.0 Progress: 100.00 Stacking attempts finished PID PPID COMM ARGS [...] 1878 1863 ncat ncat -lvnp 1234 -c echo N4GQ2CQAAAAAAEFG5JRPEAIAADRQAAAAAAAAAAAAAAAAAAAAAAAAACIAAAAEAAAAABZ6QAAAABSAAZABNQAFUAD2A5SQA2QBMQBBSAC2AJLQA3QLAEAACAABABSQGZADQMAQCADFASBQAAIALEAGOAC2AVSQMZAEMQCYGAUPBZNAOZIHUAEKCAFABGQQAWQFK4AGIAIEAACABAYDAEAG4CBRABZS65YBAEAACAABABMQAAIAMQDFUCTFBNSQVAYBMQDWIAMFAIMQAWQKMUGGKCVABVSQ4ZIKQMAWICDFBZSQVAYBMQEBMAAYAALQBIIBQMAVUCTHABNA6ZIQMQAGKDTFBKBQCZAIQMBUIAC5CRNBCZIPUAJGKBLFCNSQUZIRMUIWICAXACCQEGIAMQDYGATEAIMAAGIAUEAQCADRLFSQGZAJQMAQCADEAFJQAKIK5EAAAAAAJ3UQCAAAAB5BQVLTMFTWKORAFYXXOYLMNRSXIIDQMFZXG53POJSHUDLCNFYDGOLMNFZXILTUPB2NUALSNQKAAAAAABS6KHWNHGMBH4AWTJ3BE3XKCYFWPVQQSR6WWFSHC2WEUE3P5AP7G46OA3IBWAIA5EBAAAAA5EGAAAAA3ICVO4TPNZTSSFG2ANZXS462ARQXEZ3W3IEHAYLTON3W64TE3ICXA4TJNZ2NUBDFPBUXJWQFO5XXEZDT3ICG64DFN3NACZW2ARZGKYLE3IFHG4DMNF2GY2LOMVZ5UBDDN5SGLWQDMJUW5WQDON2HFWQFPJTGS3DM3IBWYZLO3IEG23TFNVXW42LD3ICXEYLOM5S5UALJ3IDGC4DQMVXGJWQDNFXHJKIAOINQAAAAOINQAAAA7IEHIZLTOQZC44DZ3IEDY3LPMR2WYZJ6AEAAAADTEIAAAAAIAABAEDQBAYAQQAIIAECAGDACBYARZ7YEAMIAGIQBAQBRIARGAEGAE=== | base32 -d > file.pyc [...]  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:   1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163  $ ./pycdas ../file.pyc file.pyc (Python 3.10) [Code] File Name: test2.py Object Name: Arg Count: 0 Pos Only Arg Count: 0 KW Only Arg Count: 0 Locals: 0 Stack Size: 9 Flags: 0x00000040 (CO_NOFREE) [Names] 'sys' 'argv' 'password' 'print' 'exit' 'words' 'open' 'f' 'read' 'splitlines' 'code' 'bin' 'str' 'zfill' 'len' 'mnemonic' 'range' 'i' 'append' 'int' [Var Names] [Free Vars] [Cell Vars] [Constants] 0 None 1 'Usage: ./wallet password' 'bip39list.txt' 'r' 0x26F4036773F33FD1BC4E55616472CD7F65086B670B2DD5B84BB4D16F02730E734F72E500L 2 12 'Wrong' [Disassembly] 0 LOAD_CONST 0: 0 2 LOAD_CONST 1: None 4 IMPORT_NAME 0: sys 6 STORE_NAME 0: sys 8 SETUP_FINALLY 7 (to 24) 10 LOAD_NAME 0: sys 12 LOAD_ATTR 1: argv 14 LOAD_CONST 2: 1 16 BINARY_SUBSCR 18 STORE_NAME 2: password 20 POP_BLOCK 22 JUMP_FORWARD 11 (to 46) 24 POP_TOP 26 POP_TOP 28 POP_TOP 30 LOAD_NAME 3: print 32 LOAD_CONST 3: 'Usage: ./wallet password' 34 CALL_FUNCTION 1 36 POP_TOP 38 LOAD_NAME 4: exit 40 CALL_FUNCTION 0 42 POP_TOP 44 POP_EXCEPT 46 BUILD_LIST 0 48 STORE_NAME 5: words 50 LOAD_NAME 6: open 52 LOAD_CONST 4: 'bip39list.txt' 54 LOAD_CONST 5: 'r' 56 CALL_FUNCTION 2 58 SETUP_WITH 14 60 STORE_NAME 7: f 62 LOAD_NAME 7: f 64 LOAD_METHOD 8: read 66 CALL_METHOD 0 68 LOAD_METHOD 9: splitlines 70 CALL_METHOD 0 72 STORE_NAME 5: words 74 POP_BLOCK 76 LOAD_CONST 1: None 78 DUP_TOP 80 DUP_TOP 82 CALL_FUNCTION 3 84 POP_TOP 86 JUMP_FORWARD 8 (to 104) 88 WITH_EXCEPT_START 90 POP_JUMP_IF_TRUE 47 (to 94) 92 RERAISE 1 94 POP_TOP 96 POP_TOP 98 POP_TOP 100 POP_EXCEPT 102 POP_TOP 104 LOAD_CONST 6: 0x26F4036773F33FD1BC4E55616472CD7F65086B670B2DD5B84BB4D16F02730E734F72E500L 106 STORE_NAME 10: code 108 LOAD_NAME 11: bin 110 LOAD_NAME 10: code 112 CALL_FUNCTION 1 114 LOAD_CONST 7: 2 116 LOAD_CONST 1: None 118 BUILD_SLICE 2 120 BINARY_SUBSCR 122 STORE_NAME 10: code 124 LOAD_NAME 12: str 126 LOAD_NAME 10: code 128 LOAD_METHOD 13: zfill 130 LOAD_NAME 14: len 132 LOAD_NAME 10: code 134 CALL_FUNCTION 1 136 LOAD_CONST 8: 12 138 LOAD_NAME 14: len 140 LOAD_NAME 10: code 142 CALL_FUNCTION 1 144 LOAD_CONST 8: 12 146 BINARY_MODULO 148 BINARY_SUBTRACT 150 BINARY_ADD 152 CALL_METHOD 1 154 CALL_FUNCTION 1 156 STORE_NAME 10: code 158 BUILD_LIST 0 160 STORE_NAME 15: mnemonic 162 LOAD_NAME 16: range 164 LOAD_CONST 0: 0 166 LOAD_NAME 14: len 168 LOAD_NAME 10: code 170 CALL_FUNCTION 1 172 LOAD_CONST 8: 12 174 CALL_FUNCTION 3 176 GET_ITER 178 FOR_ITER 20 (to 220) 180 STORE_NAME 17: i 182 LOAD_NAME 15: mnemonic 184 LOAD_METHOD 18: append 186 LOAD_NAME 5: words 188 LOAD_NAME 19: int 190 LOAD_NAME 10: code 192 LOAD_NAME 17: i 194 LOAD_NAME 17: i 196 LOAD_CONST 8: 12 198 BINARY_ADD 200 BUILD_SLICE 2 202 BINARY_SUBSCR 204 LOAD_CONST 7: 2 206 CALL_FUNCTION 2 208 LOAD_CONST 2: 1 210 BINARY_SUBTRACT 212 BINARY_SUBSCR 214 CALL_METHOD 1 216 POP_TOP 218 JUMP_ABSOLUTE 89 (to 178) 220 LOAD_NAME 3: print 222 LOAD_CONST 9: 'Wrong' 224 CALL_FUNCTION 1 226 POP_TOP 228 LOAD_CONST 1: None 230 RETURN_VALUE 

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:

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15  [...] case Pyc::SETUP_ANNOTATIONS: variable_annotations = true; break; /* Passing WITH_EXCEPT_START */ case Pyc::WITH_EXCEPT_START: stack.pop(); stack.pop(); stack.pop(); break; default: fprintf(stderr, "Unsupported opcode: %s\n", Pyc::OpcodeName(opcode & 0xFF)); cleanBuild = false; return new ASTNodeList(defblock->nodes()); [...] 

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:

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25  # Source Generated with Decompyle++ # File: file.pyc (Python 3.10) import sys try: password = sys.argv[1] finally: pass print('Usage: ./wallet password') exit() words = [] with open('bip39list.txt', 'r') as f: words = f.read().splitlines() None(None, None, None) if not None: pass code = 0x26F4036773F33FD1BC4E55616472CD7F65086B670B2DD5B84BB4D16F02730E734F72E500L code = bin(code)[2:] code = str(code.zfill(len(code) + (12 - len(code) % 12))) mnemonic = [] for i in range(0, len(code), 12): mnemonic.append(words[int(code[i:i + 12], 2) - 1]) print('Wrong') 

### Reversing the code

Now we can clearly see the code flow:

1. 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.
2. Print and exit instructions are probably wrongly decompiled, as they should be a part of finally block code.
3. Creation of empty list words.
4. Open bip39list.txt and read them into the words list.
5. Transform code into binary and do some operations on it.
6. By basing on the code choose which words will be a part of a mnemonic.
7. 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:

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18  >>> words = [] >>> with open('english.txt', 'r') as f: ... words = f.read().splitlines() >>> mnemonic = [] >>> code = 0x26F4036773F33FD1BC4E55616472CD7F65086B670B2DD5B84BB4D16F02730E734F72E500 >>> code = bin(code)[2:] >>> code '1001101111010000000011011001110111001111110011001111111101000110111100010011100101010101100001011001000111001011001101011111110110010100001000011010110110011100001011001011011101010110111000010010111011010011010001011011110000001001110011000011100111001101001111011100101110010100000000' >>> code = str(code.zfill(len(code) + (12 - len(code) % 12))) >>> code '001001101111010000000011011001110111001111110011001111111101000110111100010011100101010101100001011001000111001011001101011111110110010100001000011010110110011100001011001011011101010110111000010010111011010011010001011011110000001001110011000011100111001101001111011100101110010100000000' >>> for i in range(0, len(code), 12): ... mnemonic.append(words[int(code[i:i + 12], 2) - 1]) >>> mnemonic ['evidence', 'leopard', 'solution', 'layer', 'legend', 'danger', 'orient', 'project', 'silver', 'flower', 'wrong', 'path', 'stove', 'throw', 'fortune', 'report', 'nuclear', 'old', 'target', 'exact', 'broom', 'hawk', 'toss', 'paper'] >>> for word in mnemonic: ... print(word + " ", end="") evidence leopard solution layer legend danger orient project silver flower wrong path stove throw fortune report nuclear old target exact broom hawk toss paper 

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!