tmxklab
[Pwnable.xyz] BabyVM 본문
1. 문제
nc svc.pwnable.xyz 30044
1) mitigation 확인
2) 문제 확인
- default program을 실행 시킬 것인지 물어봄
- 'y'를 입력한 경우 : 이름을 입력하고 프로그램 종료
- 'n'를 입력한 경우 : program을 보내기 위해 입력을 받을 수 있음
3) 코드흐름 파악
3-1) 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, 255uLL);
}
else
{
puts("Send your program");
program_size = read_buffer(program, 4095uLL);
}
puts("Running the vm");
run_vm(program, program_size);
return 0;
}
- default 프로그램을 실행시킬 것인지 선택할 수 있음
- 'y'를 입력한 경우 memcpy에 의해 program변수에 default_program변수의 값이 복사됨
- 'y'가 아닌 다른 값을 입력한 경우 program변수에 4095bytes만큼 입력을 받음
- 마지막으로 run_vm함수의 파라미터로 program변수와 size값을 받고 종료
3-2) 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, 0x7FFFFFFEF000LL, 0x10000LL, 7LL),
(err = (unsigned int)uc_mem_write(uc, 0x400000LL, program, program_size)) != 0)
|| (uc_hook_add(uc, &trace1, 2LL, hook_syscall, 0LL, 1LL, 0LL, 699LL),
uc_reg_write(uc, 44LL, &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);
}
- 여기서 uc로 시작하는 함수들이 다수 보이는 것을 확인
- 밑에 접근방법에서 설명할 예정
3-3) default_program
- 너무 많아서 일부만 가져왔음, 뒤에서 설명할 예정
2. 접근방법
1) unicorn-engine관련 함수들
unicorn-engine이란?
→ CPU 에뮬레이터 프레임워크로 CPU 에뮬레이터는 소프트웨어에서 물리적 CPU의 내부 작동을 에뮬레이트하는 프로그램이다
→ 메모리를 매핑하고 수동으로 데이터를 사용해야하며 선택한 주소에서 에뮬레이션을 시작
→ 실제 CPU를 사용하지 않고 코드를 에뮬레이트할 수 있음
다음과 같은 상황에서 유용
→ 유해한 프로세스를 만들지 않고 멜 웨어에 특정 기능을 호출할 수 있음
→ 간단한 쉘 코드나 특정 함수 퍼징 수행할 때 사용
→ 난독 처리된 코드 에뮬레이션
코드상에 존재하는 unicorn-engine관련 함수)
-
uc_err uc_open(uc_arch arch, uc_mode mode, uc_engine uc);
- description : Create new instance of unicorn engine
-
uc_err uc_close(uc_engine uc);
- description : Close uc instance
-
const(char) uc_strerror(uc_err code);
- description : Return a string describing given error code
-
uc_err uc_mem_map(uc_engine uc, ulong address, size_t size, uint perms);
- description : Map memory in for emulation
-
uc_err uc_hook_add(uc_engine *uc, uc_hook *hh, int type, void callback, void user_data, ulong begin, ulong end, ...);
- description : Register callback for a hook event, The callback will be run when the hook event is hit
-
uc_err uc_reg_write(uc_engine uc, int regid, const void value);
- description : Write to register
-
uc_err uc_emu_start(uc_engine uc, ulong begin, ulong until, ulong timeout, size_t count);
- description : Emulate machine code in specific duration of time
추가로 API함수들에 대해 더 자세히 알고 싶으면 밑에 사이트 참고
참고 사이트)
- 공식 홈페이지
- API 함수 설명
- 소스코드
- etc
이제 다시 문제로 돌아와서 차근차근 코드흐름을 파악해보자
err = (unsigned int)uc_open(4LL, 8LL, &uc);
- 유니콘 엔진의 인스턴스를 생성하는 함수로 arch는 UC_ARCH_X86, mode는 UC_MODE_64로 설정하여 uc파라미터에 인스턴스를 생성한다.
인스턴스 생성 후 if문안의 uc관련 함수들을 실행
(if문안의 함수들이 실패할 경우 if문안의 코드 실행)
if ( err
|| (uc_mem_map(uc, 0x400000LL, 0x10000LL, 7LL),
uc_mem_map(uc, 0x7FFFFFFEF000LL, 0x10000LL, 7LL),
(err = (unsigned int)uc_mem_write(uc, 0x400000LL, program, program_size)) != 0)
|| (uc_hook_add(uc, &trace1, 2LL, hook_syscall, 0LL, 1LL, 0LL, 699LL),
uc_reg_write(uc, 44LL, &rsp_0),
(err = (unsigned int)uc_emu_start((__int64)uc, 0x400000LL, program_size + 0x400000, 0LL, 0LL)) != 0) )
{
...
}
① 첫 번째 uc_mem_map : 인스턴스에 매핑할 메모리 영역의 시작주소 0x400000과 size는 0x10000, permission은 7이므로 UC_PROT_READ | UC_PROT_WRITE | UC_PROT_EXEC로 설정
② 두 번째 uc_mem_map : 인스턴스에 매핑할 메모리 영역의 시작주소 0x0x7FFFFFFEF000과 size는 0x10000, permission은 7이므로 UC_PROT_READ | UC_PROT_WRITE | UC_PROT_EXEC로 설정
③ uc_mem_write : 시작 메모리 주소를 0x400000로 세팅하고 size만큼 메모리에 쓸 데이터인 program변수를 받음
④ uc_hook_add : 후크 이벤트에 대한 콜백함수를 등록하는 함수, 후크가 실행될 때 에뮬레이션은 일시적으로 중지
- param
- uc_engine : uc_open()에 의해 리턴된 인스턴스(uc)
- hh : hook handle(trace1)
- type : hook type(2) → UC_HOOK_INSN(2)
- callback : hook, 사용자가 정의한 함수(hook_syscall)
- user_data : 사용자가 정의한 데이터, 콜백함수에 전달됨(0)
- begin : 콜백이 적용되는 영역의 시작 주소(1)
- end: 콜백이 적용되는 영역의 끝 주소(0)
- 추가 : [begin, end]범위에 있는 경우에만 콜백이 호출, begin > end인 경우 hh(hook)는 주소 영역 체크를 패스함
- ... : 추가 인수(699)
- 추가 : type이 UC_HOOK_INSN인 경우 UC_ARCH_INS_XXX
- 699 이므로 UC_X86_INS_SYSCALL
- 참고 : https://github.com/unicorn-engine/unicorn/blob/master/include/unicorn/x86.h
⑤ uc_reg_write : 수정될 레지스터 ID(44 → UC_X86_REG_RSP)값과 regid를 등록하도록 설정할 값에 대한 포인터 rsp_0변수를 파라미터로 넘겨줌
- 참고 : include/unicorn/x86.h → enum uc_x86_reg{...}
⑥ uc_emu_start : 특정 기간동안 머신 코드 에뮬레이트 시작
- param
- uc_engine : uc_open에 의해 리턴된 인스턴스(uc)
- begin : 에뮬레이션이 시작되는 주소(0x400000LL)
- until : 에뮬레이션이 중지되는 주소(이 주소에 도달할 때)(program_size + 0x400000)
- timeout : 코드를 에뮬하는 기간(마이크로 초), 0이면 코드가 끝날 때까지 무한한 시간에 코드를 에뮬함(0LL)
- count : 에뮬레이션이 수행할 명령의 수, 값이 0이면 코드가 완성될 때까지 사용 가능한 모든 코드 에뮬(0LL)
간략한 설명)
- uc_open을 통해 유니콘 엔진 인스턴스 생성
- 두 번의 uc_mem_map을 통해 0x40000, 0x0x7FFFFFFEF000 메모리 영역에 각각 사이즈 0x10000만큼 매핑(이 때, 권한은 모두 rwx)
- uc_mem_write를 통해 program변수에 저장된 값(쉘 코드)을 0x40000에 적용
- uc_hook_add를 통해 syscall에 대한 후크 인터럽트 생성 → hook_syscall()
- uc_reg_write를 통해 RSP레지스터 값을 설정(이 때, rsp_0변수는 0x7FFFFFFFE000LL로 초기화되어 있는 상태)
- uc_emu_start를 통해 에뮬레이션을 시작하며 에뮬이 시작되는 주소는 0x400000이며 끝나는 주소는 0x400000 + programsize이다. (에뮬 기간과 수행할 명령의 수는 무제한)
마지막으로 uc_hook_add에 등록한 사용자 정의 hook_syscall 코드는 다음과 같다.(해당 코드는 syscall이 발생할 경우 인터럽트로 다음과 같이 처리)
void __cdecl hook_syscall(uc_engine *uc, void *user_data)
{
uint64_t rax_0; // [rsp+10h] [rbp-10h]
unsigned __int64 v3; // [rsp+18h] [rbp-8h]
v3 = __readfsqword(0x28u);
uc_reg_read(uc, 35LL, &rax_0);
if ( rax_0 == 1 )
{
sys_write(uc);
}
else if ( rax_0 < 1 )
{
sys_read(uc);
}
else if ( rax_0 == 2 )
{
sys_open(uc);
}
else
{
printf("ERROR: unknown syscall rax=0x%lx\n", rax_0);
}
}
- uc_reg_read를 통해 검색할 레지스터 ID 35LL(UC_X86_REG_RAX)를 등록하여 rax_0변수에 레지스터 값을 저장한다.
- rax_0의 값에 따라 system call table에 존재하는 write, read, open함수를 사용할 수 있다.
결국엔 x86기반의 64bit쉘 코드를 실행시킬 수 있도록 해주는 것 같다. 만일, default_program을 실행하면 default_program의 쉘 코드를 실행시켜주고 여기서 uc_hook_add를 통해 syscall 인터럽트 발생시 hook_syscall함수를 실행시키는 것 같다.
이제 default_program을 설명하면
1) "what is your name?"을 출력
2) 0x20만큼 입력을 받음
3, 4) "Hi"와 입력한 값을 출력
5) [rip+0x77]에 있는 값("./flag")을 filename으로 받아 파일 오픈
6) 오픈한 파일에 대해 0x200만큼 입력으로 [rbp-0x200]에 저장
7) "Reading ./flag..."을 출력
8, 9) [rbp-0x200]에 있는 값을 출력하고 종료
이 때, 여기서 발생한 syscall은 uc_hook_add함수에 등록한 hook_syscall함수로 인터럽트 처리된다.
따라서, 모든 syscall은 sys_read, sys_write, sys_open 총 3가지 밖에 사용할 수 없다.
플래그 값을 찾을 수 없는 이유)
그럼 이상한 부분을 눈치챌 수 있다.
sys_open을 통해서 ./flag를 오픈했는데 왜 flag파일의 내용에는 "this_is_not_flag"라고 출력이 되는 것일까??
(참고로 win함수에서도 cat flag가 있음)
이 부분을 따라가기 위해서 먼저 hook_syscall() 확인
void __cdecl hook_syscall(uc_engine *uc, void *user_data)
{
uint64_t rax_0; // [rsp+10h] [rbp-10h]
unsigned __int64 v3; // [rsp+18h] [rbp-8h]
v3 = __readfsqword(0x28u);
uc_reg_read(uc, 35LL, &rax_0);
if ( rax_0 == 1 )
{
sys_write(uc);
}
else if ( rax_0 < 1 )
{
sys_read(uc);
}
else if ( rax_0 == 2 )
{
sys_open(uc);
}
else
{
printf("ERROR: unknown syscall rax=0x%lx\n", rax_0);
}
}
- 여기서 syscall에 대한 인터럽트를 처리하는 함수를 확인할 수 있다.
- sys_write() → do_write()
- sys_read() → do_read()
- sys_open() → do_open()
각 do로시작하는 함수에서 open_files와 files변수가 나옴
[open_files]
[files]
- 구조체 배열, files[10]
- 여기서 files[0], files[1]에는 ops.fops_read, write에 std_read, write주소 값인 반면에 files[2]의 ops.fops_read, write에는 file_read, write주소 값이 저장됨을 확인
- 인덱스 0, 1, 2에는 각각 fd값이 0, 1, 2로 설정됨
- files[2]의 path에 "./flag"문자열이 저장된 것을 확인
- contents멤버 변수에 "this_is_not_the_flag"값 확인
file open을 할 때, files[2]변수의 값을 사용하므로 파일 읽고 쓰기 할 때 files[0, 1]과는 달리 file_read, write함수를 사용한다.
[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]
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;
}
- 뒤에서 다시 설명하겠지만 이 부분에 취약점이 발생한다. -> (off by one)
"this_is_not_flag"값이 출력되는 프로세스 정리)
1) file open할 때, rax값은 0x2를 주고 syscall을 한다.
2) 그러면, 인터럽트가 발생하여 hook_syscall함수가 실행되고
3) if문에 의해 rax값이 0x2이면 sys_open을 하게 된다.
4) sys_open함수에서 또 다시 do_open함수를 호출하고(이 때, rdi값에는 "./flag"문자열이 저장된 주소 값을 do_open의 path파라미터로 들어감)
5) do_open함수에서는 files[2].path의 값과 "./flag"문자열이 동일하면 files[2]의 fd값을 반환한다.
for ( i = 0; i < open_files; ++i )
{
if ( !strcmp(files[i].path, path) )
return files[i].fd;
}
6) fd값은 0x2이므로 rax값은 0x2로 세팅된다.
7) 그리고 sys_read, sys_write를 하게 된다.
8) 따라서, files[2]의 contents멤버 변수에 들어있는 "this_is_not_flag"값이 출력된다.
따라서 실제로 ./flag 파일을 open한 것이 아니라 files[2]의 contents값을 가져오는 것이다.
현재 syscall할 수 있는 것은 sys_read, write, open뿐이며 실제 ./flag파일을 open하려면 win함수를 이용하는 방법밖에 없다.
3. 풀이
1) files_write 취약점
files_write함수를 확인하기 전에 먼저 files구조체 배열의 구조를 살펴보자
총 크기 : 0x240
1) fd : 0x0 ~ 0x3(4bytes)
2) path : 0x4 ~ 0x27(36bytes)
3) contents : 0x28 ~ 0x227(512bytes)
4) size : 0x228 ~ 0x22f(8bytes)
5) ops : 0x230 ~ 0x23f(16bytes)
[files_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;
}
- 현재 files[2]의 size값에는 0x14로 입력되어 있는 상태이다.
- files_write함수를 실행하게 되면 size값이 0x14보다 크며 0x200bytes보다 크면 size값을 0x200만큼 재설정해준다.
- 그리고 memcpy를 통해 스택에 있는 값(buffer)을 files[2]의 contents값에 0x200 + 1bytes만큼 저장하게 된다.
- 이 부분에서 one byte overflow가 발생한다.
위의 files의 구조를 봤듯이 contents에 값을 입력할 수 있는 최대 크기는 0x200이며 1byte를 넘게 되면 size값을 건드릴 수가 있다.
따라서, 0x200(512bytes)보다 큰 값을 넣으면 sizea = 0x200으로 세팅되며 files[2].size = 0x200으로 바뀌고 contents에 0x201만큼 입력을 넣게 되면 1byte overflow가 발생하여 files[2]의 size멤버 변수에 0x2(??) 로 바뀌게 된다.
예를 들어 하위 1byte값을 0xff로 설정하면 0x2ff(767)bytes만큼 값을 contents 시작 위치에 넣을 수 있으며 size, ops부분을 모두 건드릴 수 있게 된다.
- 빨간색 : fd
- 노란색 : path
- 초록색 : contents
- 파란색 : size
- 보라색 : ops
files의 ops부분에는 각 함수의 포인터 값이 저장되어 있으며 read 또는 write관련 syscall이 발생할 경우 files ops의 files_read, write함수를 실행시킨다.
결론)
우리는 위의 1byte overflow를 이용하여 files[2].ops까지 모두 건드릴 수 있으므로 ops에 win함수 주소 값을 입력하고 syscall(read 또는 write)하면 win함수를 실행 시킬 수 있을 것이다.
2) 익스코드
익스코드는 거의 롸업 보면서 했다..
참고 사이트 :
https://mineta.tistory.com/135?category=735547
'War Game > Pwnable.xyz' 카테고리의 다른 글
[Pwnable.xyz] message (0) | 2020.09.09 |
---|---|
[Pwnable.xyz] UAF (0) | 2020.09.09 |
[Pwnable.xyz] iape (0) | 2020.05.03 |
[Pwnable.xyz] strcat (0) | 2020.05.03 |
[Pwnable.xyz] J-U-M-P (0) | 2020.05.03 |