首页
社区
课程
招聘
[原创]pediy&jd ctf_2018 之 密室逃脱 分析记录
发表于: 2018-11-7 16:47 4617

[原创]pediy&jd ctf_2018 之 密室逃脱 分析记录

2018-11-7 16:47
4617

环境配置

系统 : Windows 7
程序 : Escape.rar
要求 : 输入口令
使用工具 :OD / IDA / c32asm
可在看雪论坛中查找关于此程序的讨论:传送门

开始分析

我们用ida载入cm,直接查看main函数:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  printf("Only 'a'-'z','A'-'Z','0'-'9' accepted.\n");
  printf("Only 16 chars accepted.\n");
  printf("Serial:");
  scanf_s("%s", my_table_450B80, 17);
  sub_401050();
  system("PAUSE");
  return 0;
}

进入中间那个很可疑的子函数:

int sub_401050()
{
  // ...省略变量定义代码
  do
  {
    for ( index_of_char_table = 0; ; ++index_of_char_table )
    {
      if ( index_of_char_table >= 0x40 )        // my_char is not in char_table
      {
        printf("input error\n");
        system("PAUSE");
        exit(-1);
      }
      if ( char_table_40DA68[index_of_char_table] == (unsigned __int8)my_table_450B80[index] )
        break;
    }
    char_index[index++] = index_of_char_table;
  }
  while ( index < 16 );
  printf("\n");
  index_K = 16;
  tmp = 16;
  kind_of_index = 2 - (_DWORD)&v21;
  do
  {
    v4 = &char_index[index_K];
    res_pointer = &char_index[index_K];
    index_j = 0;
    do
    {
      v6 = (char *)&v16;
      v7 = (signed int)&v4[kind_of_index];
      index_i = 4;
      do
      {
        v8 = (v7 - 2) % 16;
        if ( *((_BYTE *)&v24 + index_j + v8) )
          *v6++ = *((_BYTE *)&index_i + v8 + tmp);
        v9 = (v7 - 1) % 16;
        if ( *((_BYTE *)&v24 + index_j + v9) )
          *v6++ = *((_BYTE *)&index_i + v9 + tmp);
        if ( *((_BYTE *)&v24 + index_j + v7 % 16) )
          *v6++ = *((_BYTE *)&index_i + v7 % 16 + tmp);
        v10 = (v7 + 1) % 16;
        if ( *((_BYTE *)&v24 + index_j + v10) )
          *v6++ = *((_BYTE *)&index_i + v10 + tmp);
        v7 += 4;
        --index_i;
      }
      while ( index_i );
      *res_pointer = *(&byte_40FEF0[64 * (v17 + (v16 << 6))] + v18);// insert res from char_table_2
      index_j += 16;
      v4 = res_pointer++ + 1;
    }
    while ( index_j < 0x100 );
    kind_of_index -= 16;
    index_K = tmp + 16;
    tmp = index_K;
  }
  while ( index_K < 0x140 );
  v11 = 0;
  do                                            // error
  {
    if ( (unsigned __int8)res_buf[v11] != byte_40FEE0[v11] )
      return printf("serial error\n");
    ++v11;
  }
  while ( v11 < 16 );
  do
    printf("%c", (unsigned __int8)char_table_40DA68[(unsigned int)(unsigned __int8)v22[v0++] >> 1]);// out put from your serial string
  while ( v0 < 16 );
  return printf("\n");
}

经过动态调试验证,程序将我们输入的数据与程序表1对比,如果输入不在表内则退出,这是一种控制输入的手段。接着,程序将数据在程序表1中的索引插入结果表的第一行(层?)。

三重循环

接下来,程序进入三重循环,结合od分析,有一行比较关键:
*res_pointer = *(&byte_40FEF0[64 * (v17 + (v16 << 6))] + v18);// insert res from char_table_2

 

这就是从固定表2中不断取数据插入结果表。

观察数据

以“0123456789abcdef”作为输入数据,当三重循环结束时,我们观察内存:

