商软A v4.7.2的序列号算法分析

准备:商软A,Ollydbg 2.0 
先说说A的加密方式。联网激活:用户通过A将用户名,邮箱,序列号和硬盘信息发送给服务器,服务器返回激活码,A写入keyfile文档以备再次运行时激活校验,主要校验的是序列号和激活码。因为有大神已经早已写了篇简析,并给出了一枚正确的序列号,所以我只是站在巨人肩上谈谈序列号算法。文章结尾写到了过调试器检测和文件完整性校验,对此不敢兴趣的读者可以忽略。一枚正确的序列号:
"YH23U-R65WC-CKPA2-RN2JB-XENVZ" 
UNICODE F(x):
59 00 48 00 32 00 33 00 55 00 2D 00 52 00 36 00
35 00 57 00 43 00 2D 00 43 00 4B 00 50 00 41 00
32 00 2D 00 52 00 4E 00 32 00 4A 00 42 00 2D 00
58 00 45 00 4E 00 56 00 5A 00


第一部分  序列号算法


1  第一次映射

用OD载入A后,对CreateFileW下条件断点[UNICODE [ESP+4]]=="F:\\A\\Options.ini",运行A,发现A和OD都崩溃了。由于在此处A使用CRC校验,不能直接bp,因此对CreateFileW内部的RETN处断点,然后bp ReadFile,对被读取的序列号下断点,一段无聊的序列号非空和长度检测后,来到
6EB41382    8A08           MOV CL,BYTE PTR DS:[EAX]
此处是系统领空“Charset”,因此执行到用户代码
6EB41397    FF15 04C0B46E  CALL DWORD PTR DS:[<&KERNEL32.MultiByteToWideChar>]
一个字符串转换函数。对转换后的UNICODE序列号下断点,OD给出弹窗

然后UNICODE序列号就被XX覆盖了。我们姑且不去探究到底发生了什么。事实上A多次读取KeyFile文件并进行检测。(因为在KeyFile文件写入正确序列号,运行程序后,再去更改KeyFile文件的序列号。当点击输入注册码,A仍然显示未注册。)好吧,让OD重新载入并运行A,点击输入注册码,同样对CreateFileW内部的RETN处下断点,一运行A就崩溃,此处也被CRC校验了。没关系,可以直接对MultiByteToWideChar下条件断点[STRING [ESP+C]]=="YH23U-R65WC-CKPA2-RN2JB-XENVZ"。事实上此时读取了两次KeyFile文件,也MultiByteToWideChar两次,对第二次转换后的UNICODE序列号下断点,运行来到

6EAF2B40    0FB701         MOVZX EAX,WORD PTR DS:[ECX]
6EAF2B43    8D49 02        LEA ECX,[ECX+2]
6EAF2B46    66:89440E FE   MOV WORD PTR DS:[ECX+ESI-2],AX; 复制数据
(注:类似上面这种数据复制或转移,在下面直接类似表示为 [ECX]-> [ECX+ESI-2]结构)
对被复制后序列号下断点,去到最后一次读取序列号②处,
[EAX] ->[EBP]; [EDX]->[EBP]①<假>; 数据访问
[EAX] ->[EBP]; [EDX]->[EBP]①<真>; 数据访问
[EAX] ->[EBP];②
上面①处运行了两次,第一次为假——即虽然访问了序列号,但不会对校验结果产生影响。我感觉马上就要到达算法代码了。我不知道之后会发生什么,当然可以继续跟下去,这是最稳妥的办法。然而我对此处序列号的尾字节下断点,运行到断点处,然后看看序列号前后的内存信息。向上翻了翻内存页,看到有一可疑处,地址[5D858690] :
54 CC 89 56 89 28 2D 2D 85 07 D3 59 FD 1D BA 0C // F``(x)
在这串数据的右边,OD没有给出任何有效信息。这串数据是什么时候写入的?和序列号是否关联?放在此处有什么作用?重新运行到①处,对[5D858690]处的内存下写入断点,果然在①处访问了序列号首字节后,A在[5D858690]处写下1C,却与②无关。此时断在

