概述
Reversing.kr是一个韩国的逆向题练习网站,题目的质量还是比较好的,比较费时间去破解,这里给出我在刷到SimpleVM这道题目的时候的一些心得和体会。
SimpleVM
###0
拿到文件之后拖入ida分析,发现报错,illegal program entry point(C023DC)。我们继续打开文件,发现程序在00C023C7的时候就中断了,所以会报这个不合法的入口点,我们通过readelf命令可以清楚的查看到program header的信息(FileSize 为13C7):
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x00c01000 0x00c01000 0x013c7 0x013c7 RWE 0x1000
LOAD 0x00019c 0x0804b19c 0x0804b19c 0x00000 0x00000 RW 0x1000
但是有一点是这样的,Linux中文件/内存映射总是页面大小的倍数,在x86上通常是4k。此处的映射长度0x13c7将四舍五入为页面大小的倍数,这意味着0x2000字节将被映射。所以其实我们通过010editor去查看该文件是可以看到(c023dc-c01000) = 13dc是有数据的。我们去更改一下FileSize和MemSize,之后拖入ida发现是正确的。我们在C023DC出查看代码如下:
; DATA XREF: LOAD:00C01018↑o
LOAD:00C023DC mov dword_C01BF0, 252E8h
LOAD:00C023DC ; ---------------------------------------------------------------------------
LOAD:00C023E6 db 0E9h
LOAD:00C023E7 db 5
LOAD:00C023E8 db 0F8h
LOAD:00C023E9 db 0FFh
LOAD:00C023E9 LOAD ends
后面的db没有被翻译出来,但是我们通过E9命令这是一个跳转的jmp命令,从而进入正常的程序流程。
###1
我们利用ida的linux_server进行虚拟机的远程调试,直接启动,ctrl+s查看段信息如下:
看LOAD段完全没有任何的突破口,但是观察到debug001和debug002这两个段。我们进去看一下,发现前面几个字节0x7f,0x45,0x4c,0x46,一个明显的elf文件我们得把他dump下来,基本的ida script模板如下:
static main(void)
{
auto fp, begin, end, dexbyte;
fp = fopen("D:\\dump.txt", "wb");
begin = 0x8048000;
end = 0x804c000;
for ( dexbyte = begin; dexbyte < end; dexbyte ++ )
fputc((dexbyte), fp);
}
得到一个dump文件。
###2
我们将dump文件拖入ida分析,shift+f12查看字符串,好的“input”字符串没有被加密,直接出来,我们定位到该函数sub_8048556().我们发现所有的函数都被重新加密了,我们看input的这个函数,
sub_8048460(1, (int)"Input : ", 8);
明显可以得出它是类似与printf()的一个函数,我这里采用了一个动态调试的方法去跟踪该函数。首先记录下该函数的位置为8048460,接着利用ida运行起来这个文件,定位到8048460这个点。快捷键c一下,code出来为jmp off_804B018,跟进可知off_804B018的值为F7E1D3C0,显然这是一个so文件的函数,我们ctrl+s发现这个函数存在libc_2.23.so中。去查看Modules窗口查看libc_2.23.so,好像并不能查看name,无妨,我们从虚拟机中拷贝一份libc_2.23.so拖入ida分析,通过F7E1D3C0与libc_2.23.so的首地址的偏移量去查找,发现这个函数就是libc中的write函数。利用这个方法,我们可以得出大部分的系统调用函数出来,大致如下:
sub_8048460 -- write()
sub_8048470 -- pipe()
sub_8048480 -- fork()
sub_8048400 -- read()
同时通过动态调试将类似与下方的代码分析称相应的字符串输出即可:
for ( i = 1; i <= 6; ++i )
{
v8 = byte_804B074[i - 1] ^ i;
write(1, (int)&v8, 1);
}
整个函数翻译如下:
unsigned int sub_8048556()
{
v16 = __readgsdword(0x14u);
if ( getuid() )
{
v9 = 0;
for ( i = 1; i <= 14; ++i )
{
v8 = access_denyed[i - 1] ^ i;
write(1, (int)&v8, 1);
}
}
else
{
write(1, (int)"Input : ", 8);
if ( pipe((int)&v3) != -1 && pipe((int)&v5) != -1 )
{
v7 = fork();
if ( v7 == -1 )
{
v9 = 0;
for ( i = 1; i <= 6; ++i )
{
v8 = error[i - 1] ^ i;
write(1, (int)&v8, 1);
}
}
else if ( v7 )
{
v13 = 0;
v14 = 0;
v15 = 0;
read(v3, (int)&v13, 9);
read(v3, (int)&dword_804B0A0, 200);
for ( i = 0; i <= 199; ++i )
*(_BYTE *)(i + 134525088) ^= 0x20u;
dword_804B0A0 = v13;
dword_804B0A4 = v14;
for ( i = 0; i <= 199; ++i )
*(_BYTE *)(i + 134525088) ^= 0x10u;
if ( sub_8048C6D() == 1 )
{
if ( dword_804B190 )
{
v9 = 0;
for ( i = 1; i <= 9; ++i )
{
v8 = Correct[i - 1] ^ i;
write(1, (int)&v8, 1);
}
}
else
{
v9 = 0;
for ( i = 1; i <= 6; ++i )
{
v8 = Wrong[i - 1] ^ i;
write(1, (int)&v8, 1);
}
}
}
else
{
v9 = 0;
for ( i = 1; i <= 6; ++i )
{
v8 = Wrong[i - 1] ^ i;
write(1, (int)&v8, 1);
}
}
}
else
{
v10 = 0;
v11 = 0;
v12 = 0;
read(0, (int)&v10, 10);
if ( (_BYTE)v12 )
{
v9 = 0;
for ( i = 1; i <= 6; ++i )
{
v8 = Wrong[i - 1] ^ i;
write(1, (int)&v8, 1);
}
}
else
{
write(v4, (int)&v10, 9);
for ( i = 0; i <= 199; ++i )
{
v0 = sub_80489AA(*(unsigned __int8 *)(i + 134525088), 3);
*(_BYTE *)(i + 134525088) = v0;
}
sub_8048410(dword_804B180);
write(v4, (int)&dword_804B0A0, 200);
}
}
}
else
{
v9 = 0;
for ( i = 1; i <= 6; ++i )
{
v8 = error[i - 1] ^ i;
write(1, (int)&v8, 1);
}
}
}
v2 = __readgsdword(0x14u);
result = v2 ^ v16;
if ( v2 != v16 )
result = sub_8048420();
return result;
}
###3
好了,代码差不多出来了,现在先分析代码的功能。首先分析:
if ( pipe((int)&v3) != -1 && pipe((int)&v5) != -1 )
{
v7 = fork();
看出来程序先建立v3和v5两个变量的管道,如果建立成功,fork出一个进程,这两个进程利用v3和v5两个变量进行数据的交互。然后我们分别分析这两个进程要做的事情。如下代码:
v10 = 0;
v11 = 0;
v12 = 0;
read(0, (int)&v10, 10);
if ( (_BYTE)v12 )
{
v9 = 0;
for ( i = 1; i <= 6; ++i )
{
v8 = Wrong[i - 1] ^ i;
write(1, (int)&v8, 1);
}
}
else
{
write(v4, (int)&v10, 9);
for ( i = 0; i <= 199; ++i )
{
v0 = sub_80489AA(*(unsigned __int8 *)(i + 134525088), 3);
*(_BYTE *)(i + 134525088) = v0;
}
sub_8048410(dword_804B180);
write(v4, (int)&dword_804B0A0, 200);
}
这段代码作用是read()输入的flag,然后通过管道1的写入v4,向进程2写入flag和写入dword_804B0A0的200个字节。当然有输入就得有接受,我们继续看进程2的接受代码。
read(v3, (int)&v13, 9);
read(v3, (int)&dword_804B0A0, 200);
for ( i = 0; i <= 199; ++i )
*(_BYTE *)(i + 0x804B0A0) ^= 0x20u;
dword_804B0A0 = v13;
dword_804B0A4 = v14;
for ( i = 0; i <= 199; ++i )
*(_BYTE *)(i + 0x804B0A0) ^= 0x10u;
if ( sub_8048C6D() == 1 )
{
if ( dword_804B190 )
看出来这个进程是判断flag的正确性,首先接受flag到v13变量中,然后在接受进程1给的200字节到804b0a0中,之后做两次的异或操作,其中 dword_804B0A0 = v13; dword_804B0A4 = v14;两行代码将flag写入到dword_804B0A0的前八个字节,得出flag最多为8个字节,最后利用sub_8048C6D函数进行flag的判断。可以得出关键的代码在sub_8048C6D函数之中,我们跳进去看一下:
signed int sub_8048C6D()
{
signed int result; // eax
while ( 2 )
{
sub_8048A48();
switch ( dword_804B190 )
{
case 2:
sub_8048B92();
continue;
case 6:
sub_8048ABB();
continue;
case 7:
sub_8048B31();
continue;
case 9:
sub_8048BCE();
continue;
case 10:
sub_8048C13();
continue;
case 11:
sub_8048C22();
result = 1;
break;
default:
result = 0;
break;
}
break;
}
return result;
}
好吧,我承认非常麻烦,但是既然是SimpleVM,复杂度肯定没有那么高,由于硬看代码对于我来说太难了,而ida的动态调试又没有那么的舒服,通过查看这几个函数,发现只有一些运算而已,所以我果断把这些函数统统f5,而涉及内存的又只有write过来的804b0a0中的200个字节,我把这200个字节在内存中dump下来存成一个文件,之后我们所有的相关代码都拷贝到一个cpp里面,利用visual studio进行一个动态的调试。(f5太强大,没办法= =)
###4
有了原生可调式的cpp,还有什么比这更爽的事情,我们开始,断点在sub_8048A48();函数上面(我把它在代码中取名叫做password_flag_change()),然后我们将那200个字节c_804B0A0放入监视器中不断的监视它的变化。
然后我们一步一步跟进。跟进的时候我们发现了非常关键的一个判断在如下函数中:
int sub_8048B31()
{
sub_8048A48();
sub_8048A0B();
dword_804B198 = dword_804B190;
sub_8048A48();
sub_8048A0B();
dword_804B194 = dword_804B190;
dword_804B198 = dword_804B198 == dword_804B190;
dword_804B190 = 8;
return sub_8048A2F();
}
dword_804B198 = dword_804B198 == dword_804B190;
这是一个是否相等的判断,它决定了dword_804B198是0还是1,所以跟踪dword_804B198和 dword_804B190两个变量的值是非常关键的一步。好了,有了跟踪的目标就非常轻松了,我们重新跟进,去寻找dword_804B198和dword_804B190的两个变量值的变化。在sub_8048A48()中,如下代码被执行了非常多次
password_flag = (unsigned __int8)(*(c_804B0A0 + (c_804B0A0[9] ^ 0x10)) ^ 0x10);
v0 = (c_804B0A0[9] ^ 0x10) + 1;
result = v0 ^ 0x10;
c_804B0A0[9] = v0 ^ 0x10;
return result;
监视一下 (c_804B0A0[9] ^ 0x10),发现它从10开始不断往上涨,而password_flag 的值等于c_804B0A0[c_804B0A0[9] ^ 0x10] ^ 0x10 ,所以我们观察c_804B0A0[10] 到c_804B0A0[199],发现它比较有规律。我们断点直接设在
dword_804B198 = dword_804B198 == dword_804B190;
这句话上。发现dword_804B190 = 9,dword_804B198 = 81。所以只要我们注意这两个值从哪儿来的就可以了,重新步进。发现如下过程:
dword_804B190= (unsigned __int8)(*(c_804B0A0 + (c_804B0A0[9] ^ 0x10)) ^ 0x10);//这句话在c_804B0A0[9] ^ 0x10 = 18时候dword_804B190为 =c_804B0A0[18]^0x10 = 96
...
dword_804B194= dword_804B190;;//之后通过这句话又将96存到dword_804B194中
...
v0 = c_804B0A0[dword_804B190] ^ 0x10;
dword_804B190= v0;//又通过这两句句话将c_804B0A0[0]也就是flag的第一位取出来放到dword_804B190中
···
dword_804B190^= dword_804B194;//接着又与dword_804B194异或,dword_804B194为刚开始的96,最终dword_804B190将得到81
···
dword_804B190= (unsigned __int8)(*(c_804B0A0 + (c_804B0A0[9] ^ 0x10)) ^ 0x10);//这句话在c_804B0A0[9] ^ 0x10 = 24时候dword_804B190为 =c_804B0A0[18]^0x10 = 9
之后的代码不逐一分析,功能是将81和9分别存在了c_804B0A0[0]和c_804B0A0[7]上(这里可以确定flag只需要7个字节就可以了),最后在
dword_804B198 = dword_804B198 == dword_804B190;
这句话中将其取出,分别赋值dword_804B198和dword_804B190。
我们整理一下,可以得出要使得dword_804B198和dword_804B190相等,那么就是c_804B0A0[9] ^ 0x10 = 18和c_804B0A0[9] ^ 0x10 = 24的时候
即
c_804B0A0[18] ^ flag[0] == c_804B0A0[24];
好了,flag出来了,按照这个思路,我们可以逐步的跟踪到flag[1],flag[2]等于哪两个字符的异或了。
我们写出以下的代码来得到flag:
printf("%c%c%c%c%c%c%c", c_804B0A0[18] ^ c_804B0A0[24], c_804B0A0[32] ^ c_804B0A0[38], c_804B0A0[46] ^ c_804B0A0[52], c_804B0A0[60] ^ c_804B0A0[66], c_804B0A0[74] ^ c_804B0A0[80], c_804B0A0[88] ^ c_804B0A0[94], c_804B0A0[102] ^ c_804B0A0[108]);
return 0;
总结
做VM还是比较耗费时间的,虽然名为SimpleVM,额,还是要花一定的时间去慢慢了解这个机制是怎么样的,所以的话,逆向还是需要耐心的。最后贴一下我用的c代码:
// ConsoleApplication1.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include "string.h"
int password_flag = -1;
int result;
unsigned char c_804B0A0[200];
int int_198;
int int_194;
int int_18c;
int sub_8048A92()
{
unsigned __int8 v0; // ST0F_1
int result; // eax
v0 = password_flag + (c_804B0A0[10] ^ 0x10);
result = v0 ^ 0x10;
c_804B0A0[9] = v0 ^ 0x10;
return result;
}
int sub_8048A2F()
{
int result; // eax
result = password_flag;
c_804B0A0[password_flag] = int_198 ^ 0x10;
return result;
}
int password_flag_change()
{
unsigned __int8 v0; // ST0F_1
int result; // eax
password_flag = (unsigned __int8)(*(c_804B0A0 + (c_804B0A0[9] ^ 0x10)) ^ 0x10);
v0 = (c_804B0A0[9] ^ 0x10) + 1;
result = v0 ^ 0x10;
c_804B0A0[9] = v0 ^ 0x10;
return result;
}
int password_flag_change2()
{
unsigned __int8 v0; // ST0F_1
int result; // eax
v0 = c_804B0A0[password_flag] ^ 0x10;
result = v0;
password_flag = v0;
return result;
}
int func1()
{
password_flag_change();
int_198 = password_flag;
password_flag_change();
int_194 = password_flag;
password_flag = int_198;
int_198 = int_194;
return sub_8048A2F();
}
int func2()
{
int_198 = password_flag;
int_18c = int_194;
password_flag_change();
int_18c = password_flag;
password_flag_change();
password_flag_change2();
int_194 = password_flag;
password_flag = int_18c;
password_flag_change2();
password_flag ^= int_194;
int_198 = password_flag;
password_flag = int_18c;
return sub_8048A2F();
}
int func3()
{
password_flag_change();
password_flag_change2();
int_198 = password_flag;
password_flag_change();
password_flag_change2();
int_194 = password_flag;
int_198 = int_198 == password_flag;
password_flag = 8;
return sub_8048A2F();
}
int func4()
{
int result; // eax
int_198 = password_flag;
password_flag_change();
int_198 = password_flag;
password_flag = 8;
password_flag_change2();
result = password_flag;
if (!password_flag)
{
password_flag = int_198;
result = sub_8048A92();
}
return result;
}
int func5()
{
password_flag_change();
return sub_8048A92();
}
int func6()
{
int result; // eax
password_flag = 0;
password_flag_change2();
int_198 = password_flag;
password_flag = 1;
password_flag_change2();
int_194 = password_flag;
password_flag = int_198;
result = int_194;
int_198 = int_194;
return result;
}
int main()
{
FILE *file = fopen("E://ctf//reversingkr//17//dumped", "r");
for (int i = 0; i <= 199; ++i)
{
c_804B0A0[i] = fgetc(file);
}
for (int i = 0; i <= 199; ++i)
c_804B0A0[i] ^= 0x20u;
c_804B0A0[0] = '1';
c_804B0A0[1] = '2';
c_804B0A0[2] = '3';
c_804B0A0[3] = '4';
c_804B0A0[4] = '5';
c_804B0A0[5] = '6';
c_804B0A0[6] = '7';
c_804B0A0[6] = '8';
for (int i = 0; i <= 199; ++i)
c_804B0A0[i] ^= 0x10u;
printf("%c%c%c%c%c%c%c", c_804B0A0[18] ^ c_804B0A0[24], c_804B0A0[32] ^ c_804B0A0[38], c_804B0A0[46] ^ c_804B0A0[52], c_804B0A0[60] ^ c_804B0A0[66], c_804B0A0[74] ^ c_804B0A0[80], c_804B0A0[88] ^ c_804B0A0[94], c_804B0A0[102] ^ c_804B0A0[108]);
return 0;
while (2)
{
password_flag_change();
switch (password_flag)
{
case 2:
func1();
continue;
case 6:
func2();
continue;
case 7:
func3();
continue;
case 9:
func4();
continue;
case 10:
func5();
continue;
case 11:
func6();
result = 1;
break;
default:
result = 0;
break;
}
break;
}
if (result == 1)
{
if (password_flag)
{
printf("Correct!\n");
}
else
{
printf("Wrong!\n");
}
}
return 0;
}
*本文作者:foyjog,转载请注明来自 FreeBuf.COM
来源:freebuf.com 2018-03-30 15:00:15 by: foyjog
请登录后发表评论
注册