前言

第二届阿里移动安全赛第二题,花了很多时间来彻底理清楚逻辑,做完这道题感觉自己在动态调试、arm汇编代码技巧上都有不少收获,所以首先我大概总结一下这道题的解题过程,题中几个难点如下:

  1. 反调试:题中出现多次反调试,但是套路都相同,都可以用相同的方法绕过
  2. 动态恢复:程序在执行的过程中不断通过mmap申请一块内存 ,mprotect修改内存为可执行 ,动态构造执行代码 。在执行完后又将代码加密回去,继续执行下一段代码。
  3. AES加密:由于在函数中并不是直接调用AES库,而是直接用AES源码进行处理,所以考验我们的是要把AES源码识别出来并用AES进行解密

题目链接:

由于程序分析过程太长了,长得都要忘了我们是要看输入怎么处理的,所以我把对输入进行处理的方式放在最前边,有个大体印象才不致忘记

  1. 首先是将输入补全,对后边的补上0到15的数

    1

  2. 进行加操作

2

  1. 与数组相加

3

  1. 进行AES加密,密钥为6BCDC67A6B2B7C9D8DA459B1AB9D0680,最后与固定字符串进行比对,求逆的过程非常简单

所以求逆的过程并不复杂,只需要整理出处理流程逆回去即可

动态调试

由于java端代码并不复杂,所以直接加载程序进入动态调试,直接程序崩溃,找到init_array段,看到了原因,一共有两个函数用来反调试,第一个和最后一个,这里我们直接修改so库来绕过反调试

4

绕过第一个的方法只用简单改一下程序逻辑

![img

6

以上为两种修改方法,总之只要让他直接返回而不去执行下面的kill函数即可,用ASShell找到arm的汇编代码,直接用winHex修改so文件

7

第二个反调试我们直接将汇编代码改为返回,即直接把最后一句指令改到函数执行的顶部,则函数直接返回,这种方法也适用于第一个反调试

8

接下来就可以直接进入主函数进行调试了,找到ch函数

9

进行输入的第一次处理,补全输入,将输入覆盖到0到15的前几个值

11

然后是一段代码反调试,这个程序所有的反调试都是这一个套路就是读取TracerPid看他的值是不是为0

12

13

有两种方法,一个是下断点到该地址,然后修改寄存器r0的值不断地改来通过这段反调试代码,一种方法是直接在内存中找到读取tracerpid时的数据修改掉它的值就可以直接跳过,所以第二种方法较为简单,特别是在应付如此多反调试的程序中,毫无疑问是最省事的。我们知道我们读取的是r6的值,所以我们希望直接修改这个数据段的tracerpid的值来直接跳过。

14

随后进行的一段代码就是动态解密函数段,解密的函数段为loc_24c8做的操作是异或,此时我们直接下断点到调用函数处,然后该跟进函数即可,点入该函数,ida中按p重新分析,f5生成伪码,在关键函数处下断,分析完了,我们可以进行动态调试了

15

进入函数后,程序先将前一段代码又加密回去,然后对输入进行第二步处理

16

接着又是一个反调试,同样的方法绕过,继续解密代码段,并且执行

进入到第三层,又要绕过一个反调试,我们直接定位输入到处理函数

17

汇编代码可知,将r0加r5的值赋给r3,r8加r5的值赋给r2,再将r2与r3相加,其中r8为我们前边处理过后的输入部分,再将加后的结果给r1加r5,r5从0取到16

18

处理过后的值

19

与前边方法一样,终于进入最后一个代码段了,这一部分代码为手写AES加密

这之前先简单的分析一下AES加密算法

AES加密算法一共有十轮,每一轮都要经过s盒代换,行移位和列混淆、轮密钥加的操作

s盒代换

s盒代换是一个查表的过程,是AES算法中唯一一步非线性的过程,所以关键就是S盒,s盒的构造方法如下:

1)初始化S盒,按行升序排列的字节初始化。行x列y的字节是xy,行号和列号从0开始计数。

2)求出每一个元素在GF(2^8)中的逆。00被映射为它自身。

3)仿射变换。对上一步中的每一个字节的每一位作如下变换

yi=xi + x(i+4)mod8 + x(i+5)mod8 + x(i+6)mod8 + x(i+7)mod8 + ci

s盒:

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
unsignedchar sBox[] =
{/* 0 1 2 3 4 5 6 7 8 9 a b c d e f */
0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76,/*0*/
0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0,/*1*/
0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15,/*2*/
0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75,/*3*/
0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84,/*4*/
0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf,/*5*/
0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8,/*6*/
0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2,/*7*/
0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73,/*8*/
0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb,/*9*/
0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79,/*a*/
0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08,/*b*/
0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a,/*c*/
0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e,/*d*/
0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf,/*e*/
0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16 /*f*/
};

输入 6A ,查找S盒x=6, y=a => 02。反过来,输入02, 查找逆S盒x=0, y=2 => 6a

行移位

第一行不变,第二行循环左移1字节,第三行循环左移2字节,第三行循环左移3字节。

20

列混淆

21

轮密钥加

22

