LCTF2016之pwn400

这道题当时没做,这两天参照@Nu1L战队的writeup调了一下,感觉挺有收获,遂做一下笔记。
首先看一下题目的交互

$ ./pwn400                                              
RSA example
What do you want to do?
1. new cipher
2. encrypt
3. decrypt
4. comment
5. exit

看来本题实现了一个基于RSA的加密工具,顺便看一下开启了哪些保护

gdb-peda$ checksec 
CANARY    : ENABLED
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : Partial

用IDA打开,发现是C++写的,之前对C++的pwn题有一种莫名的恐惧感,但仔细分析发现和C写的道理是差不多的。好吧,看一看主程序

case 1:
  cipher = operator new(0x258uLL);
  init_cipher(cipher);
  cipher1 = cipher;
  keyChain = get_keyChain(cipher);
  if ( (unsigned __int8)isKeyChainInit(keyChain) )
  {
      pubicKey = getP(keyChain);
      v25 = sub_402286(keyChain);
      pass1(keyChain, (__int64)&opt, v9);
  }

这一段主要是初始化一个cipher类

case 2:
   if ( cipher1 )
   {
       ...
       if ( length <= 0x40 )
       {
          init_textHead(cipher1, length);
          ...
          readstr((__int64)&buf, length);
          if ( (unsigned __int8)isKeyChainInit(keyChain) )
             //有密钥加密
            (*(void (__fastcall **)(__int64, char *, __int64))(*(_QWORD *)cipher1 + 16LL))(cipher1, &buf, pubicKey);
          else
            //无密钥加密(本质上是一样的)
            (*(void (__fastcall **)(__int64, char *))(*(_QWORD *)cipher1 + 24LL))(cipher1, &buf);
       }
       ...
   }

这是进行加密操作

case 3:
   if ( cipher1 )
   {
      ...
      if ( length <= 0x100 )
      {
        init_textHead(cipher1, length >> 2);
        ...
        readstr((__int64)&buf, 2 * length);
        if ( (unsigned __int8)isKeyChainInit(keyChain) )
          //有密钥解密
          (*(void (__fastcall **)(__int64, char *, __int64))(*(_QWORD *)cipher1 + 32LL))(cipher1, &buf, v25);
        else
          //无密钥解密(但本质上也是一样的)
          (*(void (__fastcall **)(__int64, char *))(*(_QWORD *)cipher1 + 40LL))(cipher1, &buf);
        if ( cipher1 )
          //解密后释放cipher类,但是这里并没有将cipher指针置空,导致UAF漏洞
          (*(void (__fastcall **)(__int64))(*(_QWORD *)cipher1 + 8LL))(cipher1);
      }
       ...
   }

这一部分进行解密操作

  case 4:
    printf("comment about my implement of RSA", &opt, a4);
    v27 = operator new[](0x80uLL);
    readstr(v27, 0x80u);

这里申请一块内存,并写入128字节的数据。结合上一步的解密操作,我们可以控制cipher类在堆里面的内存空间,当然也可以覆盖vtable。到了这里我们还缺少一个条件:一块地址已知的可控内存来存储虚函数表。但是,分析完所有的代码,我们并没有发现可以操作bss段的方法,所以我们只能想办法泄露栈地址或堆地址。这里我们就得关注一下加密函数了

int __fastcall encrypt_with_key(__int64 cipher1, __int64 plainText, __int64 pubicKey)
{
  ...
  for ( j = 0; 4 * length > j; ++j )
  {
    if ( *(_BYTE *)v13 <= 0x9Fu )
    {
      if ( *(_BYTE *)v13 <= 0x9Fu )
      {
        v4 = cipher1;
        //这里往cipher内存块写入了一个字节的数据
        *(_BYTE *)(cipher1 + 2 * j + 72) = (*(_BYTE *)v13 >> 4) + 48;
      }
    }
  ...
  }                                      
  return printCipherText(cipher1, plainText, v4);
}

这里,j 的上边界是4×length,那么表达式cipher1 + 2×j + 72所能表示的最大地址为cipher1 + 8×length + 72,length最大值为0x40,所以cipher1 + 8×length + 72最大值应为cipher+0x248
不难注意到,在初始化对象的时候,有这样一段代码

__int64 __fastcall init_cipher(__int64 cipher)
{
  __int64 keyChain; // rbx@1
  __int64 result; // rax@1

  *(_QWORD *)cipher = functions;
  keyChain = operator new(0x14uLL);
  init_keyChain(keyChain);
  result = cipher;
  //这里的keyChain是一个堆地址
  *(_QWORD *)(cipher + 0x248) = keyChain;
  return result;
}

因此,只要我们输入长度为0x40的明文字符串,加密结束后输出的密文末尾就会跟着keyChain(堆)地址。至此,我们已经可以控制eip了,那么下一步就是泄露libc地址。注意到程序进行加解密的时候都会往栈上读入一段数据,那么只需要合理构造一下Rop链在栈上的位置,就可以达到调用任意函数的目的了。

你可能感兴趣的:(LCTF2016之pwn400)