某app rsa 算法逆向踩坑记录

抓包

img_11

利用 charles 的 compose 功能 修改一个字符, 发现 403. 但是可以repeat, 但几分钟后 repeat 照样 403, 说明后台校验时间

jadx 全局搜索

img_11

进入 this.b.a 看看

img_11

是个接口, 查看交叉引用.
img_11

第一个很可疑, 我们进入看看

img_11

可以看到, 这个方法就是具体实现.

简单分析了一下, 其中 bytes 就是 时间 + url 路径 + app_type + version
关键函数是 e71.a.a, 我们进入这个函数看看.

img_11

frida hook 看看

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
function getJavaClassName(obj) {
return Java.cast(obj, Java.use("java.lang.Object")).getClass().getName();
}


Java.perform(function () {
var Base64 = Java.use("android.util.Base64");

var TargetClass = Java.use("e71");

TargetClass.a.overload("java.security.PrivateKey", "[B").implementation = function (key22, data) {
console.log("=== Hooked method a(PrivateKey, byte[]) ===");

// 打印 data
var dataHex = hexdump(data);
var dataBase64 = Base64.encodeToString(data, 0);

var StringClass = Java.use("java.lang.String");
var dataStr = StringClass.$new(data, "UTF-8");

console.log("[*] Data (hex):", dataHex);
console.log("[*] Data (base64):", dataBase64);
console.log(111)
console.log("[*] Data (string):", dataStr.toString());
console.log(111)

// 打印 key 信息
console.log("[*] PrivateKey class:", getJavaClassName(key22));

try {
var RSAPrivateKey = Java.use("java.security.interfaces.RSAPrivateKey");
if (RSAPrivateKey.class.isInstance(key22)) {
var castKey = Java.cast(key22, RSAPrivateKey);
var encoded = castKey.getEncoded();
var keyBase64 = Base64.encodeToString(encoded, 0);
console.log("[+] PrivateKey (Base64):", keyBase64);
} else {
console.log("[-] Not an RSAPrivateKey instance.");
}
} catch (e) {
console.log("[-] Exception extracting key:", e);
}

// 调用原函数
var result = this.a(key22, data);

// 打印签名结果
var resultBase64 = Base64.encodeToString(result, 0);
console.log("[+] Signature (base64):", resultBase64);
console.log("=========================================");

return result;
};

function hexdump(bytes) {
return Array.prototype.map.call(Java.array('byte', bytes), function (b) {
return ('00' + (b & 0xff).toString(16)).slice(-2);
}).join(' ');
}
});

注意这里有两个坑, 一个是如何打印 privatekey 的问题, 另外一个是查看类型的问题, 总是 not a function, 我记得之前frida 还可以用

obj.getClass().getName() 来查看一个对象的类型的, 但是这个样本就是 not a function, 然后我降frida-server版本, 从 frida-server 16.6.6,
降为 frida-server 14.2.18, 还是 not a function, 我问了下chatgpt 和 deepseek 是这么解答的

img_11

我们再次打印 privatekey 的类型

PrivateKey class: com.android.org.bouncycastle.jcajce.provider.asymmetric.rsa.BCRSAPrivateCrtKey

然后交给chatgpt 问gpt 这个类型要怎么打印私钥.
结合 gpt 给出的答案, 我们整合一下代码, 最终的代码如下

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
function getJavaClassName(obj) {
return Java.cast(obj, Java.use("java.lang.Object")).getClass().getName();
}


Java.perform(function () {
var Base64 = Java.use("android.util.Base64");
console.log(1222)

// 替换成你的实际类名,比如 com.example.MyClass
var TargetClass = Java.use("e71");

// Hook public final byte[] a(PrivateKey key, byte[] data)
TargetClass.a.overload("java.security.PrivateKey", "[B").implementation = function (key22, data) {
console.log("=== Hooked method a(PrivateKey, byte[]) ===");

// 打印 data
var dataHex = hexdump(data);
var dataBase64 = Base64.encodeToString(data, 0);

var StringClass = Java.use("java.lang.String");
var dataStr = StringClass.$new(data, "UTF-8");

console.log("[*] Data (hex):", dataHex);
console.log("[*] Data (base64):", dataBase64);
console.log(111)
console.log("[*] Data (string):", dataStr.toString());
console.log(111)

// 打印 key 信息
console.log("[*] PrivateKey class:", getJavaClassName(key22));

try {
const Base64 = Java.use("android.util.Base64");
const encoded = Java.cast(key22, Java.use("java.security.Key")).getEncoded();
const keyBase64 = Base64.encodeToString(encoded, 0);
console.log("[+] PrivateKey PKCS#8 (Base64):");
console.log(keyBase64);
} catch (e) {
console.log("[-] Exception dumping key:", e);
}

// 调用原函数
var result = this.a(key22, data);

// 打印签名结果
var resultBase64 = Base64.encodeToString(result, 0);
console.log("[+] Signature (base64):", resultBase64);
console.log("=========================================");

return result;
};

function hexdump(bytes) {
return Array.prototype.map.call(Java.array('byte', bytes), function (b) {
return ('00' + (b & 0xff).toString(16)).slice(-2);
}).join(' ');
}
});

