对hgame-week4-happyVM的思考

hgame-2019-wp归总:https://www.danisjiang.xyz/2019/02/22/hgame-2019-wp%E5%BD%92%E6%80%BB/

这篇文章是我一边做一边思考的时候一边写的,这就是为什么这篇文章会这么长,而且前面云里雾里的

IDA截图


没有截全,但是差不多就是这样

第一次看到题目的思考

现在是晚上10点,我准备开始做week4的RE题,第一题我昨天看了几眼,发现有几个点很难实现,所以暂时先搁置一边

查了一下,VM是虚拟机的意思。

但是最初看题目实在找不到所谓的虚拟机到底指什么,看了网上的很多VM re的题目都跟本题很像,但是我还是想不出来之所以被称为VM的原因是什么

整个对字符串的处理的函数受一个因素的影响,那就是一个表,表就像一个程序的流程,每一个byte代表一种处理,每运行完一个函数就将指针指向表中的下一个数据,这就有点虚拟机的意思了(虽然还是不太理解)

但是,我在这些函数中并没有看到对字符串处理的地方,这是一个疑问

看起来我知道这些函数是怎么对字符串进行影响了

它把字符串的地址存在0x602078处,之后在每个函数中都通过0x602078这个地址来找到字符串并对它进行处理

在到这一步时,我还没有仔细研究每个子函数的内容,但我通过“switch函数到底在switch谁?”&“到底怎么在子函数中影响字符串?”这两个问题对整个程序有了比较基本的认识

但是,我仍然有一个疑问,就是这个对result的判断,这是我看到这整个函数时的第一个问题,就是它是如何结束的?因为我看到这个函数的主体都在while(1)的循环之下,这势必得有一个办法返回,但是我对result的判断不太理解,看样子我们是在这个调用表的值为17h的时候返回,但是这个调用表并不是以17h结尾的,而且它中间有两个17h,这个问题在我还没有细看函数时无法解决,所以我带着它,先开始对子函数的解读

对case内函数的理解

接下来就是对子函数的解读了,做这一步就相当于对这个

实际的情况比我预想得要复杂很多,有非常多的变量,我不是很理解很多没有被明显初始化的变量到底是做什么用的,所以我用IDA PRO动态调试了一下

然后我在想,似乎这个程序的执行流程是已经确定了的,我要做的就是把这个整个流程所做的事提炼出来,然后再逆向

那么,我可以继续用switch语句,但是这次我在每个switch中输出这个函数所做的事(伪代码),这样做我最终应该能得到一个程序流程的伪代码

似乎很复杂,我所应该做的事还有很多。我觉得应该有更加好的理解方式。动态调试在有些地方也不是那么的直观,看不大懂说明我太菜了,在re方面的功力太弱了

ps:我前前后后看了两个小时了,我决定先停下。先看看别人是怎么理解的

看了十多分钟别人对VM的讲解,我大概知道了什么是VM,其实就是一种小型的解释器,将所进行的操作编程 OperationCode ,然后程序运行的时候用解释器去解释OperationCode。那么还是需要硬头皮去理解每个Bytecode所代表的含义

到这里我就有一个思考,那就是我输入的字串是否会影响到程序的执行流?如果是,那么我就只能把每一种都单独理解然后再switch。但如果不是,我是否可以用动态调试来得到每一个对字符串的操作,相当于我只要在每次有对字符操作的时候记录一下就可以了。

Emulator的指令一共有0x16个,而OperationCode似乎有57位,那么显然分析代码更加现实

试想一下,如果每次只输出对于字符的操作是不是会方便一些?

那么,接下来就试试吧

等等,这不就是brainfuck吗???!!!

现在是上午3点,过去了一个小时,我大体上完成了对解释器的解读,但是我遇到了许多问题,包括:看起来解释器在遇到17h时会结束,但是17h在OperationCode中出现地太早了,结果似乎任何实质性的改变都没有应该是我那个地方理解错了。因此,在接下来,我将着重处理这个问题

打印log

这时候我决定不单单通过眼睛看opcode,我打算模拟程序的流程,我把每一个case中的b1,b2,b3,b4,b5中的数据打印出来,再打印这个case中所作的事

代码如下

