Lab5

1、运行CraMe1.exe,提示 "u r right!" 代表成功。首先修改exe使得出现成功提示,其次不修改exe输入正确的密码达到成功的目的。

Ⅰ修改exe使得出现成功提示

Step1:分析程序的判断部分

我们先用IDA反编译成伪代码,很容易就能找到判断处。

Java 逆向工程的demo 程序逆向工程_用户名

再在汇编部分找到其对应判断程序。这里的JNZ就是跳转部分,只要v8与0不相等就会跳至wrong处。

Java 逆向工程的demo 程序逆向工程_逆向工程_02

Step2:修改程序的跳转部分

查阅资料可知:

JNZ/JNE   不为零/不等于   75                

段内直接短转移Jmp short  (IP)←(IP)+8位位移量 EB                

而地址偏移我们不希望其跳转就设置为00

也就是说我们需要把原先的0x7519换成0xEB00,就能一直跳至right。

修改前:

Java 逆向工程的demo 程序逆向工程_逆向工程_03

修改后:

Java 逆向工程的demo 程序逆向工程_用户名_04

Step3:验证程序修改后的运行结果

此时我们输入任意数都会显示right:

Java 逆向工程的demo 程序逆向工程_逆向工程_05

Ⅱ不修改exe输入正确的密码达到成功的目的

Step1:分析源程序

由于要求不能修改exe且找到其正确密码,这需要我们重新认真分析源程序。

Java 逆向工程的demo 程序逆向工程_用户名_06

我们可以进一步找到byte_415768数组的内容:

Java 逆向工程的demo 程序逆向工程_v8_07

Step2:确定解题思路并找到正确的密码

整理一下思路。从刚才的分析中我们可以知道密码已经被程序唯一确定了。密码的检验分为两个部分:

(1)密码前17位同byte_415768数组的内容逐一比较,byte_415768的索引顺序按照v9、v10、v11这样依次顺延;

(2)密码的剩余5位的ASCII码按顺序同49、48、50、52、125逐一比较。

由以上分析,不难知道我们只要顺着检验方法跑一遍就能输出正确密码。

代码如下:

#include<stdio.h>

int v6; // [esp+Ch] [ebp-194h]
int i; // [esp+D4h] [ebp-CCh]
int v8; // [esp+E0h] [ebp-C0h]
int v9; // [esp+ECh] [ebp-B4h]
int v10; // [esp+F0h] [ebp-B0h]
int v11; // [esp+F4h] [ebp-ACh]
int v12; // [esp+F8h] [ebp-A8h]
int v13; // [esp+FCh] [ebp-A4h]
int v14; // [esp+100h] [ebp-A0h]
int v15; // [esp+104h] [ebp-9Ch]
int v16; // [esp+108h] [ebp-98h]
int v17; // [esp+10Ch] [ebp-94h]
int v18; // [esp+110h] [ebp-90h]
int v19; // [esp+114h] [ebp-8Ch]
int v20; // [esp+118h] [ebp-88h]
int v21; // [esp+11Ch] [ebp-84h]
int v22; // [esp+120h] [ebp-80h]
int v23; // [esp+124h] [ebp-7Ch]
int v24; // [esp+128h] [ebp-78h]
int v25; // [esp+12Ch] [ebp-74h]
int v26; // [esp+130h] [ebp-70h]
int v27; // [esp+134h] [ebp-6Ch]
int v28; // [esp+138h] [ebp-68h]
int v29; // [esp+13Ch] [ebp-64h]
int v30; // [esp+140h] [ebp-60h]
char v31; // [esp+14Fh] [ebp-51h]
char v32[17]; // [esp+178h] [ebp-28h]
char v33; // [esp+189h] [ebp-17h]
char v34; // [esp+18Ah] [ebp-16h]
char v35; // [esp+18Bh] [ebp-15h]
char v36; // [esp+18Ch] [ebp-14h]
char v37; // [esp+18Dh] [ebp-13h]

