【Android】【攻防世界】easy-app

一开始想用ida进行动调,但是失败了

丢进jadx发现其主要flag检查逻辑都在native层中

image-20250716163557528

于是就直接去native层查看,有点繁杂,先从check开始查看:最后进行了一次base64

image-20250716163826057

base64是魔改base64,换表+密文换位,加入正常的base密文是0123,这个密文就是2013

image-20250716165217544

image-20250716165026790

所以解base64的时候需要进行换位置然后解正常的base64

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
import base64
import struct

ENCODED_STR = "e)n*pNe%PQy!^oS(@HtkUu+Cd$#hmmK&ieytiWwYkIA="
TEA_KEY = [0x42, 0x37, 0x2c, 0x21] # 实际是4字节密钥
DELTA = 0x9E3779B9

# 扩展密钥为4个32位整数 (小端序)
k = struct.unpack("<4I", bytes(TEA_KEY * 4))

def tea_decrypt_block(v0, v1, k):
total = DELTA * 32
for _ in range(32):
v1 = (v1 - (((v0 << 4) + k[2]) ^ (v0 + total) ^ ((v0 >> 5) + k[3])) & 0xFFFFFFFF)
v0 = (v0 - (((v1 << 4) + k[0]) ^ (v1 + total) ^ ((v1 >> 5) + k[1])) & 0xFFFFFFFF)
total = (total - DELTA) & 0xFFFFFFFF
return v0, v1

# 自定义编码表
custom_table = "abcdefghijklmnopqrstuvwxyz!@#$%^&*()ABCDEFGHIJKLMNOPQRSTUVWXYZ+/"
standard_table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"

# 1. 重排索引顺序 [1,2,0,3] -> [0,1,2,3]
# 将每4个字符一组的顺序从 [索引1, 索引2, 索引0, 索引3] 重排为 [索引0, 索引1, 索引2, 索引3]
reordered_str = ""
for i in range(0, len(ENCODED_STR), 4):
group = ENCODED_STR[i:i+4]
if len(group) == 4:
# 重排顺序:原位置2->新位置0, 原位置0->新位置1, 原位置1->新位置2, 原位置3->新位置3
reordered_str += group[2] + group[0] + group[1] + group[3]
else:
reordered_str += group

# 2. 映射自定义表到标准表
translated_str = ''.join(standard_table[custom_table.index(char)] if char in custom_table else char for char in reordered_str)

# 3. 添加Base64填充(如果需要)
if len(translated_str) % 4 != 0:
translated_str += '=' * (4 - len(translated_str) % 4)

# 4. Base64解码
encrypted_data = base64.b64decode(translated_str)

for i in range(0, len(encrypted_data), 4):
dword = struct.unpack("<I", encrypted_data[i:i+4])[0]
print(f"0x{dword:08X} ,",end='')
print('\n')

#0x10E14834 ,0x3D635EFC ,0xA2F3D91A ,0xFBCABA4D ,0x37702685 ,0x20C3B847 ,0x58138160 ,0xAB90BC8E ,

再往上看发现有tea加密但是我们不知道密钥,静态的是一个假密钥

image-20250716165614494

image-20250716165629933

于是用frida进行动调,拿到真正的密钥

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
function hookTest999(){
Java.perform(function(){
var libc_base = Module.getBaseAddress("libnative-lib.so") //libc基地址
var keys = libc_base.add(0x3A010); //JNZ所在偏移
console.log("libc_base : ",libc_base);
console.log("keys_address : ", keys);

// 读取内存范围内的指令
var size = 0x30;
try {
var instructionBytes = Memory.readByteArray(keys, size);
console.log("instructionBytes:\n", instructionBytes);

// 解析指令(需要Frida >= 12.7)
if (typeof Instruction !== 'undefined') {
var instructions = Instruction.parse(keys, instructionBytes);
console.log("Disassembly:", instructions);
} else {
console.warn("Instruction parser not available");
}
} catch (e) {
console.error("Memory read failed:", e);
}

// Hook MainActivity的check方法
var MainActivity = Java.use("com.example.myapplication.MainActivity");

MainActivity.check.implementation = function(input) {
// 触发指令读取
readInstructions();

// 调用原始方法
var result = this.check(input);
console.log(`Native check("${input}") returned: ${result}`);
return result;
};

console.log("[*] check() hook installed. Ready for input...");
})

}

image-20250716165758381

