分析准备
集成so文件
为了方便分析和调试,我选择了主动集成生成sign的so文件到自己写的apk中,然后主动调用。
可能是gradle版本的问题,搜索到的文章大都无效,踩坑十分多。
其实配置很简单,在src/main/
目录下建立jniLibs/armeabi-v7a/
目录,把so文件放在里面。
然后配置好build.gradle
和CMakeLists.txt
就行了,当然,不同的gradle
版本会有不同的问题,自己多搜索折腾下就好了。
以及so文件中会有两处简单的校验,直接nop两条跳转指令就解决了,十分简单,就不多说了。
总加密流程
总的加密函数执行流程:
sub_127E4——>sub_126AC——>sub_18B8——>sub_227C
核心的加密算法都在sub_126AC中,传入待加密字符串,待加密字符串长度和两个随机值,
sub_126AC会返回加密后的字节,然后sub_18B8进行base64加密,最后sub_227C会进行标准的md5加密。
sub_126AC——加密选择
sub_126AC只是加密的入口,会根据传入的两个随机数选择加密方式和相应的key。
我们定义三种加密方式为version0,version1和version2,其中version0和version1加密流程全部一样,只不过里面函数传入的常量version0是32,version1是16,我们在分析过程中以Version0为例。
Version0 加密
执行流程
加密函数执行流程:
sub_10E4C——>sub_125F0——>sub_12580——>sub_10EA4——>sub_10D70
待加密字符串会被每8个字节分为一组传进sub_10EA4进行加密,如果最后还有字节剩余,会单独进入sub_10D70加密。
sub_10EA4比特位初始化
首先看下sub_10EA4函数的流程图,先进行初始化,接着有一个循环,循环次数是传入的key0的长度。
最上面的两个长框框就是初始化过程,做了什么呢,IDA进行f5后比较简洁,我们截取一部分看下。
我们注意到分别传入的8个字节分别和0x80(b'1000 0000'),0x40(b'0100 0000'),0x20(b'0010 0000'),0x10(b'0001 0000'),0x8(b’0000 1000‘),0x4(b'0000 0100'),0x2(b'0000 0010'),0x1(b'0000 0001')进行与操作,然后赋值给一些变量,我们观察这些与操作的对象,会发现其实很有规律,这些变量正是输入的8个字节的64个比特位,后面会进行打乱比特位然后还原。
sub_10EA4打乱比特位
然后我们来看流程图中很有规律的十六个小框框,
截取一些IDA进行f5后的片段进行分析。
其中key0_i是key0的第i个字符,在循环中key0_i也会和0x80(b'1000 0000'),0x40(b'0100 0000'),0x20(b'0010 0000'),0x10(b'0001 0000'),0x8(b’0000 1000‘),0x4(b'0000 0100'),0x2(b'0000 0010'),0x1(b'0000 0001')进行与操作,结果作为条件判断,也就是判断key0_i二进制的对应比特为是0还是1。然后会进入对应的分支交换初始化过程中得到的变量,也就是打乱比特位。
sub_10EA4比特位复原
在函数最后进行的是比特位复原,四轮循环,每轮还原两个字节。
如有不清楚的地方,可以自行调试观察。
使用unicorn还原sub_10EA4算法
我们怎么还原sub_10EA4中的算法呢,过程不难就是量多,难道真的要一点点对比着IDA的f5反编译手动写出一样的逻辑吗?当然不是,这里有更简单的方法,我们可以借助unicorn来快速还原。
我们已经分析出来sub_10EA4算法做的不过是打乱传入八个字节的比特位,生成新的八个字节,也就说总的比特位只是顺便变了,并没有被改变值,那么我们可不可以找到每个比特位在打乱前和被打乱后的映射关系呢?当然可以。
我们只要借助unicorn,控制sub_10EA4函数输入的八个字节的64个二进制比特位,如果64个比特中只有一个是1,那么结果的比特位中会有几个1呢?也是只有一个,然后计算前和计算后比特位的1的索引位置就是要找的映射关系,我们进行64次计算,然后每次计算1的索引位置不同,最后就能得到全部比特位的计算前和计算后的映射关系了。
篇幅所限,unicorn的使用就不多说了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
|
可以得到映射表,例如[1,4]表示64个比特位在打乱后第1个位置的比特位会到第4个位置。
1 |
|
然后可轻松还原sub_10EA4算法。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
sub_10D70函数分析
sub_10D70函数在IDA反编译也比较清晰,我们来看下。
前面有说,待加密字符串会被每8个字节分为一组传进sub_10EA4进行加密,sub_10D70会加密最后余出来的几个字节,这6个case就是加密余出来的1到7个字节。case 0到case 6对应的六个函数都是一个模板,我们以case 0为例来分析,即sub_4B7C。
sub_4B7C初始化
sub_4B87只会加密处理一个字节,首先我们来看下sub_4B7C的流程图。
可以看到密密麻麻,其他五个case也都长这样,看到后首先第一个反应就是,这是ollvm吗?在经过仔细的分析以及动态调试之后,我判断这个并不是ollvm,没有看到控制流平坦化会带有的标志性大量的常量,也没有找到不可到达的分支,虚假控制流以及指令替换的痕迹,当然也可能是我水平太低了,没有认出来这种ollvm,总之,我还是铁着头把整个流程从头到尾看了一遍。
来看下初始化,
看样子有点像sub_10EA4,但仔细一看又很多不一样,上半部分取了传入的一个字节的比特位,还做了些其他的计算操作,赋值给变量。
而下半部分呢,则是取一些变量的地址,然后放到另一些变量地址加偏移处,这些说起来可能很模糊,但是如果看一下sub_4B7C的栈空间就会清晰很多。
为了方便理解,我们把这样连着五个变量在一起的当作一个数组,这样的数组往下拉可以看到是有八个,每个数组的前四个位置都存放着其他数组首的地址,而第五个位置则存放着前面说的输入字节的比特位经过计算后的值,其实分析后面的case就会发现,有多少输入的比特位,就会有多少个这样的数组。
初始化就这么多,而后面就开始根据这些东西进行大量的循环计算。
sub_4B7C计算分析
整个循环是根据key0的每个字节的每个比特位作为判断条件来选择分支,然后进行循环的,所有的计算类型只有两种,就是下面的两个大方框,是两个小循环。
它们的汇编如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
这两个小循环做了什么呢?其实十分简单,就是不断地查刚才我们定义的数组的前四个位置存放的变量地址值,判断是否等于另一个变量的地址值,只不过判断的过程中会不断地移动这些数组的第五个位置的值。
使用unicorn还原case0——sub_4B7C算法
这个算法似乎很难搞,确实很难搞,不同于sub_10EA4的对输入字节的比特位进行简单的交换,在获取了比特位后又进行了很多计算。
该怎么办呢,我们来看下sub_4B7C算法的最后部分。
可以看到和sub_10EA4函数最后的比特位还原部分几乎是一样的,这时候我们进行下大胆的猜测,sub_4B7C函数其实也是打乱输入字节的比特位进行了还原,只不过比特位在打乱后还进行了计算,而且每个比特位进行计算的规则都是一样的。即输入的字节第x个比特位是0的话,打乱计算还原后,第y个比特位会是m;如果输入的字节第x个比特位是1的话,第y个比特位则会是n,(x, 0)——>(y,m)(x,1)——>(y,n)。后面经过实践,证明了这样的猜想是正确的。
然后我们还是可以借助unicorn来找到所有的映射关系,还原sub_4B7C算法。思路我们进行下改变,我们可以unicorn控制输入字节的比特位,用八个比特位只有一个1的计算结果和八个比特位全是0的计算结果进行对比从而得到所有的映射关系。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
|
就结果为
1 |
|
我们可以得到为[[0, 6, 0, 1], [1, 4, 1, 0], [2, 5, 0, 1], [3, 0, 0, 1], [4, 2, 0, 1], [5, 3, 0, 1], [6, 1, 1, 0], [7, 7, 0, 1]],里面的列表每个即为[x,y,m,n],比如[6, 1, 0, 1]意味,第6个比特位在打乱计算后会放到第1个比特位上,如果第6个比特位是0,则在打乱计算后会放到第6个比特位上是0,反之是1。
然后可轻松还原sub_4B7C算法。
1 2 3 4 5 6 7 8 9 10 11 |
|
使用unicorn一步求出所有case的映射关系
好了,现在我们已经还原出第一个case,我们再会过头来看下sub_10D70,可以看到一共有七个case,分别对应的七个函数不同的只有函数起始结束地址,以及要处理的字节数,开拓下思维,这时候我们完全可以all in one,一次得到所有case的映射关系。
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
|
运行结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Version2 加密
执行流程
加密执行函数流程:sub_10DE4——>sub_12FF0——>sub_12ECC——>sub_130D0,其中sub_12ECC是重点,我们直接看sub_12ECC。
sub_12ECC初始化
进入函数sub_12ECC
,先看下开始处的汇编片段。r1
寄存器存放着key2
字符串地址,r2
寄存器存放着常量1,r3
寄存器存放着待加密字符串地址,在开辟栈空间后SP,#0x48+arg_0
地址存放着是待加密字节的长度。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
我们注意到下面这四句汇编指令,是把待加密字符串存放的地址放到r10
寄存器中,key2
字符串地址放到r7
寄存器中,常量1放到r9
寄存器,从SP,#0x48+arg_0
地址取出待加密字符串长度后放到r8寄存器中。
1 2 3 4 5 6 7 |
|
sub_12ECC加密流程
然后我们在函数sub_12ECC
中往下走,找到0x12F68
地址处开始的关键汇编片段,即sign算法加密部分,这里是一个循环。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
r10寄存器
我们抓重点,看待加密字符串是怎么被加密的,待加密字符串地址放在了r10
寄存器中,涉及r10
寄存器的汇编指令有三条。
00012F74
处的LDRB.W R0, [R10]
是取出待加密字符串的一个字节放到r0
寄存器中随后会进行计算操作;
00012F92
处的STRB.W R2, [R10],#1
,这条指令首先会把放在r2
寄存器中的计算结果值放到r10
寄存器当前存储的地址上,这个并不重要,因为还没有计算完成,后面的指令才是把最后的计算值存放值到这个地址上,重要的是这条指令随后r10
寄存器中的地址值会加一,因为这是一个基于索引后置修改取址模式;
在00012F92
和00012F9C
之间,我们可以看到取了r7
寄存器存放的地址值的一个偏移地址的值(偏移值为r1
寄存器中的值)放到了r1
寄存器中,随后把r1
和r2
寄存器中的值进行亦或,结果存放在r2
寄存器中。
最后是涉及到r10
寄存器的第三条指令00012F9C
处,STRB.W R2, [R10,#-1]
,把计算结果值放到R10,#-1
地址处,注意到r10
寄存器中的地址值刚才已经加一,所以现在减一后还是刚才存储的地址,所以这里才是计算结果最后存放的指令。
r3寄存器
好了,我们已经知道计算结果是放到了r2寄存器中,我们从00012F66
开始看,一步步看是怎么计算出来的。
可以看到,r3
作为计数器,每轮循环会在.text:00012F78 ADDS R3, #1
处加一,并在.text:00012F7E CMP R3, R8
处和r8
寄存器值即加密字符串长度进行比较。
r2寄存器
在开始处,r3首先和0xf进行与操作,结果放在r2
寄存器中,随后将与操作的结果值和SP, #0x48+var_28
地址值进行相加,相加结果仍放在r2寄存器中,随后会进行.text:00012F7A LDRB.W R2, [R2,#-0x14]
,也就是说现在r2
寄存器中存放的是SP, #0x48+var_28
地址加上一个偏移值(即(i &0xf) - 0x14)这个地址上存放的值。
r4寄存器
然后在.text:00012F70 AND.W R1, R3, #7
处把r3
寄存器中的值和7进行与操作,结果放在r1
寄存器中,然后在.text:00012F80 LDRB R4, [R7,R1]
处把r1
寄存器中的地址值作为偏移加上r7
寄存器中的值,取出这个地址中的值放在r4寄存器中,r7
寄存器前面我们说了放的是key2
,所以放在r4
寄存器中这个的值即是key2[i&7]
。
计算部分
随后便是计算部分:
1 2 3 4 5 6 7 8 |
|
此时,涉及计算的r0 r2 r4
我们都已经知道是什么了,r0
寄存器中的值我们在提r10
寄存器时候有知道它是待加密字符串取出的一个字节。
IDA F5
这部分使用IDA进行f5的效果是这样的。
sub_12ECC算法还原
现在我们想还原这个加密过程该怎么办,要看我们缺什么。待加密字符串,key2和加密计算过程我们都有了,只有一个SP, #0x48+var_28
地址加上一个偏移 -0x14
,这个地址存放有一个数组,每轮加密循环会取出一个值(数组偏移(i & 0xf)处)放在r2
寄存器中然后进行计算,我们通过静态分析无法知道这个数组值是什么,所以进行ida动态调试,可以得到这个地方存放的数组值为[0x37, 0x92, 0x44, 0x68, 0xA5, 0x3D, 0xCC, 0x7F, 0xBB, 0xF, 0xD9, 0x88, 0xEE, 0x9A, 0xE9, 0x5A]。
随后可python写出sub_12ECC函数中的加密计算过程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
sign算法全流程还原
好了,到现在所有的关键函数已经被我们分析出来了,想还原出来所有的sign加密流程已经成了一个时间问题的体力劳动了,本来想放个半成品给大家参考下,经评论区老哥友情提醒,分析流程已经足够了,这部分就先和谐掉吧...
以及文章目的是学习交流的,请勿不正确使用,用于违法行为。
后记
最近经常会在想自己大部分时候只会用frida去hook十分像一个脚本小子(逃,迫切需要提升汇编分析水平,分析到这里对我自己来说进步不小,但是要学习的东西还很多,如果有什么希望的话,就是希望能早一点摆脱对IDA f5的依赖,可以手撕汇编:)
最后,如果你有学到有用的东西,请不要忘记点赞,不枉我写了这么多。
标签:arr,sub,R2,text,某东,sign,mu,半自动化,ADDR From: https://blog.csdn.net/2403_87755661/article/details/144079560