作者论坛账号:JemmyloveJenny
先运行一下随便输入一点东西,看看错误提示
输出是 Error, please try again
用 DetectItEasy 查了一下,是32位程序无壳的,可以直接开始逆。
拖到IDA里分析一下,主要逻辑都在_main
函数里
从能看到有个循环,循环条件还很怪,地址小于字符串HappyNewYear
的地址,跟过去看一下
给了一个提示,flag
的长度是00010111
,转成10进制就是23
在前面也能看到if
语句判断了v21 != 23
运行程序输入一串长度为23的字符,可以看到输出文字有变化,更加确定了这一点
再来看一眼这个奇怪的循环,v3
是一个指针,最开始指向unk_4140B0
跟过去发现,这个东西在字符串HappyNewYear
的前面,而且四个字节一组的样子,数数总共是23个,可以猜测这边是经过加密的flag
由于解密函数不那么简单,也就不再继续静态分析了,上调试器跟着看看
我用的是 x96dbg (x32dbg)
用 Ollydbg
或者 IDA
直接调试 也一样吧,没啥区别
用x32dbg
要注意一点,它停下来的EntryPoint
并不是_main
函数,而是_start
函数
要在这个地方下个断点,再F7单步跟过去
到了入口点,看一下0x004012F6
到0x00401317
这一段 便是那个循环了,直接在之后下个断点让他跑完
果然就出来一个字符串2022HappyNewYear52PoJie
,长度刚好是23
用程序验证一下
成功~这就完事了
这道题和密码学有关哦~
仿射密码 还有 计算模反元素
在CTF中做了不少密码题,在这里看到我好激动啊啊啊啊!
还是运行一下看看,这个是要输入UID和flag的,所以每个人flag还会不一样哦,我就不截图了
一看这个文件大小就不太对劲,根初级题比有点小了,查个壳看看
果不其然,是被UPX压缩过了,这个我不知道能不能用工具直接脱
看着是UPX [modified]
大概不行吧,我就用ESP定律了
入口点经典pushad
单步一下看到只有ESP寄存器是红色的
对ESP寄存器 右键-在内存窗口中转到,接着在内存窗口中对这个地址 右键-断点-硬件,访问 几字节都无所谓
接着F9运行,等着popad
触发硬件断点
把硬件断点取消,然后在下面的jmp
大跳转上下个断点
断下来之后F7单步,来到OEP
接下来就是要用Scylla插件了,从顶上的插件菜单打开Scylla
不解释了,按照图里说的做吧
最后会获得一个xxxxx_dump_SCY.exe
这个就是脱壳之后的程序了
这道题使用C++写的
不得不说 我好讨厌C++ 源代码看着都费劲,逆向分析更恶心
C++的源代码中经常会用std::string
这个类型,但是逆向的时候好像IDA分析不出来
我没找到std::string的正确结构,在这道题里,我根据代码反推了一个它的结构,也不一定对[捂脸],如果有大佬知道请一定告诉我
复制代码 隐藏代码struct string
{
char field_0;
char field_1[3];
char *buf;
int size;
};
可以先添加上这个结构以简化分析流程
会变化的只有 我们输入的UID和key
我们紧盯着它们啥时候被使用就行了
那个sub_402460
其实就是std::string
的构造函数,或者是字符串复制函数,并不重要……但这东西耗了我好长时间(从中反推出string的结构)
前几个调用的函数都很简单,简单分析就可以知道,分别返回uid %25
,map[uid % 12]
,还有一个通过map[uid % 12]
算出来的值,这个值之后再说(最开始我也没看出来)
看一下最终判断函数,IDA最开始反编译出来的不是这个样子,参数个数比现在多,这边是我修复了std::string
这个类型之后才分析出来的
前4个参数实际是结构体的各个部分,合起来就是一个std::string
所以需要手动修改一下函数的定义
再来看参数:v18
其实就是复制后的key
v19
的话,伪代码里看不清,但实际上就是v21
(其实很明显,v21没被用过,但不应该是多余的嘛)这个v21其实很神奇,是乘法逆元,不知道也没关系v20
来源于v6
,也就是uid % 25
现在我们就知道了,key是否正确,是和uid % 25
,uid % 12
有关的,我们根据最终判断函数来分析一下
主要判断逻辑是在sub_401520
里面了,其实逻辑很简单,但是被C++这么一弄就很恶心
再次感到修复类型十分重要,这个函数里用了好多std::string
,分清楚就简单了
变量的定义大概是这样吧
这个函数前面一大串相似的函数调用,它的作用就是在初始化一大串字符串,后面的循环,便是将他们全部拼接到flag
后面得到正确的flag内容,也就是flag{Happy_New_Year_52Pojie_2022}
真正的重点在于后面
我们可以直接在字符串比较函数上下个断点,看看是在比较什么东西,动态调试看一下
我输入了自己的UID也就是520012,key随便写了个HappyNewYear
在调用字符串比较函数sub_403ED0
的地方0x00401CCE
下个断点
这就能看到比较的两个字符串了,根据调用约定,ecx寄存器里的是第一个参数,栈上的是第二个
对ecx 右键-在内存窗口中转到 就能找到std::string
的结构了,根据定义可以看到char* buf
指向的地址是0x871199
(x86架构 Little Endian顺序读取)
我们跟过去就可以看到字符串内容是LqjjkDceKcqp
,那么久可以认为这是HappyNewYear
经过变换后的结果了
第二个参数如法炮制,它的内容就是前面拼接出来的flag{Happy_New_Year_52Pojie_2022}
我们再仔细捋一捋哦HappyNewYear
变成了LqjjkDceKcqp
注意观察,这是一种替换,比如a -> q
,yY -> kK
,能看出来是替换这一点就行了
更进一步的话,再联系一下,前面求出来2个参数对吧,uid % 25
和 通过map[uid % 12]
算出来的值
我在CTF里做了不少密码题,一下就想到这是古典密码学的仿射密码了,想不出来也无所谓了
假如没看出来是仿射密码,只知道是替换的话,这个嘛…也很简单的
输入key
的时候直接输入abcdefghijklmnopqrstuvwxyz
继续在这个位置断下来,看看变换完是什么结果呗
复制代码 隐藏代码abcdefghijklmnopqrstuvwxyz 变换前
qtwzcfiloruxadgjmpsvybehkn 变换后
然后倒着查呗,这多简单呢
什么是仿射密码呢?
有两个参数A
和B
,a-z分别编号0-25
然后加密字符c就是计算c*A + B mod 26
再转换成字符
那么解密字符c就是计算(c - B)/A mod 26
再转换成字符
这些运算都是在模26的意义下进行的,加减B
都好办,那除以A
如果不能整除怎么办呢?
这就要引入一个概念,乘法逆元
举个例子吧,3和9在模26下互为乘法逆元,这是什么意思呢?3 * 9 = 27 = 1 (mod 26)
,同理还有5 * 21 = 105 = 1 (mod 26)
,7 * 15 = 105 = 1 (mod 26)
等等,在有限域运算下,两者的乘积是1,也就是互为倒数,在有限域中就称作互为乘法逆元。
当我们乘上一个乘法逆元,那么就相当于在有限域中除以了原来的数字,比如:11乘以3就是11 * 3 = 33 = 7 (mod 26)
,再除以3,可以变为乘以乘法逆元,也就是9,7 * 9 = 63 = 11 (mod 26)
又变回来了
我们再实战一下,比如说我的参数是A = 9, B = 12
,9的乘法逆元是3
对字母'b'进行加密'b'*9 + 12 = 1*9 + 12 = 21 (mod 26) = 'v'
,加密完就是'v'
对字母'b'进行解密('b' - 12) / 9 = (1 - 12) * 3 = -33 = 19 (mod 26) = 't'
,解密完就是't'
我们再对照一下之前断点得到的码表,验证了是完全正确的~
还想再说一下,论坛这道题没有出错,真的很好~密码学这些知识用不好是会出错的
要知道,乘法逆元不是一定存在,只有与26互质的这euler_phi(26) = 12
个数字才存在乘法逆元,这道题处理得很好,查了个表,还贴心地帮我们求好了乘法逆元(v21就是求了乘法逆元的结果)
理解了 仿射密码加密 的原理,这个就很简单了,我用C语言写一个
复制代码 隐藏代码#include <stdio.h>
#include <string.h>char const flag[] = "flag{Happy_New_Year_52Pojie_2022}";
int map[] = {1, 3, 5, 7, 9, 11, 15, 17, 19, 21, 23, 25};int main()
{
int uid;while (scanf("%d", &uid) != EOF)
{
int A = map[uid % 12];
int B = uid % 25;
char buf[sizeof(flag)];strcpy(buf, flag);
char *p = buf;while (*p)
{
if ('a' <= *p && *p <= 'z')
{
*p = ((*p - 'a') * A + B) % 26 + 'a';
}
else if ('A' <= *p && *p <= 'Z')
{
*p = ((*p - 'A') * A + B) % 26 + 'A';
}++p;
}
printf("%s\n", buf);
}return 0;
}
输入自己的UID按回车就好了
安卓题,想用什么用什么吧,GDA, JD-GUI, JADX 这些都可以啊
我是用了GDA看了一下,找到了验证输入的函数
核心在于MainActivity
的checkSn
函数
这是一个Native函数,也就是说在.so动态链接库里面
Java这边还提供了一个线索,flag长度是16
除此以外就没啥了,分析so去吧
用压缩软件打开apk,发现lib文件夹里只有一个lib52pojie.so文件,我们要分析的就是它了
这个是arm64-v8a架构的库,所以要用 IDA x64 打开
Native函数有两种定义方法,一种是按照类名函数名静态声明并导出,一种是在JNI_OnLoad
里调用vm->GetEnv->RegisterNatives
动态声明
看一眼它的导出表,发现并没有导出checkSn
函数,那么就说明是JNI_OnLoad
里动态注册的
那么就是要找到,通过RegisterNatives
注册的checkSn
函数是哪个呗
我原本还想用IDA静态分析出来的,可是一看那函数指针乱飞的伪代码就放弃了,这东西只能动态调试
我有个root过的Android手机,可以当作调试用的真机,调试比模拟器省事多了
把这个apk安装好,IDA的android_server64也传上去运行,准备调试~
最开始的想法是用IDA的动态调试功能,直接下断点追着分析
但这IDA真机调试好像只能用attach附加到已有进程上,不能直接启动一个app(也可能是我不会,有会的教教我)
我当时绕了不少弯路,整了JDB,DDMS这些乱七八糟的,用他们调试启动app,在JNI_OnLoad前就用IDA附加,好不容易弄完了,但是还有问题,IDA没能把文件里的地址和运行时地址对应起来,也就是说IDA不知道把断点下在哪个地址,根本断不下来,这都白费功夫
既然动态调试也跟踪不了 那我就在你必然会调用的地方守着嘛~
想法就是,知道会调用RegisterNatives,那么就在那Hook一下传入的参数呗
在网上搜Android Hook相关内容的时候,就看到了有用Frida来Hook那个RegisterNatives的脚本
这个Frida我也早就听说过它的大名,这次就试试看怎么样
安装Frida的过程我就不说了,要有Python环境什么的
然后那个Hook的脚本在这里
,把脚本内容存到文件里,然后命令行输入frida -Uf com.wuaipojie.crackme2022 --no-pause -l script.js
就能Hook到真实地址了
得到了这样一个结果,确实是通过RegisterNatives
注册了checkSn
函数,那个0x70c7f7ff74
是运行时的地址,而不是IDA里的地址
Frida也是能获取运行时地址的
复制代码 隐藏代码var base = Module.getBaseAddress("lib52pojie.so")
console.log(base)
这样就能输出lib52pojie.so
加载的基址
减一下就知道,验证函数是sub_6F74
,同时还可以利用基址计算其他函数的偏移量
这个so文件基本上没办法直接静态分析,在汇编中能看到很多的BLR X8
这种指令,也就是说调用X8
寄存器中地址的这个函数,而X8
的计算乱七八糟,IDA的F5直接报废
那么我们接下来要做的就是去除这些干扰,把代码变回正常的样子
我还是用Frida启动这个app,Frida可以获得到lib52pojie.so
的基址
然后再用IDA附加到这个进程上,手动计算一下运行时地址下断点
每到一个BLR X8
就可以在IDA的寄存器窗口中看到真正的地址,减掉lib52pojie.so
的基址之后,就能获得调用的函数在文件中的偏移了,然后把BLR X8
改成BL $(address)
即可,顺便把计算X8
所用到的指令改成nop
修改的话,我是给IDA装了一个插件叫做KeyPatch (KeyStone)
,修改效果大概如下:
修改完之后,应该就没啥难点了,IDA的F5又支棱起来了!!!
之后就是一般的逆向过程,修复类型,修复参数,重命名函数等等
其中有个用到的字符串类型,可能是C++编译器弄出来的吧(C++真讨厌)
我大概逆向了一下他的结构,不一定对
复制代码 隐藏代码struct basic_string
{
int64_t field_0;
int64_t length;
int32_t field_10;
char buf[];
};
修复完的代码大概如下
可以看到,最后的结果v5
是字符串比较的结果,下断点看了一下,这些都是无意义的数据,猜测是一种加密算法的密文比较,之后也证实了,就是变形的sm4
这个cipher
变量是怎么看出来的呢…?就是在sub_9C68
中用同样的方法修复,可以发现使用了0x357C0
那个地方的数据,跳过去一看是sm4
的vtable
这很明显是C++的类的结构,那么sub_9E90
,sub_9F50
,sub_A048
,sub_A080
,sub_A4F0
这些肯定都是sm4的类函数了
盲猜sub_9E90
,sub_9F50
是设置key和iv的,然后sub_A080
是加密函数
然后我就从里面下断点得到了key
和iv
,比较函数获得了正确的密文
复制代码 隐藏代码key = 0xc099403db00550812ea00fd803dc0e7c
iv = 0x022b26284a337015f04de065e05fc094
cipher = 6e6649305baf80c49b1b063c0500c80346ccfd42b3063ae7312b52a21cd334d8
用openssl解密试了一下,换任何模式都无法解密,加密的密文也都不同
那么可以断定,这道题修改过sm4的默认参数,这个方法行不通
sub_A048
,sub_A080
,sub_A4F0
的功能还不太确定
其中sub_A048
调用的时候传入参数是1,有可能是1代表加密,0代表解密
也有可能是sub_A4F0
是解密函数,sub_A080
设置加密模式(ECB/CBC)
想想这个sm4的代码大概率不是出题人自己写的,肯定是网上开源的代码
所以直接上Github搜索sm4的C++代码,果然就有
https://github.com/tonyonce2017/SM4
看了一眼,确定解密是单独的函数,那么我们只要拿密文调用解密函数即可
我们看一下这一段的汇编
可以很轻松地 理解这段的含义
那么我们在调用sub_A080
加密函数时,稍稍修改一下寄存器的值
把原来的输入内容地址,改成正确的密文的地址,然后修改PC
寄存器的值,改成解密函数的地址
处理器会执行PC
寄存器的位置,那么就会调用解密函数,还原出正确的明文
看一下解密结果即可
正确的flag就是[WwW.52P0Ji3.cN]
hls加密嘛…肯定是有个key的
我们把里面的文件都提取出来,有好几个加密的.ts,一个script.bundle.js,还有一个drm的请求
尝试直接用返回的drm解密失败了,说明还是得分析javascript
重点在这里
图里解释的很清楚了,drm请求的前后16字节异或,再与请求头中的h异或,即可获得正确的key
我是用cyberchef实现的
复制代码 隐藏代码https://icyberchef.com/#recipe=From_Hex('Auto')XOR(%7B'option':'Hex','string':'7b10311e6e310f0df068d9ede10475a8'%7D,'Standard',true)XOR(%7B'option':'Hex','string':'DA4E5CEAE16FED46EB6F498C9B63D53B'%7D,'Standard',false)To_Hex('Space')&input=MDhBNUU2QzJDMjYxQThBQ0I0RDc5QzQ5QUYxNjBBM0E")
得到正确的key就是a9fb8b364d3f4ae7afd00c28d571aaa9
,其实iv也是这个
然后解密就好了嘛 随便怎么弄了,我是用了openssl
复制代码 隐藏代码openssl aes-128-cbc -d -in live_00003.ts -out live_00003.decrypt.ts -K a9fb8b364d3f4ae7afd00c28d571aaa9 -iv a9fb8b364d3f4ae7afd00c28d571aaa9
然后在 live_00003.decrypt.ts 中就能看到flag了flag{like_sub_52tube}
这个flag也有点坑…我把Sub的s看成大写了,然后提交又不知道要不要加flag的大括号
第一天的三次机会就这么全错光了
活动已结束,题目打包放到爱盘供大家下载学习:
https://down.52pojie.cn/Challenge/Happy_New_Year_2022_Challenge.rar
欢迎发帖讨论分享分析过程和结果。
--官方论坛
www.52pojie.cn
--推荐给朋友
公众微信号:吾爱破解论坛
或搜微信号:pojie_52