额……

原本我还以为,cuscuta只是一个后端设计,没想到最终牵扯到的东西竟真如cuscuta一般了……

botrytis

等等等等……我知道要讲scirpophaga,但是但是,在这之前,我得先介绍一下我的新伙伴嘛……

botrytis是一个新仓库,其中包含了一些我常用的脚本和一些偷懒的方法,合理地使用它理论上可以略微加速逆向分析的速度……?

目前,botrytis只有很小一部分内容,包括了供frida使用的快捷函数,和一个叫做sweep_visualizer的小脚本(真的很小);其中frida脚本有general_utilsmemory_tracking

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
* general_utils.js提供了一些实用函数,例如快速监控函数或内存段,
* 像窜稀一样地打印寄存器信息,便捷搜索内存的函数,等等……
*/
module.exports = {
printRegisters,
arrayBufferToHex2,
printStack,
waitForModule,
printModule,
printRegister,
dumpSerials,
stringToHexString,
justMonitorFunction,
justMonitorMemory,
scanData,
};
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
/*
* 而memory_tracking.js提供了一些在指令层面跟踪内存读写的工具,用于分析内联块的输入输出参数
* 配合Frida Stalker,可以轻松便捷地获取到一个段内到底动了哪些内存区域……
*/
module.exports = {
parseCommand,
analyzeCommand,
getDirectValue,
getRegisterValue,
};

//---Example---
Interceptor.attach(module.base.add(funAddr), {
onEnter(args) {
let tid = Process.getCurrentThreadId();
this.tid = tid;
console.log(`tracing [${tid}]`);
Stalker.follow(tid, {
events: {
call: false,
ret: false,
exec: true,
block: false,
compile: false,
},
transform(iterator) {
let instruction = iterator.next();
do {
const address = instruction.address;
const module = getModuleByAddressSafe(address);
let modInfo = "";
if (module && module.name === "targetModule.so") {
const offset = ptr(address).sub(module.base);
modInfo = `[${module.name}!${offset}]`;
let isLd = instruction.mnemonic.startsWith("ld");
let isSt = instruction.mnemonic.startsWith("st");
if (isLd || isSt) {
let cmdOffset = offset;
let insStr = instruction.toString();
let insMnemonic = instruction.mnemonic;
let address = instruction.address;
let insOpStr = instruction.opStr;
iterator.putCallout((context) => {
let sp = parseInt(`${context.sp}`, 16);
let result = analyzeCommand(
context,
insStr
);
console.log(
`${
result.ld ? "LD" : "ST"
}:${result.target
.map((it) => it.toString(16))
.join(",")}`
);
});
}
}
iterator.keep();
} while ((instruction = iterator.next()) !== null);
},
});
},
onLeave(retval) {
console.log(`trace stopping [${this.tid}]`);
Stalker.unfollow(this.tid);
},
});

具体的部分可以到botrytis查看~

分析,试验,和scirpophaga

这些小家伙的名字一个比一个难记对吧哈哈

scirpophaga是一个自动化的提取工具,它的终极目标是:仅通过离体的so文件,在任意版本下,推导出C2的值,而无需动态分析

在此之前……

众所周知,在现有版本的情况下,某傻逼东西的X-Random-Challenge的生成,除必要输入外,需要依赖4个常量,分别是C1、C2、C31、C32,在前几张草稿纸中也有提到,C1目前看来是一个铁打的常量,它不随版本的变化而改变;C31、C32推测是与请求有关的常量,不同请求的C31、C32不同,但不随版本变化

所以,目前唯一棘手的地方在于C2——它是一个版本相关常量,它在3个版本的值均不同,可以推测,它在每个版本的值也都不同,若没有scirpophaga或者其类似替代方案,我们只能使用frida脚本在动态环境下将其dump出来,但这么做需要一个完整的,且处于调试环境下的Android环境,并且其自动化较为繁琐

C2_pre1,和C2_pre2

在上上张草稿纸上写过,C2的值和两个常量相关,下面展示和C2有关键联系的汇编片段:

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
; 截取自某版本module.so文件
; 0x000000000085B150 ~ 0x000000000085B214
;
; 根据动态分析,C2的值由这段汇编倒数第二行的Q0、Q1组合而成
; 而Q0、Q1(后)的值又分别等于Q0(前) XOR Q4,和Q1(前) XOR Q5
; Q3虽然参与计算,但是和C2无关,我们不理它