int main()
{
	v31 = 0;
	//索引
	v9 = 1;
	v10 = 4;
	v11 = 14;
	v12 = 10;
	v13 = 5;
	v14 = 36;
	v15 = 23;
	v16 = 42;
	v17 = 13;
	v18 = 19;
	v19 = 28;
	v20 = 13;
	v21 = 27;
	v22 = 39;
	v23 = 48;
	v24 = 41;
	v25 = 42;
	v26 = 26;
	v27 = 20;
	v28 = 59;
	v29 = 4;
	v30 = 0;
	//byte_415768数组
	char SS[] = "wfxc{gdv}fwfctslydRddoepsckaNDMSRITPNsmr1_=2cdsef66246087138";

	//找到前17位
	for (i = 0; i < 17; ++i)
	{
		//注意-1,首字母w在原程序中索引为1而不是现在的0
		printf("%c", SS[*(&v9 + i)-1]);
	}

	//输出后5位
	char S[] = { 49,48,50,52,125 };
	for (i = 0; i < 5; ++i)
	{
		printf("%c", S[i]);
	}
	printf("\n");
	return 0;
}

运行结果如下:

Java 逆向工程的demo 程序逆向工程_逆向工程_08

可以看到wctf{Pe_cRackme1_1024}就是正确密码。

Step3:验证

输入原先未修改的程序:

Java 逆向工程的demo 程序逆向工程_v8_09

成功通过!

 

2、Exe: super_mega_protection.exe

Key file: sample.key

This is a software copy protection imitation, which uses a key file. The key file contain a user (or customer) name and a serial number.

There are two tasks:

(Easy) with the help of any debugger, force the program to accept a changed key file.

(Medium) your goal is to modify the user name to another, without patching the program.

Ⅰ with the help of any debugger, force the program to accept a changed key file.

Step1:使用key文件简单测试exe文件

首先尝试一下所给程序,可以看到使用题目提供的key文件可以顺利通过,输出用户名和序列号。

Java 逆向工程的demo 程序逆向工程_逆向工程_10

我们再看一下key文件,看以看到第一行就是用户名。

Java 逆向工程的demo 程序逆向工程_Java 逆向工程的demo_11

把key文件略作修改,首字母由D改为A。

Java 逆向工程的demo 程序逆向工程_用户名_12

重新测试,发现此时无法通过,提示文件错误。

Java 逆向工程的demo 程序逆向工程_逆向工程_13

Step2:使用IDA反编译源程序

我们使用IDA反编译源程序,经过仔细推敲和分析,基本弄懂了其工作原理。详细解释见如下两张图:

主函数:

Java 逆向工程的demo 程序逆向工程_v9_14

文件读取函数:

Java 逆向工程的demo 程序逆向工程_v9_15

Step3:分析并修改跳转部分的程序代码

从上面分析我们看出,在用户名开始后的32*4=128字节处,就是我们用户序号的地址。在key文件中,刚好对应4E61BC00,值得注意的是,数字的存放顺序是两两倒序的。我们查看BC614E对应的十进制,发现正好是12345678,与我们的推理相互验证。

Java 逆向工程的demo 程序逆向工程_v9_16

我们再观察汇编码下验证跳转部分的程序。很明显这里的JNZ就是验证部分的罪魁祸首。

Java 逆向工程的demo 程序逆向工程_Java 逆向工程的demo_17

查阅资料可知:

JNZ/JNE   不为零/不等于   75                

段内直接短转移Jmp short  (IP)←(IP)+8位位移量 EB                

而地址偏移:407AE6-407A8F==57,55说明偏移的起始地址从407A8F+2=407A91算起,那么如果我们要跳至下一地址,也就是407A91,那么偏移就是00。

也就是说我们需要把原先的0x7555换成0xEB00,就能完全跳过验证程序。

修改前:

Java 逆向工程的demo 程序逆向工程_用户名_18

修改后:

Java 逆向工程的demo 程序逆向工程_v9_19

Step4:验证程序修改后的运行结果

对key文件进行修改:

Java 逆向工程的demo 程序逆向工程_Java 逆向工程的demo_20

可以看到,无论是对于原key文件,还是我们修改后的key文件,程序都可以成功接收并通过。

