记录一些比较折磨有趣的复现
hgame
感觉做hgame还是学到了挺多的唯一难过的就是出题人的题解都写得有亿点简略,让我这个菜狗懵逼被折磨的痛不欲生……
hardened
脱壳完了看主类,有一个显眼的加密:
1 | public void sendPwd(View view) { |
里面那一坨很明显是密文。
有明显的base64特征,根据函数名可以看出来是aes,外面加一个bbbbb的函数,应该就是base64加密。java里没有加密函数,那么去so库找。
libenc里,直接去找函数名aesEncryption
和bbbbb
,找到:Java_com_example_hardened_MainActivity_aesEncryption
Java_com_example_hardened_MainActivity_bbbbb
主要任务就是找aes的key和iv,以及base64加密用的码表。
在aes里找字符串,发现aes里调了一个EVP_EncryptInit_ex(v11, v12, 0LL, ooo0oO0O0O0Ooo, OO0o0o00OoOO00o0o);
点进这两个一串0和o的东西,发现分别指向31020和31050处的数据。
base64加密用的码表也是这一串东西,指向31070。
但是麻烦的地方在于,这三组数据都不是可见字符串……可以猜想做了一些改动。
去查交叉引用,发现除了aes和base64加密调用这三组数据,还有一个datadiv_decode982000934203588085
函数也调用了这三组数据。
直接看反汇编的代码比较迷……三组数据跟三个数字做了异或,但改变的三组数据并不是之前查到的指向的数据,不过其实可以猜测是我们查到的三组数据依次跟这三个数字异或。
看官方题解说是做了一些混淆……
那么直接看汇编
1 | .text:000000000000D924 ; __unwind { |
能看出来逻辑,分别是三组数据跟0x30、0x7f、0x49异或,那么写脚本复原一下即可。
1 | key_l=[0x7A, 0x65, 0x63, 0x64, 0x6F, 0x71, 0x6F, 0x7E, 0x7F, 0x62, |
学到了frida……之后有空学一学(挖坑++
server
IDA7.6打开,能恢复符号表。
函数列表里找main_main,发现上面两个函数main_HttpHandleFunc和main_encrypt,后者是主要的加密逻辑。
本题为一个 http 服务器,在9090 端口上可以使用 get 方法提交 flag。
端口号在IDA里找,提交flag在浏览器上面输入localhost:9090/?flag=xxxx
看main_encrypt的伪代码,其中函数math_big___ptr_Int__SetString()
参数都分析不出来,可以手动修改函数声明来获得更好的反编译结果。
__usercall
关键字用来自定义函数声明,观察调用math_big___ptr_Int__SetString()
的汇编代码:
1 | .text:00000000004C5971 mov rax, [rsp+arg_0] |
可以确定此函数有4个参数,分别存在rax、rbx、rcx、rdi中。观察多处对math_big___ptr_Int__SetString
交叉引用的汇编代码(主要看在main_encrypto里的交叉引用),可以确定各个参数的类型以及顺序:
1 | .text:00000000005E1C54 mov ecx, 4Dh ; 'M' |
确定参数顺序为rbx、rax、edi、ecx,其中rbx装入了一个字符串的地址,为chr*
类型,rax为一个指针,类型为__int64
,edi和ecx均为int类型。
返回参数在rax,类型为__int64
。
修改函数声明如下:__int64 __usercall math_big___ptr_Int__SetString@<rax>(char *str@<rbx>, __int64 a2@<rax>, int a3@<edi>, int a4@<ecx>)
拿到main_encrypto的反编译结果:
1 | void __golang main_encrypt(__int64 a1, __int64 a2) |
我去,我自己写的时候IDA反编译出来的字符串巨长一串,为什么写复现题解的时候就正常了,离离原上谱……什么玄学
实际上,这个函数里有四个参数,第1个是字符串,第3个是要转换的进制,第4个是字符串长度,如果字符串莫名其妙多出来一大串,按这个长度切割就可以了。
可以分析,逻辑就是RSA,然后用了两次异或。异或逻辑如下:
1 | for i in range(153): |
用的初始值num给了是0x66,这个逻辑就是用一个初始值跟box里的值异或,然后把box里原来的值又赋给num,迭代到最后。
这样的话最后一次的num值实际上取决于初始值和明文序列,而我们需要这个值来恢复明文序列,但是我们不知道明文……(废话,知道了就不用做了,所以这个值是不可知的,因而只能爆破。
可能会有多组解满足恢复出来的明文序列可以拼接成数字,所以要跑完所有可能性。
至于怎么获得密文序列……题解里没说,咱也不敢问(什……
我是去找它报wrong的字符串,然后查交叉引用,再去找分支判断的跳转,然后看上面比较的值。
1 | .text:00000000005E2072 lea rax, [rsp+1368h+var_4E8] |
下断到mov处,然后调试去拿值……
至此拿到全部信息,可以写脚本了……
这里有个坑,直接box=global_box
的话是浅复制,对box的任何改动都会影响global_box,所以用列表的copy()方法 别问我怎么知道的
1 | from Crypto.Util.number import * |
hardasm
写了用于爆破的脚本,AVX2指令集实在是太抽象了……
按照题解patch程序,感觉学好硬编码还是很重要的……
1 | import subprocess |
DNUICTF
EasyRe
虚拟机,主要分析出来esp和eip寄存器,其他可以靠猜。
(从小到大依次eax,ebx,ecx,edx 正常人都会这样写代码吧)
esp寄存器比较明显,即每次push、pop、call、ret指令都会对其进行修改。
然后是两部分:指令和操作数。
程序要完成两个功能:定义指令集,写入操作数。重点分析这两部分就行。
汇编指令特征:
对于本题:
qword_6030C8[19]:esp
qword_6030C8[20]:eip
push指令:esp++,对栈上数据赋值
1 | int __fastcall sub_400E1D(__int64 a1, __int64 a2) |
test指令:赋标志值给寄存器
1 | int sub_40103A() |
jmp指令:直接修改eip
1 | int sub_40113A() |
call指令:eip入栈,修改eip使其指向跳转地址值。
1 | int sub_401089() |
ret指令:出栈esp到eip
1 | int sub_4010EA() |
1 | opcode=[0x11,0x34,0x00,0x2A,0x05,0x10,0x14,0x09,0x17,0x00,0x24,0x05,0x03,0x11,0x1D,0x06,0x00,0x00,0x05,0x03,0x11,0x40,0x06,0x00,0x48,0x05,0x11,0x1D,0x17,0x0E,0x01,0x15,0x04,0x0F,0x01,0x16,0x02,0x00,0x00,0x04,0x03,0x05,0x10,0x14,0x32,0x05,0x09,0x02,0x13,0x1D,0x05,0x12,0x15,0x04,0x10,0x14,0x3D,0x0A,0x01,0x13,0x34,0x03,0x04,0x12,0x0E,0x01,0x15,0x04,0x07,0x01,0x16,0x02,0x00,0x00,0x04,0x03,0x05,0x10,0x14,0x55,0x05,0x09,0x01,0x13,0x40,0x05,0x12,0x59] |
1 | box=[0xA3, 0xD8, 0xAC, 0xA9, 0xA8, 0xD6, 0xA6, 0xCD, 0xD0, 0xD5, |
蓝帽杯
loader
好折磨,真的是好折磨,对我这种调试菜鸡来说真的是太痛苦了😭
不过经过这次折磨,总算是感觉摸到了一点调试的门槛,所以详细记录一下调试过程。
按照https://mp.weixin.qq.com/s/lGPtsd8hPJnltZ8OJqOCiw复现
主程序是64位,但是子程序用DIE去查会发现是32位,所以是PE文件有问题,文件头和可选头被改了,就是标识程序是32位还是64位的那两个标志位。
用stud_PE去看这两个头的偏移,然后去把这两位手动改成64位程序的值。emmm至于这个值是多少,随便拖个64位程序来看看不就好了(x
改成
这里挖个坑,我找了半天没发现主程序是怎么把子程序这两个地方改过来的,感觉很奇怪。
子程序放到IDA里分析,主函数非常的抽象,里面调了一大堆函数。直接查字符串flag,抽象的地方来了……字符串上面到unk_416DE0才有交叉引用,又被引用了一次,跳转到该函数反编译,就是主要的加密逻辑。
1 | __int64 sub_412850() |
前面先是调了一堆sub_40D4C0,猜测是什么神必库函数,finger一下,得到函数名nimRegisterGlobalMarker,在网上搜了半天也只有几个很神必的代码例子,没人说这玩意是干什么的……
最后还是万能的RX神告诉我,这是nim lang,一种编程语言。
所以字符串的引用这么抽象,反编译出来的代码更抽象
47行sub_4026B0打印欢迎语,49行sub_4031E0接受输入,后面干什么不用管,91-97行验证是否为’flag{‘开头和’}’结尾,长度是否为42。
flag长度为42,那么数字长度为36,102和131行的两个while循环把flag分成两半(flag1和flag2),存到v13和v19里,然后调用sub_411340。这个函数被调用了4次,操作的对象都是数字字符,后两次是操作程序中的字符串(称为num1和num2),可以猜测是把字符转为数字的函数。
自己输flag{123456789012345678901234567890123456}测一下
150和154行调用sub_412080,操作对象分别是flag1、num1和flag1、num2。
函数很长,静态分析根本不能看,因为是其他语言写的,不是C……只能动调。
在150行下断点,跑到call sub_582080
,看v30,双击跳转,数据转地址再跳一次,往下翻一点就能看到数据。在这个数据开头的地方下硬件断点然后跑程序
数据:
1 | hex(72057594037927936) |
用的寄存器是32位的,可以看到edx里存的是flag1的高位,与num1的高位sub了一下,如果等于0就跳转。这里执行的操作就是检查flag1的高位是否等于num1的高位,等于就跳转。
我们可以改一下ZF的值来控制跳转,看看flag1的高位如果等于num1的高位会执行什么。
可以看到edx里的值是flag1的低位,所以是比较了flag1和num1的低位,用的是跟上面操作一样的循环。
判断不等于0之后,程序正常执行,到sub_582080的结尾处。
此处是给rax右移了0x3f,即只保留最高位。eax里存的是num1,而edx存flag1,进行sub操作后,如果num1小于flag1,那么rax的最高位会变成1。
由此,如果num1小于flag1,函数返回1,否则返回0。
根据主函数里的判断,此处是要求flag1大于num1,下面的一次调用同理,要求flag1小于num2。
后面调试过程大致相同,跑到sub_581270,在flag1处下硬件断点
可以看到是一个相乘的操作,两个操作数均是flag1,即对flag1平方,结果保存在v32。
sub_582260:
对flag2平方的结果乘以b,结果保存在v35
sub_5823C0将两个操作数相减,sub_582500比较是否相等。
按照上述流程重命名一下各个函数:
1 | __int64 sub_432850() |
梳理一下逻辑就是
$flag_1^2-11flag_2^2 = 9,72057594037927936 < flag_1 < 1152921504606846976$
然后就没办法了
在线网站解密or摇密码👴来看
反正我8会整(逃
关于本文
本文作者 云之君, 许可由 CC BY-NC 4.0.