int main()
{
	char s[] = "hgame{111111111111111111111111111}";  // 试验用flag
	int s1[35];
	for (int j = 0; j < 35; j++)
	{
		s1[j] = s[j];
	}
	int q1[50];
	int s2[] = { 0x84 ,0x83 ,0x9D ,0x91 ,0x81 ,0x97 ,0xD7 ,0xBE  ,0x43 ,0x72 ,0x61 ,0x73 ,0x73 ,0x0C ,0x6A ,0x70
,0x73 ,0x11 ,0x48 ,0x2C ,0x34 ,0x33 ,0x31 ,0x36  ,0x23 ,0x34 ,0x3E ,0x5C ,0x23 ,0x4E ,0x17 ,0x11
,0x19 ,0x59,0 };
	int tmp[] = { 0x11 ,0x2D ,0x00 ,0x22 ,0x05 ,0x10 ,0x14 ,0x09  ,0x17 ,0x00 ,0x32 ,0x05 ,0x03 ,0x11 ,0x16 ,0x06
,0x00 ,0x16 ,0x05 ,0x11 ,0x16 ,0x17 ,0x0E ,0x01  ,0x15 ,0x04 ,0x0F ,0x01 ,0x16 ,0x02 ,0x00 ,0x00
,0x04 ,0x03 ,0x05 ,0x10 ,0x14 ,0x2B ,0x05 ,0x09  ,0x03 ,0x13 ,0x16 ,0x05 ,0x12 ,0x15 ,0x04 ,0x10
,0x14 ,0x36 ,0x0A ,0x01 ,0x13 ,0x2D ,0x03 ,0x04  ,0x12 };
	int v3 = 0;
	int i = 0;
	int result,v8,v2,v7;
	int v1;
	int a1;
	int b4 = 0;
	int b1 = 0;
	int b2 = 0;
	int b3 = 0;
	int b5 = 0;
	while (1)
	{
		v3 = i++;
		result = tmp[v3];
		//printf("v3 = %-3d,", v3);
		v8 = result;
		if (result == 23)
		{
			printf("%d", result);
			break;
		}
		switch (result)
		{
		case 0:
		case 8:
		case 9:
		case 0xa:
		case 0xc:
		case 0xd:
		case 0xe:
		case 0x11:
		case 0x13:
		case 0x14:
			v2 = i++;
			v7 = tmp[v2];
			break;
		default:
			break;
		}
		//printf("v8 = 0x%x\n", v8);
		switch (v8)
		{
		case 0:
			a1 = v7;
			v1 = b4++;
			q1[v1] = a1;
			printf("b1 = %-3d,b2 = %-3d,b3 = %-3d,b4 = %-3d,b5 = %-3d\n",b1,b2,b3,b4,b5);
			printf("b4++\n");
			printf("q1[v1] = v7 = %d\n", v7);
			break;
		case 1:
			a1 = b1;
			v1 = b4++;
			q1[v1] = a1;
			printf("b1 = %-3d,b2 = %-3d,b3 = %-3d,b4 = %-3d,b5 = %-3d\n", b1, b2, b3, b4,b5);
			printf("b4++\n");
			printf("q1[v1] = b1 = %d\n", b1);
			break;
		case 2:
			a1 = b2;
			v1 = b4++;
			q1[v1] = a1;
			printf("b1 = %-3d,b2 = %-3d,b3 = %-3d,b4 = %-3d,b5 = %-3d\n", b1, b2, b3, b4,b5);
			printf("b4++\n");
			printf("q1[v1] = b2 = %d\n", b2);
			break;
		case 3:
			a1 = b3;
			v1 = b4++;
			q1[v1] = a1;
			printf("b1 = %-3d,b2 = %-3d,b3 = %-3d,b4 = %-3d,b5 = %-3d\n", b1, b2, b3, b4, b5);
			printf("b4++\n");
			printf("q1[v1] = b3 = %d\n", b3);
			break;
		case 4:
			--b4;
			b1 = q1[b4];
			printf("b1 = %-3d,b2 = %-3d,b3 = %-3d,b4 = %-3d,b5 = %-3d\n", b1, b2, b3, b4, b5);
			printf("--b4\n");
			printf("b1 = q1[b4] = %d\n", q1[b4]);
			//printf("b1 = %-3d,", b1);
			break;
		case 5:
			--b4;
			b2 = q1[b4];
			printf("b1 = %-3d,b2 = %-3d,b3 = %-3d,b4 = %-3d,b5 = %-3d\n", b1, b2, b3, b4, b5);
			printf("--b4\n");
			printf("b2 = q1[b4] = %d\n", q1[b4]);
			break;
		case 6:
			--b4;
			b3 = q1[b4];
			printf("b1 = %-3d,b2 = %-3d,b3 = %-3d,b4 = %-3d,b5 = %-3d\n", b1, b2, b3, b4, b5);
			printf("--b4\n");
			printf("b3 = q1[b4] = %d\n", q1[b4]);
			break;
		case 7:
			b1 += b2;
			printf("b1 = %-3d,b2 = %-3d,b3 = %-3d,b4 = %-3d,b5 = %-3d\n", b1, b2, b3, b4, b5);
			printf("b1 += b2\n");
			break;
		case 8:
			b1 += v7;
			printf("b1 = %-3d,b2 = %-3d,b3 = %-3d,b4 = %-3d,b5 = %-3d\n", b1, b2, b3, b4, b5);
			printf("b1 += v7 = %d\n",v7);
			break;
		case 9:
			b2 += v7;
			printf("b1 = %-3d,b2 = %-3d,b3 = %-3d,b4 = %-3d,b5 = %-3d\n", b1, b2, b3, b4, b5);
			printf("b2 += v7 = %d\n", v7);
			break;
		case 0xa:
			b3 += v7;
			printf("b1 = %-3d,b2 = %-3d,b3 = %-3d,b4 = %-3d,b5 = %-3d\n", b1, b2, b3, b4, b5);
			printf("b3 += v7 = %d\n", v7);
			break;
		case 0xb:
			b1 -= b2;
			printf("b1 = %-3d,b2 = %-3d,b3 = %-3d,b4 = %-3d,b5 = %-3d\n", b1, b2, b3, b4, b5);
			printf("b1 -= b2\n");
			break;
		case 0xc:
			b1 -= v7;
			printf("b1 = %-3d,b2 = %-3d,b3 = %-3d,b4 = %-3d,b5 = %-3d\n", b1, b2, b3, b4, b5);
			printf("b1 -= v7 = %d\n", v7);
			break;
		case 0xd:
			b2 -= v7;
			printf("b1 = %-3d,b2 = %-3d,b3 = %-3d,b4 = %-3d,b5 = %-3d\n", b1, b2, b3, b4, b5);
			printf("b2 -= v7 = %d\n", v7);
			break;
		case 0xe:
			b3 -= v7;
			printf("b1 = %-3d,b2 = %-3d,b3 = %-3d,b4 = %-3d,b5 = %-3d\n", b1, b2, b3, b4, b5);
			printf("b3 -= v7 = %d\n", v7);
			break;
		case 0xf:
			b1 ^= b2;
			printf("b1 = %-3d,b2 = %-3d,b3 = %-3d,b4 = %-3d,b5 = %-3d\n", b1, b2, b3, b4, b5);
			printf("b1 ^= b2\n");
			break;
		case 0x10:
			//printf("b2 = %-3d,", b2);
			b5 = b1 == b2;
			printf("b1 = %-3d,b2 = %-3d,b3 = %-3d,b4 = %-3d,b5 = %-3d\n", b1, b2, b3, b4, b5);
			printf("b5 = b1 == b2\n");
			break;
		case 0x11:
			v1 = b4++;
			q1[v1] = i;
			i = v7;
			printf("b1 = %-3d,b2 = %-3d,b3 = %-3d,b4 = %-3d,b5 = %-3d\n", b1, b2, b3, b4, b5);
			printf("b4++\n");
			printf("q1[b4-1] = i = %d\n", i);
			printf("i = v7 = %d\n", v7);
			break;
		case 0x12:
			--b4;
			i = q1[b4];
			printf("b1 = %-3d,b2 = %-3d,b3 = %-3d,b4 = %-3d,b5 = %-3d\n", b1, b2, b3, b4, b5);
			printf("--b4\n");
			printf("i = q1[b4] = %d\n", q1[b4]);
			break;
		case 0x13:
			i = v7;
			printf("b1 = %-3d,b2 = %-3d,b3 = %-3d,b4 = %-3d,b5 = %-3d\n", b1, b2, b3, b4, b5);
			printf("i = v7 = %d\n", v7);
			break;
		case 0x14:
			if (b5 > 0)
			{
				i = v7;
			}
			printf("b1 = %-3d,b2 = %-3d,b3 = %-3d,b4 = %-3d,b5 = %-3d\n", b1, b2, b3, b4, b5);
			printf("if b5:i = v7 = %d\n", v7);
			break;
		case 0x15:
			v1 = b4++;
			printf("b1 = %-3d,b2 = %-3d,b3 = %-3d,b4 = %-3d,b5 = %-3d\n", b1, b2, b3, b4, b5);
			printf("input s1[%d]: %d\n", b3,s1[b3]);
			q1[v1] = s1[b3];
			break;
		case 0x16:
			--b4;
			s1[b3] = q1[b4];
			printf("b1 = %-3d,b2 = %-3d,b3 = %-3d,b4 = %-3d,b5 = %-3d\n", b1, b2, b3, b4, b5);
			printf("s1[%d] = %d\n", b3, q1[b4]);
			break;



		default:
			printf("error\n");
			break;
		}


	}
}

