刚来看雪论坛,就碰上腾讯的安全竞赛,恰逢盛会,怎能错过。
这段时间参加了第一阶段的比赛,四个题目,都提交了自己的答案,这里作下总结,和各位新人朋友共勉。
第一题,30多个字节
第一题是最让我心里难受的。由于是开发人员出身,找到程序的bug还是比较快的,84个字节溢出,很快找到了,但是。。。。。。就在这里卡住了。。。卡了我一个晚上,睡觉睡得都不舒服,第二个白天也是工做不安心。。。就是不知道怎么重新定向这种间接引用的指令。
call [edx]
我的思路很简单。。。就是找到一条这样的指令:
jmp [esp+xx] 或者 add esp,xx retn,xx大于等于0x28
这种指令在user32.dll里很多,但是怎么让[edx]指向这条指令。。。。搜遍内存,没找到这样的地址。。。。。就在要放弃的时候,看到那个题目下面有人说硬编码,一下子雾开云散。。。原来可以硬编码。。。。要是第一关没过,后面的估计也没有心情了。
第二题,纯编程题,擅长啊。。。花了点时间,整了个算法。
测试数据:P4 2.8G vc2008,release O2优化,算法仅需15ms,打印30ms,共45ms左右
信心大增。
第三题,keygenme,由于之前发过一遍,【第一次keygen详解,献给各位兄弟】http://bbs.pediy.com/showthread.php?t=122264,心里有数,手上不慌。但是这个比赛日期恰是周六周日,而这两天我要陪老婆。。。。不过还是花了一个通宵把注册机找出来了。
第四题,对我很难,而且周日,周一的时间,很不凑巧。基本想放弃了。昨天晚上在看雪找到了一个pespin 1.32的脱壳机,居然可以用,还提供了ollydbg脚本。不过我一开始就使用的ollydbg2.0,不支持插件,(其实我是很喜欢指令调试的,曾经的gdb,wingdb,dbx让我者迷又经常混乱。。。)所以看不懂ollydbg sript,折腾半天后在看雪上找了个教程,对着教程一条条看指令。。。不过最后还是只提交了个脱壳机脱出来的程序。。
这7-8天时间真的是够累啊,白天上班,晚上加班学习搞这些题。不过付出总会有回报,我感觉自己在安全方面慢慢入门了。呵呵呵。
另外,由于是临时会员,下载竞赛题目就花了我不少kx。哎。。。
废话少说,现在开始第三题的分析。
第三题我主要是用IDA分析,分析完后用OD验证。
keygenme的验证主流程在函数sub_4012F0()中,使用IDA逆向后c代码如下:
char __cdecl sub_4012F0()
{
//逆向代码有删减
v19 = (int)"ABCDEFGHJKMNPQRSTVWXYZ1234567890";
v0 = SendMessageA(hWnd, 13u, 33u, (LPARAM)lParam);
v1 = v0;
if ( v0 )
{
v0 = SendMessageA(dword_40DBB4, 0xDu, 0x24u, (LPARAM)&v27);
if ( v0 == 35 )
{
v3 = 0;
v2 = &v44;
do
{
v0 = 0;
while ( v3 != byte_40CF98[v0] ) // 第9,18,27个字符是‘-‘
{
++v0;
if ( (unsigned int)v0 >= 3 )
goto LABEL_9;
}
if ( *(&v27 + byte_40CF98[v0]) != 45 )
return v0;
LABEL_9:
if ( v0 == 3 )
{
LOBYTE(v0) = sub_407FB0(&v18, *(&v27 + v3));// 校验序列号中是否含有不在"ABCDEFGHJKMNPQRSTVWXYZ1234567890"中的字符
if ( (_BYTE)v0 == -1 )
return v0;
*v2++ = *(&v27 + v3);
}
++v3;
}
while ( v3 < 35 );
LOBYTE(v0) = sub_407E40(&v18, (int)&v44, 32, (int)&v38, (int)&v17); //对密码进行加密===>v38,长度==>v17
if ( (_BYTE)v0 )
{
LOBYTE(v0) = sub_401000(v1, (int)&v20, (int)lParam); //对用户名加密 ==> v20
if ( (_BYTE)v0 )
{
v4 = 20;
v6 = &v20;
v5 = &v38;
do
{
if ( *(_DWORD *)v5 != *(_DWORD *)v6 )
goto LABEL_19;
v4 -= 4;
v6 += 4;
v5 += 4;
}
while ( (unsigned int)v4 >= 4 );
if ( v4 )
{
LABEL_19:
v7 = (unsigned __int8)*v5 - (unsigned __int8)*v6;
if ( (unsigned __int8)*v5 != (unsigned __int8)*v6
|| (v8 = v4 - 1, v10 = (int)(v6 + 1), v9 = (int)(v5 + 1), v8)
&& ((v7 = *(_BYTE *)v9 - *(_BYTE *)v10, *(_BYTE *)v9 != *(_BYTE *)v10)
|| (v11 = v8 - 1, v13 = v10 + 1, v12 = v9 + 1, v11)
&& ((v7 = *(_BYTE *)v12 - *(_BYTE *)v13, *(_BYTE *)v12 != *(_BYTE *)v13)
|| (v15 = v13 + 1, v14 = v12 + 1, v11 != 1)
&& (v7 = *(_BYTE *)v14 - *(_BYTE *)v15, *(_BYTE *)v14 != *(_BYTE *)v15))) )
{
v0 = 1;
if ( v7 <= 0 )
v0 = -1;
LABEL_29:
if ( !v0 )
LOBYTE(v0) = MessageBoxA(
dword_40DBB8,
"Congratulations! \n You will be the keygen machine!",
"Success!",
0);
return v0;
}
}
v0 = 0;
goto LABEL_29;
}
}
}
}
return v0;
}
其实主流程十分清晰:
<1>调用sub_407E40对输入的序列号(去掉第8,17,26处的'-'字符)进行换算,结果存在v38
<2>调用sub_401000对输入的User Name进行换算,结果存在v20
<3>比较v38,v20处的20个字节,完全相等,则成功,否则失败。(LABEL_19到LABEL_20之间的代码是浮云)
由于是逆向,所以先看sub_401000:
char __usercall sub_401000<al>(int a1<ecx>, int a2<edi>, int a3)
{
//逆向代码有删减
v3 = a1;
v17 = 0;
memset(&v18, 0, 0x2Bu);
v12 = dword_40AB90;
v13 = dword_40AB94;
VolumeSerialNumber = 0;
v11 = 0;
v10 = 0;
v14 = dword_40AB98;
v15 = dword_40AB9C;
v16 = dword_40ABA0;
if ( a2 && a3 )
{
GetVolumeInformationA("C:\\", 0, 0, &VolumeSerialNumber, 0, 0, 0, 0); //获取卷标
sub_407950(&v17, a3, v3);
v5 = BYTE1(VolumeSerialNumber);
v6 = BYTE2(VolumeSerialNumber);
*(&v17 + v3) = VolumeSerialNumber; //把卷标加到username后
v7 = BYTE3(VolumeSerialNumber);
*(&v18 + v3) = v5;
*(&v19 + v3) = v6;
*(&v20 + v3) = v7;
v8 = sub_4078E0("Tencent");
sub_407950(&v21[v3], "Tencent", v8);
sub_407C40(&v10, (int)&v17, v3 + 11); //把Tencent字符串加到卷标后
sub_407D00(&v10, a2); //使用变形的sha1对username+c盘卷号+"Tencent"散列。
result = 1;
}
else
{
result = 0;
}
return result;
}
过程也比较清晰:
<1>获取C盘卷标,并把卷标加到username后
<2>把Tencent字符串加到卷标后
<3>使用变形的sha1算法对username+c盘卷号+"Tencent"散列,求出20个字节共160bit的散列值。
sha1变形主要变在五个输入常量上,分别改为如下:
DWORD const_value_1 = 0xB1CAB1CA;
DWORD const_value_2 = 0xCCBFCCBF;
DWORD const_value_3 = 0xBFB2D6BE;
DWORD const_value_4 = 0xF8C7D8B5;
DWORD const_value_5 = 0xEEC7BCCD;
再看对序列号进行变换的过程,这个就没那么清晰了,我看到论坛里很多兄弟也是卡在这里。
char __thiscall sub_407E40(void *this, int a2, int a3, int a4, int a5)
{
int v5; // ebx@1
int v6; // esi@1
int v7; // eax@5
char result; // al@6
int v9; // eax@9
int v10; // eax@12
int v11; // edx@13
int v12; // ebp@17
unsigned __int8 v13; // al@18
void *v14; // [sp+10h] [bp-4h]@1
v5 = 0;
v6 = 0;
v14 = this;
if ( !a2 || !a5 || a3 <= 0 )
return 0;
if ( a4 )
{
if ( *(_DWORD *)a5 <= 0 )
return 0;
sub_407DF0(this);
v9 = 0;
if ( a3 > 0 )
{
while ( *((_BYTE *)v14 + *(_BYTE *)(v9 + a2)) != -1 )
{
++v9;
if ( v9 >= a3 )
goto LABEL_12;
}
return 0;
}
LABEL_12:
v10 = sub_407980(a2, byte_40CE68, a3);
if ( v10 )
{
v11 = v10 - a2;
a3 = v10 - a2;
}
else
{
v11 = a3;
}
if ( *(_DWORD *)a5 < (5 * v11 >> 3) + 1 )
return 0;
v12 = 0;
if ( v11 <= 0 )
{
LABEL_24:
*(_DWORD *)a5 = v6;
return 1;
}
while ( 1 )
{
v13 = *((_BYTE *)v14 + *(_BYTE *)(a2 + v12));
if ( (unsigned int)v5 > 3 )
break;
v5 = (v5 - 3) & 7;
if ( v5 )
goto LABEL_22;
*(_BYTE *)(v6++ + a4) |= v13;
LABEL_23:
++v12;
if ( v12 >= v11 )
goto LABEL_24;
}
v5 = (v5 - 3) & 7;
*(_BYTE *)(v6 + a4) |= v13 >> v5;
v11 = a3;
++v6;
LABEL_22:
*(_BYTE *)(v6 + a4) |= v13 << (8 - v5);
goto LABEL_23;
}
v7 = sub_407980(a2, byte_40CE68, a3);
if ( v7 )
{
*(_DWORD *)a5 = (5 * (v7 - a2) >> 3) + 1;
result = 1;
}
else
{
*(_DWORD *)a5 = (5 * a3 >> 3) + 1;
result = 1;
}
return result;
}
我把这个函数进行了整理,再现了变换序列号的过程:
char* find_ch(char* str, char tofind, int len)
{
if(len <=0)
return 0;
while(*str != tofind)
{
--len;
++str;
if(len<=0)
break;
}
return str;
}
void sha1_serial(char* serial, int len)
{
BYTE buf[0x100];
DWORD value=0x15;
memset(encrypt_serial,0,sizeof(encrypt_serial));
memset(buf,255,0x100);
for(int i=0;i<32;++i)
{
*(buf+const_str_2[i])=i;
}
*(buf+const_char)=32;
int len1=0;
char* pos1 = find_ch(serial,const_char,len);
if(pos1)
{
len1=pos1-serial;
len = pos1-serial;
}
else
{
len1=len;
}
if(value < ((5*len1)>>3)+1 || len1<=0)//如果第一个字母是'=',则退出
{
return;
}
int index=0;
int index_encrypt=0;
int ctrl=0;
char c = 0;
while(1)
{
c = *(buf+serial[index]);
if(ctrl > 3)
{
ctrl = (ctrl-3) & 7;
encrypt_serial[index_encrypt++]|= c>>ctrl;
encrypt_serial[index_encrypt] |= c<<(8-ctrl);
}
else
{
ctrl = (ctrl-3) & 7;
if(ctrl)
{
encrypt_serial[index_encrypt] |= c<<(8-ctrl);
}
else
{
encrypt_serial[index_encrypt++] |= c;
}
}
++index;
if(index>=len1)
{
value = index_encrypt;
return;
}
}
char* pos2 = find_ch(serial,const_char,len);
if ( pos2 )
{
value = (5 * (pos2 - serial) >> 3) + 1;
}
else
{
value = (5 * len >> 3) + 1;
}
}
这样再看就非常清晰了,过程如下:
<1>算出序列号中每个字母的编号,存在buf中,编号范围是0-31,共32个符号,2进制就是00000-11111共5个bit
<2>然后把变换后的bit拼接起来,序列号共32位,故拼接后32*5=160bit=20字节
故:
设a1-a8是序列号前8位,d1-d5是散列值,则有如下关系:
/****************************************************************
* a0 a0 a0 a0 a0 a1 a1 a1 | a1 a1 a2 a2 a2 a2 a2 a3 | a3 a3 a3 a3 a4 a4 a4 a4 | a4 a5 a5 a5 a5 a5 a6 a6 | a6 a6 a6 a7 a7 a7 a7 a7
* d1 d1 d1 d1 d1 d1 d1 d1 | d2 d2 d2 d2 d2 d2 d2 d2 | d3 d3 d3 d3 d3 d3 d3 d3 | d4 d4 d4 d4 d4 d4 d4 d4 | d5 d5 d5 d5 d5 d5 d5 d5
*****************************************************************/
现在,一切清楚,注册机代码如下:
//sha1算法代码摘自网络
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <errno.h>
#include <Windows.h>
/****************************************************************
* a0 a0 a0 a0 a0 a1 a1 a1 | a1 a1 a2 a2 a2 a2 a2 a3 | a3 a3 a3 a3 a4 a4 a4 a4 | a4 a5 a5 a5 a5 a5 a6 a6 | a6 a6 a6 a7 a7 a7 a7 a7
*****************************************************************/
typedef unsigned char BYTE;
typedef unsigned int u32;
typedef struct {
u32 h0,h1,h2,h3,h4;
u32 nblocks;
unsigned char buf[64];
int count;
} SHA1_CONTEXT;
void sha1_init( SHA1_CONTEXT *hd );
void sha1_write( SHA1_CONTEXT *hd, unsigned char *inbuf, size_t inlen);
void sha1_final(SHA1_CONTEXT *hd);
BYTE encrypt_username[20];
char keygenme_serial[40];
char* const_str_1 = "Tencent";
char* const_str_2 = "ABCDEFGHJKMNPQRSTVWXYZ1234567890";
char const_char = '=';
DWORD const_value_1 = 0xB1CAB1CA;
DWORD const_value_2 = 0xCCBFCCBF;
DWORD const_value_3 = 0xBFB2D6BE;
DWORD const_value_4 = 0xF8C7D8B5;
DWORD const_value_5 = 0xEEC7BCCD;
void sha1_username(char* username, int len)
{
BYTE content[44];
memset(content,0,sizeof(content));
DWORD volumeSerialNumber;
GetVolumeInformationA("C:\\", 0, 0, &volumeSerialNumber, 0, 0, 0, 0);
memcpy(content,username,len);
memcpy(content+len,&volumeSerialNumber,4);
memcpy(content+len+4,const_str_1,strlen(const_str_1)+1);
int content_len = len + strlen(const_str_1) + 4;
SHA1_CONTEXT ctx;
sha1_init (&ctx);
sha1_write (&ctx, content, content_len);
sha1_final (&ctx);
memcpy(encrypt_username,ctx.buf,20);
}
void sha1_username_to_serial()
{
BYTE encrypt_serial[32];
int j=0;
for(int i=0;i<20; i=i+5)
{
BYTE b0 = encrypt_username[i];
BYTE b1 = encrypt_username[i+1];
BYTE b2 = encrypt_username[i+2];
BYTE b3 = encrypt_username[i+3];
BYTE b4 = encrypt_username[i+4];
encrypt_serial[j]=b0>>3;
encrypt_serial[j+1]=((b0<<2)&0x1f)|(b1>>6);
encrypt_serial[j+2]=(b1>>1)&0x1f;
encrypt_serial[j+3]=((b1<<4)&0x1f) | (b2>>4);
encrypt_serial[j+4]=((b2<<1)&0x1f)| (b3>>7);
encrypt_serial[j+5]=(b3>>2)&0x1f;
encrypt_serial[j+6]=((b3<<3)&0x1f)|(b4>>5);
encrypt_serial[j+7]=b4&0x1f;
j=j+8;
}
int n=0;
for(int k=0;k<32;++k)
{
if(n==8 || n==17 || n==26)
{
keygenme_serial[n++] = '-';
}
keygenme_serial[n++]=const_str_2[encrypt_serial[k]];
}
}
int main(int argc, char** argv)
{
if(argc <= 1)
{
printf("please input User Name!\n");
return 1;
}
if(strlen(argv[1])>32)
{
printf("User name is too long!\n");
return 1;
}
char* username = argv[1];
sha1_username(username,strlen(username));
sha1_username_to_serial();
printf("User Name=%s\n",username);
printf("License Code=%s\n",keygenme_serial);
return 0;
}
//**************sha1 代码************************
/* SHA-1 coden take from gnupg 1.3.92. */
// sha-1代码省略
欢迎各位交流。
运行:
>keygenme.exe pediy
User Name=pediy
License Code=F8S19NWX-YRHZXKMY-KCK40C8B-CPXW1ENJ
[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法