记录下第一次搞协议逆向,很遗憾比赛过程中卡在最后一步了总体协议流程如下直接分析license可以发现他和build_token建立了通信运行过程中打印了license code: xxxx-uuid这样的字符串,但并没有在license文件里找到,猜测是从build_token发送elf回来加载到主elf内存里执行代码直接上调试,ida里按照run.sh里设置--no-redirect -c 127.0.0.1:12345,本地跑./build_token -p 'r&FGW9RpqTc*aqof' -s -l 0.0.0.0:12345,从main函数return 0开始调试 刚开始可以看到retn后来到了新的区域 慢慢调试可以发现有一些脱壳的感觉,在解密一些数据代码,按照经验不停的跳过循环,最终可以发现来到一块非常大的函数 单步进入反编译后可以看到license、flag等字符串,说明这里才是真正的check逻辑 开头部分是在生成随机的UUID,慢慢调试直到要求输入,发现下图红框位置要求输入数据,分别要求4、12长度(注意回车符也算输入) 第三处要求输入长度的正是第一处输入的数值-12(不太可控,因为手动输入只能输可打印字符),因此调试到这里时我手动在第三处输入前把要求长度修改掉,从而得以输入可控长度的测试数据,我们这里成为数据流数据包结构分析完后,下图中sub_403D10函数会返回CPU架构是否支持AVX并设置一个bool字节(随后很多if-else会根据这个值选择进入到的代码,这些分支代码逻辑是相同的,可以省略很多分析过程)。我的CPU支持AVX,进入到下图红框中的数据初始化,AI分析可知是密钥扩展,但这里的密钥通过一定运算才能得到 所以直接来到最后最终轮密钥结果处,提取出来前32字节即可获得密钥为f6778d8728d8f17ce8c5c81f45c3d5fd869ca851b7575be540776f4f26c1140dif-else出来后发现对16个0字节做了加密 这种模式符合AES-256-GCM模式,里面包含2部分,CTR+GHASH随后的调试里都可以看到特征,比如下图里检查了数据流最后16字节是否等于一组结果 可以写一个代码来加密数据,结果放入数据包中的数据流部分,从而调试通过这部分AES解密解密完的数据发现检查了大小,要求不小于13字节 在往下调试的过程中发现报错Unknown frame descriptor,搜索字符串可以定位一堆报错case搜索可知是Zstandard解码器报错,解码器在输入数据开头没有识别出合法的魔数(28 B5 2F FD,可以IDA搜索到多个比较),所以才报错 python有zstandard库可以直接帮我们压缩数据,把压缩完的数据传给AES-GCM再去构造数据包即可保证这里解压不在报错这里非常明显,多处base64查表,而且上一步骤中解压的数据字符不是4的倍数会报错padding不对等等,确认这里在进行base64解码 所以只需要把我们对数据做一次base64编码再去zstd压缩、再去AES-GCM加密,最后构造包即可通过这部分来到下图这里时会出现新的报错,结合AI分析以及代码里switch-case取字段的逻辑,可知是protobuf解析 sub_40B960里要求了wire必须等于2,一共四个字段enc_data、password、salt和sha256_hash所以这里要构造符合protobuf的数据结构发现sha256加密 通过可控数据调试以及上一步骤中字段名,发现password和salt最先被读取并做了PBKDF2-HMAC-SHA256,iter正好是1000,生成了48字节 调试过程中到上图ROR8的位置时发现检查enc_data字段数据长度是否为20的倍数,不是就会跳转LABEL_80(ud2,也就是BUG())当我修改完enc_data长度为20倍数后,到下图时发现解密完的数据最后检查了最后1字节是否小于20,如果大于20会报错invalid padding 到这里可以分析出是某种对称加密(分组大小为20字节)+PKCS#7填充回过头分析对称加密算法,下图中调试经过unk_409DC0后会发现off_6EA318指向了一个256字节大小的sbox 值为和标准AES不一样,但类似AES,搜索发现确实存在20字节分组的算法。调试发现unk_409DC0里面存在表生成的函数交给AI分析可知生成了3张256字节表,用于方便后续AES计算,其中有log、exp、inv表。需要注意的是生成方式和标准不一样,标准xtime异或是0x1b,sub_41EE90里是异或了0x8d 最后出来后又在sub_41EBE0中生成了sbox(这里魔改了)出来后qword_6EA498值为13,符合AES-160的NK+6(密钥是28字节224bit,NK=7) 此外在一轮轮调试数据还发现到mixcolumn时结果变了,检查发现数值和标准的不一样 到这里应该是全部的魔改点了。调试可以发现AES-160采用的是CBC模式,前面PBKDF2-HMAC-SHA256生成的48字节,前28字节作为密钥,后20字节作为iv。最后还有一点,前面提到有padding所以应该是解密,但是上面的流程是按照加密算才和调试拿到的数据对应的,所以有可能是加密解密算法对调了下。解密后会检查sha256加密后的值是否等于protobuf里解析出来的sha256_hash值(32字节) 前面步骤全部正确后会在最终check前再次进入一个很大的函数,整体AI分析了一遍可知是在做json数据解析,要求包含license_code和sign字段,license_code值等于最开始随机生成的UUID,而sign字段经过一定加密要求最后16字节等于UUID(去除-并转为字节)通过观察报错定位到第一个是时间戳校验(license expired) 上图中计算了时间戳差值并检查是否小于60s时间戳校验后有一大串常量数据(1024字节) 继续往下走发现从上面数据中初始化了512字节,之后跳转到一个处理hex的逻辑 其中v204是单一字符,取自v196字符串,查看发现正是我们设置的sign值。下图每次读取两个字符拼接为一字节 之后是一个while循环,逻辑仍然是处理hex值,转为字节存储到了v206 又提取了固定的512字节(来自上面常量) 接着进入到一个函数,传递了sign和512字节,AI分析是大整数取模运算,符合RSA特性 通过调试拿到输入输出值测试发现是在计算到这之后基本确认是RSA-4096签名生成的sign,比赛时卡这里是因为n、d端序提取错了,导致RSA签名完的数据一直对不上调试拿到的数据所以总结来说这里sign经过n、d(私钥)解密后最后16字节要等于uuid,所以我们需要用n、e(公钥)加密uuid作为sign值构造json到这里就是一步步往回逆向,具体流程如下全流程代码如下远程连接的exp如下拿到远程flag void __fastcall sub_40C1B0(__m128i *_RDI, const __m128i *a2, _OWORD *a3)
---
来源: 看雪论坛
原文链接: https://bbs.kanxue.com/thread-289932.htm
阿里CTF2026-license
239 浏览
2 回复
真的厉害,套了这么多层这个题确实难
moshuiD
真的厉害,套了这么多层这个题确实难
其实这题被大佬们骂得很惨,套壳+rust做吐了
真的厉害,套了这么多层这个题确实难
其实这题被大佬们骂得很惨,套壳+rust做吐了