进行tea解密:

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
#include <stdio.h>
#include <stdint.h>

void Decrypt(unsigned int* vq,unsigned int* vp,unsigned int* k){
unsigned int n=32,sum,y=*vq,z=*vp;
unsigned int delta=0x9E3779B9;
sum = 0x9E3779B9 * 32;
while(n-->0){
z-=( ((y>>5)+k[3]) ^ ((y<<4)+k[2]) ^ (sum + y) );
y-=( ((z<<4)+k[0]) ^ ((z>>5)+k[1]) ^ (sum + z));
sum-=delta;
}
*vq=y;
*vp=z;
}

int main()
{
unsigned int v[8] = {0x10E14834 ,0x3D635EFC ,0xA2F3D91A ,0xFBCABA4D ,0x37702685 ,0x20C3B847 ,0x58138160 ,0xAB90BC8E};
unsigned int k[4]={0x42, 0x37, 0x2c, 0x21};
for(int j=0;j<=7;j+=2)
{
Decrypt(&v[j],&v[j+1], k);
printf("%x %x\n",v[j],v[j+1]);
}
return 0;
}

#36346065 38673534
#65353537 62303531
#61333232 34363160
#63316239 35356761

再往上分析发现有两个CheckM和check1,他们两个的作用是将输入的字符串每个byte的高位分离出来,然后把前16个数字和后16个数字进行交换,然后再和原来的低位进行组合(用动调可知,测试数据:flag{11111111BBBBBBBBSSSSSSSSdddddddd})

image-20250716170332294

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
function test666(){
Java.perform(function() {
const moduleName = "libnative-lib.so"; // 替换为实际 SO 文件名
const funcOffset = 0xff40; // 替换为 CheckM::check1 调用后的指令偏移

let baseAddr = Module.getBaseAddress(moduleName);
if (!baseAddr || baseAddr.isNull()) {
console.log("Error: Module " + moduleName + " not found or base address is null");
return;
}
console.log("Base address: " + baseAddr);

let hookAddr = baseAddr.add(funcOffset);
console.log("Hook address: " + hookAddr);

Interceptor.attach(hookAddr, {
onEnter: function(args) {
// 不需要操作
},
onLeave: function(retval) {
try {
// 获取 v38 地址:[rsp+0x168-0x110] = rsp + 0x58
let v38Addr = this.context.rsp.add(0x58);
console.log("RSP: " + this.context.rsp);
console.log("v38Addr: " + v38Addr);

if (!v38Addr || v38Addr.isNull()) {
console.log("Error: v38Addr is invalid or null");
return;
}

// 读取 v38 的标志位(第一个字节)
let flag = v38Addr.readU8();
console.log("Flag: " + flag);
let isSso = (flag & 1) === 0;

let dataPtr, dataLen;

if (isSso) {
// SSO 模式:数据紧邻 v38,长度 = flag >> 1
dataLen = flag >> 1;
dataPtr = v38Addr.add(1);
console.log("SSO mode - dataLen: " + dataLen + ", dataPtr: " + dataPtr);
} else {
// 堆模式:从 v38+8 读长度,v38+16 读指针
dataLen = Process.arch === 'x64' ? v38Addr.add(8).readU64() : v38Addr.add(8).readU32();
dataPtr = v38Addr.add(16).readPointer();
console.log("Heap mode - dataLen: " + dataLen + ", dataPtr: " + dataPtr);
}

// 检查 dataPtr 和 dataLen 是否有效
if (!dataPtr || dataPtr.isNull() || dataLen <= 0) {
console.log("Error: Invalid dataPtr or dataLen");
return;
}

// 读取并输出内容
let instructionBytes = Memory.readByteArray(dataPtr, 320);
console.log("new instructionBytes :\n", instructionBytes);
} catch (e) {
console.log("Error in onLeave: " + e);
}
}
});
});
}

所以还要把刚才tea解出的结果拼成一个长hex串这个混淆然后转换成字符

1
2
3
4
5
6
7
8
9
10
11
tea="6560343634356738373535653135306232323361603136343962316361673535"
tea1=tea[::2]
tea2=tea[1::2]

temp=tea1[16:]+tea1[:16] #高16位和低16位换位置

flag=""
for i in range(len(temp)):
flag+=temp[i]+tea2[i]
print(bytes.fromhex(flag))
#b'504fd5787e5eae02bb3101f4921c175e'