Contents

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:

./images/sn1_1.png

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

./images/sn1_2.png

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:

./images/sn1_3.png

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 :(

./images/sn1_4.png

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:

./images/sn1_5.png

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:

./images/sn1_6.png

Getting the flag

We've started analyzing the dump, and one of the first things to check are processes and bash history:

./images/sn1_5.png

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:

./images/sn1_7.png

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:

./images/sn2_1.png

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:

./images/sn2_2.png

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: <module>
    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:

./images/sn2_2.png

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:

./images/sn2_3.png

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:

./images/sn2_4.png

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:

./images/sn2_5.png

And rerun Decompyle++:

./images/sn2_6.png

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:

./images/sn2_7.png

To generate public and private key from mnemonic sentence, we can use publicly available calculator:

./images/sn2_8.png

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!

./images/ctf_needs.png

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.

./images/smoothjpg.jpg

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!