Lab5
1、运行CraMe1.exe,提示 "u r right!" 代表成功。首先修改exe使得出现成功提示,其次不修改exe输入正确的密码达到成功的目的。
Ⅰ修改exe使得出现成功提示
Step1:分析程序的判断部分
我们先用IDA反编译成伪代码,很容易就能找到判断处。
再在汇编部分找到其对应判断程序。这里的JNZ就是跳转部分,只要v8与0不相等就会跳至wrong处。
Step2:修改程序的跳转部分
查阅资料可知:
JNZ/JNE 不为零/不等于 75
段内直接短转移Jmp short (IP)←(IP)+8位位移量 EB
而地址偏移我们不希望其跳转就设置为00
也就是说我们需要把原先的0x7519换成0xEB00,就能一直跳至right。
修改前:
修改后:
Step3:验证程序修改后的运行结果
此时我们输入任意数都会显示right:
Ⅱ不修改exe输入正确的密码达到成功的目的
Step1:分析源程序
由于要求不能修改exe且找到其正确密码,这需要我们重新认真分析源程序。
我们可以进一步找到byte_415768数组的内容:
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;
}
运行结果如下:
可以看到wctf{Pe_cRackme1_1024}就是正确密码。
Step3:验证
输入原先未修改的程序:
成功通过!
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文件可以顺利通过,输出用户名和序列号。
我们再看一下key文件,看以看到第一行就是用户名。
把key文件略作修改,首字母由D改为A。
重新测试,发现此时无法通过,提示文件错误。
Step2:使用IDA反编译源程序
我们使用IDA反编译源程序,经过仔细推敲和分析,基本弄懂了其工作原理。详细解释见如下两张图:
主函数:
文件读取函数:
Step3:分析并修改跳转部分的程序代码
从上面分析我们看出,在用户名开始后的32*4=128字节处,就是我们用户序号的地址。在key文件中,刚好对应4E61BC00,值得注意的是,数字的存放顺序是两两倒序的。我们查看BC614E对应的十进制,发现正好是12345678,与我们的推理相互验证。
我们再观察汇编码下验证跳转部分的程序。很明显这里的JNZ就是验证部分的罪魁祸首。
查阅资料可知:
JNZ/JNE 不为零/不等于 75
段内直接短转移Jmp short (IP)←(IP)+8位位移量 EB
而地址偏移:407AE6-407A8F==57,55说明偏移的起始地址从407A8F+2=407A91算起,那么如果我们要跳至下一地址,也就是407A91,那么偏移就是00。
也就是说我们需要把原先的0x7555换成0xEB00,就能完全跳过验证程序。
修改前:
修改后:
Step4:验证程序修改后的运行结果
对key文件进行修改:
可以看到,无论是对于原key文件,还是我们修改后的key文件,程序都可以成功接收并通过。
Ⅱyour goal is to modify the user name to another, without patching the program.
Step1:分析程序验证的部分
首先我们需要再次仔细观察程序的验证部分。
这里猜测应该是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;
}
运行结果如下:
可看到,很快就找到了额外的用户名J8y。
Step3:修改key文件
如此,我们就可以来修改key文件:
Step4:验证修改后的运行结果
使用修改后的key文件测试,可以看到在没有修改源程序的情况下,程序依然接受了新的用户名。
至此,实验成功!
3、(选做)Reverseme1.exe
hint:Import Address Table
使用IDA反编译程序,可以看到其主函数如下。
其中sub_40102C()是最重要的,我们进一步观察它。
在函数的结束处,在定义完各个控件,使用了DialogBoxIndirectParamA函数加载对话框。
查阅资料可知:
从内存中的对话框模板创建模式对话框。在显示对话框之前,该函数将应用程序定义的值作为WM_INITDIALOG消息的lParam参数传递给对话框过程。应用程序可以使用此值初始化对话框控件。其参数DLGPROC值得我们进一步关注,代表指向对话框过程的指针。
0代表按钮隐藏,我们只要将其改为1即可显示被隐藏的按钮。
修改前:
修改后:
重新打开程序,退出按钮已经被显示出来。