0012FD18  36 37 38 39 3A 3B 3C 3D 3E 3F 00 01 02 03 04 05  6789:;<=>?.
0012FD28  08 26 1E 32 2A 30 32 3D 08 29 1B 1F 15 1D 0D 3D  &2*02=).=
0012FD38  0B 23 15 26 25 2F 36 2F 24 3F 3A 2C 1B 10 34 22  #&%/6/$?:,4"
0012FD48  12 03 0F 0C 21 2A 38 28 1F 21 21 08 39 2A 0E 15  .!*8(!!9*
0012FD58  11 02 17 1A 2F 27 24 34 23 05 2C 24 27 2A 0D 32  /'$4#,$'*.2
0012FD68  2D 2B 2F 3E 06 2D 1A 01 1C 0D 1A 33 2D 1E 2A 2A  -+/>-.3-**
0012FD78  36 1A 1B 08 28 0C 1F 36 33 34 0B 35 03 08 13 3D  6(.6345=
0012FD88  08 2D 34 2B 24 23 1E 3C 07 1F 02 2D 04 17 24 13  -4+$#<-$
0012FD98  08 0C 3E 1F 2C 3D 2C 2A 35 0F 36 33 08 1D 09 03  .>,=,*563.
0012FDA8  39 30 3A 24 27 2D 2E 0C 19 31 1A 1E 09 29 2B 08  90:$'-..1.)+
0012FDB8  38 09 03 22 32 30 15 35 12 1C 37 33 27 2E 0C 1D  8."20573'..
0012FDC8  15 2B 14 2B 11 06 2C 03 04 21 0D 28 1E 23 36 08  ++,!.(#6
0012FDD8  05 38 20 21 3B 04 05 26 0F 1E 11 2C 10 2B 05 0C  8 !;&,+.
0012FDE8  26 3C 0B 25 25 23 06 31 18 29 3C 3A 00 20 03 09  &<%%#1)<:. .
0012FDF8  0A 0D 10 25 08 2D 04 19 24 3B 2E 07 24 07 34 0F  ..%-$;.$4
0012FE08  1A 24 18 3E 0E 26 08 1B 32 3F 09 2D 05 11 17 07  $>&2?.-
0012FE18  3D 19 13 10 21 03 2E 23 3C 3F 30 02 0D 1E 28 1A  =!.#<?0.(
0012FE28  08 13 09 16 2F 26 12 38 05 01 01 06 27 34 1A 1F  ./&8'4
0012FE38  11 30 07 3E 13 01 28 22 3F 17 20 12 21 18 23 2E  0>("? !#.
0012FE48  03 1B 29 14 20 1D 0C 3B 18 39 10 09 1F 28 02 06  ) .;9.(

总计0x140字节大小的数据,每行0x10个字节,总计0x14(20)行。
也就是说,这就是附带的解释文件里说的20层楼房了:

密室逃脱
这是一个数字迷宫 分为20层楼
16个人各选择一把初始钥匙 开始游戏
16个人进入第一层 按照一定的规则 每个人都会在另外2个人的帮助下开启一个房间 拿到1把第二层楼的钥匙
当16个人都拿到了各自的二层楼钥匙后 一起上楼
第二层重复相同的操作,16个人再一起上到第三层
...
直至16个人拿到了的20层天台的钥匙
天台上有一架直升机,需要16把正确的钥匙才能开走
试试看 你能否找出16把正确的初始钥匙
有人可能会想 只要砸碎直升机的门 不就可以把直升机开走了吗?
(那不算胜利哦)
在你开着直升机远去的时候,迷宫大楼的中间 那些曾经被你点亮的房间 会组成一个横幅
只有在你拿到了正确钥匙的情况下 才会出现成功逃脱的横幅(那才算胜利哟)
程序运行起来后,请输入16个初始钥匙
如果验证正确,最后会显示出正确的横幅
否则,算作挑战失败

 

我们定位到最后的判断代码处:
if ( (unsigned __int8)res_buf[v11] != byte_40FEE0[v11] ) return printf("serial error\n");

 

这里,将最后一层的数据和固定表3对比,如果不相同则失败。

推理

从宏观的角度看,程序使用了 固定值y=F(序列号)的方式。我们想要夺旗,可分为以下三步:

  1. 确认三重循环的每一个步骤,接着写出其的逆算法。
  2. 以固定值byte_40FEE0的内容作为参数,算出前十九层的钥匙。
  3. 用第十九层的钥匙,作为索引,找出20层天台所对应最终密码。

最后,程序会取某一层的数据进行运算,并输出成功逃脱的横幅。

观点验证

我们结合动静态分析,开始逆向三重循环。

小技巧

调试中,可以使用一个小技巧,在od定位到迭代中经常访问的内存块,然后对于内部的小循环结束的地方下断点。设置好了之后,按下Ctrl + F8/F7,让其自动步进,而我们就可以慢慢观察内存的变化。这个小技巧对于调试加密函数很有用。

被隐藏的第五张表

我们在ida发现的四个if语句中:
if ( *((_BYTE *)&v24 + index_j + _ECX) )
这样的操作全在引用一个表的内容,但作者没有将表地址直接硬编码在代码中,而是采用了数据相加再间接寻址的方式,所以这张表不是很容易找到。

内存分布

name address size data
res_pointer 12FD04 4h
index_i 12FD08 4h 4 -> 0
kind_of_index 12FD0C 4h 0xFFED02DA
index_of_table2 12FD10 4h random value [0]->[1]->[2]->[0]..
index_of_res 12FD14 4h 10h->20h->30h..
res_buf 12FD18 140h
hidden_table 12FE58 100h /
Char_table2 40FEF0 40000h /
table3 40FEE0 10h /

编码

我们根据分析出的线索,用c32asm在pe文件中找出Char_table2的40000h个数据。再用c++编码还原其算法:

    do {
        size_t index_j = 0;
        size_t ESI = 0;
        do {
            size_t index_i = 4;

            do {
                ecx_index = ESI % 0x10;
                if ( hidden_table[index_j+(ecx_index%0x10)] )
                    rv.set_value(floor_table[ (ecx_index%0x10) + (tmp-0x10) ]);
                ecx_index++;

                if ( hidden_table[index_j+(ecx_index%0x10)] )
                    rv.set_value(floor_table[ (ecx_index%0x10) + (tmp-0x10) ]);
                ecx_index++;

                if ( hidden_table[index_j+(ecx_index%0x10)] )
                    rv.set_value(floor_table[ (ecx_index%0x10) + (tmp-0x10) ]);
                ecx_index++;

                if ( hidden_table[index_j+(ecx_index%0x10)] )
                    rv.set_value(floor_table[ (ecx_index%0x10) + (tmp-0x10) ]);
                ecx_index++;

                ESI += 4;
                --index_i;
            }
            while ( index_i );
            ESI++;
            size_t random_num = rv.get_random_value();

            *res_pointer++ = table2[random_num]; 
            index_j += 0x10;
        }
        while ( index_j < 0x100 );
        ESI = 0;
        index_K = tmp + 0x10;
        tmp = index_K;
    }
    while ( index_K < 0x140);

由上述代码可知,数据直接由random_num索引出;而同样的一个数据在256kb的数据中可能有N个索引,所以,无法由给定的表数据确认唯一的索引,这样就无法反推出逆算法。

灵感

我们无法写出逆算法,就无法得到原始数据。这个看似无解的逻辑令我困扰了一阵。

 

这时,我突然想到,我无法写,也不用去写逆算法,因为这个加密的逆算法可能就是其本身!再来看看被隐藏的第五张表的数据:

0012FE58  01 01 01 00 00 00 00 00 00 00 00 00 00 00 00 00  .............
0012FE68  01 00 00 01 01 00 00 00 00 00 00 00 00 00 00 00  .............
0012FE78  01 00 00 00 00 01 01 00 00 00 00 00 00 00 00 00  .............
0012FE88  00 01 00 00 00 01 00 01 00 00 00 00 00 00 00 00  .............
0012FE98  00 01 00 00 01 00 01 00 00 00 00 00 00 00 00 00  .............
0012FEA8  00 00 01 01 00 00 00 00 01 00 00 00 00 00 00 00  .............
0012FEB8  00 00 01 00 01 00 00 00 00 01 00 00 00 00 00 00  .............
0012FEC8  00 00 00 01 00 00 00 01 00 00 01 00 00 00 00 00  .............
0012FED8  00 00 00 00 00 01 00 00 01 00 00 00 01 00 00 00  .............
0012FEE8  00 00 00 00 00 00 01 00 00 00 00 01 00 01 00 00  .............
0012FEF8  00 00 00 00 00 00 00 01 00 00 00 00 01 01 00 00  .............
0012FF08  00 00 00 00 00 00 00 00 00 01 00 01 00 00 01 00  .............
0012FF18  00 00 00 00 00 00 00 00 01 00 01 00 00 00 01 00  .............
0012FF28  00 00 00 00 00 00 00 00 00 01 01 00 00 00 00 01  .............
0012FF38  00 00 00 00 00 00 00 00 00 00 00 01 01 00 00 01  .............
0012FF48  00 00 00 00 00 00 00 00 00 00 00 00 00 01 01 01  .............

这似乎暗示了一个循环。。。

尝试

我们加大数组的长度,加大循环的周期,看看能不能等到原始数据的出现:

# include <iostream>
# include <fstream>
# include <sstream>

using namespace std;

void get_table2_data(unsigned char* table2)
{
    ifstream fin;

    fin.open("c:\\char_table_2.data", ios::in | ios::binary);

    fin.read((char*)table2,0x40000);
    fin.close();

    return ;
}

class random_value{
    public:
        random_value() : mark(0) {}

        inline void set_value(unsigned char value)
        {
            values[mark] = value;
            if ( mark == 2 )
                mark = 0;
            else
                mark += 1;
        }

        inline size_t get_random_value()
        {
            size_t index = 0;
            unsigned char A = values[1];
            unsigned char B = values[2];
            unsigned char C = values[0];
            __asm{
                push eax
                push ecx
                push edx

                movzx eax,A
                movzx ecx,B
                movzx edx,C

                shl edx,0x6
                add edx,eax
                shl edx,0x6

                lea eax,ds:[edx+ecx]
                mov index,eax

                pop edx
                pop ecx
                pop eax
            }
            return index;
        }
    private:
        unsigned char values[3];
        int mark;
};

int main(void)
{
    unsigned char floor_table[0x14000] = { 0x14, 0x22, 0x1E, 0x10, 0x38, 0x30, 0x18, 0x10, 0x04, 0x1A, 
                                            0x24, 0x08, 0x02, 0x26, 0x38, 0x2A };

    // origin

    unsigned char char_index[0x140] = {        0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, 
                                            0x00, 0x01, 0x02, 0x03, 0x04, 0x05 };
    unsigned char hidden_table[0x100] = {
        0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 
        0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
        0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 
        0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 
        0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 
        0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
        0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 
        0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 
        0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 
        0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
        0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 
        0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
        0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 
        0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
        0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 
        0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
        0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 
        0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
        0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
        0x00, 0x00, 0x00, 0x01, 0x01, 0x01
    };

    unsigned char table2[0x40000];
    get_table2_data(table2);

    size_t index_K = 0x10;
    size_t tmp = 0x10;

    size_t ecx_index = 0;
    random_value rv;
    unsigned char *res_pointer = floor_table+0x10;

    do {
        size_t index_j = 0;
        size_t ESI = 0;
        do {
            size_t index_i = 4;

            do {
                ecx_index = ESI % 0x10;
                if ( hidden_table[index_j+(ecx_index%0x10)] )
                    rv.set_value(floor_table[ (ecx_index%0x10) + (tmp-0x10) ]);
                ecx_index++;

                if ( hidden_table[index_j+(ecx_index%0x10)] )
                    rv.set_value(floor_table[ (ecx_index%0x10) + (tmp-0x10) ]);
                ecx_index++;

                if ( hidden_table[index_j+(ecx_index%0x10)] )
                    rv.set_value(floor_table[ (ecx_index%0x10) + (tmp-0x10) ]);
                ecx_index++;

                if ( hidden_table[index_j+(ecx_index%0x10)] )
                    rv.set_value(floor_table[ (ecx_index%0x10) + (tmp-0x10) ]);
                ecx_index++;

                ESI += 4;
                --index_i;
            }
            while ( index_i );
            ESI++;
            size_t random_num = rv.get_random_value();

            if ( index_j == 0 && ( *(unsigned int*)(res_pointer-0x10) == 0x101E2214 ) ){
                cout << hex << res_pointer-0x10-floor_table << endl;
            }

            *res_pointer++ = table2[random_num]; 
            index_j += 0x10;
        }
        while ( index_j < 0x100 );
        ESI = 0;
        index_K = tmp + 0x10;
        tmp = index_K;
    }
    while ( index_K < 0x14000);

    cout << "Hello" << endl;

    return 0;
}

在发现数据的if语句内下断点:
cout << hex << res_pointer-0x10-floor_table << endl;
开始调试,果真断下了,此时res_pointer-0x140内存布局如下所示:

0011DA50  11 2B 21 05 3F 01 25 0B 39 13 33 09 3F 39 35 25  .+!.?.%.9.3    ?95%
0011DA60  20 30 32 16 32 12 06 32 3C 1A 16 18 30 22 24 1E   02.2..2<...0"$.
0011DA70  21 1F 17 03 27 2B 29 01 15 1F 1B 05 25 39 35 23  !...'+).....%95#
0011DA80  2E 22 24 0E 2A 36 00 0E 34 2A 32 00 14 20 14 08  ."$.*6..4*2.. ..
0011DA90  13 01 27 39 2F 25 1B 09 2D 13 11 1D 31 0B 37 17  ..'9/%.    -...1.7.
0011DAA0  3E 0A 2A 02 2A 08 24 1E 1E 3A 38 1E 2C 2A 1C 3C  >.*.*.$..:8.,*.<
0011DAB0  35 2D 31 17 3B 1D 29 03 21 0B 0B 3F 05 31 11 1D  5-1.;.).!..?.1..
0011DAC0  16 2E 36 10 08 28 02 20 06 00 28 0E 14 0A 04 22  ..6..(. ..(...."
0011DAD0  25 05 1F 2D 27 33 19 0B 01 0D 21 31 15 31 3B 17  %..-'3....!1.1;.
0011DAE0  30 1C 28 22 24 08 22 10 00 16 10 24 0C 1C 1C 06  0.("$."....$....
0011DAF0  27 3D 25 3F 2D 29 19 21 27 39 33 05 17 13 0D 15  '=%?-).!'93.....
0011DB00  38 1E 38 2A 0E 16 1E 2A 16 2C 1A 1E 3E 10 08 28  8.8*...*.,..>..(
0011DB10  19 2F 03 29 15 03 11 29 15 3F 33 09 1F 1D 23 33  ./.)...).?3    ..#3
0011DB20  3A 12 3C 0E 2C 3C 2E 08 1E 22 3C 12 26 18 0E 0E  :.<.,<..."<.&...
0011DB30  0F 1F 33 0D 2B 2B 19 19 3F 33 35 2D 27 1B 11 07  ..3.++..?35-'...
0011DB40  0C 36 16 06 06 26 04 2E 0C 0C 3A 10 10 16 36 22  .6...&....:...6"
0011DB50  0F 07 0D 15 23 3F 3F 19 11 0D 03 25 37 3B 19 31  ....#??....%7;.1
0011DB60  3A 26 00 3A 34 06 2C 3C 0E 1E 0A 26 24 18 3C 18  :&.:4.,<...&$.<.
0011DB70  0F 0B 07 0B 35 03 19 1F 0B 31 3B 33 03 07 15 37  ....5....1;3...7
0011DB80  14 22 1E 10 38 30 18 10 04 1A 24 08 02 26 38 2A  ."..80....$..&8*

将楼顶的数据与表1对比:

11 2B 21 05 3F 01 25 0B 39 13 33 09 3F 39 35 25
r  P  F  f  9  b  J  l  3  t  X  j  9  3  Z  J

逃出生天

Only 'a'-'z','A'-'Z','0'-'9' accepted.
Only 16 chars accepted.
Serial:rPFf9bJl3tXj93ZJ

yourserialisgood
请按任意键继续. . .

技术总结

这个cm花了我两天的时间,不过终于是成功逃脱了密室。总的来说,程序没有摆出繁杂的逻辑结构和反调试机制,但其的三重循环非常考验破解者的耐心和经验。这的确是一个以智慧取胜,非常有趣的谜题。


[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

最后于 2018-11-7 18:22 被顾言庭编辑 ,原因:
收藏
免费 0
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回
//