defcon XX CTF Quals Binary L33tness (b200) Write-up

先使用file查看一下bin200的属性,我们可以发现它是FreeBSD 32-bit的可执行文件。直接调出IDA来分析。

.text:08049300 sub_8049300 proc near
.text:08049300
.text:08049300 var_8 = dword ptr -8
.text:08049300 var_4 = dword ptr -4
.text:08049300
.text:08049300 lea ecx, [esp+4]
.text:08049304 ; 5: v0 = sub_8048F20(word_804BB38);
.text:08049304 and esp, 0FFFFFFF0h
.text:08049307 push dword ptr [ecx-4]
.text:0804930A push ebp
.text:0804930B mov ebp, esp
.text:0804930D sub esp, 18h
.text:08049310 movsx eax, word_804BB38
.text:08049317 mov [ebp+var_8], ecx
.text:0804931A mov [ebp+var_4], ebx
.text:0804931D mov [esp], eax
.text:08049320 call sub_8048F20
.text:08049325 mov dword ptr [esp], offset name ; Whether there is a user named 'grease'
.text:0804932C mov ebx, eax
.text:0804932E call _getpwnam
.text:08049333 test eax, eax
.text:08049335 jz short loc_8049363
.text:08049337 mov [esp], eax
.text:0804933A call checkUser ; User check
.text:0804933F add eax, 1
.text:08049342 jz short loc_8049363
.text:08049344 mov [esp], ebx ; fd
.text:08049347 mov dword ptr [esp+4], offset callback ; int
.text:0804934F call startListening
.text:08049363 mov dword ptr [esp], 0FFFFFFFFh ; status
.text:0804936A call _exit

和b400同样的套路,先检查当前有没有一个用户名为grease的账号,然后获取他的相关信息。把相关的代码nop掉就好。
程序的重点还是在callback()函数中。利用IDA的Hex-rays Decompiler,我们可以直接得到这个函数的C语言表示如下:

int __cdecl callback(int socket)
{
  char *buffer; // eax@1
  char *buffer_ptr; // esi@1
  int figure_1; // ebx@2
  char *buffer_2; // eax@4
  char *buffer_2_ptr; // esi@4
  int figure_2; // ebx@5
  void *v8; // eax@6
  void *v9; // esi@6
  int figure_3; // ebx@7
  void *v11; // eax@8
  void *v12; // esi@8
  int figure_4; // ebx@9
  char *string1; // ebx@12
  char *string2; // eax@12
  unsigned int size_of_string1; // eax@14
  char *string2_ptr; // edi@15
  signed int size_of_string2; // eax@15
  char char1_eq_char2; // zf@16
  char *string1_ptr; // esi@16
  signed int counter; // ecx@16
  int ret; // eax@21
  char test_if_ret_eq_0; // zf@21
  char *cryptingResult_1_ptr; // esi@22
  signed int length; // ecx@22
  char *cryptingResult_2_ptr; // edi@22
  char *string2_ptr_0; // [sp+28h] [bp-120h]@12
  char cryptingResult_2; // [sp+38h] [bp-110h]@21
  char cryptingResult_1; // [sp+B8h] [bp-90h]@20
  size_t size; // [sp+138h] [bp-10h]@10

  buffer = (char *)malloc(4u);
  buffer_ptr = buffer;
  if ( !buffer )
    goto x;
  recv_to_buffer(socket, buffer, 4u);           // Read first 4 chars
  figure_1 = ((unsigned __int8)buffer_ptr[2] << 8) | ((unsigned __int8)buffer_ptr[1] << 16) | (unsigned __int8)buffer_ptr[3] | ((unsigned __int8)*buffer_ptr << 24);// BE => LE
  free(buffer_ptr);
  if ( figure_1 != 0x94A4C265 )
    return 0;
  buffer_2 = (char *)malloc(4u);
  buffer_2_ptr = buffer_2;
  if ( !buffer_2 )
    goto x;
  recv_to_buffer(socket, buffer_2, 4u);
  figure_2 = ((unsigned __int8)buffer_2_ptr[2] << 8) | ((unsigned __int8)buffer_2_ptr[1] << 16) | (unsigned __int8)buffer_2_ptr[3] | ((unsigned __int8)*buffer_2_ptr << 24);
  free(buffer_2_ptr);
  if ( figure_2 != 0xFE732D6F )
    return 0;
  v8 = malloc(4u);
  v9 = v8;
  if ( !v8 )
    goto x;
  recv_to_buffer(socket, (char *)v8, 4u);
  figure_3 = (*((_BYTE *)v9 + 2) << 8) | (*((_BYTE *)v9 + 1) << 16) | *((_BYTE *)v9 + 3) | (*(_BYTE *)v9 << 24);
  free(v9);
  if ( figure_3 != 0xEEF814CB )
    return 0;
  v11 = malloc(4u);
  v12 = v11;
  if ( !v11 )
x:
    exit(0);
  recv_to_buffer(socket, (char *)v11, 4u);
  figure_4 = (*((_BYTE *)v12 + 2) << 8) | (*((_BYTE *)v12 + 1) << 16) | *((_BYTE *)v12 + 3) | (*(_BYTE *)v12 << 24);
  free(v12);
  if ( figure_4 == 0x6EC8A126 )
  {
    if ( recv_to_buffer(socket, (char *)&size, 4u) == 4 )
    {
      if ( size <= 0x400 )
      {
        string1 = (char *)malloc(size);
        string2 = (char *)malloc(size);
        string2_ptr_0 = string2;
        if ( string1 )
        {
          if ( string2 )
          {
            size_of_string1 = recv_to_buffer(socket, string1, size);
            if ( size_of_string1 == size )
            {
              string2_ptr = string2_ptr_0;
              size_of_string2 = recv_to_buffer(socket, string2_ptr_0, size_of_string1);
              if ( size_of_string2 == size )
              {
                char1_eq_char2 = 1;
                string1_ptr = string1;
                counter = size_of_string2;
                do                              // Test whether string1 == string2
                {
                  if ( !counter )
                    break;
                  char1_eq_char2 = *string1_ptr++ == *string2_ptr++;
                  --counter;
                }
                while ( char1_eq_char2 );
                if ( !char1_eq_char2 )
                {
                  if ( !crypt(256, string1, (unsigned int)(8 * size_of_string2), &cryptingResult_1) )
                  {
                    ret = crypt(256, string2_ptr_0, 8 * size, &cryptingResult_2);
                    test_if_ret_eq_0 = ret == 0;
                    if ( !ret )
                    {
                      cryptingResult_1_ptr = &cryptingResult_1;
                      length = 32;
                      cryptingResult_2_ptr = &cryptingResult_2;
                      do                        // Test whether cryptResult_1 == cryptResult_2
                      {
                        if ( !length )
                          break;
                        test_if_ret_eq_0 = *cryptingResult_1_ptr++ == *cryptingResult_2_ptr++;
                        --length;
                      }
                      while ( test_if_ret_eq_0 );
                      if ( test_if_ret_eq_0 )
                        sendKey(socket);
                      else
                        sendText(socket, "sorry\n");
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
  return 0;
}

整个过程是容易理解的:首先读入四个数字,分别是0x94a4c265、0xfe732d6f、0xeef814cb和0x6ec8a126;然后读入一个DWORD作为size,最后读入长度分别为size的两个字符串string1与string2。对这两个字符串的要求是长度相等,且经过crypt()函数运算得到的结果相同,但两个字符串不能相同。 OK,下面的重点是找出crypt()到底是个什么运算方法。我们先来看看crypt()函数的实现:

int __cdecl crypt(int a256, const void *src, __int64 size8, int *cryptingBuffer)
{
  int result; // eax@1
  AesContext v5; // [sp+28h] [bp-4A0h]@1
  char s; // [sp+B0h] [bp-418h]@1
  int v7; // [sp+4B0h] [bp-18h]@1
  int v8; // [sp+4B4h] [bp-14h]@1
  int v9; // [sp+4B8h] [bp-10h]@1

  v7 = 0;
  v5.bitNum = a256;
  v8 = 0;
  v9 = 0;
  memset(&s, 0, 0x400u);
  LOBYTE(v5.field_4) = 80;
  v5.key[0] = 0x14B62D86u;
  v5.key[1] = 0x31CF379Cu;
  v5.key[2] = 0x1BC6382Au;
  v5.key[3] = 0x752E03B3u;
  v5.key[4] = 0xD0346A2Au;
  v5.key[5] = 0xA1DC5B93u;
  v5.key[6] = 0xF9BB11D2u;
  v5.key[7] = 0xEB6A9A40u;
  v5.key[8] = 0x51B1D88Bu;
  v5.key[9] = 0xBC5B1F79u;
  v5.key[10] = 0x10B0880Eu;
  v5.key[11] = 0x9F0E71F4u;
  v5.key[12] = 0x233E7C22u;
  v5.key[13] = 0x1D440731u;
  v5.key[14] = 0xA27BE5A3u;
  v5.key[15] = 0xDFD7E6DBu;
  v5.key[16] = 0x10D9EC9Fu;
  v5.key[17] = 0x96477F0Fu;
  v5.key[18] = 0x125AEDF6u;
  v5.key[19] = 0x93E13CEFu;
  v5.key[20] = 0xFAC6CCEFu;
  v5.key[21] = 0x19198FFCu;
  v5.key[22] = 0x6A34B4DCu;
  v5.key[23] = 0x3A0273DBu;
  v5.key[24] = 0xEB2C033Eu;
  v5.key[25] = 0x399C4D77u;
  v5.key[26] = 0x2FF23788u;
  v5.key[27] = 0x223EBB81u;
  v5.key[28] = 0xE9ABCB8Cu;
  v5.key[29] = 0xF789D1BBu;
  v5.key[30] = 0x558A6467u;
  v5.key[31] = 0xB789A6A6u;
  result = crypt_1(&v5, src, size8);
  if ( !result )
    result = crypt_2(&v5, cryptingBuffer);
  return result;
}

在比赛时,关于这个算法到底是什么,我们纠结了很长时间。因为在代码中找到了Rijndael S-Box(),我们还怀疑这个算法是AES(这一点你可以从上面v5这个变量的类型命名AesContext上看出来 :D)。如果是AES的话,上面v5.key[]填充的值应该就是密钥了吧。
沿着这个思路走下去,我们……走入了死胡同。首先,通过分析crypt_1()和crypt_2()两个函数,我们发现它们不同于现有的AES算法;其次,如果真的是AES的话,实现碰撞的可能性也是微乎其微的,这一点只要简单Google一下就可以知道,绝大部分相关的论文都只有理论价值。难道defcon真的要我们去跑一组可行的AES碰撞出来?还是网上已经有一组现成的AES碰撞了?
非常凑巧地,我把v5.key[0](14B62D86)放到Google中搜索了一下,得到了如下结果:

这是……这是一个哈希函数?Tangle Hash Function,没听说过诶。打开论文之后,我们发现v5.key就是D = 256时的初始化向量!而且这个哈希算法中也用到了Rijndael S-box。 那么如果Tangle Hash存在碰撞的话……我们直接搜索 Tangle Hash collision,可以发现一篇名为Untangled的论文:

以这篇论文为线索,我们可以在作者的个人网站上找到碰撞例程,运行一下就可以得到一组合适的碰撞了:

Collision found in Tangle-256 Message 1: c8190000000000000000000000000000000000000000000000000000000000000000000000000000
Hash of message 1: f710be651ab67737a58ac452056bbf13e62abed071943617dadbf25c2dea710b
Message 2: c8190080000000800000000000000000000000000000000000000000000000000000008000000080
Hash of message 2: f710be651ab67737a58ac452056bbf13e62abed071943617dadbf25c2dea710b
XOR of hashes: 0000000000000000000000000000000000000000000000000000000000000000

附上杨坤写的Python代码:

import asyncore, socket

class SolveClient(asyncore.dispatcher):

    def __init__(self):
        asyncore.dispatcher.__init__(self)
        self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
        self.connect( ("140.197.217.155", 18703) )

        # Collision found in Tangle-256
        # Message 1:
        # c8190000000000000000000000000000000000000000000000000000000000000000000000000000
        # Hash of message 1:
        # f710be651ab67737a58ac452056bbf13e62abed071943617dadbf25c2dea710b
        # Message 2:
        # c8190080000000800000000000000000000000000000000000000000000000000000008000000080
        # Hash of message 2:
        # f710be651ab67737a58ac452056bbf13e62abed071943617dadbf25c2dea710b
        # XOR of hashes:
        # 0000000000000000000000000000000000000000000000000000000000000000

        self.buffer = "\x94\xa4\xc2\x65\xFE\x73\x2D\x6F\xEE\xF8\x14\xCB\x6E\xC8\xA1\x26"+"\x28\x00\x00\x00"+"\xc8\x19"+"\x00"*38+"\xc8\x19\x00\x80"+"\x00\x00\x00\x80"+"\x00"*27+"\x80\x00\x00\x00\x80"
    def handle_connect(self):
        pass

    def handle_close(self):
        self.close()

    def handle_read(self):
        print self.recv(8192)

    def writable(self):
        return (len(self.buffer) > 0)

    def handle_write(self):
        sent = self.send(self.buffer)
        self.buffer = self.buffer[sent:]

client = SolveClient()
asyncore.loop()
此条目发表在技术报告分类目录。将固定链接加入收藏夹。

评论功能已关闭。