就用这种简易的“调试器”,我得到了一个3000多行的输出

分析

我硬着头皮看,通过查看b1,b2,b3,b4,b5的值的变化,我发现这个程序由三段循环构成:

第一段循环:

这个循环实际上是通过b3记录输入的字符串的长度,当遇到’\0’时跳出循环

第二段循环:

第二段循环终于看到了^和对字符的修改。至于到底是什么,多看几个就会发现,实际上是将 50+3i 跟每个字符做xor

for(int j = 33, k = 50;j >= 0;j--,k += 3)
{
    s[j] ^= k;
}

第三段循环:

细看发现跟上一段循环差不多,只不过xor的初始值从50变为22

for(int j = 33, k = 22;j >= 0;j--,k += 3)
{
    s[j] ^= k;
}

逆向

最终,我得到了逆向代码

int main()
{
	int s[] = { 0x84 ,0x83 ,0x9D ,0x91 ,0x81 ,0x97 ,0xD7 ,0xBE  ,0x43 ,0x72 ,0x61 ,0x73 ,0x73 ,0x0C ,0x6A ,0x70
,0x73 ,0x11 ,0x48 ,0x2C ,0x34 ,0x33 ,0x31 ,0x36  ,0x23 ,0x34 ,0x3E ,0x5C ,0x23 ,0x4E ,0x17 ,0x11
,0x19 ,0x59,0 };
	for (int j = 33, k = 22; j >= 0; j--, k += 3)
	{
		s[j] ^= k;
	}
	for (int j = 33, k = 50; j >= 0; j--, k += 3)
	{
		s[j] ^= k;
	}
	for (int i = 0; i < 34; i++)
	{
		printf("%c", s[i]);
	}
}

