CSAW 2015 - FTP - Reversing (300pts) writeup

The challenge description was: We found an ftp service, I'm sure there's some way to log on to it. nc 54.172.10.117 12012

A binary file with the name ftp_0319deb1c1c033af28613c57da686aa7 was provided, let's have a look at it to get some informations:

mrt:~/ctf/csaw/reverse/ftp$ file ftp_0319deb1c1c033af28613c57da686aa7
ftp_0319deb1c1c033af28613c57da686aa7: ELF 64-bit LSB executable, x86-64,
version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32,
BuildID[sha1]=43afbcd9f4e163f002970b9e69309ce0f1902324, stripped

mrt:~/ctf/csaw/reverse/ftp$ strings ftp_0319deb1c1c033af28613c57da686aa7 | less
...
USER PASS PASV PORT
NOOP REIN LIST SYST SIZE
RETR STOR PWD CWD
Welcome to FTP server
%c%c%c%c%c%c%c%c%c%c
%b %e %H:%M
drwxrwxrwx 1 blankwall blankwall %d Sep 2 13:05 %s
[-] %s -- ERRNO[%d]
malloc error
receive error
send error
Please send password for user
PASS
login with USER PASS
blankwall
logged in
Invalid login credentials
Goodbye :)
...
PASV succesful listening on port: %d
re_solution.txt
Error reading RE flag please contact an organizer
USER
...

mrt:~/ctf/csaw/reverse/ftp$ objdump -M intel -d ftp_0319deb1c1c033af28613c57da686aa7 > dump

Some interesting informations already, seems like there might be a user/group named blankwall and we can also see something related to a flag with a file named re_solution.txt.

While debugging the binary I noticed I was being kicked a bit too quickly from the server so I had a quick look at the symbols:

mrt:~/ctf/csaw/reverse/ftp$ readelf -r ftp_0319deb1c1c033af28613c57da686aa7

Relocation section '.rela.plt' at offset 0x9a8 contains 47 entries:
Offset Info Type Sym. Value Sym. Name + Addend
....
0000006040b8 001500000007 R_X86_64_JUMP_SLO 0000000000000000 alarm + 0
...

Apparently they are setting a timer which might be the reason I couldn't debug for as long as I wanted, after looking in the dump for calls to alarm I ended up here:

  402693:       bf 41 00 00 00          mov    edi,0x41
402698: e8 e3 e8 ff ff call 400f80 <alarm@plt>
40269d: bf 00 00 00 00 mov edi,0x0
4026a2: e8 69 e9 ff ff call 401010 <time@plt>

So I could patch this in gdb when attaching to the process. In order to debug a server running and forking processes like this binary I use the following method:

  • run screen and create 3 windows
  • window1: server
  • window2: debugger
  • window3: client

After running the server in the first window I can switch to the second window where I am going to run gdb and attach to the process of the server running. I need to setup a couple things first:

  • I want to patch the alarm part to let me debug as long as I want
  • I need to set gdb to follow the forked child
  • I have to find somewhere I know I can set a breakpoint to trigger the debugger

Since we are debugging a networking program we can set a breakpoint on recv calls and see if we find our way in there:

    mrt:~/ctf/csaw/reverse/ftp$ grep recv dump
0000000000400e50 <recv@plt>:
4014d8: e8 73 f9 ff ff call 400e50 <recv@plt>
401e8f: e8 bc ef ff ff call 400e50 <recv@plt>

Let's set a breakpoint on both these adresses in our gdb script and start debugging, this is how my script was looking:

    nop 0x402693 0x4026a7
set follow-fork-mode child
b * 0x4014d8
b * 0x401e8f

Ready to start debugging, we are going to use the windows of the debugger and the client connecting to the server mostly which means window2 and window3.

[window1 - server]

mrt:~/ctf/csaw/reverse/ftp$ ./ftp_0319deb1c1c033af28613c57da686aa7
[+] Creating Socket
[+] Binding
[+] Listening
[+] accept loop

[window2 - gdb]

mrt:~/ctf/csaw/reverse/ftp$ gdb -p $(pidof ftp_0319deb1c1c033af28613c57da686aa7) -x gdb.x

[window3 - client]