5D6C4935    8810            MOV BYTE PTR DS:[EAX],DL
在①处对序列号尾字节下断点,重新截取一下写入的数据
7C D0 D3 71 D9 79 2D 2D 05 C5 88 56 A8 1C BA 0C// F`(x)
以下为写入详细

终于找到序列号的首次映射F`(x)。而第一次发现的F``(x)则为序列号F(x)的第二次映射,有关系如下
F(x)~ F`(x);F`(x)~ F``(x);
现在, 对此条代码下INT3断点,重新运行到①处继续跟进,
[EBP]-> [EDI+EAX];
[EDI+EAX]-> [EBP] ③<假>;
[EDI+EAX]-> [EBP] ③<真>; [EBP] ->[EDI+EAX] ④; 
[EDI+EAX]->[EBP]<假>
下面回到了③处进行循环,我现在需要找到循环过后,序列号被用来干嘛。当然能够直接在出口处下断点跳过循环是最好不过,但是由于混乱的代码片段、高强度的混淆和VMP指令,我很难办到。于是我在③的下一行设条件日志断点(条件:DL==59,表达式:LOG=1,暂停程序:从不,日志表达式值:打开条件),运行。在日志窗口中,我发现了69条记录,经过反复运行检查,确定第59条为循环出口。此处被提取的序列号还能影响F`(x)。因此从此处继续往下跟进
[EDI+EAX]-> [EBP];

6E1BCDE9    0045 04         ADD BYTE PTR SS:[EBP+4],AL

上条代码过后原来的序列号直接变成0xFF,我觉得不用再跟下去或者再去检查0xFF是否影响F`(x),该字节本身就很能说明问题。此时AL=0xA6,我需要再去找找0xA6是怎么来的。为了避免频繁运行A,我将之前的一部分循环代码(第56~60条记录)用运行跟踪记录到文档,然后导入到EXCEL进行分析,大概10w条左右的样子,定位到上面代码处。现在开始向上查找A6的来源

ADD DWORD PTR SS:[EBP+4],EAX;	EAX=0000001C,EBP=0019E5B8,
MOV EDX,DWORD PTR SS:[EBP];    EBP=0019E5BC,
MOV AL,BYTE PTR DS:[EDX];		EDX=6E2DA0DC,
AND AL,DL;						EAX=000000D9, EDX=0000007F,

(最后一条是图片)_
我找到A6是由D9计算来的。为什么不是7F?可以向上跟一下7F,发现它是由前面的一些数计算来的。如果非要跟进这些数,就迷失在A的陷阱里面了。如果修改序列号首字节,这里的7F不变。D9是由[6E2DA0DC]地址处的内存得来的,而6E2DA0DC=1C+某数,这个某数可以看成是基址,也是由前面的一些数计算来的。把此处数据提出来

static unsigned char g_rippedData[] ={
	0xC6, 0xC4, 0xC1, 0xC8, 0xD4, 0xB6, 0xCD, 0xB3, 
	0xCE, 0xB7, 0xD8, 0xC3, 0xDA, 0xC7, 0xC5, 0xD3, 
	0xC2, 0xB9, 0xD0, 0xD1, 0xB2, 0xCA, 0xB8, 0xD6, 
	0xD2, 0xD7, 0xCB, 0xB5, 0xD9, 0xD5, 0xB4, 0xCC, 
	0xB0, 0xB1, 0xC9, 0xCF
};
1C很可疑,因为最终序列号首字节就是要生成1C。继续向上跟一下,
ADD BYTE PTR SS:[EBP+4],AL;EAX=0000001B,
继续向上跟1B,
ADD BYTE PTR SS:[EBP+4],AL;EAX=0000001A,
再向上跟,没有了。这里循环到1C,不再进行循环。而1C就是最终F1`(x),伪代码:

if (regcode [i]==(g_rippedData[j]&0x7F)){
    if (i==0){
        data_A[m]=j;
    }else{
        …
    }
}
同样的方法,分析F2`(x)=0x7C,不过此时的循环到03,不再进行循环,03并非最终的F2`(x),因此还需分析后面代码直到7C写入。关键代码提取

