破解的那些破事-记手动逆向smali代码

摘要:破解的那些破事-记手动逆向smali代码。

1.起因

  最近**客户端将之前固定RSA公钥改为动态生成方式,导致模拟APP无法进行正常认证。改完后流程如下:首先客户端发送RSA公钥请求,服务端生成公钥以及相关解密信息传回给客户端,然后客户端通过解密信息解码出RSA公钥,再用该公钥加密数据进行传输。

2.过程

2.1 通过HOOK机制抓取传输的数据

  HOOK掉网络请求数据,由于该客户端基于soap,可以看到首先会发送一个action为rsaPublicKeyDeepProc的请求,很快便能判断为rsa公钥生成请求,内容参数为:

1
eyJmIjoiRVdCWERUSkpCQUNIREREWERGRlRJREFaSkhFQUdXQ1BIWkVaRlJDR0JLQlJHRkNBRUhGSkNYQ1RDR0RVSlNGSkRIR1hCUkdMR1hHWElSRlpGUUdZSUFISEJWSVpBSkdWQlRJSURDSVpDUkFGQ1dIU0NRRUlBRkZOREpGWkdOSlBHRUVDSkNKVkVJRVZJWkpDQ1VES0VHSE9EWUVZSFBJTUdYRVFIVEFBSU5HV0hUQ1ZIWUdBSURKQ0lKRU1KTElUSkVIR0JPRVFKVklQQ0JDSkpKRE1EQkZNRlFHRUJDSEZETUFQQktFQ0RCQkpJTkpERkpJR0RQSkZKTUpIQUxJWkpPRk1CSkhWR09DU0VMSFdBSkdUQVBBTEZNQ1NGRUFCQUlIVUJGQ01FRkZSSVRFS0VSSkZBR0NTSENIU0VJSVlHUUhZRENCTERKQ0xGVklWRlFDVENZSVRCRURWRkpHRkFVQlNDUkNVRlFHUEFOSlVCRkpUQUNBRUJVSERDTkRMREhESEVCQlVFT0lSRldFWUdMSVdHTkdIQ0RDVkpWQkRCQUJVQ0NBWUNIQlpKU0hKQklITkhXIiwiZyI6IjAiLCJkIjoiTVpVMExUWTNZVElUTkdJWU5DMUhNREE0TFdZNVpXTTFORFk0TlpaTFlXIiwiZSI6Ik9DMFpNSlUzTVdWSk9XRTVNR01aSllUWUpVV01DMU1NREZNWk1GSU1EUkpORE1PV000WkpaTVlRTFRSSU5NUVRZV0U1WkkxTE9XVk1ZVEhLTlpZV01aQ0xUUVlNTVFUT0dOS1lJMDRaRFpNTkdWTE1HVVpOSkFOSkkwTVRDNFlUIiwiYiI6IjI1IiwiYyI6IkFCQkRBRkhCSENCSUJKREdKQkdGIiwiYSI6IkxUTVlaREtUTkRWTE5TMUhNRFEyTFRLM1pETTBPRElaWlRESU1XIiwiaCI6Ik1UQkpMVEs0TVpJVE5NVTNOVEU1TVdWTVkyUTQifQ==

通过基本判断应该为BASE64,通过BASE64在线解码果然:

