好玩, 以后不玩了

第一题
字符串定位到校验的位置:

校验函数里就是一个字节一个字节地明文校验, 得到flag:

第二题
主函数中输入长度小于0x70时会进入check:

但是函数结构很奇怪, push ebp后进入7011b6执行了:
1 | mov [esp+arg_0], offset sub_701EEB |
也就是调整栈指针然后直接修改返回地址, 这是一个跳板, 进入跳板跳到地函数, 先是又执行了一个strlen然后push edi又进入了一个跳板:

新的跳板跳向了loc_70129F:
1 | mov [esp+arg_0], offset loc_70129F |
所以实际上check函数先是通过跳板的方法执行了一个strlen然后跳回check来达成控制流混淆, patch成下面的样子就能让IDA正常反编译了:

然后分析check的功能, 先是根据当前位的输入在一个0-9a-z的表里取idx:

然后根据这个idx以及当前所在是奇偶位来确定三个变量x, y, z, 很容易联想到是一个迷宫程序:

也就是每一步由输入的2个字节来确定, 第一个确定y, 第二个确定x, z, 同时限定起点是0x2c0e终点是0x1fb2, 每次行动完的位置与上一次的位置曼哈顿距离必须是1, 剩下的一个限制就在defactor_prime中:
1 | char __cdecl defactor_prime(int n4_1, int *p_n2, int *p_n3) |
其中sqrt是牛顿迭代法求根, 猜测这是分解质因数的程序, 调试也可以验证确实是, 接下来就是写程序打印地图:
1 | import math |
得到地图后手动解出:

根据路径生成key:
1 | path = 'dsuddssaassddnssaauasuddddwwndddnwaawwuddwwdusdsssasssd' |
第三题
程序为32位程序, 核心加密和校验在0x0402380.
由于题目不用写注册机, 只需要求name为KCTF时的序列号, 所以有关name的处理全部无视, 根据格式化字符串可以定位到在这里进行了序列号的初步处理:

以十六进制字符串形式将对应数据读取到内存后先进行了一个反转操作, 然后进入以下函数进行了按30bit进行分割:

有python逆向经验的都可以察觉到这和python底层中Py_Long类型储存大数字的形式是一样的, 结合下面某个函数中的报错:

应该是将序列号作为一个大数字利进行了一些运算, 对于这些处理大数字的函数最好的分析方法就是调试猜测功能.
下面的处理还有几百行伪代码, 反正最后解密也要从尾开始, 接下来将从校验开始向上开始分析加密流程.
在最后校验处下断点, 修改用户名可以发现只有密文发生变化, 也就是说整个加密流程由序列号单独完成:

并且最后一步加密是反转然后第一个_DWORD自增, 因为最后的密文第一个字节固定是b'J', 这里直接当是第一个字节自增就行了.
继续向上查找加密结果的来源, 在from_bignum上下断点, 最后的密文明显来自第二个参数:

结合上面对序列号初步处理的分析, 应该是将一个大数转化为字节串, 用以下函数来验证(由于程序用到多种大数, 这里给出我用到的几种转化):
1 | from idc import get_bytes |
接下来就是通过调试和拷打AI不断还原这个大数是通过什么运算得到的, 最终恢复了3个最重要的符号:

分别是大数相乘, 取模, 求模逆操作, 用伪代码来表示这个阶段的加密逻辑就是:
1 | m2 = big_num_type2(key) |
其中key是上阶段加密的结果, 并且所有相乘都化简对应的乘方, c是一个函数开头初始化的常数0x56f67550f16a00390dcf0b2715708e61c5b3f23101862fc1, 接下来就是拷打AI怎么解出上一阶段的key(不然呢, 我又不是密码手).

这里放出怎么得到的37次方是方便后面还要解类似问题时能快速上手使用解密函数, 根据c可以分解为两个质因数, AI给出了以下通用解法:
1 | from sympy import mod_inverse |
参数的含义分别是: 最后取模的结果, 模数, 模数的第一个质因数, 第二个质因数, k次方
上facordb分解一下c就能得到两个常数, 对于上面37次方的解法就是:
1 | c = 0x56f67550f16a00390dcf0b2715708e61c5b3f23101862fc1 |
求模逆是一个可逆操作, 再对c做一次就能求回来, 然后solve函数求解得到上一阶段的结果key.
这里验证结果的方法是找到一开始通过字节串转化为大数的地方将字节串替换为求出的大数的小端序字节串, 然后让程序运行到最后校验处看看是否匹配密文.
现在基本上可以确定序列号就是作为大数不断进行运算得到最终结果, 实际上并没有针对序列号字节串的加密, 但是在最后一个阶段恢复的符号前面并没有任何一处调用, 猜测题目可能使用了多种数论库进行大数运算.
而中间常见的自增1再反转的操作可以通过调试来分析实际对一个大数的影响, 这里以倒数第二阶段到上面那个阶段为例探索这个操作的实际效果, 倒数第二阶段用到的是bignum1at:

