2016kxctf第28题

2016kxctf第28题

Posted by gxkyrftx on January 4, 2019

0.前言

1.全部是自己实现的,有问题欢迎讨论

2.调试过程中的文件及相关idb

https://github.com/gxkyrftx/kxctf/tree/master/2016.12.%E7%AC%AC28%E9%A2%98

3.教程参考 https://www.kanxue.com/book-35-374.htm

1.程序流程分析

1.0 关闭aslr

使用CFF

6

把relocation info stripped from file 选上,意思就是从文件中删除的重新定位信息。

然后定位到main函数,f5如下

1.1 main

整体的逻辑 1.获取输入字符串 2.判断长度是否符合要求 3.md5算法,初始化四个参数,计算64轮 4.rc4算法,以一字节为密钥,解密一些东西 具体分析见注释

  printf("--- CrackMe By Try2Crack ---\n");
  printf("Serial:");
  sn = 0;
  memset(&Dst, 0, 0x63u);                       // 内存空间初始化
  scanf("%s", &sn);                             // 获取输入的字符串
  length_sn = strlen(&sn);                      // 获取输入字符串的长度,判断范围在不在[0x3D-0x64]之间
  if ( length_sn > 0x64 )
    return 1;
  if ( length_sn <= 0x3C )
    return 1;
  v4 = (char *)&v16 + 15;
  do
    v5 = (v4++)[1];
  while ( v5 );
  *(_WORD *)v4 = '==';//在sn末尾+“==”
  v4[2] = 0;
  _mm_storeu_si128((__m128i *)&v16, (__m128i)0i64);//变量赋值
  memset(&v11, 0, 0x54u);
  v10 = 0;
  v11 = 0;
  v12 = 0x67452301;
  v13 = 0xEFCDAB89;
  v14 = 0x98BADCFE;                             // md5实现过程,与https://github.com/leimingshan/MD5/blob/master/md5c.c基本相同
  v15 = 0x10325476;                             // md5的参数,小端序排列
  md5_updata(&sn, (unsigned int *)&v10, strlen(&sn));// 分成512位的块,每个块生成128bit的向量,经过64轮变换
  md5_final((unsigned int *)&v10);              // 填充
  rc4(1, (int)&v20, (int)&word_405020, 0x34A00, &word_405020);// 此处判断为rc4算法,是对word_405020中一些文本进行运算。sn不直接参与运算,因为sn没有出现
  v7 = load(v6);
  v8 = v7;
  if ( !v7 || sub_4012EE(v19 - 48, (int)v7, v21 - 48, v22 - 48) )
    return 1;
  sub_40143D((unsigned __int8 *)&sn);
  sub_402849(v8);
  return 0;
}

1.2 md5函数的判断

1.根据四个参数初步确定

2.跟进md5_updata函数,与md5实现算法比较,如下图所示。

1 3.可以看到,函数结构,参数与md5是相同的,因此确定为md5算法。

1.3 rc4算法判断

1.rc4原理

RC4的原理分为三步:
1、初始化ST
for i=0 to 255 do
   S[i]=i;
   T[i]=K[ imodkeylen ];
2、初始排列S
j=0;
for i=0 to 255 do
   j= ( j+S[i]+T[i])mod256;
   swap(S[i],S[j]);
3、产生密钥流
i,j=0;
for r=0 to len do  //r为明文长度,r字节
   i=(i+1) mod 256;
   j=(j+S[i])mod 256;
   swap(S[i],S[j]);
   t=(S[i]+S[j])mod 256;
   k[r]=S[t];