SHL AL,CL;	EAX=00000003, ECX=51636005,	//0x03<<0x05=0x60,
NOT DL; 	EDX=516D0060,				//0x9F=~0x60
NOT AL ;	EAX=0000001C,				//前面写入的0x1C
AND AL,DL;	EAX=000000E3, EDX=516D009F,	//0x83=0xE3&0x9F
NOT AL ;	EAX=00000083,				//0x7C=~0x83
 

这里唯一奇怪的就是05(我把此处数据命名为h_rippedData),怎么来的呢?又是自动生成的常数。后面的数与F2`(x)=0x7C的生成方式一样,只不过在序列号相同位生成第2个F`(x)的时候用SHR而非SHL,就不再赘述了。在SHR和SHL处下条件断点拦截后面的h_rippedData就行了。

2  第二次映射

同样的道理,对F`(x)下断点,A被断在

6CDDF0BB   8B00            MOV EAX,DWORD PTRDS:[EAX]        ; 2次映射 访问
6CDB46B3   8910            MOV DWORD PTRDS:[EAX],EDX        ; 2次映射 写入
得出F``(x)访问/写入详细

写入

访问

①②③③④

71D3D004

2D2D79D9

 

 

71D3D054

 

 

 

 

2D2D79D9

 

0CBA1CA8

 

 

5688C005

0CBA1CA8

 

 

5688C785

 

①②④③

71D3C054

2D2D79D9

 

 

71D3CC54

 

 

 

 

2D2D79D9

 

0CBA1CA8

 

 

56880785

0CBA1CA8

 

 

56890785

 

①②③④

71C1CC54

2D2D79D9

 

 

71C9CC54

 

 

 

 

2D2D79D9

 

0CBA1CA8

 

 

56810785

0CBA1CA8

 

 

56930785

 

②①

7009CC54

 

 

 

③④

 

2D2D79D9

 

 

7689CC54

 

 

 

 

2D2D79D9

 

0CBA1CA8

 

 

50130785

0CBA1CA8

 

 

51D30785

 

①②④③

1689CC54

2D2D79D8

 

 

5689CC54

 

 

 

 

2D2D79D9

 

0CBA1CA8

 

 

 

0CBA1CA8

 

 

01D30785

 

 

 

59D30785

 

①③④

5689CC54

2D2D7809

 

 

5689CC54

 

 

 

 

2D2D7889

 

0CBA1CA0

 

 

 

0CBA1CBD

 

 

59D30785

 

①③④

5689CC54

2D2D0089

 

 

5689CC54

 

 

 

 

2D2D2889

 

0CBA1C1D

 

 

 

0CBA1DFD

 

 

59D30785

 

先跟踪一下数据,
[EAX]->[EBP]
[EBP]->[EDI+EAX]
接着访问了 2D2D79D9 5688C505,马上又回到③处,作4次停留,然后跳走。提代码(第3次停留到写入71D3D004)分析

SHL EAX,CL;		EAX=0000001F,ECX=00000003,
NOT EAX;		EAX=000000F8,
AND EAX,EDX;	EAX=FFFFFF07,EDX=71D3D07C,

写得很明白,71D3D004最终还是由1F和03决定,1F是A自动生成的常数1,去找找03如何生成

MOV DWORD PTR SS:[EBP+4],EAX	EAX=00000001,EBP=0019E5A0,
SHL EAX,CL;						EAX=00000001,ECX=6D023A01,
ADD DWORD PTR SS:[EBP+4],EAX;	EAX=00000002,EBP=0019E5A0,

这里都得去验证哪个1是常数,哪个1是变量。注意之前读取过2D2D79D9 5688C505,那么尝试改变5688C505为12345678重新运行A,发现之前的03变成了01。再跟进5688C505

AND EAX,EDX;	EAX=5688C505,EDX=0000007F,
AND EAX,EDX;	EAX=00000005,EDX=00000001,

由5688C505&0x7F&0x01得到01。至此,71D3D004分析完毕,再看看一下71D3D054的生成过程

SHRD EAX,EDX,CL					EAX=5688C505,	ECX=68903807,	EDX=0CBA1CA8,
AND EAX,EDX						EAX=50AD118A,	EDX=0000001F,
SHL EAX,CL						EAX=0000000A,	ECX=00000003,
NOT EDX;						EDX=00000050,
AND EAX,EDX;					EAX=8E2C2FFB,	EDX=FFFFFFAF,
NOT EAX;						EAX=8E2C2FAB,

分析发现SHRD中CL也是自动生成的常数。至于下面的数,分析方法相同。只是规律不明显,分析每组数的写入都要导出代码。

3  比较映射值

写完F``(x)之后,程序还在继续访问F``(x)。最后访问的是0CBA1DFD,跟进,在③处停留8次跳走,第6次为有效数据,跟进,接着将数据计算为65D0,又在下面代码处被覆盖了,并且[ESP+4]变成了FFFF

67CEB661    66:0145 04      ADD WORD PTR SS:[EBP+4],AX

之前[EBP+4]处的值就是65D0,是我们一直跟过来的,计算方法很容易找到。而对应的9A2F怎么找呢?只有再提代码,倒着提(第8次停留到最后,第7次到第8次停留。。。),最后找到第4次停留处(EAX=00000028)取出的数据。第3次到第4次之间的代码太长,一个晚上都没提出来。只有反方向去跟踪、分析了,

67C837AF   8B1438          MOV EDX,DWORD PTRDS:[EDI+EAX]           ;③处
Ctrl+R查看跳转来源,这种方法要求来源代码必须有跳转地址。像return 4,call <edx>就不行了,得另外想办法。关键代码

6E45CEF6    66:891438       MOV WORD PTR DS:[EDI+EAX],DX   ; 末尾1次
6E443912    66:8B55 00      MOV DX,WORD PTR SS:[EBP]       ; 末尾3次
找到了来源处就换种方法跟,因为存储数据到[EBP]后可能运行了很长段代码才被上面代码访问。直接对[EBP]处的数据下硬件条件断点,重新运行代码。不过似乎出了点问题,下断点[0019E9BC]==00A165D0能行,WORD [0019E9BC]==65D0就不行,不理解。用这种方法继续向上跟进

6E46F269    66:8945 04      MOV WORD PTR SS:[EBP+4],AX
6E4435F5    66:D3E8         SHR AX,CL
6E523B25    66:8B45 00      MOV AX,WORD PTR SS:[EBP]
6E47F7F5    66:8945 00      MOV WORD PTR SS:[EBP],AX//末尾4次
6E4C3871    66:8B0438       MOV AX,WORD PTR DS:[EDI+EAX]  ; A3
6E4D11EE    FF7424 0C       PUSH DWORD PTR SS:[ESP+0C]
6E4D11F2    8F45 00         POP DWORD PTR SS:[EBP]
6E535E72    66:2145 04      AND WORD PTR SS:[EBP+4],AX
6E535E67    66:8B45 00      MOV AX,WORD PTR SS:[EBP]
6E4CF965    F755 00         NOT DWORD PTR SS:[EBP]
6E535E72    66:2145 04      AND WORD PTR SS:[EBP+4],AX
6E4CF965    F755 00         NOT DWORD PTR SS:[EBP]
6E444704    66:8945 00      MOV WORD PTR SS:[EBP],AX
6E4446FC    66:36:8B00      MOV AX,WORD PTR SS:[EAX]
E47F7F5    66:8945 00      MOV WORD PTR SS:[EBP],AX      ; 末尾4次
6E46F269    66:8945 04      MOV WORD PTR SS:[EBP+4],AX
6E4435F5    66:D3E8         SHR AX,CL

一步步跟到这里发现代码循环回去了,在上面几条关键循环代码处下条件记录断点拦截一下,下面是记录结果


接着我们要讨论的关键就在这张表中。这张表由”SHR”、”NOT”、”AND” 3条指令构成(后面简称S、N、A),需要找到它的循环条件,什么时候S,什么时候N,什么时候A。写完F``(x)后,对F``(x)数据下访问断点,前面8个数据每个都被访问了8次,然后F``(x)。猜想是F``1(x)~ F``8(x)经过计算得到65D0。结合表中数据,猜想SNA是头,后面NANANA按条件附加。拦截几个此处访问和写入数据期间的代码,得到表中数据的计算方法。然而发现这8个数据只能生成8*8=64组数据,而表中的S有327/3=109组,因此需要拦截后面的代码再分析。事实上A也访问了第9个数据到14个数据,可能是在第二次映射刚完成时被保留下来的吧,计算方法一样,第14个数据只被访问了5次就跳走。

4  code

#include <Windows.h>
#include <stdio.h>

char data_A[16]={0};
char* data="YH23UR65WCCKPA2RN2JBXENVZ";
//char* data="DNAZLBDYDTA8AL97RXAUFNK56";
int flag;DWORD password;

DWORD shld(DWORD a,DWORD b,int c){
	_asm{
		MOV EAX,a
		MOV EDX,b
		MOV ECX,c
		SHLD EAX,EDX,CL
	}
}
DWORD shrd(DWORD a,DWORD b,int c){
	_asm{
		MOV EAX,a
		MOV EDX,b
		MOV ECX,c
		SHRD EAX,EDX,CL
	}
}

void F1x(){
	int m=0,n=1;
	char order[]={1,2,1,2,2,1,2,1,1,2,1,2,2,1,2,1,1,2,1,2,2,1,2,1,1};
	static unsigned char g_rippedData[] ={
		0xC6, 0xC4, 0xC1, 0xC8, 0xD4, 0xB6, 0xCD, 0xB3, 
		0xCE, 0xB7, 0xD8, 0xC3, 0xDA, 0xC7, 0xC5, 0xD3, 
		0xC2, 0xB9, 0xD0, 0xD1, 0xB2, 0xCA, 0xB8, 0xD6, 
		0xD2, 0xD7, 0xCB, 0xB5, 0xD9, 0xD5, 0xB4, 0xCC, 
		0xB0, 0xB1, 0xC9, 0xCF
	};
	static unsigned char h_rippedData[] ={
		0x5,0x3,0x2,0x7,0x1,0x4,0x4,
		0x1,0x6,0x2,0x3,0x0,0x5,0x3,
		0x2,0x7,0x1,0x4,0x4,0x1,0x6,0x2,
		0x3,0x0,0x5,0x3,0x2,0x7,0x1,
		0x4,0x4,0x1,0x6,0x2,0x3,0x0
	};
	
	for (int i=0;i<25;i++){
		for(int j=0;j<36;j++){
			if (data[i]==(g_rippedData[j]&0x7F)){
				for (int k=0;k<order[i];k++,n++){
					if (i==0){
						data_A[m]=j;
					}else{
						if (k==0){
							if (n%12%5%2){
								data_A[m]=~(~data_A[++m]&~(j<<h_rippedData[n-2]));
							}else{
								data_A[m]=~(~data_A[m]&~(j<<h_rippedData[n-2]));
							}
						}else{
							if (n%12%5%2){
								data_A[m]=~(~data_A[++m]&~(j>>h_rippedData[n-2]));
							}else{
								data_A[m]=~(~data_A[m]&~(j>>h_rippedData[n-2]));
							}
						}
					}	
				}
				break;
			}
		}
	}
}
void F2x(){
	int flag1,flag2,flag3;
	static unsigned char deviation1[]={
		0x01,0x08,0x0F,0x16,0x1D,0x24,0x2B
	};
	static unsigned char deviation2[]={
		0x07,0x0C,0x11,0x16,0x1B,0x00,0x05
	};
	static unsigned int deviation3[]={
		0xF80,0x1F000,0x3E0000,0x7C00000,0xF8000000,0x1F,0x3E0
	};
	struct data_AA{
		DWORD data_A1,data_A2,data_A3,data_A4;
	}data;

	memcpy(&data,data_A,sizeof data_A);
	flag=data.data_A3&0x7F;
	for (int i=0;i<7;i++){
		flag1=(((flag>>i)&0x01)<<0x01)+deviation1[i];
		if (i<5){
			flag2=shrd(data.data_A3,data.data_A4,deviation2[i])&0x1F;
			flag3=shrd(data.data_A1,data.data_A2,flag1)&0x1F;
			if (i==4){
				data.data_A2=~shld(0,0x1F,flag1)&data.data_A2;
				data.data_A2=~(~data.data_A2&~shld(0,flag2,flag1));
			};
			data.data_A1=~(0x1F<<flag1)&data.data_A1;
			data.data_A1=~(~data.data_A1&~(flag2<<flag1));
			data.data_A3=data.data_A3&~deviation3[i];
			data.data_A3=~(~data.data_A3&~(flag3<<deviation2[i]));
		}else{
			flag1=flag1&0x1F;
			flag2=(data.data_A4>>deviation2[i])&0x1F;
			flag3=(data.data_A2>>flag1)&0x1F;

			data.data_A2=~(0x1F<<flag1)&data.data_A2;
			data.data_A2=~(~data.data_A2&~(flag2<<flag1));
			data.data_A4=data.data_A4&~deviation3[i];
			data.data_A4=~(~data.data_A4&~(flag3<<deviation2[i]));
		}
	}
	password=data.data_A4>>(0x2D&0x1F);
	memcpy(data_A,&data,sizeof data_A);
}
void Compare(){
	int k;WORD a=0xFFFF,b=0x8408,aa;
	for (int j=0;j<14;j++){
		for (int i=0;i<8;i++){
			aa=a;a>>=1;
			flag=~(~(data_A[j]>>i)&~(char)aa)&~((data_A[j]>>i)&(char)aa)&0x01;
			if (flag) a=~(b&a)&~(~b&~a);
			if (j==13&&i==4) break;
		}
	}
	if (password==a){
		printf("\n序列号正确,%x=%x\n",password,a);
	}else{
		printf("\n序列号错误,%x<>%x\n",password,a);
	}
}

int main(){
	printf("F(x):");
	for(int i=0;i<25;i++){printf("%02x ",(unsigned char)data[i]);}
	F1x();
	printf("\nF`(x):");
	for(int i=0;i<16;i++){printf("%02x ",(unsigned char)data_A[i]);}
	F2x();
	printf("\nF``(x):");
	for(int i=0;i<16;i++){printf("%02x ",(unsigned char)data_A[i]);}
	Compare();
	system("PAUSE");
}


