There were two challenges with the same binary, Reversing300 and Exploitables300.

Challenge Text:

Reversing 300:

We found an ftp service, I’m sure there’s some way to log on to it. nc 54.172.10.117 12012

Exploitables300:

This challenge is a follow up to FTP, now exploit the service.

First observations

Looking at strings showed a few interesting hits, like "logged in\n" at 0x4030a6. This is XRef’d at 0x401774 which is part in the function that handles the authentification.

Can haz creds?

reversing this functions showed that there is a hardcoded username that the user is strcmp’d to and that some sort of verification function is called on the password.

00000000004016fa         mov        rax, qword [ss:rbp+var_B8]                  ; XREF=iz_PASS+27
0000000000401701         mov        rdx, qword [ss:rbp+var_A0]
0000000000401708         mov        qword [ds:rax+offsetof(context, password)], rdx
000000000040170c         mov        rax, qword [ss:rbp+var_B8]
0000000000401713         mov        rax, qword [ds:rax+offsetof(context, password)]
0000000000401717         mov        rdi, rax                                    ; argument #1 for method hash_passwd
000000000040171a         call       hash_passwd
000000000040171f         mov        rax, qword [ss:rbp+var_B8]
0000000000401726         mov        rax, qword [ds:rax+offsetof(context, username)]
000000000040172a         mov        edx, 9                                      ; argument "n" for method j_strncmp
000000000040172f         mov        esi, default_user_name                      ; "blankwall", argument "s2" for method j_strncmp
0000000000401734         mov        rdi, rax                                    ; argument "s1" for method j_strncmp
0000000000401737         call       j_strncmp
000000000040173c         test       eax, eax
000000000040173e         jne        invalid_username
0000000000401740         mov        rax, qword [ss:rbp+var_B8]
0000000000401747         mov        rax, qword [ds:rax+offsetof(context, password)]
000000000040174b         mov        rdi, rax                                    ; argument "passwd" for method hash_passwd
000000000040174e         call       hash_passwd
0000000000401753         cmp        eax, 0xd386d209
0000000000401758         jne        invalid_passwd

This hash is totally secure

This password hashing is pretty simple, here is some reversed pseudocode for it:

int hash_passwd(char *passwd) {
    uint32_t x = 0x1505;
    uint32_t i = 0;
    while (passwd[i] != 0) {
        x += passwd[i] + (x << 0x5):
        i++;
    }
    return x;
}

The value of hash_passwd(passwd) obviously has to equal 0xd386d209. The operations of this hashing function don’t seem to be too expensive, in fact the function is O(n) in the password length, so I decided to implement a bruter in C that would take tries on stdin and quit on a collision. I used john in incremental mode to generate the input.

At first I used strncmp to strip the newline off the end of the input strings:

int main() {
    unsigned char buf[MAX_LEN];
    while(fgets(buf, MAX_LEN, stdin)) {
        buf[strcspn(buf, "\r\n")] =0;
        if (hash_passwd(buf) == 0xd386d209) {
    	    printf("%s", buf);
    	    return 0;
        }
    }
}

Debug all teh binaries!

However, this didn’t really give me a match in reasonable time, so I decided to do some dynamic analsis.

gdb-peda$ continue
Continuing.
[+] Creating Socket
[+] Binding
[+] Listening
[+] accept loop
[+] socket fd: 4
[+] accept loop
[New process 25911]
[Switching to process 25911]
[----------------------------------registers-----------------------------------]
RAX: 0x605227 ("my_test_passwd\n")
RBX: 0x0
RCX: 0x30 ('0')
RDX: 0x605227 ("my_test_passwd\n")
RSI: 0x7fffffffd4f0 --> 0x445753534150 ('PASSWD')
RDI: 0x605227 ("my_test_passwd\n")
RBP: 0x7fffffffd580 --> 0x7fffffffdf20 --> 0x7fffffffdf80 --> 0x0
RSP: 0x7fffffffd4b8 --> 0x40171f (mov    rax,QWORD PTR [rbp-0xb8])
RIP: 0x401540 (push   rbp)
R8 : 0xffff
R9 : 0x0
R10: 0x0
R11: 0x0
R12: 0x401130 (xor    ebp,ebp)
R13: 0x7fffffffe060 --> 0x1
R14: 0x0
R15: 0x0
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x401539:    call   0x401452 <error>
0x40153e:    leave
0x40153f:    ret
=> 0x401540:    push   rbp
0x401541:    mov    rbp,rsp
0x401544:    mov    QWORD PTR [rbp-0x18],rdi
0x401548:    mov    DWORD PTR [rbp-0x4],0x1505
0x40154f:    mov    DWORD PTR [rbp-0x8],0x0
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffd4b8 --> 0x40171f (mov    rax,QWORD PTR [rbp-0xb8])
0008| 0x7fffffffd4c0 --> 0x7fffffffd580 --> 0x7fffffffdf20 --> 0x7fffffffdf80 --> 0x0
0016| 0x7fffffffd4c8 --> 0x7fffffffd5c0 --> 0x4
0024| 0x7fffffffd4d0 --> 0x0
0032| 0x7fffffffd4d8 --> 0x1800000006
0040| 0x7fffffffd4e0 --> 0x605227 ("my_test_passwd\n")
0048| 0x7fffffffd4e8 --> 0x605220 ("PASSWD my_test_passwd\n")
0056| 0x7fffffffd4f0 --> 0x445753534150 ('PASSWD')
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Breakpoint 1, 0x0000000000401540 in ?? ()

