首页
社区
课程
招聘
[原创]CRC校验原理及绕过
发表于: 2025-8-15 19:35 12136

[原创]CRC校验原理及绕过

2025-8-15 19:35
12136

模块下载及介绍

这里所说的crc校验是狭义的,范围只限于android平台的hook检测部分,绕过的方式也不会围绕构造碰撞等密码学相关的方式展开。CRC校验(循环冗余校验,Cyclic Redundancy Check),即通过数学算法生成一个固定长度的校验码(CRC值)。检测inline hook时,可以通过计算内存中目标so与磁盘上对应so的crc值,二者对比从而得出是否被hook的结论。这里选择其他算法也是可以的,选择crc是因为实现起来比较简单,计算也不会消耗太多性能

要想绕过我们就得先来分析一下它究竟是如何检测的?以下是我之前写的注入检测的app的相关代码

主要流程十分清晰,通过扫描maps拿到目标so的完整路径和内存地址,分别计算磁盘so和内存so的.text段的crc值且进行对比,从而判断对应so是否被inline hook(因为Inline hook会修改函数入口处的指令,使其跳转到自己实现到的hook函数位置)

当然so的地址也可以从linker中拿,这里不多赘述,因为核心还是对比内存和磁盘中的crc值

知道了原理,我们就可以绕过了。既然原理是对比磁盘和内存,那么不就只有两种思路吗。一是伪造内存里的so,二是伪造磁盘上的so

直接修改内存里的so,使其变得跟磁盘里的.text段一致是不太现实的,那不就相当于是绕了一大圈然后手动unhook了吗?这样做没什么意义。但是可以退一步,从如何获取内存里目标so的地址入手,上述我们获取地址的方式是扫描/proc/self/maps里目标so的名字,然后拿到可执行段的地址,那么我们只需要让app拿到的地址是指向我们自己申请的地址,同时把原本so对应的地址设置为匿名内存段即可,我们可以在申请的内存里map正常so的可执行段,这样app通过这种方式获取到的内存数据就和磁盘里的一模一样了,crc值当然也是一样的

当然如果app从linker里获取地址的话上述方法就没效果了。先简单说一下则呢么从linker里拿吧,主要就是通过解析solist_get_head函数地址,拿到soinfo链表头,然后遍历链表,通过so_name过滤目标so,从而拿到soinfo地址,根据base偏移拿到基地址。这种要绕过的话,一样可以在frida里拿到对应soinfo的地址,然后自己根据偏移把base字段的值改成自己map的内存地址就行,但是不同版本偏移可能不一样,需要手动适配或者动态计算qaq

这样做会有个弊端,正常的maps里是不会有匿名的可执行内存段的,上述方法会引入这个检测点,为了解决这个问题,可以使用我的这个工具进行隐藏Apatch内核模块分享

我个人是比较喜欢这种方式的,因为它比较统一,无论你是从maps里获取还是从linker里获取,最后都是要和磁盘的文件进行比较,我直接把磁盘的文件''替换''掉不就好了嘛。但是不能真的替换了,而是把原本的文件重定向成我们自己的,当然这种方式也不是完全没有问题,依旧有继续对抗的空间。你可以通过hook libc里的相关函数实现重定向的效果,但是以防app通过svc直接call系统调用,我就用内核模块的方式hook内核函数进行重定向了,还是这个模块Apatch内核模块分享

重定向的核心原理就是通过拦截 open系统调用对应调用链上的内核函数,修改其传递的路径参数为我们需要重定向到的路径,从而实现重定向。接下来就讲一下如何搭配上述内核模块进行crc校验的绕过

既然要绕过crc校验,那必须使磁盘so的.text段和内存so的.text段内容一致,所以在重定向之前,需要去dump内存里的so,直接整体dump即可

frida中:

其他hook框架中:

假设dump之后保存的路径为652K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4M7K6i4K6u0W2L8%4u0Y4i4K6u0r3x3e0V1&6z5q4)9J5c8V1#2S2N6r3S2Q4x3V1k6y4j5i4c8Z5e0f1H3`.">dsathsodst_path,原本so的路径为dstpath,原本so的路径为src_path,要绕过的目标app的uid为0b1K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4M7K6i4K6u0W2L8%4u0Y4i4K6u0r3x3e0V1&6z5q4)9J5c8V1#2S2N6r3S2Q4x3V1k6y4j5i4c8Z5e0f1H3`.">uidMAP:uid,我们就可以写入命令`MAP:uid,我们就可以写入命令MAP:uid:da7K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4M7K6i4K6u0W2L8%4u0Y4i4K6u0r3x3e0V1&6z5q4)9J5c8V1#2S2N6r3S2Q4x3V1k6y4j5i4c8Z5e0f1H3`.">srath:src_path:srcpath:dst_path`进行文件重定向。那么该如何写入呢?模块提供了两套通信方式,其中一种是通过字符设备,但这是面向普通用户的,因为我们注入的进程往往没有权限访问/dev下的文件,所以面向开发者设计了另一套通信方式,我们可以通过调用libc.so的getcwd函数,并把命令写入到参数中传递给内核模块