第二部分  调试器检测


说起调试器检测,大家可能觉得没什么难的,用插件过掉好了,大多数情况是这样。不过在我开启OllyExt v1.73全部过滤选项+Vic Plug-In 2隐藏PEB后,A还是给出了一个检测到调试器的弹窗。要知道这可比StrongOD强力多了。虽然在点击确定后,程序可以继续运行,但我本着严谨的态度决心探究一下到底是什么原因导致这个弹窗的发生。看到弹窗的标题Letarm.dll,自然想到该弹窗是由Letarm.dll引发的。开启插件全部过滤选项,对LoadLibraryW下条件断点[UNICODE [ESP+4]]=="Letarm.dll",运行到Letarm.dll载入为止,然后bp CheckRemoteDebuggerPresent。虽然在载入Letarm.dll之前A已经进行过CheckRemoteDebuggerPresent,但此处难保不会再次检测。在这里这个函数不会起作用,但可以得到函数的调用方式。运行,断在函数内部,此时堆栈跳转信息是错的,根本找不出是哪里跳来的。

0019E62C  [56F5870F  蜏囵  ; 返回到 Letarm.56F58F47 来自 Letarm.56F5870F  
0019E630   FFFFFFFF  ..  
0019E634   0019EEF8    
对从Letarm.dll载入到CheckRemoteDebuggerPresent之间运行跟踪代码,找到了

56F275B0      9C            PUSHFD  
56F275B1      9C            PUSHFD  
56F275B2      FF7424 38     PUSH DWORD PTR SS:[ESP+38]  
56F275B6      C2 3C00       RETN 3C   
原来是由RETN 3C调用的函数。函数的调用方式找到了,可CheckRemoteDebuggerPresent并不是导致弹窗的关键。对RETN 3C下条件记录断点记录一下从Letarm.dll导入到弹窗出现为止经过此处的次数---26次。对此处下执行26次的条件断点,运行,堆栈

0019E5E0   77426D40  浀睂  ; ntdll.NtQueryInformationProcess  
此时将跳往NtQueryInformationProcess,步进一下,看看堆栈

0019E620   56F5870F  蜏囵  ; 返回到 Letarm.56F58F47 来自 Letarm.56F5870F  
0019E624   FFFFFFFF  ..  
0019E628   0000001E   .  
0019E62C   0019EEF8    
0019E630   00000004   .  
0019E634   00000000  ..  
OD没有给注释,不过我确信这就是NtQueryInformationProcess的调用参数,看看第3个参数1E(十进制30),对应的是ProcessDebugObjectHandle,因此此处确实在检测调试器。NtQueryInformationProcess可以用下面的值检测调试器
ProcessDebugPort = 7,
ProcessDebugObjectHandle = 30,
ProcessDebugFlags = 31,
SystemKernelDebuggerInformation = 35,

事实上CheckRemoteDebuggerPresent内部就是用NtQueryInformationProcess 检测ProcessDebugPort = 7实现的。当运行到返回时,函数返回信息缓存处[0019EEF8]仍然为0,证明此处的检测也被插件过滤掉了。那么直接将此处到MessageBoxW参数传入之间运行跟踪导出代码分析一下
56F5898D      8910          MOV DWORD PTR DS:[EAX],EDX               ; Msg参数传入EDX=00200041  
有一个地方比较可疑, 
04CF87D8    PUSH DWORD PTR SS:[ESP+3C]  
04CF87DC    RETN 40  
04CF0A19    POP SS  
04CF7B8D    PUSHAD  
04CF7B8E    BSWAP EAX  
执行04CF0A19后直接跳到04CF7B8D处继续执行,这是什么鬼?没有CALL或RETN,程序不可能正常进入系统领空。我姑且认为遭遇了异常,04CF0A19之后直接进入SEH代码,出来后跳到04CF7B8D处继续执行。实际的情况还是需要手动跟踪一下,在RETN 40处下断点。发现在执行POP SS后又返回了RETN 40,没有进入SEH代码,真是莫名其妙,接着A才正常起来。运行,又断在RETN 40,紧接着的是INT3断点,这才跳到了SEH代码。运行,抵达Msg参数传入。关键就在POP SS处,重新运行A到此处,CPU窗口向上翻页时,POP SS突然变成另外一条指令。这是遇到花指令了。右键让OD分析代码

04D20A17      FE            DB FE  
04D20A18      79            DB 79                                    ; CHAR 'y'  
04D20A19      17            DB 17  
04D20A1A      9C            DB 9C  
04D20A1B      E8            DB E8  

原来是一些无意义的数据,在OD中A跳转这里后将这些数据当成了指令执行。解决办法,直接将POP SS改成INT3,让程序去执行预先SEH代码就行了。


第三部分 文件完整性校验


对于一个庞大的商业性的程序来说,有很多可供探索的地方,比如改动UI文字。修改程序是一件很酷的事情。首先在内存中搜索菜单栏中的”文件”,找到字符串出处LangCRes.dll,确定这是一个提供UI文字的dll。将”文件”改成”开始”,重新运行程序,A崩溃。A对LangCRes.dll进行了完整性校验,修改被发现了。一般来说程序的完整性校验方式有散列值校验、校验和、数字签名等,但都要动用到一个函数CreateFile,不打开文件怎么进行校验呢。因此我对CreateFileW下条件断点[UNICODE [ESP+4]]=="LangCRes.dll",之后运行到返回,步进出来,发现A处于WINTRUST领空,查了下是系统文件,作用之一就是进行数字签名验证,关键验证函数是WinVerifyTrust。我想现在A应该已经处在WinVerifyTrust内部了吧。查一下MSDN关于WinVerifyTrust返回值说明:

Return value
If the trust provider verifies that the subject is trusted for the specified action, the return value is zero. No other value besides zero should be considered a successful return.

因此运行A到用户代码,修改返回值为0,运行,A不再崩溃。之后通过HOOK或内存补丁修改WinVerifyTrust的返回值为0就行了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值