BBox

SO层算法

注意,不同平台的rand()不一样,一定要在linux的环境跑伪随机数

cpp
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
// so层算法的流程和逆向
#include <bits/stdc++.h>
using namespace std;

struct randList
{
int len;
int num[2333];
} r;

void encrypt(int seed)
{
// time_t time_0 = time(0);
char encoded_flag[] = "0123456789012345678901234567890123456789";
char dest[255];
strncpy(dest, encoded_flag, 0xFFu);
dest[255] = 0;
// int seed = time_0 / 1000000 / 100;
srand(seed);
int len = strlen(dest);
r.len = 0;
for (int i = 0; i < (len >> 2); i++)
{
char *destByte;
destByte = &dest[4 * i];
for (int k = 0; k < 4; k++)
{
r.num[r.len++] = rand();
}
*destByte ^= r.num[r.len - 4];
destByte[1] ^= r.num[r.len - 3];
destByte[2] ^= r.num[r.len - 2];
destByte[3] ^= r.num[r.len - 1];
int j = 32;
signed int destDWORD = *(unsigned int *)destByte;
do
{
if (destDWORD >= 0)
destDWORD *= 2;
else
destDWORD = (2 * destDWORD) ^ 0x85B6874F;
--j;
} while (j);
*(unsigned int *)&dest[4 * i] = destDWORD;
}
// for (int i = 0; i < 40; i++)
// {
// printf("0x%2x, ", (unsigned char)dest[i]);
// }
// printf("\n------------------------------------\n");
}
void decrypt(int seed)
{
int len = 40;
int randLen = r.len;
unsigned char enc[] = {
0x33, 0xC0, 0xC8, 0xA3, 0xF3, 0xBF, 0x1D, 0x1A, 0x3B, 0x41,
0xB7, 0xC6, 0xF1, 0x5E, 0x86, 0x52, 0x52, 0xCF, 0x6B, 0x1E,
0xC5, 0xF9, 0xCB, 0xBF, 0xED, 0x7B, 0x62, 0xF1, 0xF7, 0x43,
0x48, 0x54, 0xFB, 0x85, 0x4C, 0xD9, 0x35, 0x30, 0xF2, 0x6E
};
// unsigned char enc[] = {
// 0xfe, 0xf, 0x1a, 0x6c, 0xc, 0x23, 0x31, 0xd8, 0x53, 0x26, 0x68, 0x8e, 0xf1, 0x3a, 0xca, 0x47, 0x2c, 0xa2, 0xc2, 0xc7, 0xbf, 0xaf, 0xb, 0x20, 0x2e, 0x1, 0x7c, 0x2c, 0xb6, 0x25, 0xe1, 0xa2, 0xc5, 0x4b, 0xe, 0x9f, 0x43, 0x8c, 0x18, 0x9e};
for (int i = (len >> 2) - 1; i >= 0; i--)
{
signed int encDWORD = *(unsigned int *)&enc[4 * i];
for (int j = 0; j < 32; j++)
{
if ((encDWORD & 1) == 0)
{
encDWORD = (encDWORD >> 1) & (0x80000000 - 1);
}
else
{
encDWORD = ((encDWORD ^ 0x85B6874F) >> 1) | 0x80000000;
}
}
*(unsigned int *)&enc[4 * i] = encDWORD;
unsigned char *encByte = &enc[i * 4];
encByte[3] ^= r.num[--randLen];
encByte[2] ^= r.num[--randLen];
encByte[1] ^= r.num[--randLen];
*encByte ^= r.num[--randLen];
}

for (int i = 0; i <= 40; ++i)
{
bool flag = true;
for (int j = 0; j < 40; ++j)
{
if ((enc[j] ^ i) < 32 || (enc[j] ^ i) > 128)
{
flag = false;
break;
}
}
if (flag)
{
putchar('\n');
printf("xor: %d, seed: %d\n", i, seed);
for (int j = 0; j < 40; ++j) putchar(enc[j] ^ i);
putchar('\n');
}
}

// printf("==============================\n");
// for (int i = 0; i <= 40; ++i)
// {
// for (int j = 0; j < 40; ++j)
// {
// putchar(enc[j] ^ i);
// }
// putchar('\n');
// }

}

int main()
{
freopen("test.out", "w", stdout);
for (int i = 0; i <= 20; ++i)
{
encrypt(i);
decrypt(i);
}

}

JAVA 层去混淆

strange.encode() 方法被上了混淆,函数名字和变量名字被改成了奇奇怪怪的unicode字符降低可读性,可以rename一下,把非法字符给重命名一下

QQ_1727948142787

