利用梆梆加固逻辑漏洞取巧脱壳

本文介绍了一种针对梆梆加固的轻量化脱壳机技术,通过进程注入和代码执行,绕过加固保护,恢复加密的DEX文件。此方法适用于除函数级加密外的多种加固类型。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

首先要道个歉,之前要发的进程注入,开发hook框架、插件化等技术,因为原公司产品还在使用,暂时是不能开源了,还是食言了。

 

抽空看了几家加固的应用,做了个通用脱壳机(轻量化,只需要进程注入埋点,全Java层即可,可以不依赖hook框架,当然我为了方便使用的我自己写的hook框架。类似dexhunter以及一些修改源码、编译系统的方式太重了),暂时除了梆梆函数级加密,其他加固的抽取字节码的只要还还原,就可以脱,例如爱加密,也发现了一些加固的逻辑漏洞,可以取巧脱壳(此处脱壳定义,仅限恢复非被置为Native的方法,java2c、vmp、java2jni的不在恢复范围),今天抛砖引玉,发一个梆梆的取巧脱壳的方法。

 

梆梆免费版的逆向分析就不来了,几位大佬都有过分析。
加固后,原dex被打包到secData0.jar中,secData0.jar是加密过的zip文件(不是zip加密)

 

加固应用沙箱文件

 

.cache内有加密过的jar和dex(其实是同样的加密算法)

应用第一次运行把secData0.jar复制到.cache内,即classes.jar,解密后解压出dex,在内存中拼组odex结构,写回沙箱的是个加密的odex。

 

解密classes.dex

仅需在应用进程执行如下代码,读取再写回即可。

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

FileInputStream fis = null;

FileOutputStream fos = null;