2.题目中的函数部分内容

  do                                            // 以下是KSA(密钥调度算法)过程
    v21[v7++] = v6++;                           // 初始化s[i]
  while ( v7 <= 255 );
  v8 = 0;
  do
  {
    v20[v8] = *(_BYTE *)(v8 % a1 + v5);         // 重复使用密钥
    ++v8;
  }
  while ( v8 <= 255 );
  v9 = 0;
  LOBYTE(v10) = 0;
  v11 = 256;
  do                                            // 交换s[i],s[j]
  {
    v12 = v9;
    v13 = v21[v9];
    v10 = (unsigned __int8)(v10 + v20[v9++] + v13);
    v21[v12] = v21[v10];
    v21[v10] = v13;
    --v11;
  }
  while ( v11 );                                // 以下是prga(伪随机子密码生成算法)及解密过程
  if ( a4 - 1 >= 0 )
  {
    v14 = 0;
    v15 = a5;
    v19 = a4;
    do
    {
      v11 = (unsigned __int8)(v11 + 1);
      v16 = v21[v11];                           // 交换s[i]和s[j]
      v17 = (unsigned __int8)(v14 + v16);
      v21[v11] = v21[v17];
      v21[v17] = v16;
      v14 += v16;
      *v15 = v15[a3 - (_DWORD)a5] ^ v21[(unsigned __int8)(v16 + v21[v11])];//直接把密钥流与明文异或
      ++v15;
      --v19;
    }
    while ( v19 );
  }
  return 0;

可以看出两者完成的功能一致,基本可判定为rc4算法。

1.4 rc4运算后的内容

rc4(1, (int)&v20, (int)&word_405020, 0x34A00, &word_405020) 首先我们知道,rc4,是把[word_405020,word_405020+0x34A00]这段空间内的字符做了解密。继续往下看load函数,部分伪代码如下。

v1 = 0;
  if ( word_405020 != 'ZM' )                    // 判断是不是MZ头
  {
    SetLastError(0xC1u);
    return 0;
  }
  if ( !length_cmp_34A00((int)this, dword_40505C + 0xF8) )
    return 0;
  v3 = (char *)&word_405020 + dword_40505C;
  if ( *(_DWORD *)((char *)&word_405020 + dword_40505C) != 'EP' || *((_WORD *)v3 + 2) != 0x14C || v3[56] & 1 )// 判断pe头
  {
    SetLastError(0xC1u);
    return 0;
  }

可以看到,在这个函数中,判断解密的内容,首部是不是MZ,紧接着还有PE,可以判断解密出来的东西,是一个PE文件。根据这一点,对[word_405020,word_405020+0x34A00]这段空间内的字符,使用rc4算法,进行穷举,只有一个字节的密钥,而且将输入字符集限制在”0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ”,穷举可行。穷举代码如下:

#coding : utf-8
class RC4:
    def __init__(self, key):
        self.K = []
        self.S = []
        self.key = key
        self.S = [s for s in range(256)]    
        for i in range(256):
            self.K.append(self.key[i % len(self.key)])
        j = 0
        for i in range(256):
            j = (j + self.S[i] + self.K[i]) % 256
            self.S[i], self.S[j] = self.S[j], self.S[i]
    def encrypt(self, data):
        i, j = 0, 0
        keystream = []
        out = []
        dataLength = len(data)
        for k in range(dataLength):
            i = (i + 1) % 256
            j = (j + self.S[i]) % 256
            tmp = (self.S[i] + self.S[j] % 256) % 256
            keystream.append(self.S[tmp])
            out.append(data[k] ^ keystream[k])
        return out

if __name__ == '__main__':
    keystream='0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
    for x in keystream:
        a = RC4([ord(x)])
        encryptData = a.encrypt([0xB2, 0xB4])
        showList = []
        for i in encryptData:
            showList.append(hex(i))
        print(showList),x

调了一个小时。。哎,水平一天不如一天。。 最终结果如下:密钥为I

2

关于密钥位置的确定,可以通过ida动态调试的方法,例如,将“0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ”作为序列号直接输入,可以看到,经过rc4函数后,单步步过f8,然后双击v20,可以看到我输入之后,v20,到底被赋了什么样的值。我的如下图:

8

然后下面这张图是密钥输入时被加载的起始位置

9

将0x0019fef8-0x0019fed0=0x28=40,也就是说,输入的序列号sn[40]是rc4的密钥。

1.5 求解lua.dll

刚才穷举除了密钥为“I”,如何解密这段内容?我使用了ida动态调试,在rc4函数的地方下断点,然后run,输入序列号(位数在[0x3D-0x64]之间),为了方便,我就输70个I,结果如下。 3

然后f8,一直f8,等到调用rc4之后,会发现,解密出来了。如下图 4

然后将这段pe文件dump出来,具体操作:file——>script commend,输入idc脚本如下