我们再次 frida hook 一下,结果如下
img_11

可以看到 私钥已经出来了, 剩下的就是如何 使用私钥进行 rsa sha256签名了, 下面给出 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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;


class MyClass {
public static void main(String[] args) throws Exception {
// 假设你已经有 PrivateKey 对象
PrivateKey privateKey = loadPrivateKey(); // 你需要从文件或 Base64 解析

//String data = "hello world";
String data = "1750927494\n/graphql/v2\nXXX-xxxxxxx\n11.31.0\n";
byte[] dataBytes = data.getBytes("UTF-8");

// 创建 SHA256withRSA 签名对象
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initSign(privateKey);
signature.update(dataBytes);
byte[] signed = signature.sign();

// 输出 Base64 签名结果
String signatureBase64 = Base64.getEncoder().encodeToString(signed);
System.out.println("Signature (Base64): " + signatureBase64);
}

// 示例:从 PKCS#8 Base64 字符串中加载私钥
public static PrivateKey loadPrivateKey() throws Exception {
String base64Key =
"MIIEvAxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+ "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+ "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+ "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+ "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+ "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+ "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+ "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+ "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+ "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+ "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+ "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+ "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+ "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+ "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+ "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+ "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+ "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+ "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+ "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+ "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+ "xxxxxxxxxxxxxxxx==";

// 用 MIME 解码器自动忽略换行
byte[] keyBytes = Base64.getMimeDecoder().decode(base64Key);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
return KeyFactory.getInstance("RSA").generatePrivate(keySpec);
}
}

然后运行 结果如下

img_11

然后用 java 生成的结果, 替换 charles 中的请求的加密参数. 发现可以成功获取到数据, 到此基本就成功了.

如何用 python 还原一份呢, 下面给出代码. 已下代码经过验证, 可成功通过校验.

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
import base64
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.backends import default_backend


def load_private_key():
base64_key = """
MIIEvXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXX=="""

# 移除空白和换行
base64_key = "".join(base64_key.split())

# 解码Base64并加载私钥
key_bytes = base64.b64decode(base64_key)
private_key = serialization.load_der_private_key(
key_bytes,
password=None,
backend=default_backend()
)
return private_key


def main():
# 加载私钥
private_key = load_private_key()

# 准备要签名的数据
data = "1750929449\n/graphql/v2\nXXXX-XXXXX\n11.31.0\n"
data_bytes = data.encode('utf-8')

# 使用SHA256withRSA进行签名
signature = private_key.sign(
data_bytes,
padding.PKCS1v15(),
hashes.SHA256()
)

# 输出Base64编码的签名
signature_base64 = base64.b64encode(signature).decode('utf-8')
print("Signature (Base64):", signature_base64)


if __name__ == "__main__":
main()

python 需要安装 cryptography

1
$ python3 -m pip install cryptography -i https://pypi.tuna.tsinghua.edu.cn/simple

通过私钥 进行签名, 使用的是 SHA256withRSA

改为 python 后,同样也可以生成加密结果 并且可以通过校验.

总结
  • 遇到的核心问题, getClass().getName() 会报错 not a function, 可能是由于android 高版本的优化问题. 并不是 frida-server 和 frida 以及 frida-tools 的问题.
  • privateKey 如何打印的问题, 上面已经给出答案. 还有些是通过 keyStore 这种, 这种导出就很麻烦.
  • python3.8 安装 frida 14.2.18 会遇到 frida-14.2.18-py3.8-linux-x86_64.egg 问题, 直接下载 放到指定位置就好.
  • 关于 RSA private, 或者 public key 也可以考虑 hook base64 的 decode 方法, 一般公钥要base64 解码后然后加载字节数据.
收获

在分析这个样本的过程中, 想借助一些文章, 发现了一些站点, 貌似可以低成本下载文档。

1
2
http://toolman.dedyn.io:9192/#/main/csdnPaper
https://github.com/bigintpro/csdn_downloader

充值了一块钱, 结果是个垃圾文章, 骗加公众号的, 无语子.

另外记录一下本人使用的 frida稳定的版本

1
2
3
4
5
6
7
8
9
10
11
12
pip install frida==14.2.18
pip install frida-tools==9.2.5
pip install objection==1.11.0

# 下面是本人使用到的 objection 的脚本, 这里记录一下
objection -g com.xxxxxxx.android explore -P ~/.objection/plugins
plugin wallbreaker objectsearch com.xxxxxxx.android.internal.auth.signing.util.RSASigner
plugin wallbreaker objectdump 0x4142 --fullname


# http://littleshark.space/2024/08/25/Frida%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/Frida%E4%BD%BF%E7%94%A8%E5%A4%A7%E5%85%A8/
# https://bbs.kanxue.com/thread-277929.htm