可以看到 v14 控制着程序的控制流,存在两种混淆

QQ_1727948296566

需要一些 smail 语法知识,先放一篇链接

Smail语法

  1. 执行另一个类的 hashCode 函数获得对应的值,这个函数 JEB 5.0 以上且 windows 版本 (为什么有限定呢,因为和一位师傅测过 mac 版不行) 是可以自动帮我们处理的

    QQ_1727948365899

    QQ_1727948424438

  2. 寻找另一个 Class 里的某个变量

    QQ_1727948481252

    这个变量是 public static int 类型,从而欺骗JEB没法直接计算这个变量的值从而去混淆,因此一个思路就是把这个变量添加 final 修饰符,即改为 public static final int 型,这样变量变为只读,就自动被JEB优化掉了

    QQ_1727948513740

    此外在计算 v14 的时候使用的是 sget 指令寻找这个变量的,那么第二种去混淆的方式就是修改 sget 指令为 const 指令,也能达到同样的效果

    QQ_1727948538769

这里我们考虑把所有的 sget 都改成 const_16 指令,使用 JEB 脚本搭配 dexlib2 库实现

JEB2 文档:IDalvikInstruction | JEB API Documentation (pnfsoftware.com)

dexlib2 手册:Overview (dexlib2 2.5.2 API) (javadoc.io)

思路如下:

  1. 导入 dex 文件,遍历所有 dex 文件的 field 字段,然后筛选出被混淆后的非法变量名,建立变量名 -> 值的映射方便后续处理
  2. 找到目标类 strage 类,遍历里面的所有方法以及每个方法的每个指令,找到所有 SGET 指令替换成 CONST 指令
  3. 新建一个 dex 文件,保存 patch 过后的文件信息生成新的 dex 文件

JEB 脚本环境搭建

这里用IDEA maven搭建环境

QQ_1727947475561

创建完成后添加 src 文件夹,然后配置依赖

xml
1
2
3
4
5
6
7
8
9
10
11
12
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.0.1-jre</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.smali/dexlib2 -->
<dependency>
<groupId>org.smali</groupId>
<artifactId>dexlib2</artifactId>
<version>2.5.2</version>
</dependency>

关键去混淆逻辑如下

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
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
package com.epsilon;

import org.jf.dexlib2.AccessFlags; // 导入访问标志相关类
import org.jf.dexlib2.Opcode; // 导入操作码相关类
import org.jf.dexlib2.Opcodes; // 导入操作码集合类
import org.jf.dexlib2.builder.BuilderInstruction; // 导入构建指令的类
import org.jf.dexlib2.builder.MutableMethodImplementation; // 导入可变方法实现的类
import org.jf.dexlib2.builder.instruction.BuilderInstruction21c; // 导入具有 21c 操作码的指令
import org.jf.dexlib2.builder.instruction.BuilderInstruction21s; // 导入具有 21s 操作码的指令
import org.jf.dexlib2.dexbacked.DexBackedClassDef; // 导入基于 DEX 的类定义
import org.jf.dexlib2.dexbacked.DexBackedDexFile; // 导入基于 DEX 的 DEX 文件
import org.jf.dexlib2.dexbacked.DexBackedField; // 导入基于 DEX 的字段
import org.jf.dexlib2.iface.*; // 导入接口定义
import org.jf.dexlib2.iface.instruction.Instruction; // 导入指令接口
import org.jf.dexlib2.DexFileFactory; // 导入 DEX 文件工厂类
import org.jf.dexlib2.iface.reference.FieldReference; // 导入字段引用接口
import org.jf.dexlib2.immutable.ImmutableClassDef; // 导入不可变类定义
import org.jf.dexlib2.immutable.ImmutableDexFile; // 导入不可变 DEX 文件
import org.jf.dexlib2.immutable.ImmutableMethod; // 导入不可变方法
import org.jf.dexlib2.immutable.value.ImmutableIntEncodedValue; // 导入不可变整数编码值

import java.io.File; // 导入文件类
import java.io.IOException; // 导入 IO 异常类
import java.util.*; // 导入集合类
import java.util.function.Consumer; // 导入函数式接口 Consumer
import java.util.regex.Pattern; // 导入正则表达式类

