-
-
[原创]某企业壳底层对抗与内核级脱壳机定制实战记录
-
发表于: 1天前 669
-
某企业壳底层对抗与内核级脱壳机定制实战记录
"在 CSDN 发过一篇精简版的思路。这次重新整理了底层源码、硬件断点细节以及完整的修复脚本。"
目标 App:某企业级加固 App
加固特征:内存级环境指纹检测 + 硬件级反调试 + SO 强混淆 VMP + Fake Size 指令抽空假象 + 段级加密与苛刻解密时机控制
实战环境:Pixel, Android 10, 自编译定制内核
本文记录了一次完整的底层对抗与逆向分析实录:从物理内存重写工具绕过环境指纹检测,到利用 Linux 内核级硬件断点实现无篡改的 ART 挂钩;分析其Fake Size相关的指令抽取机制。在完成 Java 层分析后,针对 Native 层开发 C 语言的预读轮询提取工具,通过时间冻结与 ELF 离线缝合技术,完成了目标 SO 的内核级脱壳,并分析了其私有 DRM 视频加密机制。
1. 环境伪装与底层反检测
在对 libTargetPlayer.so 实施操作之前,需要先构建一个安全的运行环境。
企业级加固的防御体系会检查系统属性(防篡改)、扫描内存(防调试注入)、校验文件路径(防 Root),以及检测系统源码级的编译特征(防自编译内核)。
我在定制内核中实施了多层级的底层隐匿方案,主要拦截点如下:
- VFS 路径拦截:在
fs/namei.c拦截su、magisk,使其返回ENOENT。同时将针对/system/build.prop等配置文件的读取请求重定向。 - 内核态硬件断点伪装:在
kernel/ptrace.c拦截应用层通过PTRACE_GETREGSET请求查询硬件断点的操作,返回全 0 的寄存器包。 /proc/self/maps内存过滤:在fs/proc/task_mmu.c中,强行丢弃包含frida、zygisk等关键字的内存段条目。- 反自杀拦截:在
kernel/signal.c中拦截应用发向自身的SIGKILL或SIGABRT。 - 属性物理覆写:加固壳会绕过 API 直接
mmap读取/dev/__properties__的物理内存。需通过底层工具myresetprop_ult直接在内存上进行属性修改。
物理内存覆写的 myresetprop_ult.c
该工具通过解析 Android 属性树,定位目标属性在物理内存中的偏移并覆写。
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdint.h>
#include <stdlib.h>
#define PROP_VALUE_MAX 92
struct prop_info {
uint32_t serial;
char value[PROP_VALUE_MAX];
};
extern const struct prop_info* __system_property_find(const char* name);
int overwrite_prop(const char* name, const char* new_val, int is_fp) {
const struct prop_info* pi = __system_property_find(name);
if (!pi) return -1;
FILE* maps = fopen("/proc/self/maps", "r");
char line[512], target_path[256] = {0};
uintptr_t map_start = 0, map_end = 0;
while (fgets(line, sizeof(line), maps)) {
uintptr_t start, end;
char path[256] = {0};
if (sscanf(line, "%lx-%lx %*s %*s %*s %*s %s", &start, &end, path) >= 2) {
if ((uintptr_t)pi >= start && (uintptr_t)pi < end) {
map_start = start;
map_end = end;
if (strlen(path) > 0) strcpy(target_path, path);
break;
}
}
}
fclose(maps);
size_t offset = (uintptr_t)pi - map_start;
int fd = open(target_path, O_RDWR);
struct stat st;
fstat(fd, &st);
void* map = mmap(NULL, st.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
close(fd);
// 基于序列锁机制并发安全地覆写属性值
struct prop_info* target_pi = (struct prop_info*)((char*)map + offset);
uint32_t serial = target_pi->serial;
target_pi->serial = serial | 1;
__sync_synchronize();
strcpy(target_pi->value, new_val);
__sync_synchronize();
target_pi->serial = (serial + 2) & ~1;
munmap(map, st.st_size);
return 0;
}
int main(void) {
overwrite_prop("ro.debuggable", "0", 0);
overwrite_prop("ro.secure", "1", 0);
overwrite_prop("ro.build.tags", "rel-keys", 0);
overwrite_prop("ro.build.type", "user", 0);
overwrite_prop("ro.boot.verifiedbootstate", "green", 0);
overwrite_prop("ro.boot.flash.locked", "1", 0);
overwrite_prop("ro.boot.vbmeta.device_state", "locked", 0);
overwrite_prop("ro.boot.veritymode", "enforcing", 0);
overwrite_prop("ro.build.fingerprint", "google/marlin/marlin:10/QP1A.190711.020/5871711:user/rel-keys", 0);
return 0;
}
# 需依托定制内核运行。该工具内部已通过提权代码静默获取了相应权限。
adb shell "/data/local/tmp/myresetprop_ult"
2. 基于内核硬件断点的无痕 ART 挂钩与 Dex 提取
要获取解密后的 Dex,常规在用户态对 libart.so 实施 Inline Hook 易触发防御机制。目标持续扫描代码区完整性,并轮询 TracerPid 与 CPU 调试寄存器。
为规避检测,探针被下沉至 Linux 内核态。基于定制内核的 perf_event 子系统,配置 CPU 硬件调试寄存器设置硬件断点,监控 art::ClassLinker::LoadClass 的执行地址。陷入内核层后,可安全读取保存了 DexFile 结构体指针的寄存器。
内核态硬件断点捕获逻辑
在内核驱动模块中,注册 perf_event 拦截特定的虚拟内存执行地址。
#include <linux/perf_event.h>
#include <linux/hw_breakpoint.h>
#include <linux/uaccess.h>
unsigned long target_loadclass_addr = 0x7a30010000;
#define FORCE_DUMP_SIZE 0x2000000
#define OFFSET_BEGIN 0x08
static void sniper_handler(struct perf_event *bp, struct perf_sample_data *data, struct pt_regs *regs) {
// 对于 ARM64,ClassLinker::LoadClass 第三参数存储在 x2 寄存器
unsigned long dex_file_ptr = regs->regs[2];
unsigned long dex_base = 0;
// 使用探针函数安全读取用户态内存,规避 Kernel Panic
if (probe_kernel_read(&dex_base, (void *)(dex_file_ptr + OFFSET_BEGIN), sizeof(dex_base)) == 0) {
printk(KERN_INFO "[Sniper] 成功捕获 DexFile 对象,基址: 0x%lx\n", dex_base);
sniper_dump_async(current, dex_base, FORCE_DUMP_SIZE);
}
}
3. 分析与突破 Fake Size 机制
初步提取的 Dex 文件静态反编译时出现内存越界异常,代码块显示为空。
结构分析表明,加固层在加载阶段已将 Dalvik 字节码解密并存在物理内存块末端。为阻碍提取,设计了 Fake Size 防御:在解密后篡改传给 ART 的 Dex 头部 file_size 字段。
常规转储通常依赖该声明尺寸,导致隐藏在尾部的真实代码区域被截断遗漏。
自动化搜索与提取工具:extract_dex.py
针对这种情况,可通过扫描内存文件定位被隐匿的 Dex 头,并验证 Fake Size 的影响。
import sys
import struct
import os
def extract_dex(file_path):
with open(file_path, 'rb') as f:
data = f.read()
magic_signatures = [b'dex\n035\0', b'dex\n037\0']
found_any = False
for magic in magic_signatures:
offset = 0
while True:
idx = data.find(magic, offset)
if idx == -1:
break
found_any = True
if idx + 0x24 <= len(data):
file_size = struct.unpack('<I', data[idx + 0x20 : idx + 0x24])[0]
if 0 < file_size < 100 * 1024 * 1024:
extract_size = min(file_size, len(data) - idx)
extracted_data = data[idx : idx + extract_size]
out_path = os.path.join(os.path.dirname(file_path), f"dumped_dex_{hex(idx)}.dex")
with open(out_path, 'wb') as out_f:
out_f.write(extracted_data)
offset = idx + len(magic)
# 针对加固抹除 Magic 的情况进行探测 (通过定位大端字节序标记)
if not found_any:
offset = 0
endian_tag = b'\x78\x56\x34\x12'
while True:
idx = data.find(endian_tag, offset)
if idx == -1:
break
dex_start = idx - 0x28
if dex_start >= 0:
header_size = struct.unpack('<I', data[dex_start + 0x24 : dex_start + 0x28])[0]
if header_size == 0x70:
found_any = True
file_size = struct.unpack('<I', data[dex_start + 0x20 : dex_start + 0x24])[0]
if 0 < file_size < 100 * 1024 * 1024:
extract_size = min(file_size, len(data) - dex_start)
extracted_data = bytearray(data[dex_start : dex_start + extract_size])
# 重构被抹除的 Magic
extracted_data[0:8] = b'dex\n035\0'
out_path = os.path.join(os.path.dirname(file_path), f"dumped_hidden_{hex(dex_start)}.dex")
with open(out_path, 'wb') as out_f:
out_f.write(extracted_data)
offset = idx + 4
if __name__ == "__main__":
extract_dex(sys.argv[1])
4. 越界物理提取与 Dex 结构修复重建
为应对尺寸伪装,我已在内核驱动模块中设定了超出常规规模的连续物理采集阈值(例如 0x2000000 即 32MB),实施强制越界提取。
原始数据的超量提取会导致导出流包含冗余数据,物理尺寸发生改变也会破坏 Dex 头部的校验链。需按以下流程重建二进制结构:
- 覆写物理尺寸:将伪造的 Size 覆写为实际有效尺寸。
- 重算 SHA-1 签名:必须针对包含真实代码的全局数据重新计算并回写。
- 重算 Adler32 校验和:校验和必须放在最后一步重新计算。
自动化重建工具:fix_dex.py
import struct
import hashlib
import zlib
import sys
def fix_dex(file_path, new_size):
with open(file_path, 'rb') as f:
data = bytearray(f.read())
data[32:36] = struct.pack('<I', new_size)
if len(data) > new_size:
data = data[:new_size]
elif len(data) < new_size:
data.extend(b'\x00' * (new_size - len(data)))
sha1 = hashlib.sha1()
sha1.update(data[32:])
data[12:32] = sha1.digest()
checksum = zlib.adler32(data[12:]) & 0xffffffff
data[8:12] = struct.pack('<I', checksum)
out_path = file_path.replace('.dex', '_fixed.dex')
with open(out_path, 'wb') as f:
f.write(data)
if __name__ == '__main__':
actual_size = int(sys.argv[2]) if len(sys.argv) > 2 else 18 * 1024 * 1024
fix_dex(sys.argv[1], actual_size)
执行该脚本完成重构后,隐匿的业务方法字节码即可在工具中恢复。
5. 绕过 ptrace 限制的 Native 层外置轮询提取方案
Native 层的安全防护具备更高的分析门槛。关键代码段在装载初期保持加密状态,仅在业务逻辑触发时解密。
由于存在反调试机制,通过常规 ptrace 尝试附加目标进程易引发异常。为此开发了基于外部轮询的 C 语言工具 ghost_dumper。该方案基于 /proc/[pid]/mem 接口实施带外读取,规避了进程注入。
ghost_dumper.c 提取工具核心实现
该工具静默提权后,通过解析 /proc/[pid]/maps 定位 libTargetPlayer.so 的基址。静态分析表明,目标 SO 在解密完成时,会在 0x1EA038 动态写入核心函数指针。工具启动高频 pread 轮询该地址,当值变更为合法地址时判定解密完成,随后下发 SIGSTOP 强制冻结进程。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/syscall.h>
#include <signal.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <dirent.h>
#define TARGET_SO "libTargetPlayer"
#define APP_NAME "com.target.app"
struct DumpHeader {
unsigned int magic;
unsigned long start;
unsigned long end;
unsigned long size;
char perms[8];
};
int get_pid_by_name(const char *name) {
DIR *dir;
struct dirent *ent;
char buf[512];
if (!(dir = opendir("/proc"))) return -1;
while ((ent = readdir(dir)) != NULL) {
long lpid = atol(ent->d_name);
if (lpid < 0) continue;
snprintf(buf, sizeof(buf), "/proc/%ld/cmdline", lpid);
FILE *fp = fopen(buf, "r");
if (fp) {
if (fgets(buf, sizeof(buf), fp) != NULL && strcmp(buf, name) == 0) {
fclose(fp);
closedir(dir);
return (int)lpid;
}
fclose(fp);
}
}
closedir(dir);
return -1;
}
int main(int argc, char **argv) {
freopen("/data/local/tmp/ghost_dumper.log", "w", stderr);
setvbuf(stderr, NULL, _IONBF, 0);
if (syscall(__NR_setresuid, 1337, 1337, 1337) != 0) {
return 1;
}
int pid = get_pid_by_name(APP_NAME);
char maps_path[256];
snprintf(maps_path, sizeof(maps_path), "/proc/%d/maps", pid);
unsigned long base_addr = 0;
char line[512];
FILE *fp = fopen(maps_path, "r");
while (fgets(line, sizeof(line), fp)) {
if (strstr(line, TARGET_SO) && strstr(line, " 00000000 ")) {
char *dash = strchr(line, '-');
if (dash) {
*dash = '\0';
base_addr = strtoull(line, NULL, 16);
break;
}
}
}
fclose(fp);
char mem_path[256];
snprintf(mem_path, sizeof(mem_path), "/proc/%d/mem", pid);
int mem_fd = open(mem_path, O_RDONLY);
unsigned long target_addr = base_addr + 0x1EA038;
unsigned long long val = 0;
int max_retries = 120000;
// 高频轮询特定内存地址判定解密状态
while (max_retries-- > 0) {
if (pread(mem_fd, &val, sizeof(val), target_addr) == sizeof(val)) {
if (val != 0) break;
}
usleep(1000);
}
// 解密完成后下发 SIGSTOP 冻结进程以防内存变化
kill(pid, SIGSTOP);
fp = fopen(maps_path, "r");
while (fgets(line, sizeof(line), fp)) {
if (strstr(line, TARGET_SO)) {
unsigned long start, end;
char perms[5];
sscanf(line, "%lx-%lx %4s", &start, &end, perms);
if (perms[0] == 'r') {
size_t size = end - start;
char *buf = malloc(size);
pread(mem_fd, buf, size, start);
struct DumpHeader hdr;
hdr.magic = 0xDEADBEEF;
hdr.start = start;
hdr.end = end;
hdr.size = size;
// 封装协议头,通过标准输出流回传
write(STDOUT_FILENO, &hdr, sizeof(hdr));
write(STDOUT_FILENO, buf, size);
free(buf);
}
}
}
fclose(fp);
return 0;
}
6. 跨ELF内存解析与缝合
通过上述方案流出的二进制数据,需经由 PC 端脚本进行接收解析,并还原回原 ELF 文件。
PC 端解析与数据缝合
通过 receiver.py 接收数据,并通过 patch_so.py 将导出的内存合并回原 ELF 文件。
1. receiver.py (拆分二进制流)
import sys
import struct
def main():
while True:
header_data = sys.stdin.buffer.read(32)
if not header_data:
break
magic, start, end, size, perms = struct.unpack('<IQQQ8s', header_data)
if magic != 0xDEADBEEF:
break
perms_str = perms.decode('utf-8').strip('\x00')
chunk = sys.stdin.buffer.read(size)
filename = f"dump_{hex(start)}_{hex(end)}_{perms_str}.bin"
with open(filename, "wb") as f:
f.write(chunk)
if __name__ == "__main__":
main()
2. patch_so.py (ELF 结构修复)
import shutil
original_so = "libTargetPlayer_encrypted.so"
decrypted_bin = "dump_rwxp.bin"
output_so = "libTargetPlayer_decrypted.so"
PATCH_SIZE = 0x1D7170
with open(decrypted_bin, "rb") as f:
decrypted_data = f.read()
shutil.copy2(original_so, output_so)
with open(output_so, "r+b") as f:
f.seek(0)
f.write(decrypted_data[:PATCH_SIZE])
7. 动态分析与核心逻辑提取
对重构后的 libTargetPlayer_decrypted.so 静态分析时发现,通过在进程冻结窗口期获取的内存快照,捕获到一个未混淆的初始化函数。
__int64 target_dlopen_init()
{
__int64 handle;
handle = dlopen("libtargetdrm.so", 2);
if ( handle )
{
qword_1EA038 = dlsym(handle, "TARGET_CDRMC_Get_KeyId");
qword_1EA040 = dlsym(handle, "TARGET_CDRMC_Decrypt_AES_CBC");
}
}
逻辑分析与验证
加密逻辑存在于动态加载的 libtargetdrm.so 中。分析确认 TARGET_CDRMC_Decrypt_AES_CBC 执行标准 AES-CBC 解密。
LDR X0, [SP, #0x20]
LDR X1, [SP, #0x20]
LDR W2, [SP, #0x28]
BL Decrypt_AES_CBC
该解密过程采用了原地解密 (In-place Decryption)。可借助动态框架拦截 TARGET_CDRMC_Decrypt_AES_CBC 函数的执行过程提取明文数据流。
结语与延展探讨
本文梳理了底层硬件断点追踪、跨域物理内存提取、系统调用管控及基于异步轮询的内存缝合方案。利用操作系统极底层的接口干预,为分析高度混淆代码及强校验环境提供了一条切实可行的技术路径。
在实战复盘中,虽然受限于设备无法使用 eBPF 等无痕插桩技术,且未进行VMP虚拟化还原,但正是因为捕捉到了其核心解密依赖底层模块的架构缺陷,从而避免了正面的死磕。在真实的攻防对抗中,持续探寻架构漏洞并实施侧信道绕过。
[内核课程]《Windows内核攻防实战》!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。