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}