public class sgetUnflatter {
private static final Map<String, Integer> valCache = new HashMap<>(); // 字段值缓存
private static final List<ClassDef> Classes = new ArrayList<>(); // 存储所有类
private static final List<ClassDef> newClasses = new ArrayList<>(); // 存储新类
private static final List<Method> newMethods = new ArrayList<>(); // 存储新方法
private static final Pattern noramlNamePattern = Pattern.compile("^[a-zA-Z0-9_]+$"); // 正则表达式,匹配正常名称

public static void main(String[] args) {
try {
// 输入 DEX 文件路径
String inDex = "D:\\BACKUP\\CTF\\reWorkstation\\SCTF2024\\_media_file_task_c197ad0e-6a26-4feb-8da0-1b4637cc5f0d\\bbAndroid_1.0_flow\\classes.dex";
String targetClassName = "Lcom/example/bbandroid/strange;"; // 目标类名称
String outDex = "D:\\BACKUP\\CTF\\reWorkstation\\SCTF2024\\_media_file_task_c197ad0e-6a26-4feb-8da0-1b4637cc5f0d\\bbAndroid_1.0_flow\\classes3.dex"; // 输出 DEX 文件路径

// 加载 DEX 文件
DexFile dexFile = DexFileFactory.loadDexFile(new File(inDex), Opcodes.getDefault());
collectVal(new File(inDex)); // 收集输入 DEX 文件中的字段值
collectVal(new File("D:\\BACKUP\\CTF\\reWorkstation\\SCTF2024\\_media_file_task_c197ad0e-6a26-4feb-8da0-1b4637cc5f0d\\bbAndroid_1.0_flow\\classes2.dex")); // 收集第二个 DEX 文件中的字段值
Classes.addAll(dexFile.getClasses()); // 获取并存储所有类

ClassDef targetClass = null; // 目标类定义
for (ClassDef classDef : Classes) {
// 查找目标类
if (classDef.getType().equals(targetClassName)) {
targetClass = classDef;
} else {
newClasses.add(classDef); // 将非目标类添加到新类列表
}
}

if (targetClass != null) { // 如果找到目标类
for (Method method : targetClass.getMethods()) { // 遍历目标类的方法
MethodImplementation methodImplementation = method.getImplementation(); // 获取方法实现
if (methodImplementation == null) {
continue; // 如果没有实现,跳过
}
printInstructions(methodImplementation); // 打印当前方法的指令

MutableMethodImplementation mutableImplementation = new MutableMethodImplementation(methodImplementation); // 创建可变方法实现

int instructionCount = mutableImplementation.getInstructions().size(); // 获取指令数量
for (int i = 0; i < instructionCount; i++) {
BuilderInstruction instruction = mutableImplementation.getInstructions().get(i); // 获取当前指令
// 检查指令是否为 SGET
if (instruction.getOpcode().name().equals("SGET")) {
System.out.println("SGET found: " + instruction); // 打找到的 SGET 指令
// 获取字段初始化值
BuilderInstruction21c instruction21c = (BuilderInstruction21c) instruction; // 转换为具体指令类型

FieldReference fieldRef = (FieldReference) instruction21c.getReference(); // 获取字段引用
Integer fieldValue = getFieldValue(fieldRef); // 获取字段值
// 用 CONST 指令替换 SGET 指令
BuilderInstruction21s instruction21s = new BuilderInstruction21s(Opcode.CONST_16, instruction21c.getRegisterA(), fieldValue);
mutableImplementation.replaceInstruction(i, instruction21s); // 替换指令
}
}
// 创建新的不可变方法,并添加到新方法列表中
newMethods.add(new ImmutableMethod(
method.getDefiningClass(),
method.getName(),
method.getParameters(),
method.getReturnType(),
method.getAccessFlags(),
method.getAnnotations(),
method.getHiddenApiRestrictions(),
mutableImplementation));
}
// 创建新的不可变类定义,并添加到新类列表中
ImmutableClassDef newClassDef = new ImmutableClassDef(
targetClass.getType(),
targetClass.getAccessFlags(),
targetClass.getSuperclass(),
targetClass.getInterfaces(),
targetClass.getSourceFile(),
targetClass.getAnnotations(),
targetClass.getFields(),
newMethods);
newClasses.add(newClassDef); // 将新类添加到新类列表

// 创建一个新的不可变 DEX 文件并写入磁盘
DexFile newDexFile = new ImmutableDexFile(Opcodes.getDefault(), newClasses);
DexFileFactory.writeDexFile(outDex, newDexFile); // 写入新的 DEX 文件
}
} catch (IOException e) {
e.printStackTrace(); // 处理 IO 异常
}
}

// 从缓存中获取字段的值
public static Integer getFieldValue(FieldReference fieldRef) {
return valCache.get(fieldRef.toString());
}

// 打印方法实现中的所有指令
public static void printInstructions(MethodImplementation methodImplementation) {
for (Instruction instruction : methodImplementation.getInstructions()) {
System.out.println(instruction.getOpcode());
}
}

// 收集 DEX 文件中的字段值
public static void collectVal(File file) throws IOException {
DexBackedDexFile dexFile = DexFileFactory.loadDexFile(file, Opcodes.getDefault());

Set<? extends DexBackedClassDef> classes = dexFile.getClasses(); // 获取 DEX 文件中的所有类

// 遍历每个类,查找静态字段
classes.forEach((Consumer<DexBackedClassDef>) dexBackedClassDef -> {
Iterable<? extends DexBackedField> staticFields = dexBackedClassDef.getStaticFields(); // 获取静态字段

for (DexBackedField field : staticFields) {
if (isExtraField(field)) { // 检查字段是否符合条件
ImmutableIntEncodedValue initialValue = (ImmutableIntEncodedValue) field.getInitialValue(); // 获取字段的初始值
if (initialValue == null) {
continue; // 如果没有初始值,跳过
}
// 将字段值存储到缓存中
valCache.put(field.toString(), initialValue.getValue());
}
}
});
}

// 判断字段是否是额外的字段
public static boolean isExtraField(DexBackedField field) {
return "I".equals(field.getType()) && (field.accessFlags & AccessFlags.FINAL.getValue()) == 0 && !noramlNamePattern.matcher(field.getName()).matches();
}
}