mrt:~/ctf/csaw/reverse/ftp$ nc localhost 12012

[window2 - gdb]

gdb$ c

[window3 - client]

Welcome to FTP server
HELP

[window2 - gdb]

=> 0x4014d8: call 0x400e50 <recv@plt>
0x4014dd: mov QWORD PTR [rbp-0x8],rax
0x4014e1: cmp QWORD PTR [rbp-0x8],0x0
0x4014e6: jns 0x4014f2
0x4014e8: mov edi,0x403047
0x4014ed: call 0x401452
0x4014f2: mov rax,QWORD PTR [rbp-0x10]
0x4014f6: leave
-----------------------------------------------

Breakpoint 1, 0x00000000004014d8 in ?? ()

Our first breakpoint was caught properly, let's step outside this call and look further what is happening in the code:

[window2 - gdb]

=> 0x40276a: mov rax,QWORD PTR [rbp-0x970]
0x402771: mov rdi,rax
0x402774: call 0x400ef0 <strlen@plt>
0x402779: mov DWORD PTR [rbp-0x974],eax
0x40277f: mov DWORD PTR [rbp-0x978],0x0
0x402789: jmp 0x4027b6
0x40278b: mov rax,QWORD PTR [rbp-0x970]
0x402792: lea rdx,[rax+0x1]
-----------------------------------------------
0x000000000040276a in ?? ()
gdb$ x/s $rax
0x65d010: "HELP\n"

We found our input with the command "HELP" where the program is getting its length, stepping further:

[window2 - gdb]

=> 0x40281a: mov edi,0x4034a6
0x40281f: call 0x401060 <strncasecmp@plt>
0x402824: test eax,eax
0x402826: jne 0x4028a3
0x402828: mov eax,DWORD PTR [rbp-0x4a0]
0x40282e: cmp eax,0x1
0x402831: jne 0x402873
0x402833: mov eax,DWORD PTR [rbp-0x984]
------------------------------------------------
0x000000000040281a in ?? ()
gdb$ x/s 0x4034a6
0x4034a6: "USER"

gdb$ delete breakpoints
gdb$ b * 0x40281a

We stepped until we noticed a call to compare our string with 0x4034a6 with the value "USER", this is an interesting part of the program and it might try to compare other commands after that so we don't need the first breakpoints anymore and will set one at this address instead. We can also have a quick look if after the string "USER" there are other strings:

[window2 - gdb]

gdb$ x/20s 0x4034a6
0x4034a6: "USER"
0x4034ab: "Cannot change user "
0x4034c0: "send user first\n"
0x4034d1: "HELP"
0x4034d6: "login with USER first\n"
0x4034ed: "REIN"
0x4034f2: "PORT"
0x4034f7: "PASV"
0x4034fc: "STOR"
0x403501: "RETR"
0x403506: "QUIT"
0x40350b: "LIST"
0x403510: "SYST"
0x403515: "SIZE"
0x40351a: "NOOP"
0x40351f: "PWD"
0x403523: "RDF"
0x403527: "Command Not Found :(\n"
0x40353d: "[+] Creating Socket"
0x403551: "socket error"

gdb$ c
Continuing.

It looks like a list of all available commands we can run on the FTP server, if we continue we should get the output of "HELP" containing these:

[window3 - client]

USER PASS PASV PORT
NOOP REIN LIST SYST SIZE
RETR STOR PWD CWD

One command is missing from the list, "RDF". Let's see what happens when running this command and continuing after the breakpoint:

[window3 - client]

RDF
login with USER first

We need to login before running any of these commands apparently, let's try to auth ourselves. Using the command "USER" and admin we are going to step in the program once the breakpoint is caught and we see another round of string compare:

[window3 - client]

USER admin
Please send password for user admin
PASS admin


[window2 - gdb]

-stepping in a bit-

=> 0x40162d: mov QWORD PTR [rbp-0xa0],rax
0x401634: mov rax,QWORD PTR [rbp-0xa0]
0x40163b: mov QWORD PTR [rbp-0x98],rax
0x401642: mov rax,QWORD PTR [rbp-0xa0]
0x401649: mov rdi,rax
0x40164c: call 0x400ef0 <strlen@plt>
0x401651: mov DWORD PTR [rbp-0xa4],eax
0x401657: mov DWORD PTR [rbp-0xa8],0x0
----------------------------------------------------

