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.
我们先运行exe文件和key:super_mega_protection.exe sample.key
可以看到注册文件的用户名:Dennis Yurichev,所以我们可以推测super_mega_protection.exe从sample.key中得到用户名计算出注册码。
题目要求我们更改密钥文件,所以我们将文件改成如图所示:
先用IDA打开super_mega_protection.exe文件,找到main函数,F5反编译:
我们可以看到在输出正确的Serial number之前,有3个if判断来确定是否输入了正确的key文件,所以我们只要保证这3个if判断接受更改后的密钥文件即可。
再用OD打开文件,由于要输入参数key,所以点击OD的文件->打开,选择打开super_mega_protection.exe文件,参数指定new.key,如图所示:
点击打开,就可以正常破解了。
查找字符串,下断点,找到代码判断的关键位置,有3个JNZ跳转对应之前的if跳转:
第一个JNZ是检查是否有key文件参数输入,所以不用修改,我们只需要把后面两个JNZ改成JZ,即可让它们不跳转,输出Serial number
然后找到关键跳转的地方,如下图所示:
在此处下断点,然后点击运行
发现接下来的跳转不成立,直接改掉,将jnz改成jz即可,其中:
汇编指令 | 对应的机器码 | 作用 |
---|---|---|
JZ/JE | 74 | Z=1,为零/等于则跳转 |
JNZ/JNE | 75 | Z=0 ,不为零/不等于则跳转 |
重新运行:super_mega_protection(OD修改后的).exe new.key
所以我们要完整的逆向出判断的算法,才能修改为另一个用户名且通过校验。
我们继续用IDA反编译,查看每部分算法的作用:
源代码:
int __cdecl main(int argc, const char **argv, const char **envp)
{
const char *v3; // eax
char *v4; // ebx
__int16 v5; // ax
char v7; // [esp+4h] [ebp-24h]
int v8; // [esp+1Ch] [ebp-Ch]
sub_4025A0();
puts("Super-mega-protected software");
if ( argc != 2 )
sub_4016F0("Usage: %s \n" , (unsigned int)*argv);
v3 = (const char *)sub_401720((char *)argv[1], (int)&v8);// 负责打开key文件,之后v7被赋值为文件长度,v3赋值为key文件的内容
v4 = (char *)v3;
if ( v8 != 132 ) // 文件长度是否等于132字节
sub_4016F0("%s: incorrect size!\n", (unsigned int)argv[1]);
v5 = strlen(v3); // 字符串的长度
if ( (unsigned __int16)sub_4015F0((int)v4, v5) != -7131 )// 将key文件内容的指针数值和长度作为输入,要求输出-7131
sub_4016F0("Keyfile is incorrect\n", v7);
puts("Registration info:");
printf("Username=%s\n", v4);
printf("Serial number: %d\n", *((_DWORD *)v4 + 32));
free(v4);
return 0;
}
其中sub_401720函数是负责打开key文件,之后v7被赋值为文件长度,v3赋值为key文件的内容:
源代码:
void *__cdecl sub_401720(const char *a1, size_t *a2)
{
FILE *v2; // eax
FILE *v3; // ebx
int v4; // eax
void *v5; // ebp
char v7; // [esp-28h] [ebp-28h]
v2 = fopen(a1, "rb"); // v2文件指针
v3 = v2; // v3 = v2
if ( !v2 )
sub_4016F0("Cannot open file %s\n", (char)a1);// fseek文件指针定位到0(文件开头)、1(文件当前位置)、2(文件末尾)文件末尾,偏移0个字节
// ftell得到文件位置指针当前位置相对于文件首的偏移字节数
// malloc分配了v4个字节,并返回了指向这块内存的指针
if ( fseek(v2, 0, 2) || (v4 = ftell(v3), *a2 = v4, v5 = malloc(v4), fseek(v3, 0, 0)) )// 把文件指针v2定位到文件末尾
// v4是文件指针v3是文件末尾,ftell就是返回文件末尾到文件开始的
// 申请一块v4长度的地址,返回这块地址的指针给v5
// v3文件指针定位到文件开始位置
sub_4016F0("fseek()\n", v7);
if ( fread(v5, *a2, 1u, v3) != 1 ) // fread读取v3指向的数据赋值给v5,每次读取*a2字节数,读取1次
sub_4016F0("Cannot read file %s\n", (char)a1);
fclose(v3);
return v5; // 返回key文件的内容v5
}
其中关键的一句代码是:
if ( fseek(v2, 0, 2) || //把文件指针v2定位到文件末尾
(v4 = ftell(v3), //v4是文件指针v3是文件末尾,ftell就是返回文件末尾到文件开始的偏移字节数也就是整个文件的长度给v4
*(_DWORD *)a2 = v4, //
v5 = malloc(v4),//申请一块v4长度的地址,返回这块地址的指针给v5
fseek(v3, 0, 0)) )//v3文件指针定位到文件开始位置
得到了key文件的长度
再回到主函数,其中判断的关键就是sub_4015F0((int)v4, v5)函数:
sub_4015F0函数的两个参数是key文件的首地址和用户名的长度,作用是将key文件中的用户名逐个字节进行计算,第一个字节v5和结果v4(初始值位0xFFFF),计算的结果v4再和下一个字节v5一起计算,计算用户名的长度这么多次,最后得到的结果与-7131进行比较。
不相等则输出“Keyfile is incorrect”,相等则继续,输出Username和Serial number,其中Serial number是*((_DWORD *)v4 + 32),实际上就是key文件最后的四个字节(字符):0x00BC614E,按照%d格式输出就是12345678:
因为关键的算法不能逆向推导,所以现在的破解思路就是暴力破解:key文件的长度为132字节,最后四个字节为Serial number,可以自己指定,也可以随机。前面128字节可以遍历0X00到0XFF,直到计算得到-7131,可以认为当前的字节是一个正确的key,中间部分用00填充,最后四个字节Serial number可以自己指定。
所以我们尝试遍历了一个字节,但是没有找到正确的key;在遍历二个字节的时候找到了一个正确的key:1E 64,后续还有很多其他的解,原理都是一样的,就不一一求解了。
源代码:
#include
#include
#include
/**
*
* char current_character:当前的字符,unsigned int v4上一次计算的结果
*/
int sub_4015F0(char current_character,unsigned int v4){
int v3; // ecx
///unsigned int v4; // edx
unsigned int v5; // eax
///代表8位无符号数,表示一个字节255以内
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
///直接用当前字符来代替源代码的数字和字符之间的转换,68
v5 = (unsigned char)current_character;
///255
v6 = v4;
///32767
v7 = v4 >> 1;
///64503
v8 = v7 ^ 0x8408;
///(68^255)&1 =1
if ( !(((unsigned __int8)v5 ^ v6) & 1) )
///32767
v8 = v7;
///-43(325);
v9 = v8 ^ (v5 >> 1);
///32251
v10 = v8 >> 1;
///63987
v11 = v10 ^ 0x8408;
///!(-43&1) = 0
if ( !(v9 & 1) )
v11 = v10;
///31993
v12 = v11 >> 1;
v13 = v11 ^ (v5 >> 2);
v14 = (v11 >> 1) ^ 0x8408;
if ( !(v13 & 1) )
v14 = v12;
v15 = v14 >> 1;
v16 = v14 ^ (v5 >> 3);
v17 = (v14 >> 1) ^ 0x8408;
if ( !(v16 & 1) )
v17 = v15;
v18 = v17 >> 1;
v19 = v17 ^ (v5 >> 4);
v20 = (v17 >> 1) ^ 0x8408;
if ( !(v19 & 1) )
v20 = v18;
v21 = v20 >> 1;
v22 = v20 ^ (v5 >> 5);
v23 = (v20 >> 1) ^ 0x8408;
if ( !(v22 & 1) )
v23 = v21;
v24 = v23 >> 1;
v25 = v23 ^ (v5 >> 6);
v26 = (v23 >> 1) ^ 0x8408;
if ( !(v25 & 1) )
v26 = v24;
v27 = v26 >> 1;
v28 = v26 ^ (v5 >> 7);
v4 = (v26 >> 1) ^ 0x8408;
// v28的最低位是0则进入,最低位是1则跳过
if ( !(v28 & 1) )
v4 = v27;
///55835
return v4;
}
/**
*两轮for循环找到2个字节的key
*/
int main(){
///v4的初始值63082
int v4 = 0xFFFF;
///两轮循环找到2个字节的key
for(unsigned __int8 i = 0; i<0xFF ; i++){
///第一次的结果v4,赋值给临时变量temp1
unsigned int temp1 = sub_4015F0((char)i,v4);
for(unsigned __int8 j = 0; j<0xFF ; j++){
///第二次的结果v4,赋值给临时变量temp
unsigned int temp = sub_4015F0((char)j,temp1);
///对结果作变换,非:按位取反,v29=-55836=0xFFFF25E4
int v29 = ~temp;
///printf("v29的值:%d\n",v29);
///-7131,计算得到result;(unsigned __int8 *)转化为__int8字节指针,让后面的+1是加4个字节的地址
int result = (__int16)((v29 << 8) | *((unsigned __int8 *)&v29 + 1));
///其中int BYTE1 = (__int16)(v29) >>8; =0x25=37
/// 上面这一行的代码等价于int result = (__int16)((v29 << 8) | (__int16)v29 >>8);
if(result == -7131){
///30,100
printf("0x%x 0x%x计算得到正确的result值%d\n",i,j,result);
return 0; }
}
}
return 0;
}
中间的字节我们用00填充,最后的四个字节我们指定为0x004F5DA2=5201314,所以我们最终得到的这一个key就是:
带入原程序运行:super_mega_protection.exe 111.key
成功破解,将用户名修改为另一个用户名,而不需要修补程序。
有几个需要注意的点:
(1)复现的伪代码中有两处代码_BYTE和BYTE1,其实都是IDA头文件defs.h中的宏定义,详细的可以参考我的另一篇博文:IDA的def.h文件;
(2)还有__int8、__int16、__int32、__int64之间的转换;
(3)EDX、DX、DH寄存器之间的赋值。
1、_BYTE == unsigned char
#define unsigned char BYTE;
2、BYTE1(v29) = *((unsigned __int8 *)&v29 + 1)=37
#define BYTE1(x) BYTEn(x, 1)
这里的BYTE1(v29)就是v29地址上开始的第一个字节,即v29[1],(unsigned __int8 *)转化为__int8字节指针,让后面的+1是加4个字节的地址;
另外,BYTE1(v29)在OD中的代码是:movzx eax,dh,dh寄存器的值为0x25=37:
所以我们也可以BYTE1(v29) = (__int16)(v29) >>8,这也是代码中的等价语句int result = (__int16)((v29 << 8) | (__int16)v29 >>8);的由来。