去混淆后再把 dex 文件通过 MT 管理器丢到 apk 包里,再用JEB反编译就能去混淆,前面 base64 后面一个 xor 结束

f2022ad295e997751b358af6ea40f4e

loginDemo

JEB 直接解密字符串,然后JAVA层就直接看干净了,输入用户名密码,一通操作以后生成一个 secretKey,再进 generateSignature(就是一个base)然后发包,题目提供了一个 har 文件,找个在线网站打开即可 HAR File Viewer | Analyze HTTP Archive Files Online Free (jam.dev)

QQ_1727959379041

QQ_1727959548835

写个 frida 脚本 hook 一下

javascript
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
setImmediate(function() {
Java.perform(function() {
let GoodCard = Java.use("com.example.sctf1.GoodCard")
GoodCard["anything"].implementation = function(str) {
let res = this.anything(str)
console.log(`GoodCard.anything(${str}) => ${res}`)
return res
}
let LoginActivity = Java.use("com.example.sctf1.LoginActivity")
LoginActivity["transform"].implementation = function(str) {
let res = this.transform(str)
console.log(`LoginActivity.transform(${str}) => ${res}`)
return res
}
let Getstr = Java.use("com.example.sctf1.Getstr");
Getstr["getNothing"].implementation = function (str) {
let res = this.getNothing(str);
console.log(`Getstr.getNothing(${str}) => ${res}`);
return res;
}
})
})

LoginActivity.transform(91a2b3c4d5e6f7g8h) => 5700490097005000980051009900520010000530010100540010200550010300560010400
Getstr.getNothing(5700490097005000980051009900520010000530010100540010200550010300560010400) => 10920965897793165772967607288421651552028172975664190885921344187916272153119330529444807969017289291134790535571637383877329321331424818768810595493239910734909096389294728981684289927416388298966989976535291024157987441541391839325979489755005179570827818182588092515113281201667788928076754870896272280070287946147028373209707779248001055887238305421589739567543955943293875618990217040525863960618183129133447412302295137874448394196228342380780306654884529986860844799059162757896177416819114763379309454773325892056798789054990627910675695354635958820758222936845536816810673795733691085675576616370278863106087

结合一些输入和算法特征,能看出 SO 里面的 getNothing 是个 RSA,pq 连在一起了,但是可以看出其长度都是 309,手动切分一下

QQ_1728019444131

QQ_1728019604661

