刚来看雪论坛,就碰上腾讯的安全竞赛,恰逢盛会,怎能错过。
这段时间参加了第一阶段的比赛,四个题目,都提交了自己的答案,这里作下总结,和各位新人朋友共勉。

第一题,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
上传的附件 keygen.zip