#include<idc.idc>
static main(void)
{
auto i,fp;
fp=fopen("e:\\dump_encode.bin","wb");
for(i=0x405020;i<0x405020+0x34A00;i++)
fputc(Byte(i),fp);
}

得到了dump_encode.bin,因为是pe文件,直接上ida,可以看到导出表中的导出函数,所以它是lua.dll

5

1.6 load函数

这个需要动态调试,可以看到,这个函数里面一直在不停的调用alloc等这样的分配空间的函数,结合自己调试与大佬的分析,这是在进行内存分段加载,text,data,rdata等

单步步过load函数,看到ecx中已经加载了lua.dll,所以此函数功能为,分段加载lua.dll

7

1.7 sub_4012EE

在此处中调用的:if ( !v7 || sub_4012EE(v19 - 48, (int)v7, v21 - 48, v22 - 48) ),其中v7来源于load函数,返回了到了lua.dll。v19,v21,v22的内容,通过动态调试也可以找到,只介绍v19,如下图:

10

0x0019feee-0x0019fed0=0x1e=30,也就是sn[30]。 下面开始分析函数功能,函数内容如下:

BOOL __usercall sub_4012EE@<eax>(int a1@<edx>, int a2@<ecx>, int a3, int a4)
{
  int v4; // esi
  _DWORD *v5; // edi
  int (__cdecl *v6)(_DWORD, _DWORD); // eax

  v4 = a1;
  v5 = (_DWORD *)a2;
  dword_443240 = (int (*)(void))sub_4027A5((_DWORD *)a2, (const char *)1);
  dword_443220 = (int (__cdecl *)(_DWORD, _DWORD))sub_4027A5(v5, (const char *)(v4 - 5));
  dword_44221C = (int (__cdecl *)(_DWORD, _DWORD, _DWORD, _DWORD, _DWORD, _DWORD))sub_4027A5(v5, (const char *)5);
  dword_442214 = (int (__cdecl *)(_DWORD, _DWORD))sub_4027A5(v5, (const char *)7);
  dword_44323C = (int (__cdecl *)(_DWORD, _DWORD))sub_4027A5(v5, (const char *)8);
  dword_443234 = (int (__cdecl *)(_DWORD))sub_4027A5(v5, (const char *)9);
  dword_442218 = (int (__cdecl *)(_DWORD, _DWORD, _DWORD, _DWORD, _DWORD))sub_4027A5(v5, (const char *)(2 * a4 + 4));
  dword_443230 = (int (__cdecl *)(_DWORD, _DWORD))sub_4027A5(v5, (const char *)0xA);
  dword_442210 = (int (__cdecl *)(_DWORD, _DWORD))sub_4027A5(v5, (const char *)0xB);
  dword_443224 = (int (__cdecl *)(_DWORD, _DWORD, _DWORD))sub_4027A5(v5, (const char *)0xC);
  dword_443228 = (int (__cdecl *)(_DWORD))sub_4027A5(v5, (const char *)0xD);
  dword_443244 = (int (__cdecl *)(_DWORD, _DWORD, _DWORD))sub_4027A5(v5, (const char *)(a3 + 2));
  v6 = (int (__cdecl *)(_DWORD, _DWORD))sub_4027A5(v5, (const char *)2);
  dword_44322C = v6;
  return !dword_443240
      || !v6
      || !dword_443220
      || !dword_442218
      || !dword_44221C
      || !dword_443244
      || !dword_442214
      || !dword_44323C
      || !dword_443234
      || !dword_443230
      || !dword_442210
      || !dword_443224
      || !dword_443228;
}

1.8 sub_4027A5

观察到其中多次调用sub_4027A5函数,他有两个参数参数a1,dll中的一个位置;a2是一个常量,但是不停的在变化,1,7,8,9,A,B,C,D之类的。可以联想到pe中的基础知识,既然加载了dll,那么肯定要使用它的导出函数,这个是pe中最重要的,导出函数的使用有两种方式,一种是按函数名,一种是按函数序号。既有地址,又有常量,猜测是按序号使用dll中的导出函数。下面看看它在干嘛