Java 逆向工程的demo 程序逆向工程_Java 逆向工程的demo_21

Ⅱyour goal is to modify the user name to another, without patching the program.

Step1:分析程序验证的部分

首先我们需要再次仔细观察程序的验证部分。

Java 逆向工程的demo 程序逆向工程_用户名_22

Java 逆向工程的demo 程序逆向工程_v8_23

这里猜测应该是IDA反编译成伪代码的问题,把无符号数解释成了有符号数,导致了误解。

Step2:确定破解思路并找到可通过验证的结果

那么,我们现在来整理一下思路。我们的目标是将用户名修改为其他的,却不能修改程序。首先在这里我们知道,key文件长度固定为132字节,这不能变,如此就能通过长度检测那一关。另一关是用户名内容和长度一起进行验证,结果如果等于0xE425,即58405,则通过,这也是最为重要的一关。

我们该如何做到这一点呢?答案就是暴力破解。诚然,我们不知道用户名长度,也不知道用户名内容。所以我们的基本想法就是:用户名长度从1开始往上枚举,对于每一种长度,我们遍历其在可显示字符上的全排列,对于每一种排列情况代入计算函数,看结果是否与58405相等,若相等则说明找到了。

额外的,需要说明的是,源程序的验证函数直接复制的话,会提示_BYTE和BYTE1没有定义。

查阅IDA逆向宏定义知需要添加如下定义:

typedef unsigned char _BYTE;
#define BYTEn(x, n)   (*((_BYTE*)&(x)+n))
#define BYTE1(x)   BYTEn(x,  1)

代码如下:

#include<stdio.h>
#include<string.h>
#include <stdlib.h>
typedef unsigned char _BYTE;
#define BYTEn(x, n)   (*((_BYTE*)&(x)+n))
#define BYTE1(x)   BYTEn(x,  1)
char Base[] = "QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm0123456789";

//程序用户名和i长度验证
int __cdecl sub_4015F0(int a1, __int16 a2)
{
	int result; // eax
	int v3; // ecx
	unsigned int v4; // edx
	unsigned int v5; // eax
	unsigned __int8 v6; // di
	unsigned int v7; // edx
	unsigned int v8; // esi
	char v9; // di
	unsigned int v10; // esi
	unsigned int v11; // edx
	unsigned int v12; // edi
	char v13; // si
	unsigned int v14; // edx
	unsigned int v15; // edi
	char v16; // si
	unsigned int v17; // edx
	unsigned int v18; // edi
	char v19; // si
	unsigned int v20; // edx
	unsigned int v21; // edi
	char v22; // si
	unsigned int v23; // edx
	unsigned int v24; // edi
	char v25; // si
	unsigned int v26; // edx
	unsigned int v27; // esi
	int v28; // eax
	int v29; // edx

	result = 0;
	v3 = a1;
	if (a2)
	{
		v4 = 0xFFFF;
		do
		{
			v5 = *(unsigned __int8 *)(++v3 - 1);
			v6 = v4;
			v7 = v4 >> 1;
			v8 = v7 ^ 0x8408;
			if (!(((unsigned __int8)v5 ^ v6) & 1))
				v8 = v7;
			v9 = v8 ^ (*(_BYTE *)(v3 - 1) >> 1);
			v10 = v8 >> 1;
			v11 = v10 ^ 0x8408;
			if (!(v9 & 1))
				v11 = v10;
			v12 = v11 >> 1;
			v13 = v11 ^ (*(_BYTE *)(v3 - 1) >> 2);
			v14 = (v11 >> 1) ^ 0x8408;
			if (!(v13 & 1))
				v14 = v12;
			v15 = v14 >> 1;
			v16 = v14 ^ (*(_BYTE *)(v3 - 1) >> 3);
			v17 = (v14 >> 1) ^ 0x8408;
			if (!(v16 & 1))
				v17 = v15;
			v18 = v17 >> 1;
			v19 = v17 ^ (*(_BYTE *)(v3 - 1) >> 4);
			v20 = (v17 >> 1) ^ 0x8408;
			if (!(v19 & 1))
				v20 = v18;
			v21 = v20 >> 1;
			v22 = v20 ^ (*(_BYTE *)(v3 - 1) >> 5);
			v23 = (v20 >> 1) ^ 0x8408;
			if (!(v22 & 1))
				v23 = v21;
			v24 = v23 >> 1;
			v25 = v23 ^ (*(_BYTE *)(v3 - 1) >> 6);
			v26 = (v23 >> 1) ^ 0x8408;
			if (!(v25 & 1))
				v26 = v24;
			v27 = v26 >> 1;
			v28 = v26 ^ (v5 >> 7);
			v4 = (v26 >> 1) ^ 0x8408;
			if (!(v28 & 1))
				v4 = v27;
		} while (v3 != a1 + (unsigned __int16)(a2 - 1) + 1);
		v29 = ~v4;
		result = (v29 << 8) | BYTE1(v29);
	}
	return result;
}