一点总结

在之前,在我还没有接触逆向这个事情之前,就有人跟我讲,一般的逆向不难,逆虚拟机才难。今天,我感受到了,因为这还只是一个为题目而生的小型虚拟机。

运行了这么长的代码,最后就为了执行三个循环,足以看到虚拟机的优缺点

看了几篇文章,发现现在虚拟机是一个热门的东西,只要是反逆向,基本都会在核心代码上加上虚拟机,以此来加大逆向的难度

这又是ctf的一个趋势,以后势必会出现更多的VM题,下次有空再去总结一波逆向VM的常见技巧

后续

今天o爷爷介绍了更加合适的做法(就是资料中chip学长的理解成汇编代码然后打log的方式),我决定再来看一遍case,然后总结一下每个case所对应的汇编指令。我觉得这个的一个好处就是不用跑程序,直接把opcode打出来就是汇编代码了,比较直观而且方便解读

(这里原先忘记保存了,名称改了,v6==原来的v7,v7==v8)

这里这个v6的值是下一个opcode,暂时不知道它的汇编指令是什么

从函数内以及之前的调试可以看出,这个q1是栈,b4是一个控制栈的指针,那么在汇编里它应该相当于RSP

那么其实这个sub_400a4f应该是push xxx

这个case应该是push 下一个字符,到底怎么表述我不知道,先暂时放在这

那么这些分别对应 push v6,push b1,push b2,push b3

后面这些的话,进去看到其实这个就是pop

所以这些就对应pop b1,pop b2,pop b3(b1,b…对应什么等会再追究)

case 7的这个也比较明了,就是 add b1,b2

接下来这几个都是有两个参数的了,里面也是加法

所以这个三个分别是 add b1,v6; add b2,v6; add b3,v6

case 0xb的汇编就是sub b1,b2

这几个相当于sub b1,v6; sub b2,b6; sub b3,v6

case 0xf就是xor b1,b2

case 0x10就是test b1,b2

case 0x11的参数是v6,里面这个参数是i,i是opcode的指针,所以这个是跳转用的。push 当前的地址并跳转,哇哦,这个vm的汇编真的是麻雀虽小,五脏俱全,还能函数调函数

case 0x12的参数是&i,那么这个应该就是ret了吧。

有点懒得写了,先鸽一下,反正想应该是这么想,等我明后天再来补这个坑吧

说点什么

avatar

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据

  Subscribe  
提醒