int __fastcall sub_4027A5(_DWORD *a1, const char *a2)
{
  int v2; // ebx
  unsigned int v3; // edi
  _DWORD *v4; // esi
  unsigned int v5; // ecx
  _DWORD *v6; // ecx
  const char *v8; // [esp+Ch] [ebp-Ch]
  int v9; // [esp+10h] [ebp-8h]
  unsigned __int16 *v10; // [esp+14h] [ebp-4h]

  v2 = a1[1];
  v3 = 0;
  v8 = a2;
  if ( !*(_DWORD *)(*a1 + 0x7C) )
    goto LABEL_10;
  v4 = (_DWORD *)(v2 + *(_DWORD *)(*a1 + 0x78));
  if ( !v4[6] && !v4[5] )
    goto LABEL_10;
  if ( !HIWORD(a2) )
  {
    if ( (unsigned int)(unsigned __int16)a2 >= v4[4] )
    {
      v5 = (unsigned __int16)a2 - v4[4];
      goto LABEL_13;
    }
LABEL_10:
    SetLastError(0x7Fu);
    return 0;
  }
  v6 = (_DWORD *)(v2 + v4[8]);
  v9 = v2 + v4[8];
  v10 = (unsigned __int16 *)(v2 + v4[9]);
  if ( v4[6] <= 0u )
    goto LABEL_10;
  while ( stricmp(a2, (const char *)(v2 + *v6)) )
  {
    ++v3;
    ++v10;
    v6 = (_DWORD *)(v9 + 4);
    a2 = v8;
    v9 += 4;
    if ( v3 >= v4[6] )
      goto LABEL_10;
  }
  v5 = *v10;
LABEL_13:
  if ( v5 > v4[5] )
    goto LABEL_10;
  return v2 + *(_DWORD *)(v4[7] + 4 * v5 + v2);
}

a1中的内容,由于按小端徘地址,a1的内容为0x000000F0。

debug017:00474538 db 0F0h
debug017:00474539 db    0

分别加了0x7c=0x16c,加了0x78=0x168之后,得到结果如图所示,确定是对lua.dll导出表的操作无疑,然后也大体确定了1.7 sub_4012EE中,多次调用sub_4027A5是在按函数序号导出函数。这些dword 字段,实际就是导出函数的名字。 11

1.9 判断sub_4012EE中的参数内容

将sub_4012EE中dword字段的函数名字,与lua.dll导出函数中的名字一一对应,发现最后又三个函数名称是无法确定的,需要根据sub_4012EE中的参数内容分析。如下图所示

12

其中三个未确定的函数,是因为他们的序号由输入决定。现在需要根据函数名称,去寻找函数序号。如何寻找?这三个函数的参数的数量是不一样的,根据这一点判断出来,分别对应参数个数为5,3,2.

1.10 sub_40143D

然后进入sub_40143D,可以看到,里面在各种调用lua.dll中的函数,把不知道的函数,和参数一对比,就可确定函数名称,从而确定函数序号,最后确定sn某一位的值,在此以上面的v19也就是sn[30]为例,sub_40143D关于by_a1_sn_30_的部分代码如下:

  v1 = this;
  v2 = luaL_newstate();//分配空间
  luaL_openlibs(v2);//加载标准类库
  dword_44322C(v2, v1);这是lua_pushstring,没有改过来,功能:将这个值设置到栈,赋值
  by_a1_sn_30_(v2, &unk_404138);//这是lua_setglobal,功能为设置全局变量,点进去unk_404138就是lua脚本中的L

两个参数,对应lua_setglobal,

int __cdecl lua_setglobal(int a1, int a2)

函数序号为3,根据

dword_443220 = (int (__cdecl *)(_DWORD, _DWORD))sub_4027A5(v5, (const char *)(v4 - 5));

倒推得到v4为8, 根据

 if ( !v7 || lua_call(sn[30] - 0x30, (int)v7, sn[46] - 0x30, sn[58] - 0x30) )

倒退的到sn[30]的asill码为0x38,就是‘8’ 同理可得sn[46] == ‘4’ ,sn[58] == ‘0’

1.10 luaL_loadbufferx

luaL_loadbufferx(v2, &unk_439A60, 33876, &unk_40413C, 0) 从439A60开始的33876字节,里面又是一大串文本,结合上一步刚调用了rc4,动态调试一下,在luaL_loadbufferx处下断点,然后输入一串满足要求: sn[30] == ‘8’ && sn[40] == ‘I’ && sn[46] == ‘4’ && sn[58] == ‘0’的字符串 可以看到如下结果