//递归求解全排列,并对每一种排列情况,调用sub_4015F0看是否等于58405
int  re[100];
char flag[100];
void  pailie(int a[], int N, int K, int level)
{
	if (level >= K)
	{
		char ss[100];
		for (int j = 0; j < level; j++)
		{
			ss[j] = Base[re[j]];
		}
		char * temp = (char *)(ss);
		unsigned int v4 = (int)temp;
		unsigned __int16 result = (unsigned __int16)sub_4015F0(v4, K);
		printf("长度:%d 计算结果:%d\n", K, result);
		if (result == 58405)
		{
			printf("We found it! 用户名:");
			for (int j = 0; j < level; j++)
			{
				printf("%c", ss[j]);
			}
			printf("\n");
			exit(0);
		}
		else
			return;
	}
	for (int i = 0; i < N; i++)
	{
		if (flag[i] == false)
		{
			flag[i] = true;
			re[level++] = a[i];
			pailie(a, N, K, level);//递归下去
			level--;
			flag[i] = false;
		}
	}
}

int main()
{
	int j;
	unsigned int v5; // eax
	v5 = 0;
	//供全排列函数使用
	int a[62] = { 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61 };

	//从1开始,尝试每一种用户名长度
	for (v5 = 1; v5 <= 5; v5++)
	{
		pailie(a, 62, v5, 0);	
	}

	return 0;
}

运行结果如下:

Java 逆向工程的demo 程序逆向工程_v8_24

可看到,很快就找到了额外的用户名J8y。

Step3:修改key文件

如此,我们就可以来修改key文件:

Java 逆向工程的demo 程序逆向工程_v8_25

Step4:验证修改后的运行结果

使用修改后的key文件测试,可以看到在没有修改源程序的情况下,程序依然接受了新的用户名。

Java 逆向工程的demo 程序逆向工程_逆向工程_26

至此,实验成功!

3、(选做)Reverseme1.exe

hint:Import Address Table

使用IDA反编译程序,可以看到其主函数如下。

Java 逆向工程的demo 程序逆向工程_v9_27

其中sub_40102C()是最重要的,我们进一步观察它。

Java 逆向工程的demo 程序逆向工程_逆向工程_28

在函数的结束处,在定义完各个控件,使用了DialogBoxIndirectParamA函数加载对话框。

查阅资料可知:

从内存中的对话框模板创建模式对话框。在显示对话框之前,该函数将应用程序定义的值作为WM_INITDIALOG消息的lParam参数传递给对话框过程。应用程序可以使用此值初始化对话框控件。其参数DLGPROC值得我们进一步关注,代表指向对话框过程的指针。

Java 逆向工程的demo 程序逆向工程_Java 逆向工程的demo_29

0代表按钮隐藏,我们只要将其改为1即可显示被隐藏的按钮。

修改前:

Java 逆向工程的demo 程序逆向工程_Java 逆向工程的demo_30

修改后:

Java 逆向工程的demo 程序逆向工程_v9_31

重新打开程序,退出按钮已经被显示出来。

Java 逆向工程的demo 程序逆向工程_v8_32