tmxklab

[Pwnable.xyz] BabyVM 본문

War Game/Pwnable.xyz

[Pwnable.xyz] BabyVM

tmxk4221 2020. 6. 23. 20:29

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함수들에 대해 더 자세히 알고 싶으면 밑에 사이트 참고

 

참고 사이트)

 

이제 다시 문제로 돌아와서 차근차근 코드흐름을 파악해보자

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)

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_filesfiles변수가 나옴

 

[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
Comments