欢迎来到 嗅灵易学

零基础也能上手的脚本技术课,一对一答疑带你入门

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

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

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 确定外没啥规律,直接尝试了所有组合,成功跳出对话框

后记

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


注意:上传附件及图片大小不得大于30M。

⚠️ 版权声明:
本博客所有内容(含教程、源码、工具)仅供个人技术学习与研究交流使用,严禁商用、倒卖、二次分发及非法用途
未经作者书面授权,任何组织或个人不得转载、复制或用于其他平台,违者将追究相关责任。

0 0 0 举报
复制成功