首页
社区
课程
招聘
[原创]CTF2017 第七题 不问少年crackme 解题报告
2017-6-15 00:02 3352

[原创]CTF2017 第七题 不问少年crackme 解题报告

2017-6-15 00:02
3352

CTF2017 第七题 不问少年crackme

前言

拿到这题发现启动不起来(win10),后来光研究为什么启动不起来就用了好几个小时,最后猜测应该是程序打乱了原有的段,并且 vs2015 编译了 CFC( Control Flow Guard)导致的,如果不是哪位大神指点下

思路

  1. 用 IDA 粗略分析,因为是 vs2015 静态编译,并且①用了很多 STL 模板类,②函数有堆栈保护代码,导致翻阅甚是痛苦,最后准备采用 OllyDBG 辅助分析

  2. OllyDBG 环境准备

    • 程序入口有 TLS 解密,所以不能有默认的入口断点,你可以设置断在系统处,删除入口断点(或者用插件直接断 TLS)
    • 程序有几个 IsDebuggerPresent 调用的地方,准备好隐藏调试插件
    • 忽略 int3 软中断,程序有个地方用了 SetUnhandledExceptionFilter 和 int3 组合来改变程序流程,忽略软中断不影响自身中断
  3. 找到编辑框获取点,这种 CrackMe 一般都有获取编辑框输入内容的过程

    • 开始猜测 GetWindowText,在 OD 下了断点发现都不是的
    • 然后猜测发送了 WM_GETTEXT 消息,IDA 查看 SendMessage 引用,没有发送这个消息的
    • 最后翻导入表,发现了 GetDlgItemText 函数,查看引用直接发现了主要过程函数
  4. 然后就是用枯燥的调试来补充 IDA 的反汇编结果,期间还编译了带符号的静态 vs2015 程序来比较相似 STL 函数

程序流程

  1. 首先是刚才获取文本的主要函数
int __thiscall OnMsg464(int a1, int a2, int a3, int a4, int a5)
{
  stack_prot_begin();
  this_ = a1;
  alloc(buf, 0, 101);
  GetDlgItemTextA(*(HWND *)(this_ + 4), 1000, buf, 100);
  basic_string_new_((int)&ipt, buf);            // 获取输入到 ipt
  v16 = 0;
  _BYTE key[] = {0x50, 0x45, 0x44, 0x49, 0x59};
  xor_code(encode_input, 426, key, 5);          // 解密输入处理函数
  *(_DWORD *)key = v7;
  ipt_ = v7;
  basic_string_new((int)&ipt_, (int)&ipt);
  LOBYTE(v16) = 0;
  encode_input(this_ + 64, ipt_);               // 处理输入
  xor_code(encode_input, 426, key, 5);          // 还原输入处理函数到加密状态
  check_result(this_ + 64);                     // 检测处理结果
  v16 = -1;
  basic_string_Tidy((int)&ipt, 1, 0);
  stack_prot_end();
  return result;
}


  1. 输入处理函数,太长了,不发代码了,说下流程

    • 将输入每一位字节与 0xCC 异或
    • 存在一数组,是 EpY07v!Vwb2UnTu5SHP1Oazei9@kRZF8IrdCJcDQKs3mGMlgBqyfNXhAo4x6WjtL每一字节与 0xCC 的结果
    • 将第一步的结果与该数组进行 算法1(参考解码代码)
    • 将结果每一字节先与 0xCC 异或,再将 前3bit 与后5bit 交换
  2. 验证阶段1,该阶段做了对比操作与内置的结果(参考解码代码)进行了对比。没有优化 IDA 结果,大家随意看看

int __thiscall check_part1(int a1)
{
  v21 = 72;
  stack_prot_begin();
  v2 = v1;
  to_hex_string(v1, (int)&a3);                  // 先转 hex 字符串
  i = 0;
  v21 = 0;
  v4 = v17;
  LOBYTE(a4) = 0;
  *(_OWORD *)a2 = *(_OWORD *)"0123456789ABCDEF";
  v8 = 0;
  v9 = 0;
  v10 = 0;
  basic_string_new___((int)&v8, (int)a2, (int)&a3, a4);
  a1a = 0;
  v12 = 0;
  v13 = 0;
  LOBYTE(v21) = 2;
  vector_byte_alloc__((int)&a1a, 40);
  if ( v4 > 0 )
  {
    do                                          // 进行算法2,将 hex字符串 转成每一字节 不超过4的数组
    {
      v5 = (int *)&a3;
      v20 = a4;
      if ( v18 >= 16 )
        v5 = a3;
      v6 = std::find(v8, v9, (char *)v5 + i);
      LOBYTE(a4) = (char)(v6 - v8) / 4;
      BYTE1(a4) = (char)(v6 - v8) % 4;
      vector_byte_push_((int)&a1a, &a4);
      ++i;
    }
    while ( i < v4 );
  }
  if ( !(((v12 - a1a) ^ (*(_DWORD *)(v2 + 28) - *(_DWORD *)(v2 + 24))) & 0xFFFFFFFE) )// 要20个字符
  {                                             // 要走到这里,最后的返回才是真
    v20 = 2 * ((*(_DWORD *)(v2 + 28) - *(_DWORD *)(v2 + 24)) >> 1);
    v19 = a1a;
    memcmp(*(_DWORD *)(v2 + 24), a1a, v20);     // 与内置的结果对比
  }
  LOBYTE(v21) = 1;
  vector_byte_free(&a1a);
  LOBYTE(v21) = 0;
  basic_string_free((int)&v8);
  v21 = -1;
  basic_string_Tidy((int)&a3, 1, 0);
  stack_prot_end();
  return result;
}


  1. 验证阶段2,大家很奇怪,为啥验证阶段1都对比了内置结果了,还要有验证阶段2呢,其实这个程序厉害的地方就是 算法1 存在多解,要用该阶段还原成功最终的对话框代码,以确定那个唯一解
int __thiscall check_result(int a1)
{
  stack_prot_begin__(24);
  this = v1;
  if ( (unsigned __int8)check_part1(v1) )       // 验证阶段1
  {
    i = 0;
    tmpexe = VirtualAlloc(0, 0x1000u, 0x1000u, 0x40u);
    memcpy_(tmpexe, 0x1000u, OkMessageCode, 148u);
    tmpexe_p = tmpexe;
    this_ = this;
    while ( (signed int)i < 148 )               // 用对话框输入的字符串解码显示结果函数
    {
      v5 = i++ % *(_DWORD *)(this + 52);
      v6 = (_DWORD *)(this + 36);
      if ( *(_DWORD *)(this + 56) >= 16u )
        v6 = (_DWORD *)*v6;
      *tmpexe_p++ ^= *((_BYTE *)v6 + v5);
    }
    thisa = *(_DWORD *)(this + 52);
    v7 = this_ + 36;
    if ( *(_DWORD *)(v7 + 20) >= 0x10u )
      v7 = *(_DWORD *)v7;
    *tmpexe_p ^= *(_BYTE *)(i % thisa + v7);
    *(_DWORD *)(tmpexe + 97) += (char *)OkMessageCode - (tmpexe + 96) + 96;
    *((_DWORD *)tmpexe + 35) = (char *)&OkMessageCode[34] + *((_DWORD *)tmpexe + 35) - (_DWORD)(tmpexe + 139) + 3;
    ((void (__cdecl *)(_DWORD))tmpexe)(0);      // 调用显示结果函数
                                                // 如果未解码成功而抛出的指令异常
                                                // 是会被程序设置的全局异常处理捕捉到的
    VirtualFree(tmpexe, 0x1000u, 0x8000u);
  }
  stack_prot_end_();
  return result;
}

解码

  1. 首先上解码代码
# 内置的结果33码
code = [
		0x02, 0x02, 0x00, 0x03, 0x00, 0x00, 0x02, 0x03, 0x00, 0x01, 0x02, 0x03, 0x02, 0x00, 0x00, 0x02,
		0x02, 0x00, 0x02, 0x02, 0x02, 0x03, 0x02, 0x03, 0x01, 0x00, 0x02, 0x02, 0x02, 0x01, 0x02, 0x03,
		0x02, 0x02, 0x02, 0x01, 0x02, 0x03, 0x02, 0x03, 0x02, 0x01, 0x00, 0x03, 0x02, 0x02, 0x02, 0x02,
		0x02, 0x02, 0x00, 0x03, 0x01, 0x02, 0x02, 0x03, 0x02, 0x00, 0x00, 0x02, 0x02, 0x02, 0x02, 0x02,
		0x00, 0x00, 0x00, 0x02, 0x01, 0x00, 0x02, 0x02, 0x02, 0x03, 0x00, 0x02, 0x01, 0x00, 0x00, 0x03]
t = ""
for i in xrange(len(code)/4):
	# 一步将 33码 转成 原字节,跳过hex转换
    x = ((code[4*i]*4 + code[4*i+1]) << 4) + (code[4*i+2]*4 + code[4*i+3])
	# 交换 前5bit 和 后3bit,不与 0xCC异或
	# 因为后面的数组是用的原始数组,均跳过0xCC异或
    t += chr(((x>>3) | (x<<5)) &0xFF)
dt = "EpY07v!Vwb2UnTu5SHP1Oazei9@kRZF8IrdCJcDQKs3mGMlgBqyfNXhAo4x6WjtL"
def idxcalc(idx,i):
	# 这就是 算法1 选择对应字符的过程
	# 输入 idx 为原字符在上表中的位置
	# 输入 i 为原字符在原始字符串的位置
	# 输出 目标字符在上表中的位置
	# PS: 这个算法因为 idx==0 > idx=1 而存在多解
    if idx == 0:
        idx = 1
    for _ in xrange(i+1):
        idx = (idx + (idx/5+5)) % 64
        if idx == 0:
            idx = 1
    return idx
from collections import defaultdict
r = ""
for i in xrange(20): # 20 是输入字符串长度限制
    m = defaultdict(str)
    for j in xrange(64):
		# 用 算法1,枚举出该位置所有字符的 目的字符 -> 原始字符 字典
		# 注意是 1对多 关系
        m[dt[idxcalc(j, i)]] += dt[j]
    k = m[t[i]]
    if len(k) == 1:
		# 如果命中了则输出该字符
        r += k
    else:
		# 没命中输出所有可能字符
        r += "[" + k + "]"
print r


  1. 结果是

B[wj]n[ds][YlA][bt][Pi][He][dcs][Pi]y[25][0go][1a][7B4][@rK][JGX][9ID]O[kF]

  1. 接下来可以
    • 写程序,组合不同的结果向对话框发送设置文本消息和按钮消息
    • 手测 + 猜,我就是选择这个,前面几个试出来的,中间的 2017 猜的,最后的4个字节除了 O 确定外没啥规律,直接尝试了所有组合,成功跳出对话框

后记

写了几个小时,大大求评精



[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

收藏
点赞3
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回