1
{"f":"EWBXDTJJBACHDDDXDFFTIDAZJHEAGWCPHZEZFRCGBKBRGFCAEHFJCXCTCGDUJSFJDHGXBRGLGXGXIRFZFQGYIAHHBVIZAJGVBTIIDCIZCRAFCWHSCQEIAFFNDJFZGNJPGEECJCJVEIEVIZJCCUDKEGHODYEYHPIMGXEQHTAAINGWHTCVHYGAIDJCIJEMJLITJEHGBOEQJVIPCBCJJJDMDBFMFQGEBCHFDMAPBKECDBBJINJDFJIGDPJFJMJHALIZJOFMBJHVGOCSELHWAJGTAPALFMCSFEABAIHUBFCMEFFRITEKERJFAGCSHCHSEIIYGQHYDCBLDJCLFVIVFQCTCYITBEDVFJGFAUBSCRCUFQGPANJUBFJTACAEBUHDCNDLDHDHEBBUEOIRFWEYGLIWGNGHCDCVJVBDBABUCCAYCHBZJSHJBIHNHW","g":"0","d":"MZU0LTY3YTITNGIYNC1HMDA4LWY5ZWM1NDY4NZZLYW","e":"OC0ZMJU3MWVJOWE5MGMZJYTYJUWMC1MMDFMZMFIMDRJNDMOWM4ZJZMYQLTRINMQTYWE5ZI1LOWVMYTHKNZYWMZCLTQYMMQTOGNKYI04ZDZMNGVLMGUZNJANJI0MTC4YT","b":"25","c":"ABBDAFHBHCBIBJDGJBGF","a":"LTMYZDKTNDVLNS1HMDQ2LTK3ZDM0ODIZZTDIMW","h":"MTBJLTK4MZITNMU3NTE5MWVMY2Q4"}

json格式,初步看无论哪个值都不可能是RSA公钥明文。看来只能通过如何处理这段数据来做进一步分析。

2.2 反编译APK后生成smali文件

  通过反编译工具(网上随便下)生成smali文件后,全盘搜索rsaPublicKeyDeepProc定位到smali文件,代码量相当大,又经过混淆,不过通过关键信息还是能看到诸如对json数据的处理:

1
2
3
4
const-string v4, "f"
const-string v6, ""
invoke-virtual {v2, v4, v6}, Lorg/json/JSONObject;->optString(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
move-result-object v4

抓住这些值一步步跟踪,耐心很重要,最终发现调用了解码:

1
invoke-direct {p0, v0, v2}, Lcom/chinapost/publiclibrary/g;->a([CS)Ljava/lang/String;

关键问题是调用了它,又不能像C一样直接嵌入汇编那样在java文件里引用smali代码,到这一步只能分析算法了。

2.3 smali分析过程

  解码代码如下:

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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
.method private a([CS)Ljava/lang/String;
.locals 6
.parameter
.parameter
.prologue
const/4 v1, 0x0
.line 473
array-length v0, p1
div-int/lit8 v0, v0, 0x2
new-array v2, v0, [C
move v0, v1
.line 475
:goto_0
array-length v3, p1
div-int/lit8 v3, v3, 0x2
if-lt v0, v3, :cond_0
.line 484
new-instance v3, Ljava/lang/StringBuffer;
invoke-direct {v3}, Ljava/lang/StringBuffer;-><init>()V
.line 485
:goto_1
array-length v0, v2
if-lt v1, v0, :cond_1
.line 497
invoke-virtual {v3}, Ljava/lang/StringBuffer;->toString()Ljava/lang/String;
move-result-object v0
return-object v0
.line 478
:cond_0
mul-int/lit8 v3, v0, 0x2
aget-char v3, p1, v3
sget v4, Lcom/chinapost/publiclibrary/g;->h:I
sub-int/2addr v3, v4
mul-int/lit8 v3, v3, 0x1a
.line 479
mul-int/lit8 v4, v0, 0x2
add-int/lit8 v4, v4, 0x1
aget-char v4, p1, v4
sget v5, Lcom/chinapost/publiclibrary/g;->h:I
sub-int/2addr v4, v5
add-int/2addr v3, v4
.line 480
int-to-char v3, v3
aput-char v3, v2, v0
.line 475
add-int/lit8 v0, v0, 0x1
goto :goto_0
.line 487
:cond_1
shr-int/lit8 v0, p2, 0x8
int-to-short v0, v0
.line 488
aget-char v4, v2, v1
int-to-short v4, v4
.line 489
xor-int/2addr v0, v4
int-to-short v0, v0
.line 490
add-int/2addr v4, p2
int-to-short v4, v4
sget v5, Lcom/chinapost/publiclibrary/g;->f:I
mul-int/2addr v4, v5
sget v5, Lcom/chinapost/publiclibrary/g;->g:I
add-int/2addr v4, v5
int-to-short p2, v4
.line 492
if-gez v0, :cond_2
.line 493
add-int/lit16 v0, v0, 0x100
int-to-short v0, v0
.line 495
:cond_2
int-to-char v0, v0
invoke-virtual {v3, v0}, Ljava/lang/StringBuffer;->append(C)Ljava/lang/StringBuffer;
.line 485
add-int/lit8 v1, v1, 0x1
goto :goto_1
.end method

如果光这么看可能就要疯了,想到Smaili2JavaUI,坑爹的是居然“提示这个文件可能进行过优化”:

1
2
3
private String a(char[] p1, short p2) {
// :( Parsing error. Please contact me.
}

那只有一步步分析了,注释很重要,开始注释:

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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
method private a([CS)Ljava/lang/String;
.locals 6 # 6个寄存器
.parameter # 参数1字符数组
.parameter # 参数2长度
.prologue
const/4 v1, 0x0 #定义常量0
.line 473
array-length v0, p1 # 参数1字符数组长度(438)
div-int/lit8 v0, v0, 0x2 # 参数1长度除2(219)
new-array v2, v0, [C #创建一个新数组,长度为一半(219)
move v0, v1 # 设置变量为0
.line 475
:goto_0
array-length v3, p1 # 长度438
div-int/lit8 v3, v3, 0x2 #长度219
if-lt v0, v3, :cond_0 变量v0<219跳到cond_0
.line 484
new-instance v3, Ljava/lang/StringBuffer;
invoke-direct {v3}, Ljava/lang/StringBuffer;-><init>()V
.line 485
:goto_1
array-length v0, v2 # 新数据长度(219)
if-lt v1, v0, :cond_1 # 变量v1<219跳到cond_1
.line 497
invoke-virtual {v3}, Ljava/lang/StringBuffer;->toString()Ljava/lang/String;
move-result-object v0
return-object v0
.line 478
:cond_0
mul-int/lit8 v3, v0, 0x2 # 当前位置记录到v3
aget-char v3, p1, v3 # 获取字符串数组中的v3位置的值
sget v4, Lcom/chinapost/publiclibrary/g;->h:I #获取变量h
sub-int/2addr v3, v4 # 当前值减去变量h存放到v3
mul-int/lit8 v3, v3, 0x1a # v3乘以0x1a存放到v3
.line 479
mul-int/lit8 v4, v0, 0x2 # 索引v0成2位当前字符串位置(偶数)
add-int/lit8 v4, v4, 0x1 # 当前字符串位置(奇数)
aget-char v4, p1, v4 # 获取当前字符串位置的值(奇数)
sget v5, Lcom/chinapost/publiclibrary/g;->h:I # 获取变量h
sub-int/2addr v4, v5 # 当前位置的值减去变量h存放回v4
add-int/2addr v3, v4 # 偶数位计算的值+奇数为计算的值=v3
.line 480
int-to-char v3, v3 # 将v3转成char
aput-char v3, v2, v0 # 将v3存入到新创建的数组中
.line 475
add-int/lit8 v0, v0, 0x1 #索引加1
goto :goto_0
.line 487
:cond_1
shr-int/lit8 v0, p2, 0x8 # v0=p2>>0x8;获取参数2高8位
int-to-short v0, v0 # v0转成short
.line 488
aget-char v4, v2, v1 # 获取新数组v2的索引v1的值到v4 v4=v2[v1];
int-to-short v4, v4 # v4=(short)v4
.line 489
xor-int/2addr v0, v4 # 与参数2高8位异或,异或结果放入v0
int-to-short v0, v0 # v0=(short)v0;
.line 490
add-int/2addr v4, p2# v4 = 当前数组元素v4+参数2p2
int-to-short v4, v4
sget v5, Lcom/chinapost/publiclibrary/g;->f:I #获取变量f
mul-int/2addr v4, v5 # v4=v4*f;
sget v5, Lcom/chinapost/publiclibrary/g;->g:I#变量g
add-int/2addr v4, v5# v4 = v4+g;
int-to-short p2, v4 #改变参数2
.line 492
if-gez v0, :cond_2 # 如果异或结果大于等于0
.line 493
add-int/lit16 v0, v0, 0x100 # 异或结果v0=v10+0x100;
int-to-short v0, v0 # v0=(short)v0;
.line 495
:cond_2
int-to-char v0, v0 # v0=(char)v0;
invoke-virtual {v3, v0}, Ljava/lang/StringBuffer;->append(C)Ljava/lang/StringBuffer; #添加到StringBuffer中
.line 485
add-int/lit8 v1, v1, 0x1 # 索引+1
goto :goto_1
.end method

通过边分析边注释,很快明白算法大概意思是:
 1、传入一个字符数组。
 2、传入一个计算数值。
 3、创建一个新的字符数组。
 4、将字符数组通过奇数位和偶数位俩俩合并到新的字符数组。
 5、对新的字符数组通过计算数字对每一位进行计算得出新的字符并保存,同时更新计算数值。

根据该思想一行行代码对照完成:

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
private String caculatePublicRSA(char[] p1, short p2, int h,int f, int g) {
int p2Length = p1.length;
int p2NewLength = p2Length/2;
char[] newChars = new char[p2NewLength];
int firstIndex = 0;
while (firstIndex < p2NewLength) {
// 计算偶数位
int currentEventIndex = firstIndex * 2;
char currentEventChar = p1[currentEventIndex];
int firstValue = (currentEventChar - h) * 0x1a; // 得出第一个值
// 计算奇数位
int currentOddIndex = firstIndex * 2 + 1;
char currentOddChar = p1[currentOddIndex];
int secordValue = currentOddChar - h; // 得出第一个值
int resultValue = firstValue + secordValue;
// 存入到新数组中
newChars[firstIndex] = (char)resultValue;
firstIndex++;
}
StringBuilder strBuilder = new StringBuilder();
int secondIndex = 0;
while(secondIndex < p2NewLength) {
short high8Bits = (short) (p2 >> 8); // 高8位
char currentChar = newChars[secondIndex];
short shortResult = (short) (currentChar^high8Bits);
int secondValue = currentChar + p2;
secondValue = secondValue*f + g;
p2 = (short) secondValue;
if (shortResult<0) {
shortResult = (short) (shortResult + 0x100);
}
char realResult = (char)shortResult;
strBuilder.append(realResult);
secondIndex++;
}
return strBuilder.toString();
}

碰到全局变量用参数表示,例如h、f、g,再分析合适设置来设置传入值。最终一次验证解码成功:

1
2
3
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCaHHq6qavIJHu/KdTny9kzEkx30Jeg8Q1k45ZW\nB
y/uSaUGr1nK4d3wXJ22zG8aCA3QdoY5VX5Fr+A78gtmV8gtLL/Sj0kq7w7L4xJH/yC8KIkm/i
GfNhtb1EyOFhQipt6uYAJemxkSl0BD2CBOcVWTTpAJr5U3NoFhRuVj+lZQIDAQAB

3. 总结

  Smali代码不容易看的原因是变量表达用的是寄存器,同一个变量在程序运行时表达了不一样的意思,因此在编写smali转java代码时有必要对需要翻译的smali行进行注释,注释时用容易记住语言表达,例如某某索引等。
  另外指令查询是不可少的:http://blog.sina.com.cn/s/blog_6400e5c50102v3le.html