13

把这一段dump出来,脚本如下。

#include<idc.idc>
static main(void)
{
auto i,fp;
fp=fopen("e:\\stepa.luac","wb");
for(i=0x439A60;i<0x439A60+0x8454;i++)
fputc(Byte(i),fp);
}

然后使用工具,对stepa.luac文件进行编译,工具使用如下

https://github.com/viruscamp/luadec
git clone https://github.com/viruscamp/luadec
cd luadec
git submodule update --init lua-5.3
cd lua-5.3
make linux
cd ../luadec
make LUAVER=5.3

编译完成后,进入luadec找到可执行文件luadec,然后输入,进行编译

./luadec stepa.luac

得到结果如图所示,得到了一个lua脚本。

14

代码如下

-- params : ...
-- function num : 0 , upvalues : _ENV
ZZBase64 = {}
-- DECOMPILER ERROR at PC70: Confused about usage of register: R0 in 'UnsetPending'

ZZBase64.__code = {"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "+", "/"}
-- DECOMPILER ERROR at PC73: Confused about usage of register: R0 in 'UnsetPending'

ZZBase64.__decode = {}
for k,v in pairs(ZZBase64.__code) do
  -- DECOMPILER ERROR at PC87: Confused about usage of register: R5 in 'UnsetPending'

  (ZZBase64.__decode)[(string.byte)(v, 1)] = k - 1
end
-- DECOMPILER ERROR at PC92: Confused about usage of register: R0 in 'UnsetPending'

ZZBase64.encode = function(text)
  -- function num : 0_0 , upvalues : _ENV
  local len = (string.len)(text)
  local left = len % 3
  len = len - left
  local res = {}
  local index = 1
  for i = 1, len, 3 do
    local a = (string.byte)(text, i)
    local b = (string.byte)(text, i + 1)
    local c = (string.byte)(text, i + 2)
    local num = a * 65536 + b * 256 + c
    for j = 1, 4 do
      local tmp = (math.floor)(num / 2 ^ ((4 - j) * 6))
      local curPos = tmp % 64 + 1
      res[index] = (ZZBase64.__code)[curPos]
      index = index + 1
    end
  end
  if left == 1 then
    (ZZBase64.__left1)(res, index, text, len)
  else
    if left == 2 then
      (ZZBase64.__left2)(res, index, text, len)
    end
  end
  return (table.concat)(res)
end

-- DECOMPILER ERROR at PC95: Confused about usage of register: R0 in 'UnsetPending'

ZZBase64.__left2 = function(res, index, text, len)
  -- function num : 0_1 , upvalues : _ENV
  local num1 = (string.byte)(text, len + 1)
  num1 = num1 * 1024
  local num2 = (string.byte)(text, len + 2)
  num2 = num2 * 4
  local num = num1 + num2
  local tmp1 = (math.floor)(num / 4096)
  local curPos = tmp1 % 64 + 1
  res[index] = (ZZBase64.__code)[curPos]
  local tmp2 = (math.floor)(num / 64)
  curPos = tmp2 % 64 + 1
  res[index + 1] = (ZZBase64.__code)[curPos]
  curPos = num % 64 + 1
  res[index + 2] = (ZZBase64.__code)[curPos]
  res[index + 3] = "="
end

-- DECOMPILER ERROR at PC98: Confused about usage of register: R0 in 'UnsetPending'

ZZBase64.__left1 = function(res, index, text, len)
  -- function num : 0_2 , upvalues : _ENV
  local num = (string.byte)(text, len + 1)
  num = num * 16
  tmp = (math.floor)(num / 64)
  local curPos = tmp % 64 + 1
  res[index] = (ZZBase64.__code)[curPos]
  curPos = num % 64 + 1
  res[index + 1] = (ZZBase64.__code)[curPos]
  res[index + 2] = "="
  res[index + 3] = "="
end

-- DECOMPILER ERROR at PC101: Confused about usage of register: R0 in 'UnsetPending'

ZZBase64.decode = function(text)
  -- function num : 0_3 , upvalues : _ENV
  local len = (string.len)(text)
  local left = 0
  if (string.sub)(text, len - 1) == "==" then
    left = 2
    len = len - 4
  else
    if (string.sub)(text, len) == "=" then
      left = 1
      len = len - 4
    end
  end
  local res = {}
  local index = 1
  local decode = ZZBase64.__decode
  for i = 1, len, 4 do
    local a = decode[(string.byte)(text, i)]
    local b = decode[(string.byte)(text, i + 1)]
    local c = decode[(string.byte)(text, i + 2)]
    local d = decode[(string.byte)(text, i + 3)]
    local num = a * 262144 + b * 4096 + c * 64 + d
    local e = (string.char)(num % 256)
    num = (math.floor)(num / 256)
    local f = (string.char)(num % 256)
    num = (math.floor)(num / 256)
    res[index] = (string.char)(num % 256)
    res[index + 1] = f
    res[index + 2] = e
    index = index + 3
  end
  if left == 1 then
    (ZZBase64.__decodeLeft1)(res, index, text, len)
  else
    if left == 2 then
      (ZZBase64.__decodeLeft2)(res, index, text, len)
    end
  end
  return (table.concat)(res)
end

-- DECOMPILER ERROR at PC104: Confused about usage of register: R0 in 'UnsetPending'

ZZBase64.__decodeLeft1 = function(res, index, text, len)
  -- function num : 0_4 , upvalues : _ENV
  local decode = ZZBase64.__decode
  local a = decode[(string.byte)(text, len + 1)]
  local b = decode[(string.byte)(text, len + 2)]
  local c = decode[(string.byte)(text, len + 3)]
  local num = a * 4096 + b * 64 + c
  local num1 = (math.floor)(num / 1024) % 256
  local num2 = (math.floor)(num / 4) % 256
  res[index] = (string.char)(num1)
  res[index + 1] = (string.char)(num2)
end

-- DECOMPILER ERROR at PC107: Confused about usage of register: R0 in 'UnsetPending'

ZZBase64.__decodeLeft2 = function(res, index, text, len)
  -- function num : 0_5 , upvalues : _ENV
  local decode = ZZBase64.__decode
  local a = decode[(string.byte)(text, len + 1)]
  local b = decode[(string.byte)(text, len + 2)]
  local num = a * 64 + b
  num = (math.floor)(num / 16)
  res[index] = (string.char)(num)
end

dk = function(k, l)
  -- function num : 0_6 , upvalues : _ENV
  (ZZBase64.decode)(text)
end

t1{一个数组,很长~} //上面的都是base64相关的,有解码,有编码。
_ENV.cp = (ZZBase64.decode)(L)//l解码,l来源于sub_40143D,中的设置全局变量,其实就是输入的sn,对sn进行base64解码
if ((_ENV.string).len)(_ENV.cp) ~= 46 then//解码后的长度不是46,退出
  _ENV.g_v1 = nil
end
if (_ENV.cp)[1] ~= "." then//解码后的[1]的位置不是.,退出
  _ENV.g_v1 = nil
else
  if (_ENV.cp)[2] ~= "t" then//解码后的[2]的位置不是t,退出
    _ENV.g_v1 = nil
  else
    if (_ENV.cp)[3] ~= "r" then//解码后的[3]的位置不是r,退出
      _ENV.g_v1 = nil
    else
      if (_ENV.cp)[4] ~= "y" then//解码后的[4]的位置不是y,退出
        _ENV.g_v1 = nil
      else
        if (_ENV.cp)[5] ~= "2" then//解码后的[5]的位置不是2,退出
          _ENV.g_v1 = nil
        else
          if (_ENV.cp)[6] ~= "c" then//解码后的[6]的位置不是c,退出
            _ENV.g_v1 = nil
          else
            if (_ENV.cp)[7] ~= "r" then//解码后的[7]的位置不是r,退出
              _ENV.g_v1 = nil
            else
              if (_ENV.cp)[8] ~= "a" then//解码后的[8]的位置不是a,退出
                _ENV.g_v1 = nil
              else
                if (_ENV.cp)[9] ~= "c" then//解码后的[9]的位置不是c,退出
                  _ENV.g_v1 = nil
                else
                  if (_ENV.cp)[10] ~= "k" then//解码后的[10]的位置不是k,退出
                    _ENV.g_v1 = nil
                  end
                end
              end
            end
          end
        end
      end
    end
  end
end
_ENV.sk = ((_ENV.string).sub)(_ENV.cp, 2, 10)//要求全部满足,sk就是2-10位“try2crack
_ENV.g_v1 = ((_ENV.string).sub)(_ENV.cp, 11)//g_v111位后面的
if _ENV.g_v1 ~= 0 then//判断一下不是0return t1{一个大数组}
  return t1
else
  return nil
end

1.11 lua赋值

1.10说到,最后luaL_loadbufferx,如果满足条件,返回了一个大的数组,随后的这个for循环,就是在给byte_442220进行赋值,动态调试可以看到,进入for循环,一直f7,可以看到byte_442220的 值一直在改变。for循环里调用了lua的函数

 for ( i = lua_next(v2, -2); i; i = lua_next(v2, -2) )
  {
    v4 = lua_tointeger(v2, -1, 0);
    v5 = dword_443238;
    byte_442220[dword_443238] = v4;
    dword_443238 = v5 + 1;
    lua_settop(v2, -2);
  }

结果如下图所示

15

1.12 lua_getglobal

紧接下来的lua_getglobal

if ( !lua_getglobal(v2, "sk") )
  {
    v6 = lua_tolstring(v2, -1, 0);
    printf("error\n", v6);
    return 1;
  }

寻找了sk这个变量,在lua脚本里有,也就是“try2crack”

随后的主要代码,v7来源于v2,是字符串“try2crack”,可以静态看到,以下功能就是对“try2crack”求md5

  v14 = 0;
  v16 = 0x67452301;
  Dst = 0;
  v17 = 0xEFCDAB89;
  v18 = 0x98BADCFE;
  v19 = 0x10325476;
  md5_updata(v7, (unsigned int *)&v14, strlen(v7));
  md5_final((unsigned int *)&v14);

验证通过,则调用rc4算法,解密另外一个脚本

1.13 rc4第二次解密

rc4(16, (int)&v20, (int)byte_442220, dword_443238, byte_442220);

下断点,输入满足上述所有条件要求的sn,然后可以看到,这个rc4又解密了一个stepb.lua,把它dump出来,上面的脚本改一下就好了。其余流程一样。可以得到stepb.lua,他的内容为

-- params : ...
-- function num : 0 , upvalues : _ENV
getn = function(t)
  -- function num : 0_0 , upvalues : _ENV
  local x = 0
  for k,v in pairs(t) do
    x = x + 1
  end
  return x
end

bs = function(ar, mode)
  -- function num : 0_1 , upvalues : _ENV
  local l = getn(ar)//获取ar的长度,ar是一个table,表
  for i = 1, l - 1 do//有点像冒泡法排序,一个一个向后走
    for j = 1, l - i do
      -- DECOMPILER ERROR at PC23: Unhandled construct in 'MakeBoolean' P1

      if mode ~= 0 and ar[j + 1] < ar[j] then//mode==1返回最小值
        ar[j] = ar[j + 1]
      end
      if ar[j] < ar[j + 1] then//mode==0返回最大值
        ar[j] = ar[j + 1]
      end
    end
  end
end

vc = function(k)
  -- function num : 0_2 , upvalues : _ENV
  local x = (math.sqrt)((string.len)(k))//参数k的长度然后sqrt,根号36=6x=6
  local r = {}
  local idx = 1
  local s = 0
  for i = 1, x do//二维的,看一下就想起了6*6矩阵,计算各行的值
    for j = 1, x do//j循环,计算一行的值
      s = s + (string.byte)(k, j + (i - 1) * x)//
    end
    r[idx] = s//计算出一行的值,放入另外的一个数组r
    s = s - (s)//归零
    idx = idx + 1//+1,下一行
  end
  for i = 1, x do//同上,计算各列的值
    for j = 1, x do
      s = s + (string.byte)(k, i + (j - 1) * x)
    end
    r[idx] = s
    s = s - (s)
    idx = idx + 1
  end
  for i = 1, x do//这是在计算主对角线之和
    s = s + (string.byte)(k, i + (i - 1) * x)
  end
  r[idx] = s
  s = s - (s)
  idx = idx + 1
  for i = 1, x do//这是在计算副对角线之和
    s = s + (string.byte)(k, (x - i) * x + i)
  end
  r[idx] = s
  s = s - (s)
  idx = idx + 1
  //以上的vc函数,是一个六阶幻方。
  local a = {}
  local b = {}
  bs(r, 1)//最小值表
  for i = 1, getn(r) do
    a[i] = r[i]
  end
  bs(r, 0)//最大值表
  for i = 1, getn(r) do
    b[i] = r[i]
  end
  for i = 1, getn(a) do//对于表中的每一个元素,判断是否相等,也就是判断幻方是不是成立
    if a[i] ~= b[i] then
      return 1
    end
  end
  return 0
end

r = vc(g_v1)//g_v1是作为参数,调用vc函数,g_v1是啥?1.10中的lua脚本出现的,它是base64解码后的字符串,2-10位为sk11位之后的都是g_v111-46,总计36个字符。vc函数的功能在上面。
if r == 0 then
  g_r = "锟斤拷证通锟斤拷"//这里应该是“验证通过”,解码出问题了,因为下面的是nil,退出。
else
  g_r = nil
end

没解过幻方,大佬提供了一个思路,手动构造一个幻方,然后进行穷举,找到满足条件的幻方。

1.14 其他函数

也没其他了,最后就剩下一个了,sub_402849,这是内存释放的。算法分析完毕。

2.破解代码

先说整题的验证要求:

1.sn[30] == ‘8’ && sn[40] == ‘I’ && sn[46] == ‘4’ && sn[58] == ‘0’

2.输入序列号后判断长度在不在[0x3D-0x64]之间

3.末尾加上“==”

4.base64解码后的结果分为两部分,一部分解码后是“.try2crack”,另外一部分的36个字符,满足6阶幻方

#include "stdafx.h"
#include <string.h>
 
const char * base64char = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
 
char * base64_encode(const unsigned char * bindata, char * base64, int binlength)
{
    int i, j;
    unsigned char current;
 
    for (i = 0, j = 0; i < binlength; i += 3)
    {
        current = (bindata[i] >> 2);
        current &= (unsigned char)0x3F;
        base64[j++] = base64char[(int)current];
 
        current = ((unsigned char)(bindata[i] << 4)) & ((unsigned char)0x30);
        if (i + 1 >= binlength)
        {
            base64[j++] = base64char[(int)current];
            base64[j++] = '=';
            base64[j++] = '=';
            break;
        }
        current |= ((unsigned char)(bindata[i + 1] >> 4)) & ((unsigned char)0x0F);
        base64[j++] = base64char[(int)current];
 
        current = ((unsigned char)(bindata[i + 1] << 2)) & ((unsigned char)0x3C);
        if (i + 2 >= binlength)
        {
            base64[j++] = base64char[(int)current];
            base64[j++] = '=';
            break;
        }
        current |= ((unsigned char)(bindata[i + 2] >> 6)) & ((unsigned char)0x03);
        base64[j++] = base64char[(int)current];
 
        current = ((unsigned char)bindata[i + 2]) & ((unsigned char)0x3F);
        base64[j++] = base64char[(int)current];
    }
    base64[j] = '\0';
    return base64;
}
 
int _tmain(int argc, _TCHAR* argv[])
{
    
    unsigned char buf[256] = { 0 };
    char key[20] = { 0 };
#define N       6
    unsigned char xx[N][N] = {
        1, 32, 31, 33, 5, 9,
        12, 8, 31, 27, 11, 22,
        20, 17, 15, 18, 18, 23,
        17, 19, 21, 22, 18, 14,
        35, 26, 5, 9, 29, 7,
        26, 9, 8, 2, 30, 36,
    };
 
    unsigned char ss[128] = { ".try2crack" };
    for (unsigned char x = 0; x < 0xff - 36; x++)
    {
        int k = 10;
        for (int i = 0; i < N; i++)
        {
            int s = 0;
            for (int j = 0; j < N; j++)
            {
                ss[k++] = xx[i][j] + x;
            }
        }
        base64_encode(ss, (char *)buf, k);
        if (buf[30] == '8' && buf[40] == 'I' && buf[46] == '4' && buf[58] == '0')
            break;
    }
    printf("%s", buf);
    return 0;
}

完结,程序输出结果,及验证结果如下图

16


本文访问量: