references:
上回我们说到CrackHead的注册机编写,但是由于触及到了我的知识盲区,所以就暂时放在那里没写
现在我知道了如何下内存断点,那么问题就迎刃而解了
我们知道0x40339C
这个地址决定了ESI的值,但是我们并不知道这块地址中的值是怎么来的,通过在这个地址下内存断点,可以定位到更改该内存内容的代码
可以看到更改内存的代码位于ntdll内,这些都是系统代码,我们一路返回即可,最后定位到CrackHead的代码,在此下断点即可,此时内存断点就可以清除了,当然你不清除也没啥影响
我们只需要把这一部分代码搞明白就行了:
从倒数第三行可以看到,ESI的值被写入了0x40339C
中,而EDI又是通过下面这一段循环代码计算出来的
0040143E |. 33FF XOR EDI,EDI
00401440 |> 8BC1 MOV EAX,ECX
00401442 |. 8B1E MOV EBX,DWORD PTR DS:[ESI]
00401444 |. F7E3 MUL EBX
00401446 |. 03F8 ADD EDI,EAX
00401448 |. 49 DEC ECX
00401449 |. 83F9 00 CMP ECX,0
0040144C |.^75 F2 JNZ SHORT CrackHea.00401440
首先EDI被清空,然后进行循环,循环次数为ECX的值,EAX被初始化为ECX的值,在这段循环代码中,我们只需要知道ESI、ECX、EBX的值即可计算出EDI的值
00401431 |. 8D35 9C334000 LEA ESI,DWORD PTR DS:[40339C]
00401437 |. 0FB60D EC33400>MOVZX ECX,BYTE PTR DS:[4033EC]
从这两行代码我们可以知道ESI的值为x040339C
,那么EBX的值就是内存地址为0x40339C
处的内容,ECX的值为内存地址0x4033EC
处的内容
这上面有一条指令可能大家之前没有见过,就是MOVZX
(move with zero-extend),该指令跟MOV差不多,就是高位补0而已
比方说你把一个16位的值通过MOVZX
指令放到EAX(32bit)中,那么MOVZX会自动将EAX的高16bit置0
再往上看
0040140C /$ 60 PUSHAD
0040140D |. 6A 00 PUSH 0 ; /RootPathName = NULL
0040140F |. E8 B4000000 CALL <JMP.&KERNEL32.GetDriveTypeA> ; \GetDriveTypeA
00401414 |. A2 EC334000 MOV BYTE PTR DS:[4033EC],AL
00401419 |. 6A 00 PUSH 0 ; /pFileSystemNameSize = NULL
0040141B |. 6A 00 PUSH 0 ; |pFileSystemNameBuffer = NULL
0040141D |. 6A 00 PUSH 0 ; |pFileSystemFlags = NULL
0040141F |. 6A 00 PUSH 0 ; |pMaxFilenameLength = NULL
00401421 |. 6A 00 PUSH 0 ; |pVolumeSerialNumber = NULL
00401423 |. 6A 0B PUSH 0B ; |MaxVolumeNameSize = B (11.)
00401425 |. 68 9C334000 PUSH CrackHea.0040339C ; |VolumeNameBuffer = CrackHea.0040339C
0040142A |. 6A 00 PUSH 0 ; |RootPathName = NULL
0040142C |. E8 A3000000 CALL <JMP.&KERNEL32.GetVolumeInformation>; \GetVolumeInformationA
第一行的PUSHAD指令意为push all general-purpose register,就是把所有通用寄存器压栈
这一段代码其实就是两个系统函数的调用
下面这行代码将GetDriveTypeA
的返回值放到了地址为0x4033EC
的内存中
00401414 |. A2 EC334000 MOV BYTE PTR DS:[4033EC],AL
因为该函数的返回值最大值为6,所以AL(8bit)就足够存放返回值了
然后是GetVolumeInformationA
00401425 |. 68 9C334000 PUSH CrackHea.0040339C ; |VolumeNameBuffer = CrackHea.0040339C
这个是该函数的第二个参数,根据文档,该参数为传出参数,存放的是卷标的名称,之前之所以测了好几台机器都是一样的注册码,就是因为那几台机器都没有卷标
卷标就是这个东西,是用户自己设置的,默认是没有的
那么现在我们现在就已经获取到了所有需要的变量的值了,写出对应的C代码计算出序列号就完事了
编写思路如下:
ECX的初始值,0x4033EC指针的值我们已经拿到了,就是GetDriveType的返回值
EBX的初始值,0x40339C指针的值,这个值我们也有,就是GetVolumeInformation的传出参数VolumeName的值
EAX = ECX
EBX = VolumeName前四个字节(由于小端序的原因,需要进行翻转(高位在高址))
EAX=EAX*EBX EDX存放高32位,EAX存放低32bit 本次计算与EDX无关,我们只需要低32bit的值就行了
EDI=EDI+EAX EDI初始值为0
ECX用于循环计数
我直接用codeblocks编译的
#include <windows.h>
#include <stdio.h>
int main(int argc, char **argv) {
char volume_name[256]={0};
GetVolumeInformation( NULL,volume_name,11,NULL,NULL,NULL,NULL,NULL);
printf("[+] volume name: %s\n", volume_name);
char _24_31[3]={0};
char _16_23[3]={0};
char _8_15[3]={0};
char _0_7[3]={0};
sprintf(_24_31, "%02X", (unsigned char)volume_name[0]);
sprintf(_16_23, "%02X", (unsigned char)volume_name[1]);
sprintf(_8_15, "%02X", (unsigned char)volume_name[2]);
sprintf(_0_7, "%02X", (unsigned char)volume_name[3]);
char* hex_string = (char*)malloc(8+1);
strcpy(hex_string, _0_7);
strcat(hex_string, _8_15);
strcat(hex_string, _16_23);
strcat(hex_string, _24_31);
unsigned int ecx = GetDriveType(NULL);
printf("[+] drive type: %u\n", ecx);
unsigned int ebx = (unsigned int)strtoul(hex_string, NULL, 16);
printf("[+] ebx initial value: 0x%x\n", ebx);
free(hex_string);
printf("[+] ecx initial value: %u\n", ecx);
unsigned int eax = 0;
unsigned int edi = 0;
int i = 0;
for(i=0; i<ecx; i++) {
eax = ecx-i;
eax *= ebx;
edi += eax;
}
printf("[+] edi value: 0x%x\n",edi);
const unsigned int const_value = (unsigned int)strtoul("797A7553", NULL, 16);
printf("[+] calculated serial number: %u\n", const_value^edi);
return 0;
}