Temporary breakpoint 12, 0x000000000040162d in ?? ()
gdb$ x/s $rax
0x65d220: "PASS admin\n"

-stepping out more-

=> 0x4016b4: movzx eax,BYTE PTR [rax]
0x4016b7: cmp al,0x20
0x4016b9: jne 0x4016c3
0x4016bb: add QWORD PTR [rbp-0xa0],0x1
0x4016c3: lea rax,[rbp-0x90]
0x4016ca: mov edx,0x4
0x4016cf: mov rsi,rax
0x4016d2: mov edi,0x403081
0x4016d7: call 0x401060 <strncasecmp@plt>
0x4016dc: test eax,eax
0x4016de: je 0x4016fa
0x4016e0: mov rax,QWORD PTR [rbp-0xb8]
0x4016e7: mov eax,DWORD PTR [rax]
0x4016e9: mov esi,0x403086
0x4016ee: mov edi,eax
----------------------------------------------------
0x00000000004016b4 in ?? ()
gdb$ x/s $rax
0x65d224: " admin\n"

gdb$ x/s 0x403081
0x403081: "PASS"

-stepping more-

=> 0x40172f: mov esi,0x40309c
0x401734: mov rdi,rax
0x401737: call 0x400e90 <strncmp@plt>
0x40173c: test eax,eax
0x40173e: jne 0x40178d
0x401740: mov rax,QWORD PTR [rbp-0xb8]
0x401747: mov rax,QWORD PTR [rax+0x28]
0x40174b: mov rdi,rax
----------------------------------------------------
0x000000000040172f in ?? ()
gdb$ x/s $rax
0x65d015: "admin"

gdb$ x/s 0x40309c
0x40309c: "blankwall"

gdb$ delete breakpoints
gdb$ b * 0x40172f
Breakpoint 17 at 0x40172f
gdb$ c
Continuing.

When running strings earlier on the binary we noticed the possibility of this user called "blankwall", this is indeed the user we need to input in order to keep going with the auth part of this program. Removing all earlier breakpoints and setting a new one there we let the program continue to use this user instead and keep on debugging:

[window3 - client]

Invalid login credentials
USER blankwall
Please send password for user blankwall
PASS admin

[window2 - gdb]

=> 0x40174b: mov rdi,rax
0x40174e: call 0x401540
0x401753: cmp eax,0xd386d209
0x401758: jne 0x40178c
0x40175a: mov rax,QWORD PTR [rbp-0xb8]
0x401761: mov DWORD PTR [rax+0x4c0],0x1
0x40176b: mov rax,QWORD PTR [rbp-0xb8]
0x401772: mov eax,DWORD PTR [rax]
----------------------------------------------------
0x000000000040174b in ?? ()
gdb$ x/s $rax
0x65d225: "admin\n"

We passed the first user check, what is this function at 0x401540 doing and why is it checking a static value of 0xd386d209 ? Probably checking our password "admin" (including newline) after generating a serial out of it. Let's have a good look at this function:

    401540:       55                      push   rbp