LDR Q3, [SP,#0xC0]
LDR X0, [SP,#0x100]
ADRL X14, stru_1996E90
STR Q3, [SP,#0x400]
LDR Q3, [SP,#0xB0]
ADD X11, SP, #0x5C0+s
LDP Q0, Q1, [SP,#0x3E0] ; 这里是Q0, Q1(前)的来源,来源于栈
MOV W8, #0xFFFFFFED
STR Q3, [SP,#0x410]
LDR Q3, [SP,#0xA0]
STR XZR, [SP,#0x3D0]
STUR WZR, [X11,#0x87]
MOV W11, #0x262
LDP Q4, Q5, [X14] ; 这里是Q4、Q5被拉来的地方
; X14就在不远处,显然,它们是从stru_1996E90读来的
CMP X0, #0
STR W11, [SP,#0x438]
MUL W11, W0, W8
CSEL W8, W8, W11, EQ
MOVI V2.2D, #0
ADD X15, SP, #0x400
STR Q3, [SP,#0x420]
MOV W13, #0x63D
DUP V3.16B, W8
MOV W12, WZR
MOV W9, WZR
MOV W17, WZR
MOV W10, WZR
MOV X16, XZR
MOV W1, #1
STR W13, [SP,#0x440]
MOV W13, #0x3DB
MOV W11, #5
STR Q2, [SP,#0x4B0]
STR Q2, [SP,#0x4A0]
STR Q2, [SP,#0x490]
STR Q2, [SP,#0x480]
STR Q2, [SP,#0x470]
ORR X8, X15, #4
EOR V0.16B, V0.16B, V4.16B
EOR V1.16B, V1.16B, V5.16B
MUL V2.16B, V4.16B, V3.16B
MUL V3.16B, V5.16B, V3.16B
ADD X0, SP, #0x470
STR XZR, [SP,#0x3C8]
STR XZR, [SP,#0x3B8]
STR XZR, [SP,#0x3B0]
STR D8, [SP,#0x430]
STP Q0, Q1, [SP,#0x3E0] ; Q0和Q1即为最终值
STP Q2, Q3, [X14]

为了方便起见,我们将这里的Q4、Q5所代表的值命名为C2_pre1;将这里的Q0、Q1(前)所代表的值命名为C2_pre2(按分析顺序定的序号),结合汇编,不难看出C2就等于C2_pre1 XOR C2_pre2

C2_pre1

为了方便,这里先不管上上张草稿的发现,从头开始讲吧

出于各种原因,我当时选择先看看更神秘的C2_pre1,根据汇编,也不难看出C2_pre1来源于一个神秘的地址,即stru_1996E90,在IDA跟踪这个地址,发现它是一个BSS段;在IDA中搜索这个BSS段的引用,发现这个BSS段足足有75处引用

这种引用量按静态分析的话我要当场砸电脑了,所以我尝试使用frida监控这段内存,但是不巧的是,MemoryAccessMonitor并没有给我满意的结果——在我的机器上,它根本就没有显示任何结果(不知道是不是设备的原因);既然这样行不通的话,就要使用更土的法子了,那就是直接把所有引用到它的地方全部hook个遍,然后打印调用栈

结果是好的,这种方法拿到了MemoryAccessMonitor根本拿不到的东西:

1
2
3
4
5
6
7
8
9
10
11
12
13
suses_23874132 triggered!
0x7b8f458128 target.so!0x1653128
0x7b8e95f2c8 target.so!0xb5a2c8
...
suses_8761688 triggered!
0x4adf4219
0x7bb443bc88
0x7bb45836d0
0x7bb4401610
0x7ce78ee1fc
0x7b8f458128 target.so!0x1653128
0x7b8ecf620c target.so!0xef120c
...

在之前的草稿中说到,第二个trigger是我们刚刚的引用,那我们就去看看第一处引用到底往这个段里写了什么奇奇怪怪的玩意

这个函数没有输入参数,这十分令人欣慰;并且,在最末尾,可以清晰的看到写入操作:

1
2
3
4
5
6
7
8
9
BL              sub_16C4A54
LDP Q0, Q1, [SP,#0x60]
LDR X8, [SP]
STP Q0, Q1, [X0]

; 而上面那个函数更加简单:
; sub_16C4A54
ADRL X0, stru_1996E90
RET

至此,我们就可以直接通过模拟运行,最后读出Q0和Q1的值,即C2_pre1的值

C2_pre2

这个值十分棘手,我分析了栈的内存访问,还尝试了往一些栈空间里写一些奇奇怪怪的东西,但是都以失败告终

正当我不知道该怎么办的时候,我突然发现这个超大函数,前半段似乎没有外部引用……

并且根据分析,这个函数的4个输入参数均为指针,我尝试了直接运行这个函数,结果是出乎意料的,它……成功了一半

具体实现

刚刚的分析结果表明,C2_pre1的提取只需要简单地调用那个函数,最后取到Q0和Q1的值就行了……

但是等等!这个函数引用了一些Data和Rodata段的内容,而Elf文件直接装载到内存里时是不存在这些内容的,我们需要读Elf文件,然后读出所有PT_LOAD段,再写到内存里:

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
struct ElfInit {
mem_offset: u64,
file_offset: u64,
file_sz: u64,
mem_sz: u64,
v_data: Vec<u8>,
}

fn try_parse_elf(bin: &[u8]) -> Result<Vec<ElfInit>, goblin::error::Error> {
let elf = Elf::parse(bin)?;
let mut output = vec![];
for it in &elf.program_headers {
if it.p_type == goblin::elf64::program_header::PT_LOAD {
let va = it.p_vaddr;
let filesz = it.p_filesz;
let memsz = it.p_memsz;
let offset = it.p_offset as usize;
let range = &bin[offset..offset + filesz as usize];
output.push(ElfInit {
mem_offset: va,
file_offset: offset as u64,
file_sz: filesz,
mem_sz: memsz,
v_data: Vec::from(range),
});
}
}
Ok(output)
}

至于Elf的文件结构方面,这里不再赘述


然后就是直接调用大函数得到C2_pre2对吧?这个我会!

……但是再等等!大函数中有一些外部引用,而我们只有裸的so文件,unicorn可不会自己下载libc.so然后自己就装载上去;大函数还有4个输入参数,怎么办?

上文说过,根据动态分析,那四个输入参数全都是地址……我们先假设这些地址不会干扰到我们的数值生成(且事实证明,这个假设是成立的)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//...
let para_addr = 0x6000_0000;
let para_size = 0x0000_5000;
let para1 = 0x0000_1000;
let para2 = 0x0000_2000;
let para3 = 0x0000_3000;
let para4 = 0x0000_4000;
//...
uc.mem_map(para_addr, para_size, Prot::ALL)?;
//...
uc.reg_write(RegisterARM64::X0, para_addr + para1)?;
uc.reg_write(RegisterARM64::X1, para_addr + para2)?;
uc.reg_write(RegisterARM64::X2, para_addr + para3)?;
uc.reg_write(RegisterARM64::X3, para_addr + para4)?;
//...

而对于那些外部引用,根据静态分析也不难得出,都是一些字符串操作相关的内容,和我们的C2_pre2提取也没有什么关系,所以……

1
2
3
4
5
6
7
8
9
10
11
fn uc_fill(uc: &mut Unicorn<'_, ()>, from: u64, to: u64) -> Result<(), unicorn_engine::uc_error> {
//好吧,我承认这非常暴力,但是很好玩
let len = (to - from) as usize;
if !len.is_multiple_of(4) {
panic!("not a valid length");
}
let mut o = vec![0u8; len];
o.iter_mut().skip(3).step_by(4).for_each(|it| *it = 0x91);
uc.mem_write(from, &o)?;
Ok(())
}

你知道我要干什么的……

C2_pre1和C2_pre2的值已经全部能够提取出来,也就能算出C2了,加上傻逼919防护意识淡薄,我们可以直接通过直接匹配来获取到这两个目标函数的位置……

1
2
3
4
5
6
7
8
9
10
11
log::info!("[*] searching in input, len: 0x{:x}", input.len());
let pos1 = input
.windows(FRAG1.len())
.position(|it| it == FRAG1)
.unwrap();
log::info!("[+] found C2_pre1 fn offset: 0x{:x}", pos1);
let pos2 = input
.windows(FRAG2.len())
.position(|it| it == FRAG2)
.unwrap();
log::info!("[+] found C2_pre2 fn offset: 0x{:x}", pos2);

简单,粗暴,但能用(被打)

至此,距离实现scirpophaga的短期目标已经没有什么距离了

具体项目可以参考scirpophaga

之后……?

之后就先准备一下农业气象学考试(?)以及cuscuta的重构吧……

如果新来的大佬也觉得之前那个架构没问题的话,我就更有信心了……