可以看到实际上就是最高一个_DWORD自增, 那么下面就不探索大数和字节串之间的转化了, 只会越绕越晕, 只把它当成一个数处理就行.
倒数第二阶段也能恢复出乘, 取模, 模逆的符号:

运算对应的伪代码:
1 | key = code |
也能转化为和最后一个阶段一样的问题, 能用一样的解法, 这里对应的是10 * 3 + 1 = 31次
接下来继续向上分析也是一样的方法, 分析到倒数第三阶段还是一样的加密方式, 这时候已经开始不需要每次都验证每个函数的作用了, 因为上面说了程序用到了多种数论库(也可能是一个库的同样效果不同实现的函数?), 每一阶段都有自己的乘, 取模, 模逆, 这里直接通过这个加密模式来猜测每一个函数对应的运算即可, 其中有几个函数IDA识别参数有问题手动调一下参数个数能分别结果放在哪个参数即可, 最终一共是有9个阶段, 全部是同类型加密, 解密代码:
1 | from Crypto.Util.number import inverse |
第四题
注意到函数列表里IDA识别出来的TLS回调, 其中初始化了一些资源, 这些都不重要, 重点看看其中的反调试.
0000000140003774调用了0000000140007303来执行系统调用, 此处系统调用号为0x19, 接触过VMP的反调试就知道这是ZwQueryInformationProcess的系统调用, 第一个参数为自身进程句柄, 二个参数7说明查询的是调试器信息:

将检测到调试器调用先前分配的可执行页的分支patch掉:

main+373是一样的反调试, main+1ae处有同样的反调试手法, 调用的是SetInformation, 将调试器从进程剥离.
另外main+1c9注册了一个异常处理函数(0000000140006D40), 一上来就清除了进程的调试寄存器, 虽然不一定会用到硬件断点但还是patch掉:

若触发软件断点就会根据一个全局变量进行不同的操作:

之前的反调试就是将ctol设置为4然后执行int 3来让程序退出的, 由于不用写注册机, 所以这里不分析用户名的处理, 直接跳到处理序列号的地方, 对应ctol为3:

核心处理逻辑在decryptstring(0000000140005D6C), 这是先前用来解密一些字符串使用的函数, 也就是说需要编写的是相应的加密函数, 从公开序列号可以看出最后一步的加密应该是base64编码(对应题目中的0000000140001684), 但是使用题目中发现的表会发现编码不回原来的字符串, 调试会发现题目中的解码过程将每3个字符合成时竟然是大端序的, 那只能自己手搓一个对应的base64编码函数了:
1 | table = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0123456789+/' |
然后程序进行了一个类似AES的操作(000000014000593C):
1 | _QWORD *__fastcall aes(__int64 a1, _QWORD *key) |
从使用的SBOX可以看出是标准逆SBOX, 但是其中的列混合步骤用的貌似是白盒:

tbl是一个整整5 * 0x100字节的盒, 拷打AI写出爆破脚本后来到最后一步解密, 是一个简单的异或, 调试拿到异或的字节流即可, 完整脚本:
1 | from base64 import b64encode, b64decode |
沪赛毁了我的第五题(其实没有沪赛也做不出来)
第六题
简单整体分析
先简单运行一下程序, 会发现是一个易语言写的GUI, 结合文件大小应该是静态编译的. 然后随意输入一些数据点击登录没有反应, 题目说明中提到验证失败没有弹窗或弹出错误提示, 再试了一下更长的输入发现有错误提示的图片了, 二分法可以快速定位到这个临界值是31字节, 说明要提交的答案大概率就是31字节长的.
接下来开始分析, 既然是WindowsGUI程序, 获取输入框内容大概率要用到GetWindowTextW, 刚好题目也导入了这个函数, 为了避开一些初始化时的反调试(如果有的话), 下面的调试都是启动程序后附加调试的.
在GetWindowTextW下好断点后点击登录成功断下, step out发现获取到了输入:

硬件断点跟踪数据流到某个地方发现反汇编失败了:

观察一下会发现一条陌生的指令bextr, 应该就是它导致了这个错误, 并且这个错误发生后IDA的反汇编器会直接摆烂不再可以进行反汇编.
继续跟踪数据流发现了输入长度和一个熟悉的立即数进行了比较:

并且这个函数没有那条神必指令, 重启IDA来到这个函数进行反汇编, 若输入长度>= 0x1f就会把输入当参数进到do_check(12351552), 从f5结果来看输入只进到了这几个函数里进行处理:

调试可以发现前两个都是拷贝函数, 第三个函数真正进行了加密操作, 而为了确定加密的范围继续追踪这一步的密文, 下一次程序断下就来到了最后一个函数中, 并且和一段字节进行了比较:

在内存中搜索和加密结果比较的字节串可以在数据段找到:

修改比较结果发现可以弹出正确提示:

第一步加密
也就是说真正的加密逻辑只用看encrypt(12352547), 继续硬断追踪数据流, 第一步加密是将输入零扩展成DWORD:

再跟到下一步会发现在123533C6进行了写回:

在上面两个调用上下断点, 实际上callsm就是一个跳板函数, 用于调用放在EBX中的函数, 第一个跳板在每i次调用时从未被零扩展的ser取出ser[(4 - (i & 3)) * (i >> 2) : (i >> 2) << 2], 例如输入的是1234, 那么每次取出的就是:
1 | '?234' |
这里1替换成?是因为调试会发现每次取出一个DWORD时最高字节都会变化, 这个变化才是真正的第二步加密, 这里的两个跳板实际上执行的就是第二步加密完后再零扩展存入.
取出的DWORD来自[ebp-30h], 向上溯源, 这个值来自:

跳板执行的do_xor实际上是将第二个参数和第五个参数进行异或:

再向上溯源, 这个异或的字节来自[ebp-0x64]:

是一个有意义的字节串, 应该就是异或的key, 到这里总结一下真正的第一步加密:
- 对输入的下标
4 * i的字节异或一个字节流
第二步加密
继续跟踪数据流, 第二个字节的处理先是加了0x100:

这里用浮点数实现貌似是易语言特性, 接下来这个值被当作下标从一个表中取出一个字节存回.
第三步加密
第三个字节的断点在12353581被触发, 准备通过跳板进行异或操作, 和它异或的字节就是上面加密完的第二个字节:
在这两个操作上下断点会发现它们是交替执行的, 总结一下这两步加密
- 奇数下标的字节+0x100取表存回
- 偶数下标的字节异或前一个字节
接下来要解决的问题就是执行轮次的问题, 从f5的结果来看大概是执行114514 * 0x20轮:

在那两个操作上下条件断点分析轮次问题:
1 | last = open('log.txt', 'r').read() |

此时的大轮是0x1442次, 简单计算一下:

我的输入是0x20字节, 从子轮次次数和两次操作数量不同和计算结果来看, 有一个字节实际上没有参加2, 3步加密, 使得2, 3步加密的顺序是2; 3; 2;...; 3; 2导致取表的次数比异或的次数多了很多, 调试会发现是第1个字节没有参加这个过程.
然后就得到之前和密文对比的加密结果了.
至此可以写出keygen:
1 | table = [0xB5, 0xF4, 0x53, 0x8F, 0xC2, 0x9F, 0x17, 0x33, 0x47, 0x2, 0x5D, 0x55, 0x42, 0x2F, 0xBD, 0xC0, 0xA3, 0x66, 0x48, 0xCD, 0xB0, 0xE6, 0x11, 0xD6, 0xA8, 0x3, 0xED, 0xED, 0xA6, 0x79, 0x76, 0xCE, 0xC9, 0x0, 0x56, 0x13, 0x92, 0x21, 0xC2, 0xA7, 0x8D, 0x47, 0x44, 0x7D, 0x34, 0x19, 0xBE, 0x82, 0x10, 0x7, 0xAC, 0xD0, 0x21, 0x23, 0xA9, 0x24, 0x80, 0x33, 0x35, 0x92, 0x43, 0x4, 0xB5, 0x77, 0xA1, 0x1, 0xBB, 0xB0, 0x57, 0x3, 0x88, 0x9, 0x49, 0x6B, 0xCF, 0xF8, 0x6D, 0x6F, 0xBC, 0x8C, 0xE5, 0xB1, 0x35, 0xA0, 0x6B, 0x16, 0x60, 0x54, 0xF2, 0xD5, 0x65, 0xBE, 0x8A, 0xCE, 0x75, 0xDC, 0x85, 0x1E, 0xB, 0xCD, 0xD8, 0xF0, 0x71, 0x41, 0xC4, 0x95, 0x87, 0x2F, 0xB5, 0xD8, 0xC0, 0xC6, 0x6A, 0x8B, 0x6D, 0xA5, 0x56, 0x66, 0x3E, 0x4E, 0x46, 0x12, 0x5, 0xD8, 0x45, 0x80, 0xBE, 0xE5, 0xBC, 0x7F, 0xCD, 0xD4, 0xDE, 0x8E, 0x86, 0x38, 0x43, 0xEE, 0xF2, 0x88, 0xD3, 0xFC, 0xD0, 0x18, 0xE6, 0xBE, 0xDB, 0x47, 0xAA, 0xBC, 0x4B, 0xFA, 0xC4, 0x11, 0x9E, 0x4A, 0x3A, 0xC1, 0x98, 0x7A, 0x90, 0x4D, 0x89, 0x2C, 0x31, 0x85, 0xCE, 0xD4, 0x11, 0x9E, 0x9A, 0x6C, 0x91, 0x84, 0xF7, 0x6A, 0xA3, 0x71, 0x7, 0xEF, 0x2E, 0xBF, 0x90, 0x41, 0xB4, 0xFB, 0xB7, 0x7B, 0x32, 0x3A, 0xC, 0x83, 0x47, 0xB0, 0xC7, 0x3D, 0x99, 0x7E, 0x51, 0xFE, 0x75, 0xCC, 0x7, 0x44, 0xB5, 0x18, 0x3A, 0xA4, 0xE7, 0xCD, 0x7A, 0x3, 0xAB, 0x18, 0x14, 0x9, 0x5D, 0xF7, 0xD9, 0xD3, 0xF4, 0x93, 0x21, 0xE8, 0x2A, 0xCF, 0x10, 0x6F, 0xDE, 0x21, 0x18, 0x9F, 0xB6, 0xA1, 0xBF, 0x76, 0x8, 0x5F, 0xA3, 0xAE, 0xFB, 0xFA, 0xBB, 0xED, 0xE9, 0x6E, 0xDF, 0x3C, 0x8, 0x2E, 0x8B, 0xBA, 0x4A, 0x73, 0xE0, 0x91, 0x5E, 0x7C, 0x6D, 0xFF, 0xAE, 0xE2, 0xA7, 0x39, 0x5F, 0x99, 0xCE, 0x6E, 0xF1, 0x95, 0x19, 0x80, 0x87, 0xB1, 0x96, 0x58, 0xCD, 0x54, 0xFC, 0x4C, 0x6C, 0x9C, 0x1E, 0x1A, 0x40, 0x42, 0xE, 0x65, 0xBE, 0x13, 0x8D, 0x4D, 0x85, 0x66, 0xC3, 0xBC, 0x11, 0xDE, 0xFE, 0xA2, 0x2C, 0xDA, 0xC5, 0xC8, 0xD3, 0xB7, 0xB4, 0x48, 0x5A, 0x45, 0xEA, 0x18, 0x89, 0xE5, 0xE0, 0xF9, 0x52, 0x35, 0xEC, 0x1B, 0x47, 0xAF, 0xDB, 0xA0, 0x1F, 0x12, 0xE9, 0xB3, 0x3F, 0xC7, 0x24, 0x33, 0xE6, 0xBD, 0x46, 0x2A, 0x88, 0x53, 0x76, 0x7A, 0x0, 0x6F, 0xD8, 0x3C, 0xD, 0x81, 0x59, 0xD0, 0xAA, 0xDD, 0x1, 0x75, 0xD1, 0x26, 0xAC, 0x77, 0x4A, 0x9, 0x5, 0x8B, 0xF, 0xF3, 0x2, 0x17, 0xED, 0x57, 0xF2, 0x5D, 0x70, 0x8, 0xB2, 0x3E, 0xEF, 0xC1, 0xA9, 0x2F, 0xF5, 0xA8, 0x6, 0x60, 0x51, 0x9F, 0x1C, 0xCC, 0x72, 0x8C, 0x31, 0x98, 0x29, 0xEB, 0xAD, 0x64, 0x9E, 0xF8, 0xB0, 0x30, 0x78, 0xE4, 0x9A, 0x62, 0xE1, 0x9B, 0x1D, 0x63, 0x10, 0x84, 0x74, 0xC0, 0xDC, 0x15, 0x49, 0x7D, 0x4B, 0xCB, 0xFB, 0x16, 0xB, 0x56, 0x2D, 0xA1, 0xE7, 0x34, 0x27, 0x86, 0xEE, 0xA, 0xC2, 0xCF, 0x50, 0xC6, 0x55, 0x9D, 0xD4, 0x61, 0x8F, 0x41, 0xC9, 0xD5, 0x94, 0xBB, 0x20, 0x79, 0x8E, 0x92, 0xBA, 0x68, 0xE8, 0xA3, 0x25, 0x3A, 0x7F, 0xD9, 0xBF, 0xA5, 0x5B, 0x14, 0xDF, 0xFD, 0x37, 0x44, 0x23, 0xD6, 0x83, 0x32, 0xA6, 0xD7, 0x7E, 0x5C, 0x6A, 0x3D, 0xB8, 0xF0, 0xE3, 0x7B, 0x28, 0xC, 0xD2, 0xCA, 0xB6, 0xC4, 0x43, 0x22, 0x82, 0x3, 0xF6, 0x7, 0xFA, 0x97, 0x21, 0x90, 0x6B, 0x4E, 0xA4, 0xAB, 0x93, 0x67, 0xF4, 0x71, 0x38, 0xF7, 0x3B, 0x69, 0x2E, 0x4F, 0xB5, 0xB9, 0x2B, 0x8A, 0x36, 0x91, 0x73, 0x4, 0x42, 0xD5, 0x18, 0x8, 0x0, 0xEF, 0x12, 0x8B, 0x67, 0x29, 0x50, 0x46, 0x17, 0xB9, 0x9, 0x24, 0x9E, 0xFC, 0xF0, 0x9E, 0xE4, 0x52, 0xB7, 0x2E, 0xC7, 0x2F, 0xD1, 0x7, 0x2, 0x6F, 0x7D, 0x3, 0x53, 0xEA, 0x0, 0xDD, 0xDD, 0x49, 0x31, 0xA0, 0xCB, 0x18, 0x3B, 0x5F, 0x36, 0x1C, 0x9F, 0x27, 0x48, 0xE6, 0x78, 0x32, 0xA2, 0xA8, 0x3, 0x5D, 0xFC, 0x48, 0x5E, 0xDC, 0xB, 0xB3, 0x90, 0x2D, 0xA8, 0x74, 0xCA, 0x4A, 0x2E, 0x85, 0xED, 0x23, 0x24, 0x64, 0x4B, 0x4B, 0x1C, 0x6A, 0xB2, 0xF2, 0xDA, 0x59, 0xA7, 0x13, 0xB9, 0x34, 0xEF, 0xEE, 0x4B, 0x53, 0x54, 0xB9, 0x40, 0xB6, 0xA5, 0x93, 0x89, 0x9A, 0xFF, 0xB9, 0xBD, 0x4A, 0x4B, 0xFC, 0xBB, 0x38, 0x8, 0x73, 0x91, 0x4C, 0x4B, 0x6D, 0x9C, 0x7C, 0x3, 0xA9, 0xF1, 0x9D, 0x82, 0xCA, 0xFC, 0x78, 0x39, 0x5, 0x67, 0x21, 0xC3, 0x1D, 0x3D, 0x84, 0x26, 0x91, 0x50, 0x41, 0x55, 0x14, 0xD8, 0xBA, 0xF9, 0x3D, 0x5C, 0x69, 0x70, 0x80, 0xD6, 0x78, 0x16, 0x5D, 0x12, 0x8B, 0xC4, 0xD7, 0x57, 0xE1, 0x97, 0x28, 0x49, 0x9B, 0xF3, 0xB3, 0xE, 0x5B, 0xC7, 0x3A, 0xB0, 0x11, 0x12, 0x51, 0xC2, 0x12, 0xA6, 0x12, 0x47, 0x6B, 0x2C, 0x13, 0xCF, 0x74, 0x68, 0x95, 0xE3, 0xA8, 0xBE, 0xFE, 0xA3, 0xB3, 0xF5, 0x8A, 0xAE, 0xCD, 0x3C, 0x3D, 0x42, 0x47, 0x6A, 0x1C, 0xA5, 0x63, 0x8A, 0x9C, 0xC3, 0x69, 0x97, 0x5B, 0x18, 0xF7, 0x84, 0xE, 0xD0, 0x99, 0x7F, 0xBA, 0x2D, 0x99, 0x77, 0x28, 0x2A, 0x19, 0xDC, 0x93, 0x5E, 0x5E, 0xA6, 0xA3, 0x22, 0x6F, 0x98, 0x9F, 0xF6, 0xDF, 0xC6, 0xDE, 0x21, 0xE7, 0x55, 0x7E, 0x98, 0xB8, 0x82, 0x59, 0x21, 0xE, 0xE5, 0x35, 0xB8, 0x9, 0xF7, 0x3B, 0x32, 0x39, 0xD3, 0xAB, 0x20, 0xF7, 0x39, 0xCD, 0xF6, 0xFC, 0xD8, 0x2B, 0x6D, 0x2C, 0xCD, 0xFD, 0x25, 0xB3, 0x67, 0xE5, 0x8F, 0x53, 0x2D, 0xDC, 0xA, 0xFC, 0x22, 0x6C, 0x4C, 0x9E, 0x47, 0x21, 0x4, 0x3B, 0x62, 0x3A, 0xBD, 0x40, 0xFE, 0xA3, 0x6, 0x15, 0xB3, 0x28, 0xD0, 0xF3, 0xA7, 0xE3, 0x17, 0xF6, 0x55, 0xF6, 0xC5, 0x73, 0x8D, 0x80, 0xD3, 0x8B, 0xBC, 0xC9, 0xB1, 0x0, 0x6E, 0xC0, 0xE8, 0x48, 0x11, 0xA8, 0xFE, 0xE0, 0xFC, 0xE, 0x99, 0xE3, 0xB0, 0xFE, 0xE8, 0xDB, 0x5D, 0x76, 0x3F, 0xD7, 0xA8, 0x1B, 0x1, 0xBE, 0xAB, 0x2B, 0xC3, 0xE2, 0x3D, 0xB3, 0xAE, 0xD8, 0x74, 0x2, 0x25, 0x88, 0x69, 0x5D, 0xA8, 0x80, 0x3B, 0xF4, 0xF9, 0x8E, 0x57, 0x15, 0x7D, 0x8D, 0xF6, 0xA0, 0xE4, 0x7F, 0xE7, 0xBB, 0xD, 0xDC, 0x8E, 0xC6, 0x23, 0x2A, 0x2D, 0x92, 0xD, 0xCE, 0x62, 0xCD, 0x5, 0x22, 0xF1, 0xC1, 0x86, 0xC7, 0xC4, 0x3F, 0x6C, 0x3D, 0x30, 0xD5, 0x57, 0xB0, 0x7A, 0x47, 0x50, 0x15, 0x9A, 0x3D, 0xAF, 0x76, 0x3E, 0x3A, 0x3B, 0x8A, 0x12, 0xCD, 0x94, 0x89, 0x3F, 0xB, 0xCE, 0x3E, 0x31, 0x3C, 0x5F, 0x5E, 0x9E, 0xD5, 0x3B, 0x18, 0xC4, 0xA7, 0x3D, 0xED, 0xF2, 0x55, 0xC9, 0xC2, 0x49, 0xB, 0xB0, 0x34, 0xC4, 0x6D, 0x53, 0x2B, 0x76, 0xCE, 0xC, 0xB2, 0x13, 0xA3, 0xC9, 0x6, 0xB2, 0x37, 0xFA, 0xEC, 0xD1, 0xA0, 0xAE, 0x48, 0x9A, 0xF1, 0xF8, 0xEC, 0x65, 0xB1, 0x98, 0xAE, 0x7D, 0x8C, 0xD7, 0xBD, 0x27, 0x49, 0xB3, 0x35, 0xE0, 0xFC, 0x3C, 0xF0, 0xE7, 0x7D, 0x3E, 0xA0, 0xFB, 0x18, 0x20, 0x1A, 0x66, 0x86, 0xC, 0xF5, 0x3A, 0x1C, 0x51, 0x54, 0xDB, 0x43, 0x5, 0x0, 0xBD, 0x28, 0xEE, 0xBA, 0x6F, 0xB5, 0xA3, 0xCF, 0xD9, 0xBF, 0xEE, 0xEC, 0xC2, 0x81, 0x75, 0x34, 0x95, 0x49, 0x99, 0x90, 0x64, 0x71][0x100:] |
需要注意的是用chr输出最后的字符会发现有乱码, 但是密文的解密有互相依赖性, 不可能某一部分成功解密而其他不行, 结合这是易语言写的程序, 大概率是中文编码, 结果还真是.
第七题
题目为了混淆用string_to_code(buf)替换了很多立即数, 实际上string_to_code就是一个map, 用以下patcher可以美化一下代码:
1 | from idc import GetDisasm, patch_byte |
程序第一步是将输入的前10字节从十六进制字符转成数字, 然后通过这些hexnum初始化了两个矩阵:
1 | idx1 = 0; |
然后判断了一些条件后检验输入的11~14字节是否满足某些条件, 一眼丁真出asas:
1 | cns1 = 1; |
写个z3就能解出符合条件的14字节输入了:
1 | from z3 import * |
坏消息是总共有400多个可行解, 这时候观察一血会发现他在一小时二十分左右提交了正确序列号, 如果他的python, z3版本和我相同并且是从头试到尾那么他应该是20分左右解出题目, 花了1个小时交序列号, 在第30~39个内大概率有正确序列号, 找到正确序列号在第31个:
1 | ... |
下次出题的自己能不能验证一下有没有多解
第八题
第九题
DIE查出来一个Enigma Virtual Box壳, 直接放弃静态分析, 通过附加调试的方式进行动态分析
定位主函数
输入SN前在IDA挂起线程, 然后输入后步出到用户领空为止:

查看RAX可以看到刚刚输入的SN字符串, 同时可以观察出程序使用的库底层储存字符串的逻辑:

1 | struct str |
对Name的处理
程序先对Name进行了简单的hash, 最终结果存放在v8:

当输入的是公开序列号对应的Name时v8为55, 输入KCTF时v8为27
对SN的处理
展开SN
第一次用到SN在main+12B, 观察处理完的结果, 看到了大量重复的9, 结合序列号的格式, 大概率是{digit}{count}l的格式, 只是最后的l被省略, 而这里处理完的字符串因为太长是用类似链表的方式储存的, 偏移8, 10h, 18h的成员分别代表当前段, 上一段, 当前段的长度:

除法运算
下一次对SN的处理在main+276, 同时还用到了上面Name的hash, 它被以十进制字符串的形式储存起来了:

通过硬件断点跟踪数据流发现它和SN用空格拼接起来了:

步出至用户领空, 发现这个拼接的字符串上面还有一个路径字符串:


结合上面用空格拼接的字符串, 很有可能是当作命令行参数传给了这个释放出来的PE
找到这个PE, 发现是一个python打包程序, 解包反编译后得到以下py代码:
1 | from decimal import Decimal, getcontext |
和上面的想法差不多, 通过命令行参数进行了一个除法, 得到的是十进制字符串, 但是有3个命令行参数, 找到最后拼接的作为精度的参数:

实际上到这一步就不用分析下面的程序的, 因为程序给出了能通过校验的55 / N1的N1, 对应到KCTF的27, 只需要算出27 / N2 = 27 / (N1 * 27 / 55) = 55 / N1的N2即为正确答案:
1 | from decimal import Decimal, getcontext |
第十题
没有任何技巧, 只有数据流追踪
输入的数据第一步处理在0000000140044310, 大概可以看出来是一个将数据转化为二进制串的函数, 第一个参数是结果, 第二个参数为要转化的数据, 最后是要转化成的长度, 只是把0换成2, 1换成3然后拼接每一次转化的结果, 以16字节为一块:

然后就是不停下硬件断点跟踪, 直到进到000000014000E5A0进行了第二步处理, 第一步处理得到的binstr的每一位作为一级索引, 配合一个同样由2和3组成的key在一张9 * 9的表(00000001400C10C0)中取值:

再继续跟这一步得到的密文, 在sub_14000CB50进行了最终校验.
然后在拼接完二进制字符串后的00000001400587A6下断点进行加密轮次分析, 一共断下3次, 说明密文长度应该为0x30, 然后就是跟踪每一块的二进制字符串到加密和校验部分分别获取key和密文, 其中加密函数在程序运行中大概要被调用30000多次, 所以还是使用数据追踪的方式获取key, keygen如下:
1 | from Crypto.Util.number import long_to_bytes |
总结
题目质量配不上比赛名气