frida中使用此函数:

其他框架如dobby等,直接导入对应头文件就能使用了,不做演示

这样之后内核模块就会把特定uid对应的app访问的e56K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6i4M7K6i4K6u0W2L8%4u0Y4i4K6u0r3x3e0V1&6z5q4)9J5c8V1#2S2N6r3S2Q4x3V1k6y4j5i4c8Z5e0f1H3`.">srathsrc_path重定向到srcpath重定向到dst_path,结合上面的dump函数使用,就可以达到粗略绕过crc校验的目的

这种方式也有一个弊端,就是需要开发者自己把握写入命令的时机。我通常会在安装hook之后dump出内存中的对应so然后重定向,当然frida中这个时机似乎不是很好把控,我们可以先找出是哪个so进行了crc校验(hook linker看看加载到哪个so时进程挂了,大概就是在那个so里进行的检测),然后hook android_dlopen_ext,当加载到该so时,然后再dump+重定向。这样就可以过掉它的检测了~记得使用完要手动clear一下内核模块里的数据,否则可能会影响app的下一次启动

在frida中完整的使用流程:

当然真实情况可能更加复杂,但是思路大概应该也许可能是一样的,通过这种方式应该是可以过掉的

同时你也可以像处理selinux那样,在你要执行的特殊操作前后先重定向,再结束操作后解除重定向,总之就是比较自由

在dobby中使用就更简单了,在安装hook之后立马重定向即可(不过如果app的正常操作中使用到了对应文件,那app是会受影响的,解决方案就是hook正常的操作,然后先解除重定向,然后再leave时安装重定向,frida同理)

似乎绕了一圈下来,这个方法也不是那么简单哈哈哈哈,还是没能实现“静默”的绕过,需要使用者自行把握时机。再次抛砖引玉,欢迎大家批评指正,提出更好的方案( 3ω3)

上传的apk是用于检测是否被注入的,感兴趣的可以过一下,都是常规检测点