然后通过反射方法执行了另一个 dex 里面的 say_hello 函数,异或了一个密钥生成了 secretKey,再连接上一串固定字符用 base64 编码发包

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
import base64
enc = "YgNxAGMDawJjZQR6B2IGYANiYQVxAG8JYQhkawZ3AW8JagluYAN2Am4GbQRmZAR6AWwBbQNlZANxCWsCaAlhZQRxAm4CbgRkZgl1AWIIawhmYAh3B2MBaAJmaghwAWoEbwliYgBwCWkIbQNuawlyAW0FYAhuYgB1Am8CawRuYAJxCG8CbgRgZAF3BWgJYQlhZwFzAGsJbwFlagV0CW0FawhgawNyCGkAbQNjYQh3Bm4FbwNkYAJ6A2IDaANmYwl2A2sEaQRlaglyAm0HaARmYQZ7AWsIbwZhYwB0B2sIYAlvYgN2AWsHYQJiZAByAGgDYANmYQB0BmwJYABhZwl1CGkEaQlmZQJzBm0IYQdjYwh6A20BbwVhZQl3CGoIaABmYAN1AW0IbQFvYAN0B2gFYAVhZAF1BW8HYQZvagZ6A20CaAhjZAl6BWkCYQhvZAN1AGgDaQJhYQZzCW8BYABjZAN0AW8BbQViZwVyA28JagJiagd1CGgDawhvYQJwB28GagNvYwd6AG0BYQFuawVyBGoBaQFkZAV1A24HbgdlYgd1AWwIYQNkagV6BW0DbglvZAl0A2kFaAhjawB3AmwAbwZgZwV3BGMEaQllYAhzAGsEYAFhYAJ6Bm4GaQdkagl6A20DawdkZgd3BW0JbgRgYQZyB2kJbANnZgdwA2gFaQFnYAh7BW4GbAdhYgd7Bm4DawJmawN0CWMDbgJgYwh0AmwGYQRiYQN2BmIGaAVkYwh2BmsDbAFhaglzB2kAbQBjZwJ1BGkCaghiZgByAWkJaAhhagZ3AG0JaghiYQVwAmMIaAFjYgVzBGh0dHA6Ly80Ny4xMDkuMTA2LjYyOjkwOTB7Im5hbWUiOiJTQ1RGIiwicGFzc3dvcmQiOiI4ODg4ODg4OCJ9"
enc_b64decoded = base64.b64decode(enc)[:-62]
key = b"S0C0Z0Y0W"
enc = bytearray()
for i in range(0, len(enc_b64decoded)):
enc.append(enc_b64decoded[i] ^ key[i % len(key)])
enc = bytes(enc)
q = 144819424465842307806353672547344125290716753535239658417883828941232509622838692761917211806963011168822281666033695157426515864265527046213326145174398018859056439431422867957079149967592078894410082695714160599647180947207504108618794637872261572262805565517756922288320779308895819726074229154002310375209
p = 106697219132480173106064317148705638676529121742557567770857687729397446898790451577487723991083173010242416863238099716044775658681981821407922722052778958942891831033512463262741053961681512908218003840408526915629689432111480588966800949428079015682624591636010678691927285321708935076221951173426894836169

n = p * q
phi_n = (p - 1) * (q - 1)
e = 65537
d = pow(e, -1, phi_n)

# 解密密文
m = str(pow(int(enc), d, n))
print(m)
m = ''.join(chr(int(i, 10)) for i in m.split("00")[:-1])

index = "gahbicjdlemfn"
table = "abcdefghijlmn"

flag = ""

for i in range(0, len(m)):
flag += m[index.index(table[i])]
print(flag)

UDS

修一下内存,关键逻辑在这儿

QQ_1728029403622

先进TEA,密钥是

QQ_1728030405599

密文是

QQ_1728030448543

然后解出来是 RC4的密钥

QQ_1728030506386

运行一个RC4解密,密文获得需要复现main前面的初始化函数

QQ_1728030609222

QQ_1728030644993

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
import idaapi
data_addr = 0x8004EC8
dest_addr = 0x20000000
len = 0x194

i = 0
j = 0
while i < len:
tByte = idaapi.get_byte(data_addr + i)
i += 1
low = tByte & 7
high = tByte >> 4
if low == 0:
low = idaapi.get_byte(data_addr + i)
i += 1
if high == 0:
high = idaapi.get_byte(data_addr + i)
i += 1
print(hex(tByte), hex(low), hex(high))
low -= 1
while low:
print(f"patch byte at {hex(dest_addr + j)} by {hex(data_addr + i)} : {hex(idaapi.get_byte(data_addr + i))}")
idaapi.patch_byte(dest_addr + j, idaapi.get_byte(data_addr + i))
i += 1
j += 1
low -= 1
if tByte & 8 != 0:
t = idaapi.get_byte(data_addr + i)
i += 1
t = idaapi.get_byte(dest_addr - t)
high_2 = high + 2
high_2 -= 1
while high_2 >= 0:
print(f"patch byte at {hex(dest_addr + j)} by {hex(data_addr + i)} : {hex(idaapi.get_byte(data_addr + i))}")
idaapi.patch_byte(dest_addr + j, idaapi.get_byte(data_addr + i))
i += 1
j += 1
high_2 -= 1
else:
high -= 1
while high >= 0:
print(f"patch byte at {hex(dest_addr + j)} by {hex(data_addr + i)} : {hex(idaapi.get_byte(data_addr + i))}")
idaapi.patch_byte(dest_addr + j, 0)
j += 1
high -= 1

密文如下

QQ_1728030727014

解密即可