前言
由于该apk的flag在java层并没有什么东西,只有一个输入长度为16,java就不用进行分析了,打开so之后发现并没有静态注册的内容,也就是说checkSn
(该函数是我们要分析的函数),该函数是动态注册。
但是我们hook了RegisterNative
之后得到该函数注册的偏移地址之后发现,so始终跳转不过去,很有可能是因为其通过内嵌汇编的方式进行了编码,在编译期是不会编译内嵌汇编的代码的,也就是说这部分静态分析不了,因为我们正常拿到的so都是编译好的so。
给出apk文件所能获得到的链接https://down.52pojie.cn/Challenge/Happy_New_Year_2022_Challenge.rar
原帖链接:https://www.52pojie.cn/thread-1582582-1-1.html
动态调试
使用KingTrace,通过KingTrace遍历整个so看看长度。
好家伙,3w+(其实还是比较好的情况)。
所谓逆向分析,一般从最终的结果或者从某个点开始进行切入,由于本次结果返回的是一个boolean类型,即返回false则表明结果有误,而该flag是否有误肯定会进行对比之后再进行判断,而如何对比?汇编里面主要用两条指令进行if判断,cmp和cmn,两者的区别主要在于例如:cmp x0, x1 => x0 - x1 是否大于等于0, 而cmn x0, x1 => x0 + x1 是否大于等于0为判断是否为真,如是而已。
我们可以试着搜索一下这两个指令。
cmn:
cmp:
本程序没有cmn则我们重点分析cmp指令相关代码。
要找到什么样的cmp代码
cmp指令的位置在文件当中靠后,且比较之后,且比较后会在几个指令这样ret,且此时的某个寄存器的值应该为0x0。
看图2我框出来的位置,此时w0还不是0x0。
再看图一,影响w0的因素是w10,因为0x63 | 0x00 | 0x01 => 0x63 | 0x01,也就是说如果0x00 | 0x01变成0x01 | 0x01则结果将不为0!
我们通过unidbg的unidbg的Patch Code来对opcode进行修改达到逻辑修改的目的。
1 | public void unidbgPatchCode() { |
结果:
返回true了!
以0x7848为起始点,往上追溯最近的x0或w0.
打印x21的结果:d107a923fc131730c5abb640363c3df7bd5e181a9dcc8977a61d15c983800a1
我们看看既然这玩意跟结果有关,且其存放在X0寄存器上,我们知道一般传参范围为x0~x7,可以看到mov x0和x1和x2,一般这样的赋值就意味着要把这三个值传递给下一个函数,也就是刚才我们调用的函数。
也就证明,该程序对比输入参数和flag的部分就在0x780c这个子程序里。言下之意刚才找到的x21、x20一个是输入,一个是flag(x3刚才赋值的是0x20也就是32,也就是32字节,指的是hex的长度吧)。
那当前的思路已经很明确了:
- 先确认输入和flag是哪个。
- 沿着操作flag的地方向上追溯,寻找算法特征。
如何确定flag和输入
重放!修改输入的值然后再对比两个hex的值。
x20的值:6e6649305baf80c49b1b063c0500c80346ccfd42b3063ae7312b52a21cd334d8
x21的值:d107a923fc131730c5abb640363c3df7bd5e181a9dcc8977a61d15c983800a10
修改前:
修改后:
可以发现x21发生了变化,而x20没有发生变化,也就是说,只要我们解开了6e6649305baf80c49b1b063c0500c80346ccfd42b3063ae7312b52a21cd334d8就能拿到flag了。
老法子向上追溯找到0x40398018
出现的地址
量不是很大,看来可以直接一个一个debugger看看,直到找到最初出现或者出现一部分值的地方。
我们可以看到第一处出现这个地址的地方实际上已经赋值好对应的值了。
往上推。
顺势找到这里
结果:
说明校验值是在内存里被写死了。
虽然0x40398018
这个地址被写死了,但是其最终比较却不用来做比较,那我们看看其最终使用的地址0x40398058
,通过unidbg的memory trace write
来监控该地址。
效果很棒!
我们来到第一个发现其最开始的地方并没有被设置值,debugger之后按s来到这里。
看起来是循环赋值,看看跳到哪里。
此时的x0为我们关注的地址
x1:
可以看到x1里面包含了加密完成之后的值和原输入。既然如此还是从入参的加密入手,因为要对比的话两者的加密必然是同一种!
看看0xbffff450
这个地址是如何被写入的。
看看后面这两位,拼接起来d107a923fc131730c5abb640363c3df7bd5e181a9dcc8977a61d15c983800a1
输入的最终结果!
我们看看那串地址咋来的。
通过右移一定位数之后得到最终的结果,我们看看那一串长串咋来的。
通过一连串异或操作得来。
我们搜索这一串地址0x0acc4
第32行开始发现其x19的差别很大,就跟第1行
我们这里可以认为前16个字节和后16个字节是不一样的算法生成的。
那我们的分析主要分为从第一行开始和从第32行开始分析。
从第一行开始分析
第一轮ret后返回到0x8fec
这个地址,那其跳转的位置就是0x8fec-0x4=0x8fe8
可以看到传递了5个参数
1 | 第一轮 |
对应关系:
上一轮入参 | 下一轮入参 | |
---|---|---|
x1 | 会参与与result的异或运算 | 上一轮x2 |
x2 | 上一轮x3 | |
x3 | 上一轮x4 | |
x4 | 上一轮result | |
x5 |
所以只要知道最初的x1,我们可以回溯到32轮之后的结果。我们全局搜索第一个x1 0x3315191c
。
然后找到这个地方。
这个地方也就是最终的加密处啦~
接下来我们先看看入参怎么参与加密的或者是如何被同样的加密的。我们的输入是1234567890123455
,把他转成hex.
31 32 33 34 35 36 37 38 39 30 31 32 33 34 35 35
对应的话我们可以搜索0x31或0x32这样慢慢找到他们共同的疑似参与加密处。
看到这里我们有理由怀疑0x9b3c
为入参调用加密函数的地方。
可以看到这里就是参与加密了(因为trace文件在更改入参的时候没有重新trace所有最后的地方是0x36),再看看最后的结果。
对比第一轮加密的flag入参
完全是可以对得上的,但是由于我们是全局文本搜索,难免的会出现顺序问题,所以我们就可以知道我们的输入是如何参与加密的了。但是还要再看看w9 = 0x0以下的这部分
d1开头然后不按顺序来组装发现其可以拼成d107a923fc131730c5abb640363c3df7
,即加密之后的前16个字节!
加密的流程
- 先对输入与“异或表”进行异或,对应的异或表为:
0x2, 0x2b, 0x26, 0x28, 0x4a, 0x33, 0x70, 0x15, 0xf0, 0x4d, 0xe0, 0x65, 0xe0, 0x5f, 0xc0, 0x94
。 - 根据刚才得出的对应关系进行异或。
- 当前32轮加密完成之后再分别对加密的结果与“结果异或表”:
0x0, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10
进行异或操作,该结果很可能作为后32轮的入参,后续需要留意。
我们暂时先得出那么多,因为还需要对着arm进行代码还原看看他做了啥。
加密算法还原
找到有用到这5个参数的地方
然后一行一行的还原。。。。,这就是自研算法的还原方法了,无捷径纯手动还原。
需要注意的地方:
- ldp指令的指令公式为:``LDP Xn, Xd, [Xt]`,意思是取Xt寄存器地址里的值的前8个字节给Xn,后8个字节分给Xd
分析一下这段,因为这个x12也有用到,那我们需要分析其具体在x29的偏移是多少。”ldp x9, x12, [x29, #-0x70]”
动态调试的时候发现了这个位置,跟刚才的结论是一致的也就是说,这里的x12的值为x29的地址-(0x70+0x8)
也就是等价于ldr x12, [x29, #-0x68]
加密效果:
解密算法还原
flag加密之后的前半段为6e6649305baf80c49b1b063c0500c803
,后半段为46ccfd42b3063ae7312b52a21cd334d8
。
到这里我们需要重新梳理一遍加密流程:
- 先对输入与“异或表”进行异或,对应的异或表为:
0x2, 0x2b, 0x26, 0x28, 0x4a, 0x33, 0x70, 0x15, 0xf0, 0x4d, 0xe0, 0x65, 0xe0, 0x5f, 0xc0, 0x94
。 - 异或之后的结果为加密上半段密文的四个参数x1-x4。
- 当前32轮加密完成之后再分别对加密的结果与“结果异或表”:
0x0, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10
进行异或操作,生成后半段密文加密前的4个参数。 - 这四个参数再走同样的加密函数最终生成密文。
通过对加密流程的梳理,梳理目前可用的情报。
1 | 输入:未知 |
我们知道结果是一长长串的16进制数,但是我们知道了最后32轮的入参(调用encrypt函数即可)
我们可以得到它的所有结果,首先x19在最初当中被赋值为x1的,且中途并没有进行操作,我们知道上图最后一个列表的最后一个数字是上一个列表的result,我们要怎么推出上一轮的入参x1(举例:也就是0x1664a7a7fe8744)?
我们复习一下这个规律,也就是我们已经有了最后一轮的结果,也就有了上一轮的x2, x3, x4, result,而x5在轮数当中是固定的,那我们要求的也就是上一轮的x1如是而已。
程序代码
本人在上面只提供了算法思路,到这里我提供整个代码供大家进行复现
1 | input_xor_tab = [0x2, 0x2b, 0x26, 0x28, 0x4a, 0x33, 0x70, 0x15, 0xf0, 0x4d, 0xe0, 0x65, 0xe0, 0x5f, 0xc0, 0x94] |