int check_library_integrity(const char* soname) {
    if (!soname) {
        LOGE("Invalid soname parameter");
        return DETECT_RESULT_ERROR;
    }
 
    // 初始化CRC表
    if (!is_crc32_table_initialized())
        generate_crc32_table();
     
 
    // 遍历maps获取目标so可执行段信息
    map_info_t exec_info = find_memory_mapping(soname, "x");
    if (!exec_info.is_valid) {
        LOGE("Failed to find executable mapping for %s", soname);
        return DETECT_RESULT_ERROR;
    }
 
    int fd = open(exec_info.pathname, O_RDONLY);
    if (fd < 0) {
        LOGE("Failed to open %s: %s", exec_info.pathname, strerror(errno));
        return DETECT_RESULT_ERROR;
    }
 
    struct stat st;
    if (fstat(fd, &st) != 0) {
        LOGE("Failed to get file stats: %s", strerror(errno));
        close(fd);
        return DETECT_RESULT_ERROR;
    }
 
    void* file_data = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
    if (file_data == MAP_FAILED) {
        LOGE("Failed to mmap file: %s", strerror(errno));
        close(fd);
        return DETECT_RESULT_ERROR;
    }
 
    close(fd);
 
    // 解析ELF文件
    Elf64_Ehdr* ehdr = (Elf64_Ehdr*)file_data;
    if (memcmp(ehdr->e_ident, ELFMAG, SELFMAG) != 0) {
        LOGE("Invalid ELF file");
        munmap(file_data, st.st_size);
        return DETECT_RESULT_ERROR;
    }
 
    Elf64_Phdr* phdr = (Elf64_Phdr*)(file_data + ehdr->e_phoff);
    int result = DETECT_RESULT_CLEAN;
 
    // 检查可执行段
    for (int i = 0; i < ehdr->e_phnum; i++) {
        if (phdr[i].p_type == PT_LOAD && (phdr[i].p_flags & PF_X) &&
            phdr[i].p_offset == exec_info.offset) {
 
            uint32_t disk_crc = crc32((uint8_t*)file_data + phdr[i].p_offset, phdr[i].p_memsz);
            uint32_t mem_crc = crc32((uint8_t*)exec_info.start, phdr[i].p_memsz);
 
            if (disk_crc != mem_crc) {
                LOGD("%s executable segment modified: disk=%08x, mem=%08x",
                     soname, disk_crc, mem_crc);
                result = DETECT_RESULT_SUSPICIOUS;
            }
            break;
        }
    }
 
    munmap(file_data, st.st_size);
    return result;
}
int check_library_integrity(const char* soname) {
    if (!soname) {
        LOGE("Invalid soname parameter");
        return DETECT_RESULT_ERROR;
    }
 
    // 初始化CRC表
    if (!is_crc32_table_initialized())
        generate_crc32_table();
     
 
    // 遍历maps获取目标so可执行段信息
    map_info_t exec_info = find_memory_mapping(soname, "x");
    if (!exec_info.is_valid) {
        LOGE("Failed to find executable mapping for %s", soname);
        return DETECT_RESULT_ERROR;
    }
 
    int fd = open(exec_info.pathname, O_RDONLY);
    if (fd < 0) {
        LOGE("Failed to open %s: %s", exec_info.pathname, strerror(errno));
        return DETECT_RESULT_ERROR;
    }
 
    struct stat st;
    if (fstat(fd, &st) != 0) {
        LOGE("Failed to get file stats: %s", strerror(errno));
        close(fd);
        return DETECT_RESULT_ERROR;
    }
 
    void* file_data = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
    if (file_data == MAP_FAILED) {
        LOGE("Failed to mmap file: %s", strerror(errno));
        close(fd);
        return DETECT_RESULT_ERROR;
    }
 
    close(fd);
 
    // 解析ELF文件
    Elf64_Ehdr* ehdr = (Elf64_Ehdr*)file_data;
    if (memcmp(ehdr->e_ident, ELFMAG, SELFMAG) != 0) {
        LOGE("Invalid ELF file");
        munmap(file_data, st.st_size);
        return DETECT_RESULT_ERROR;
    }
 
    Elf64_Phdr* phdr = (Elf64_Phdr*)(file_data + ehdr->e_phoff);
    int result = DETECT_RESULT_CLEAN;
 
    // 检查可执行段
    for (int i = 0; i < ehdr->e_phnum; i++) {
        if (phdr[i].p_type == PT_LOAD && (phdr[i].p_flags & PF_X) &&
            phdr[i].p_offset == exec_info.offset) {
 
            uint32_t disk_crc = crc32((uint8_t*)file_data + phdr[i].p_offset, phdr[i].p_memsz);
            uint32_t mem_crc = crc32((uint8_t*)exec_info.start, phdr[i].p_memsz);
 
            if (disk_crc != mem_crc) {
                LOGD("%s executable segment modified: disk=%08x, mem=%08x",
                     soname, disk_crc, mem_crc);
                result = DETECT_RESULT_SUSPICIOUS;
            }
            break;
        }
    }
 
    munmap(file_data, st.st_size);
    return result;
}
function dumpModule(soName = '', output_path = '') {
    var module = Process.getModuleByName(soName);
    if (module === null) {
        console.log("[!] Module not found: " + soName);
        return;
    }
 
    console.log("[*] Found module: " + module.name);
    console.log("[*] Base address: " + module.base);
    console.log("[*] Size: " + module.size);
 
    // 读取内存内容
    try {
        var buffer = module.base.readByteArray(module.size);
        console.log("[*] Successfully read " + module.size + " bytes");
 
        // 保存到文件
        var file = new File(output_path, "wb");
        file.write(buffer);
        file.close();
        console.log("[*] Dump saved to: " + output_path);
    } catch (e) {
        console.log("[!] Error: " + e);
    }
}
function dumpModule(soName = '', output_path = '') {
    var module = Process.getModuleByName(soName);
    if (module === null) {
        console.log("[!] Module not found: " + soName);
        return;
    }
 
    console.log("[*] Found module: " + module.name);
    console.log("[*] Base address: " + module.base);
    console.log("[*] Size: " + module.size);
 
    // 读取内存内容
    try {
        var buffer = module.base.readByteArray(module.size);
        console.log("[*] Successfully read " + module.size + " bytes");
 
        // 保存到文件
        var file = new File(output_path, "wb");
        file.write(buffer);
        file.close();
        console.log("[*] Dump saved to: " + output_path);
    } catch (e) {
        console.log("[!] Error: " + e);
    }
}
int dump_memory(int pid, uint64_t start, uint64_t size, uint64_t file_offset , const char *output_file) {
    char mem_path[MAX_LINE];
    snprintf(mem_path, sizeof(mem_path), MEM, pid);
 
    int mem_fd = open(mem_path, O_RDONLY);
    if (mem_fd < 0)
        return 1;
 
    int out_fd = open(output_file, O_RDWR);
    if (out_fd < 0) {
        close(mem_fd);
        return 1;
    }
 
    if (lseek(out_fd, file_offset, SEEK_SET) == -1) {
        close(mem_fd);
        close(out_fd);
        return 1;
    }
 
    char buffer[BUF_SIZE];
    uint64_t remaining = size;
    uint64_t offset = start;
    ssize_t total_written = 0;
 
    while (remaining > 0) {
        size_t to_read = remaining > sizeof(buffer) ? sizeof(buffer) : remaining;
        ssize_t bytes_read = pread(mem_fd, buffer, to_read, offset);
        if (bytes_read < 0)
            break;
 
        if (bytes_read == 0)
            break;
 
        ssize_t bytes_written = write(out_fd, buffer, bytes_read);
        if (bytes_written != bytes_read)
            break;
 
        total_written += bytes_written;
        remaining -= bytes_read;
        offset += bytes_read;
    }
 
    close(mem_fd);
    close(out_fd);
 
    if (total_written == size) {
        printf("Memory dumped to %s at offset %llx successfully, total bytes: %lld\n",
               output_file, file_offset, (long long)total_written);
        return 0;
    } else {
        printf("Memory dump incomplete, wrote %lld of %lld bytes at offset %llx\n",
               (long long)total_written, (long long)size, file_offset);
        return 1;
    }
}
int dump_memory(int pid, uint64_t start, uint64_t size, uint64_t file_offset , const char *output_file) {
    char mem_path[MAX_LINE];
    snprintf(mem_path, sizeof(mem_path), MEM, pid);
 
    int mem_fd = open(mem_path, O_RDONLY);
    if (mem_fd < 0)
        return 1;
 
    int out_fd = open(output_file, O_RDWR);
    if (out_fd < 0) {
        close(mem_fd);
        return 1;
    }
 
    if (lseek(out_fd, file_offset, SEEK_SET) == -1) {
        close(mem_fd);
        close(out_fd);
        return 1;
    }

传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!

上传的附件:
收藏
免费 240
支持
分享
最新回复 (176)
雪    币: 2660
活跃值: (7264)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
学习
2025-8-15 19:57
0
雪    币: 204
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
3
学习
2025-8-15 20:32
0
雪    币: 23
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
4
1
2025-8-15 20:37
0
雪    币: 43
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
5
wow
2025-8-15 21:00
0
雪    币: 494
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
6
直接重定向文件也是能够被app通过fd获取真实打开的文件名来检测到吧
2025-8-15 21:18
1
雪    币: 2334
活跃值: (3970)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7
666
2025-8-15 21:44
0
雪    币: 1907
活跃值: (1519)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
8
安卓逆向test 直接重定向文件也是能够被app通过fd获取真实打开的文件名来检测到吧
当然,而且不止这一种检测方式。但是依旧可以处理
2025-8-15 21:57
0
雪    币: 7
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
9
大佬,yyds
2025-8-15 22:38
0
雪    币: 1
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
10
yyds
2025-8-15 22:52
0
雪    币: 1
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
11
先dump,再就是io重定向实现绕过
2025-8-15 23:03
0
雪    币: 97
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
12
大佬
2025-8-15 23:14
0
雪    币: 8234
活跃值: (4768)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
13
学习学习
2025-8-15 23:58
0
雪    币: 156
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
14
大佬,dbus通信检测怎么过啊,它向所有端口发认证请求,一定会返回REJECT,然后必被检测
2025-8-16 06:34
0
雪    币: 2188
活跃值: (599)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
15
学习
2025-8-16 07:05
0
雪    币: 260
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
16
1
2025-8-16 10:00
0
雪    币: 7373
活跃值: (10784)
能力值: ( LV17,RANK:797 )
在线值:
发帖
回帖
粉丝
17
区间内存地址不一定从 maps 和 linker 获取。
crc 原始值也不一定从文件读取计算,可以前后对比,不涉及任何系统调用。
crc 值的比较也不一定本地实现,可以作为密钥或直接作为设备指纹信息的一种。
2025-8-16 10:16
5
雪    币: 220
活跃值: (56)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
18
1
2025-8-16 10:33
0
雪    币: 1907
活跃值: (1519)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
19
无名侠 区间内存地址不一定从 maps 和 linker 获取。 crc 原始值也不一定从文件读取计算,可以前后对比,不涉及任何系统调用。 crc 值的比较也不一定本地实现,可以作为密钥或直接作为设备指纹 ...
感谢大佬补充
2025-8-16 10:58
0
雪    币: 426
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
20
7
2025-8-16 12:23
0
雪    币: 4834
活跃值: (4602)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
21
11111111111
2025-8-16 15:15
0
雪    币: 3036
活跃值: (3889)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
22
666
2025-8-16 15:40
0
雪    币: 20
活跃值: (1330)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
23
6666
2025-8-17 03:40
0
雪    币: 21
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
24
666
2025-8-17 18:53
0
雪    币: 9148
活跃值: (5152)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
25
有的检测是通过计算so中某些片段的crc值传递给服务器当作token比如nexon的ngsm,所以伪造磁盘上的so可能没用
2025-8-17 19:47
2
游客
登录 | 注册 方可回帖
返回