401541: 48 89 e5 mov rbp,rsp
401544: 48 89 7d e8 mov QWORD PTR [rbp-0x18],rdi ; var1 rbp-0x18: "admin\n"
401548: c7 45 fc 05 15 00 00 mov DWORD PTR [rbp-0x4],0x1505 ; var2 rbp-0x4: 0x1505
40154f: c7 45 f8 00 00 00 00 mov DWORD PTR [rbp-0x8],0x0 ; var3 rbp-0x8: 0 (index)
401556: eb 2a jmp 401582 <error+0x130>
401558: 8b 45 fc mov eax,DWORD PTR [rbp-0x4] ; eax = 0x1505
40155b: c1 e0 05 shl eax,0x5 ; eax << 5
40155e: 89 c2 mov edx,eax ; edx = eax
401560: 8b 45 fc mov eax,DWORD PTR [rbp-0x4] ; eax = 0x1505
401563: 8d 0c 02 lea ecx,[rdx+rax*1] ; ecx = rdx + rax
401566: 8b 45 f8 mov eax,DWORD PTR [rbp-0x8] ; eax = index
401569: 48 63 d0 movsxd rdx,eax ; QWORD rdx = DWORD eax
40156c: 48 8b 45 e8 mov rax,QWORD PTR [rbp-0x18] ; rax = var1 ("admin\n")
401570: 48 01 d0 add rax,rdx ; rax += rdx
401573: 0f b6 00 movzx eax,BYTE PTR [rax] ; eax = var1[index]
401576: 0f be c0 movsx eax,al ;
401579: 01 c8 add eax,ecx ; var1[index] + ecx
40157b: 89 45 fc mov DWORD PTR [rbp-0x4],eax ; var2 = eax
40157e: 83 45 f8 01 add DWORD PTR [rbp-0x8],0x1 ; var3 + 1 (next char)
401582: 8b 45 f8 mov eax,DWORD PTR [rbp-0x8] ; eax = var3
401585: 48 63 d0 movsxd rdx,eax ; QWORD rdx = DWORD eax
401588: 48 8b 45 e8 mov rax,QWORD PTR [rbp-0x18] ; rax = var1 ("admin\n")
40158c: 48 01 d0 add rax,rdx ; rax += rdx
40158f: 0f b6 00 movzx eax,BYTE PTR [rax] ; eax = var1[index]
401592: 84 c0 test al,al ; reached null byte?
401594: 75 c2 jne 401558 <error+0x106> ; keep looping
401596: 8b 45 fc mov eax,DWORD PTR [rbp-0x4]
401599: 5d pop rbp
40159a: c3 ret

Our input "admin" (including newline) after this function returns:

    => 0x401753:    cmp    eax,0xd386d209
0x401758: jne 0x40178c
0x40175a: mov rax,QWORD PTR [rbp-0xb8]
0x401761: mov DWORD PTR [rax+0x4c0],0x1
0x40176b: mov rax,QWORD PTR [rbp-0xb8]
0x401772: mov eax,DWORD PTR [rax]
0x401774: mov esi,0x4030a6
0x401779: mov edi,eax

gdb$ p/x $eax
$4 = 0xf1728e58

Our input returned 0xf1728e58, after making a python script to generate this serial and check if it's matching to be sure we are doing it correctly I ended up with this:

#!/usr/bin/env python

import itertools, string

def enc(password):
var1 = password
var2 = 0x1505
for var3 in var1:
var2 = (var2 << 5) + var2 + ord(var3)
return var2

print "0x%X\n" % (enc("admin\n") & 0xFFFFFFFF)
mrt:~/ctf/csaw/reverse/ftp$ ./enc_pass.py
0xF1728E58

It's matching, so now we know how to generate the serial correctly from our input and while it seems really simple to reverse I just couldn't do it even after hours...

I ended up using the serial generation to bruteforce and match the final value it should have: 0xd386d209

Not really impressive I know, but it worked and I was going crazy over this.

#!/usr/bin/env python

import itertools, string

def enc(password):
password += chr(0xA)
var1 = password
var2 = 0x1505
for var3 in var1:
var2 = (var2 << 5) + var2 + ord(var3)
return var2

# http://stackoverflow.com/a/11747419
def bruteforce(charset, maxlength):
return (''.join(candidate)
for candidate in itertools.chain.from_iterable(itertools.product(charset, repeat=i)
for i in range(1, maxlength + 1)))

for attempt in bruteforce(string.ascii_lowercase, 10):
print "{}\r".format(attempt),
if (enc(attempt) & 0xFFFFFFFF ) == 0xD386D209:
print attempt
break
mrt:~/ctf/csaw/reverse/ftp$ ./enc_pass.py
cookie

I first tried lowercase charset and it worked and we had our full credentials now. Let's login as "blankwall" and use the password "cookie" and finally try that unlisted command we found:

mrt:~/ctf/csaw/reverse/ftp$ nc 54.175.183.202 12012
Welcome to FTP server
USER blankwall
Please send password for user blankwall
PASS cookie
logged in
RDF
flag{n0_c0ok1e_ju$t_a_f1ag_f0r_you}

We got our flag:

flag{n0_c0ok1e_ju$t_a_f1ag_f0r_you}