首页
社区
课程
招聘
[原创]某企业壳底层对抗与内核级脱壳机定制实战记录
发表于: 1天前 669

[原创]某企业壳底层对抗与内核级脱壳机定制实战记录

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),以及检测系统源码级的编译特征(防自编译内核)。

我在定制内核中实施了多层级的底层隐匿方案,主要拦截点如下:

  1. VFS 路径拦截:在 fs/namei.c 拦截 sumagisk,使其返回 ENOENT。同时将针对 /system/build.prop 等配置文件的读取请求重定向。
  2. 内核态硬件断点伪装:在 kernel/ptrace.c 拦截应用层通过 PTRACE_GETREGSET 请求查询硬件断点的操作,返回全 0 的寄存器包。
  3. /proc/self/maps 内存过滤:在 fs/proc/task_mmu.c 中,强行丢弃包含 fridazygisk 等关键字的内存段条目。
  4. 反自杀拦截:在 kernel/signal.c 中拦截应用发向自身的 SIGKILLSIGABRT
  5. 属性物理覆写:加固壳会绕过 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 头部的校验链。需按以下流程重建二进制结构:

  1. 覆写物理尺寸:将伪造的 Size 覆写为实际有效尺寸。
  2. 重算 SHA-1 签名:必须针对包含真实代码的全局数据重新计算并回写。
  3. 重算 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内核攻防全技术栈,打造具备自动化能力的内核开发高手。

最后于 1天前 被kudos5566编辑 ,原因:
收藏
免费 0
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回