| tags:Reversing Pwnable categories:Writeups series:CSAW15 Quals
CSAW 2015 Quals - FTP and FTP2
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