Apparently, the last \n character does get passed to the password hashing function. Unfortunately, this knowledge didn’t help my brute forcer to break the password more quickly

Z3 to the Rescue!

I had heard good things about z3 before, And this problem looked like it could be tackled by a constraint solver, so I decided to give z3 a try. It’s my first try at using Z3, so I’m sorry for the poor code, and if there are some things I could have done better, I’d love to hear them.

To send the password to the FTPd, it would be useful to be able to type it, and previous flags had proven to be alphanumeric including underscores and curly braces (at that point I had the suspicion that the password might be the flag, so I added these assumptions as a constraints. I also knew that the last character would have to be a \n, so I also added this constraint. There was no information about the length of the password, so I iteratively created models with longer BitVecs until the model is satisfied.

from z3 import *

def hash_passwd(passwd):
    h = 0x1505
    for c in passwd:
        h += ord(c) + (0xFFFFFFFF & (h<<5))
        h &= 0xFFFFFFFF
    return h
    
def solve(max_len):
    s = Solver()
    xs = list(BitVecs(' '.join(['c%d' % i for i in range(0,max_len)]), 32))
    h = BitVec('hs', 32)
    s.add(h == 0xd386d209)
    h = 0x1505
    for i, c in enumerate(xs):
        if i == len(xs)-1:
    	    # last char has to be \n
    	    s.add(c == ord('\n'))
        else:
    	    # assume alphanumeric and a few extras
    	    s.add(Or([
    		    And([c >= ord('0'),c <= ord('9')]),
    		    And([c >= ord('A'),c <= ord('Z')]),
    		    And([c >= ord('a'),c <= ord('z')]),
    		    c == ord('_'),
    		    c == ord('{'),
    		    c == ord('}'),
    		]))
            h = h + (0xFFFFFFFF & (h << 5)) + c
    s.add(h == 0xd386d209)
    if s.check() == sat:
        m = s.model()
        passwd = "".join([chr( m[c].as_long() ) for c in xs])
        print "hash(%s) = 0x%s" % (passwd, hex(hash_passwd(passwd)))
        return True
    else:
         return False

if __name__=="__main__":
    i = 1
    while not solve(i):
        i+=1

This gave me a solution in about a second:

hash(dNpJie
) = 0x0xd386d209

real	0m1.093s
user	0m1.065s
sys	0m0.022s

Logging in

Welcome to FTP server
USER blankwall
Please send password for user blankwall
PASS dNpJie
logged in
HELP
USER PASS PASV PORT
NOOP REIN LIST SYST SIZE
RETR STOR PWD CWD

Ok, so PASV mode is supported, let’s try it, and get a ls:

PASV
PASV succesful listening on port: XXXXX
LIST
LIST complete

hmm, let’s see what we got:

$ nc 54.172.10.117 XXXX
drwxr-xr-x 1     0     0         4096 Sep 20 05:22 ftp_0319deb1c1c033af28613c57da686aa7
drwxr-xr-x 1     0     0         4096 Sep 20 05:22 .bashrc
drwxr-xr-x 1     0     0         4096 Sep 20 05:22 .bash_history
drwxr-xr-x 1     0     0         4096 Sep 20 05:22 run.sh
drwxr-xr-x 1     0     0         4096 Sep 20 05:22 flag.txt
drwxr-xr-x 1     0     0         4096 Sep 20 05:22 .profile
drwxr-xr-x 1     0     0         4096 Sep 20 05:22 .bash_logout
drwxr-xr-x 1     0     0         4096 Sep 20 05:22 re_solution.txt
drwxr-xr-x 1     0     0         4096 0         4096  .selected_editor

turns out, there’s a flag.txt…

Then it was just a matter of RETR‘ing flag.txt. re_solution.txt turned out to be the flag for Reversing300, so solving the challenge got us two flags at once. Something about this tells me this wasn’t inteded…

LOLWUT?

Looking at the challenge again later, it seems that the intended way to get the solution to Reversing300 was to use the custom RDF FTP command. In the handler for the RETR, there is a check for the letter f in the filename - RETR‘ing flag.txt should never have worked. This appears to have been the Exploitable300 part of the challenge, but it appears that the LIST operation or the previous RETR of re_solution.txt overwrote enough bytes that this check did not fail. Therefore the Exploitable300 part was circumventable by just “using” the FTP service

But still, it was a nice chance to learn some z3.

plonk