我在网上找了一点AES代码进行大概分析

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
// 字节代换, 行移位, 列混淆,轮密钥加操作
// sm_te 变换包括了字节代换,行移位,列混淆操作
t1 = (sm_te1[w1 >> 24] ^ sm_te2[(w2 >> 16) & 0xFF] ^
sm_te3[(w3 >> 8) & 0xFF] ^ sm_te4[w4 & 0xFF]
) ^ m_ke[i][0];
t2 = (sm_te1[w2 >> 24] ^ sm_te2[(w3 >> 16) & 0xFF] ^
sm_te3[(w4 >> 8) & 0xFF] ^ sm_te4[w1 & 0xFF]
) ^ m_ke[i][1];
t3 = (sm_te1[w3 >> 24] ^ sm_te2[(w4 >> 16) & 0xFF] ^
sm_te3[(w1 >> 8) & 0xFF] ^ sm_te4[w2 & 0xFF]
) ^ m_ke[i][2];
t4 = (sm_te1[w4 >> 24] ^ sm_te2[(w1 >> 16) & 0xFF] ^
sm_te3[(w2 >> 8) & 0xFF] ^ sm_te4[w3 & 0xFF]
) ^ m_ke[i][3];

sm_te数组生成过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void getTE(u32_t te1[], u32_t te2[], u32_t te3[], u32_t te4[], unsigned char sbox[], size_t num)
{
for (size_t i = 0; i < num; ++i) {
te1[i] = (u32_t(mul(sbox[i], 2)) << 24) | (u32_t(sbox[i]) << 16)
| (u32_t(sbox[i]) << 8) | u32_t(mul(sbox[i], 3));
te2[i] = (u32_t(mul(sbox[i], 3)) << 24) | (u32_t(mul(sbox[i], 2)) << 16)
| (u32_t(sbox[i]) << 8) | u32_t(sbox[i]);
te3[i] = (u32_t(sbox[i]) << 24) | (u32_t(mul(sbox[i], 3)) << 16)
| (u32_t(mul(sbox[i], 2)) << 8) | u32_t(sbox[i]);
te4[i] = (u32_t(sbox[i]) << 24) | (u32_t(sbox[i]) << 16)
| (u32_t(mul(sbox[i], 3)) << 8) | u32_t(mul(sbox[i], 2));
}

首先看t1的生成过程,是分别将w1>>24,w2>>16,w3>>8,w4>>4分别对应的就是D0,D5,D10,D15,其中w代表一个字也就是4个字节,AES处理都是一个字一个字地读入

然后我们就找sm_te数组的生成过程,拿te1举例,就是将sbox的相应位置代入,也就是sbox[i]进行了s盒变换,然后列混淆并拼成一个字,t1整个生成过程转换成数学公式就是

1
2
3
4
5
te1=2*s[D0]|s[D0]|s[D0]|3*s[D0]
te2=3*s[D5]|2*s[D5]|s[D5]|s[D5]
te3=s[D10]|3*s[D10]|2*s[D10]|s[D10]
te4=s[D15]|s[D15]|3*s[D15]|2*s[D15]
t1=te1^te2^te3^te4

所以不难看出生成的每个t1就是竖行排列的字,再与轮密钥异或,第一段代码是行移位和轮密钥加的混合,第二段则是列混淆和s盒代换的混合。

AES算法差不多就介绍完了,至于其他的密钥扩展就不再深究了。现在来分析反汇编中的AES算法部分

轮密钥加的过程

23

每个数值取出来的是字,DWORD也就是四个字节,AES是分组密码这个是第一步,密钥我们可以通过查看到为6BCDC67A6B2B7C9D8DA459B1AB9D0680

24

v18为输入的第一个字的地址

25

查看v25的值可以看到这是一个s盒,每隔一个字节为s盒

26

然后我们进入十轮循环,首先是字节代换的操作

27

行移位过程,生成的第二个字的顺序D5|D6|D7|D1

28

看到这里我们已经大概看出来了这是一个AES加密过程,好吧,是我分析不下去了。手动分析AES的过程实在是太艰辛,而且又是反汇编的看都看不懂,所以我们就根据这个题的特征来看,这个题表现为AES的特征主要有两点,第一是十轮加密,第二是AES盒,第三是数据之间相互组合的过程都符合AES的三轮数据处理,本来加密算法我们知道也不是你随便编一个就行了,所以现存的最保险的就只有AES加密了,而且再怎么自己编写个算法也不可能进行十轮迭代,有这个水平的只有AES了吧!为了证明他是AES,我们可在后边找到结果进行验证,现在我们让程序继续执行下去,执行到最后加密结果处。

整理一下加密之前的数据为:

29

加密过后的结果

30

将加密后的结果与下面的数组作比较

31

好了,现在整个程序的分析就到这了,只需逆向解密就行了

总结

没有绝对的安全,只有相对的攻防。这个题整个解题过程不算很难,但是调试的过程需要耐心,最难的应该是最后对AES源代码的分析吧,这个分析不出来前边做的所有工作都废了,之前的反调试和动态解密过程都是一个套路。里边用到的很多加固技巧也对以后做题有很大的启示。终于把一道困扰我大半年的安卓题给分析完了!加油!


最伟大的思想和行动往往需要最微不足道的开始

Comments

⬆︎TOP