-
-
[原创]CTF2017 第七题 不问少年crackme 解题报告
-
发表于: 2017-6-15 00:02 3851
-
拿到这题发现启动不起来(win10),后来光研究为什么启动不起来就用了好几个小时,最后猜测应该是程序打乱了原有的段,并且 vs2015 编译了 CFC( Control Flow Guard)导致的,如果不是哪位大神指点下
用 IDA 粗略分析,因为是 vs2015 静态编译,并且①用了很多 STL 模板类,②函数有堆栈保护代码,导致翻阅甚是痛苦,最后准备采用 OllyDBG 辅助分析
OllyDBG 环境准备
找到编辑框获取点,这种 CrackMe 一般都有获取编辑框输入内容的过程
然后就是用枯燥的调试来补充 IDA 的反汇编结果,期间还编译了带符号的静态 vs2015 程序来比较相似 STL 函数
输入处理函数,太长了,不发代码了,说下流程
验证阶段1,该阶段做了对比操作与内置的结果(参考解码代码)进行了对比。没有优化 IDA 结果,大家随意看看
B[wj]n[ds][YlA][bt][Pi][He][dcs][Pi]y[25][0go][1a][7B4][@rK][JGX][9ID]O[kF]
写了几个小时,大大求评精
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; }
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; }
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; }
# 内置的结果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
B[wj]n[ds][YlA][bt][Pi][He][dcs][Pi]y[25][0go][1a][7B4][@rK][JGX][9ID]O[kF]
- 程序入口有 TLS 解密,所以不能有默认的入口断点,你可以设置断在系统处,删除入口断点(或者用插件直接断 TLS)
- 程序有几个 IsDebuggerPresent 调用的地方,准备好隐藏调试插件
- 忽略 int3 软中断,程序有个地方用了 SetUnhandledExceptionFilter 和 int3 组合来改变程序流程,忽略软中断不影响自身中断
- 开始猜测 GetWindowText,在 OD 下了断点发现都不是的
- 然后猜测发送了 WM_GETTEXT 消息,IDA 查看 SendMessage 引用,没有发送这个消息的
- 最后翻导入表,发现了 GetDlgItemText 函数,查看引用直接发现了主要过程函数
- 将输入每一位字节与 0xCC 异或
- 存在一数组,是
EpY07v!Vwb2UnTu5SHP1Oazei9@kRZF8IrdCJcDQKs3mGMlgBqyfNXhAo4x6WjtL
每一字节与 0xCC 的结果 - 将第一步的结果与该数组进行 算法1(参考解码代码)
- 将结果每一字节先与 0xCC 异或,再将 前3bit 与后5bit 交换
- 写程序,组合不同的结果向对话框发送设置文本消息和按钮消息
- 手测 + 猜,我就是选择这个,前面几个试出来的,中间的 2017 猜的,最后的4个字节除了 O 确定外没啥规律,直接尝试了所有组合,成功跳出对话框
用 IDA 粗略分析,因为是 vs2015 静态编译,并且①用了很多 STL 模板类,②函数有堆栈保护代码,导致翻阅甚是痛苦,最后准备采用 OllyDBG 辅助分析
-
OllyDBG 环境准备
- 程序入口有 TLS 解密,所以不能有默认的入口断点,你可以设置断在系统处,删除入口断点(或者用插件直接断 TLS)
- 程序有几个 IsDebuggerPresent 调用的地方,准备好隐藏调试插件
- 忽略 int3 软中断,程序有个地方用了 SetUnhandledExceptionFilter 和 int3 组合来改变程序流程,忽略软中断不影响自身中断
-
找到编辑框获取点,这种 CrackMe 一般都有获取编辑框输入内容的过程
- 开始猜测 GetWindowText,在 OD 下了断点发现都不是的
- 然后猜测发送了 WM_GETTEXT 消息,IDA 查看 SendMessage 引用,没有发送这个消息的
- 最后翻导入表,发现了 GetDlgItemText 函数,查看引用直接发现了主要过程函数
然后就是用枯燥的调试来补充 IDA 的反汇编结果,期间还编译了带符号的静态 vs2015 程序来比较相似 STL 函数
- 首先是刚才获取文本的主要函数
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课