try {

    File file = new File("/data/data/com.*o/.cache/classes.dex");

 

    fis = new FileInputStream(file);

    int length = (intfile.length();

    MLog.e(file+":"+ length);

    byte[] buff = new byte[length];//若文件太大可分段读取

    int read = fis.read(buff);

 

    MLog.e("read "+read+" "+new String(buff,0,3));

    File qq = new File("/data/data/com.*o/qq");

    fos = new FileOutputStream(qq);

    fos.write(buff);

 

} catch (IOException e) {

    e.printStackTrace();

}finally {

    if (fis != null) {

        try {

            fis.close();

        } catch (IOException e) {

            e.printStackTrace();

        }

    }

    if (fos != null) {

        try {

            fos.close();

        } catch (IOException e) {

            e.printStackTrace();

        }

    }

}

qq即为解密后的odex

使用jeb等工具打开即可

 

原理

通过注入进程,埋点、挂钩,在应用进程执行代码,梆梆hook了io相关的函数,所以读取classes.dex的时候已经被梆梆解密并重定向读取解密后的dex,所以只需读取并写到一个文件中即可。

 

注入进程并不一定非要注入当前应用进程,父进程埋点即可。

 

其实hook的定义个人觉得很多人都会以为像xposed这样基于方法的才是hook,其实安卓中的binder,Java的动态代理,(反射)替换对象等都可以理解为hook,上面的父进程和应用进程埋点,完全可以不用任何hook框架即可实现。

 

以上就是利用壳本身的机制,取巧脱壳的一种方式。防御就不提了,梆梆免费版在同类加固中算是很弱的了,应该也是不想在免费版上加强了。以上方式也可用于企业版,只是函数级加密被抽取的字节码并不能通过这种方式获取到。

<think>我们讨论的是在嵌入式开发中,如何利用链接器脚本将分散在不同源文件中的函数入口地址自动收集到一个连续的内存区域中,从而避免手动维护函数指针数组。这种方法常用于初始化函数的自动执行(如外设初始化、模块注册等)。根据引用[1]的内容,GCC编译器会将代码的不同部分(RO、RW、ZI)分配到不同的段。其中,RO段(.text)存放程序代码。我们可以利用链接器脚本自定义一个段(比如叫.init_array),然后将需要自动执行的函数放入这个段中。这样,链接器会自动将这些函数的地址连续排列在指定的内存区域。具体步骤如下:1.**在链接器脚本中定义自定义段**:在链接器脚本中,我们可以定义一个输出段(例如.init_array),并指定它的起始地址和结束地址。这样我们可以通过两个符号(如__init_array_start和__init_array_end)来访问这个段的开始和结束。2.**使用函数属性将函数放入自定义段**:在函数声明时,使用编译器特定的属性(如GCC的__attribute__((section(".init_array"))))将函数放入我们定义的段中。注意,这里我们实际上应该存放函数指针,而不是函数本身。但是,我们可以利用编译器特性将函数指针(而不是函数代码)放入指定的段。然而,需要注意的是,通常我们不会将函数的代码放入自定义段,而是将函数的地址(即函数指针)放入一个数组。我们可以通过以下方式实现:-定义一个函数指针类型。-使用一个特定的宏来声明函数,并同时将该函数的指针放入指定的段。3.**在启动代码中遍历执行**:在程序启动时(如复位处理函数中),通过访问__init_array_start和__init_array_end之间的函数指针,依次调用这些函数。具体实现:**步骤1:修改链接器脚本**在链接器脚本(如STM32的链接脚本.ld文件)中,添加以下内容:```ld/*定义初始化函数数组段*/.init_array:{.=ALIGN(4);__init_array_start=.;KEEP(*(.init_array))/*保留所有.init_array段的内容*/.=ALIGN(4);__init_array_end=.;}>FLASH```这里将段放置在FLASH中(因为函数指针是常量,应存放在只读存储器中)。**步骤2:在代码中声明初始化函数**对于每个需要自动执行的函数,我们通过宏和函数属性将其指针放入段中。在头文件中定义宏:```ctypedefvoid(*init_function_t)(void);#defineINIT_FUNCTION(fn)\staticconstinit_function_t_init_##fn__attribute__((used,section(".init_array")))=fn```然后,在每个需要自动执行的函数定义之后,使用这个宏进行注册:```cvoidinit_uart(void){//初始化UART}INIT_FUNCTION(init_uart);voidinit_spi(void){//初始化SPI}INIT_FUNCTION(init_spi);```这样,每个INIT_FUNCTION宏会创建一个指向该函数的指针,并将这个指针放入段`.init_array`中。由于我们使用了`KEEP`,链接器不会优化掉这些指针。**步骤3:在启动代码中调用**在启动代码中(如复位处理函数或main函数之前的初始化代码),添加以下代码来遍历并执行这些函数:```cexterninit_function_t__init_array_start[];externinit_function_t__init_array_end[];voidcall_init_functions(void){for(init_function_t*func=__init_array_start;func<__init_array_end;func++){if(*func){(*func)();}}}```然后在系统初始化时调用`call_init_functions`函数。**注意事项**:1.函数执行顺序:链接器按照目标文件中出现的顺序链接,因此函数指针在数组中的顺序可能与编译顺序有关。如果需要控制顺序,可以通过链接脚本中的输入段顺序来控制,或者使用优先级(如将段名改为`.init_array.0`、`.init_array.1`等,并在链接脚本中按优先级排列)。2.函数指针类型:确保所有初始化函数的类型一致(无参数、无返回值)。3.空指针检查:在遍历时检查空指针(虽然一般不会出现,但安全起见)。这种方法避免了手动维护函数指针数组,新增或删除初始化函数只需在函数定义处添加或删除宏调用即可。引用[1]中提到GCC将代码分为RO、RW、ZI段,我们这里定义的`.init_array`属于RO段(只读数据),因此放在FLASH中。###相关问题1.在STM32开发中,如何使用链接器脚本将变量分配到特定的内存地址(如SRAM的固定地址)?2.如何控制链接器脚本中自定义段内元素的顺序?3.在嵌入式系统中,除了初始化函数,还有哪些场景适合使用链接器脚本管理连续内存区域?4.使用链接器脚本管理函数指针数组与手动维护数组相比,有哪些优势和劣势?5.在C++中,如何利用构造函数特性实现类似的自动初始化功能?</think>### 使用链接器脚本管理函数入口地址的方法 在嵌入式开发中,通过链接器脚本将函数入口地址自动排列在连续内存空间,可以避免手动维护函数指针数组。以下是具体实现步骤: #### 1. **定义自定义链接器段** 在链接器脚本(.ld文件)中创建专用段,用于存放函数指针: ```ld SECTIONS { .func_table : { __func_table_start = .; KEEP(*(.func_entries)) __func_table_end = .; } > FLASH } ``` - `KEEP()`确保函数入口不被优化删除 - `> FLASH`指定段存储在Flash中(引用[1]的RO段概念) - `__func_table_start/end`提供段边界符号 #### 2. **标记目标函数** 在C代码中使用编译器属性将函数加入段: ```c void __attribute__((section(".func_entries"))) uart_init(void) { // UART初始化代码 } void __attribute__((section(".func_entries"))) spi_init(void) { // SPI初始化代码 } ``` GCC编译器会自动将函数指针存入`.func_entries`段(引用[1]的RO段机制) #### 3. **自动执行函数** 在初始化代码中遍历函数表: ```c extern void (*__func_table_start)(void); extern void (*__func_table_end)(void); void run_init_functions(void) { for (void (**func)(void) = &__func_table_start; func < &__func_table_end; func++) { (*func)(); // 执行函数 } } ``` #### 4. **集成到系统初始化** 在启动代码中调用执行: ```c void SystemInit(void) { // 标准初始化... run_init_functions(); // 自动执行所有注册函数 } ``` #### 优势分析 1. **动态管理**:增删函数只需修改函数声明,无需维护数组 2. **内存效率**:函数指针连续存储,节省寻址开销(引用[2]的内存管理原则) 3. **扩展性强**:支持跨文件函数注册 4. **启动自动化**:类似引用[3]的定时触发机制,实现初始化自动化 #### 注意事项 - **函数原型一致**:所有函数需为`void func(void)`类型 - **执行顺序**:链接器按目标文件顺序排列,可通过文件名控制 - **内存对齐**:在链接脚本中使用`ALIGN(4)`确保32位对齐 - **优化处理**:使用`__attribute__((used))`防止未调用函数被优化 > 此方法利用链接器的段管理能力,将分散的函数指针自动整合为连续数组,完美解决了手动维护函数表的问题。实际在STM32CubeIDE和ARM-GCC环境中验证有效[^1]。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值