吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 1678|回复: 30
上一主题 下一主题
收起左侧

[Android 脱壳] 360分析复现(中)

  [复制链接]
跳转到指定楼层
楼主
chenchenchen777 发表于 2024-12-30 14:18 回帖奖励
本帖最后由 chenchenchen777 于 2024-12-30 16:55 编辑

360数字加固解析

这里把之前早期的照片补上了,文章是复现于oacia的APP加固dex解密流程分析,并且发现了其他不同的地方进行的补充和说明

加固特征:

application 的第一入口是android:name="com.stub.StubApp"

Application 的 onCreate 和 attachBaseContext 是 Application 的两个回调方法,通常我们会在其中做一些初始化操作,attachBaseContext 在 onCreate 之前执行

a.b():

这里其实是有对应的字符串加密的,在对应的com.qihoo.util.a里面有着对应的解密函数,应该是在执行过程中对于字符串进行动态解密的

这里通过jeb的脚本功能,去实现指令级别的脚本编写,当函数去调用这个解密函数时提前把字符串进行解密的同时直接进行赋值

JEB脚本:

from com.pnfsoftware.jeb.client.api import IScript, IconType, ButtonGroupType
from com.pnfsoftware.jeb.core import RuntimeProjectUtil
from com.pnfsoftware.jeb.core.units.code.java import IJavaSourceUnit
from com.pnfsoftware.jeb.core.units.code import ICodeUnit, ICodeItem
from com.pnfsoftware.jeb.core.output.text import ITextDocument
from com.pnfsoftware.jeb.core.units.code.java import IJavaSourceUnit, IJavaStaticField, IJavaNewArray, IJavaConstant, IJavaCall, IJavaField, IJavaMethod, IJavaClass
from com.pnfsoftware.jeb.core.events import JebEvent, J
from com.pnfsoftware.jeb.core.util import DecompilerHelper
methodName = ['Lcom/qihoo/util/a;', 'a']

class dec_str_360jiagu(IScript):
        def run(self, ctx):
                print('start deal with strings')
                self.ctx = ctx
                engctx = ctx.getEnginesContext()
                if not engctx:
                        print('Back end engines not initialized')
                        return
                projects = engctx.getProjects()
                if not projects:
                        print('There is no opened project')
                        return
                units = RuntimeProjectUtil.findUnitsByType(projects[0], IJavaSourceUnit, False)
                for unit in units:
                        javaClass = unit.getClassElement()
                        print('[+] decrypt:' + javaClass.getName())
                        self.cstbuilder = unit.getFactories().getConstantFactory()
                        self.processClass(javaClass)
                        unit.notifyListeners(JebEvent(J.UnitChange))
                print('Done.')

        def processClass(self, javaClass):
                if javaClass.getName() == methodName[0]:
                        return
                for method in javaClass.getMethods():  #遍历类方法
                        block = method.getBody()#代码块
                        i = 0
                        while i < block.size():
                                stm = block.get(i)#代码
                                self.checkElement(block, stm)
                                i += 1

                /*block.get(i): block.get(i) 根据索引 i 返回 block 中的第 i 条指令。这些指令可能是:
                    IJavaCall:方法调用指令。
                    IJavaAssignment:赋值指令。
                    IJavaCondition:条件判断(例如 if 语句)指令。
                    IJavaLoop:循环(例如 while、for 等)指令。
                    IJavaReturn:返回指令。  */

        def checkElement(self, parent, e):
                try:
                        if isinstance(e, IJavaCall):#假如这里的指令是IJavaCall,那么它就有getMethod()方法了,这个方法:它返回的是与此调用相关的 IJavaMethod 对象(即表示被调用方法的对象)
                                mmethod = e.getMethod()
                                mname = mmethod.getName()#  例如 someMethod
                                msig = mmethod.getSignature()  #例如 someMethod(int, String)
                                if mname == methodName[1] and methodName[0] in msig:  #函数名是a,对应类是Lcom/qihoo/util/a;
                                        v = []
                                        for arg in e.getArguments():
                                                if isinstance(arg, IJavaConstant):  #IJavaConstant是字面量,返回的是arg是不是常量的Boolean值
                                                        v.append(arg.getString())
                                        if len(v) == 1:
                                                decstr = self.decryptstring(v[0])  #解密 
                                                parent.replaceSubElement(e, self.cstbuilder.createString(decstr))   #直接把需要调用解密代码的字符串进行解密,然后修改调用函数,直接替换成解密之后的字符串
                        for subelt in e.getSubElements():
                                if isinstance(subelt, IJavaClass) or isinstance(subelt, IJavaField) or isinstance(subelt, IJavaMethod):
                                        continue
                                self.checkElement(e, subelt)   #递归调用,上面在判定是否当前这个函数有子函数,有就进行递归处理
                except:
                        print('error')

        def decryptstring(self, string):
                src = []
                for index, char in enumerate(string):
                        src.append(chr(ord(char) ^ 16))
                return ''.join(src).decode('unicode_escape')

通过这个JEB脚本已经可以把上面的字符串来进行解密了,但是我试过了虽然已经打印了done,但是还是没用,修改了JEB代码也没用,于是我去下载了JEB5.18版本,这个最新版本可以直接把字符串混淆除去了。

之后就是看attachBaseContext类了,这里是能够控制执行的早于JNI_OnLoad的位置

这里去判定之后去实现了加载不同的so文件

我们直接去看对应的libjiagu_a64.so——>对应的应该是arm64的so

正常情况下应该去看导出表和导入表,看看最可能出现的关键函数,但是这里的导出表和导入表是空的,那么就说明这里不是通过静态链接来到,而是通过动态加载时确定的导出表和导入表的函数地址的,所以我们要dump下在执行过程中的so文件![image-20241212115609375]

这里的dump_so和脱壳的原理是一样的,通过去找到对应的文件起始地址和文件的大小来进行对于dump文件的写入

dump_so:

通过遍历module去确定是dlopen加载的,通过获取对应的so文件的相关信息并进行dump

function Hook_dlopen(target, soName, callback) {
    Interceptor.attach(target, {
        onEnter: function (args) {
            var pathptr = args[0]; // 获取库路径参数,android_dlopen_ext("libjiagu_64.so") or dlopen("libjiagu_64.so")
            if (pathptr !== undefined && pathptr != null) {
                var path = ptr(pathptr).readCString();
                if (path.indexOf(soName) >= 0) {
                    this.is_can_hook = true;
                    // 如果库路径中包含目标库名,执行回调函数
                    // callback(path);
                }
            }
        },
        onLeave: function (retval) {
            if(this.is_can_hook) {
                callback("libjiagu_64.so");
            }
            // 这里可以处理函数返回值
        }
    });
}
var dlopen = Module.findExportByName(null, "dlopen");
var android_dlopen_ext = Module.findExportByName(null, "android_dlopen_ext");
Hook_dlopen(dlopen,"libjiagu_64.so",dump_so)
Hook_dlopen(android_dlopen_ext,"libjiagu_64.so",dump_so)

function dump_so(so_name) {
    // 获取加载的模块信息
    var libso = Process.getModuleByName(so_name);
    console.log("[name]:", libso.name);
    console.log("[base]:", libso.base);
    console.log("[size]:", ptr(libso.size));
    console.log("[path]:", libso.path);

    // 构造文件路径
    var file_path = "/data/data/com.dytool/" + libso.name + "_" + libso.base + "_" + ptr(libso.size) + ".so";
    var file_handle = new File(file_path, "wb");

    // 如果文件句柄有效,进行内存保护并导出文件
    if (file_handle && file_handle != null) {
        Memory.protect(ptr(libso.base), libso.size, 'rwx'); // 设置内存区域的可读写执行权限
        var libso_buffer = ptr(libso.base).readByteArray(libso.size); // 读取内存内容
        file_handle.write(libso_buffer); // 写入文件
        file_handle.flush(); // 刷新文件缓冲
        file_handle.close(); // 关闭文件
        console.log("[dump]:", file_path); // 输出文件保存路径
    }
}

同时这里需要算法转发,把frida中对应的获取的sodump下的数据转发到本机上面进行

adb shell
./data/local/tmp/fsarm64 -l 0.0.0.0:1234
adb forward tcp:1234 tcp:1234
frida -H 127.0.0.1:1234 -l .\hook.js -f "com.oacia.apk_protect"

frida反调试:

这里面有关于frida的反调试的信息,其中的主要的反调试检测其实是对于frida的部分检测了,我们通过在dlopen加载了对应的libjiagu_a64.so的so中去查看对应可能出现的open函数,查看

// 挂钩 android_dlopen_ext 函数,当加载特定动态库时进行处理
function my_hook_dlopen(soName = '') {
    // 找到 android_dlopen_ext 函数的地址并挂钩
    Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"), {
        onEnter: function (args) {
            // 获取加载的库路径
            var pathptr = args[0];
            if (pathptr !== undefined && pathptr != null) {
                var path = ptr(pathptr).readCString();
                // 如果路径中包含目标动态库名,则标记为可以进行 hook
                    if(path.indexOf("libjiagu_64") !== -1) {
                        this.is_can_hook = true;
                        console.log("hook libjiagu_64 started ......");
                        // 执行 Hook 任务
                        //...
                    }

            }
        },
        onLeave: function (retval) {
            // 如果满足条件,执行 dump 操作
            if (this.is_can_hook) {
                // dump_so(soName); // Dump 目标 SO 文件
                hook_open();

            }
        }
    });
}

// Dump SO 文件到指定目录
function dump_so(so_name) {
    try {
        // 获取指定 SO 模块的相关信息
        var libso = Process.getModuleByName(so_name);
        console.log("[name]:", libso.name);
        console.log("[base]:", libso.base);
        console.log("[size]:", ptr(libso.size));
        console.log("[path]:", libso.path);

        // 设置 dump 文件的保存路径
        var file_path = "/data/data/com.oacia.apk_protect/" + libso.name + "_" + libso.base + "_" + ptr(libso.size) + ".so";

        // 创建文件句柄并准备写入
        var file_handle = new File(file_path, "wb");
        if (file_handle != null) {
            // 修改内存权限为可读写执行
            Memory.protect(ptr(libso.base), libso.size, 'rwx');

            // 读取内存中的 SO 数据
            var libso_buffer = ptr(libso.base).readByteArray(libso.size);

            // 写入到文件
            file_handle.write(libso_buffer);
            file_handle.flush();
            file_handle.close();

            console.log("[dump]:", file_path); // 输出 dump 文件路径
        }
    } catch (e) {
        console.error("Error during dump: " + e.message);
    }
}

// 延时执行 hook 操作
setImmediate(function () {
    my_hook_dlopen("libjiagu_64.so"); // 传入目标库名
});
function hook_open(){
    var pth = Module.findExportByName(null,"open");
  Interceptor.attach(ptr(pth),{
      onEnter:function(args){
          this.filename = args[0];
          console.log("",this.filename.readCString())
      },onLeave:function(retval){
      }
  })
}

这里就可以返回对应的open的参数是什么了

这里全是open读取的内容其中最需要注意了的就是这里读取了这么多的maps了,因为这里大概率是frida检测的了

frida检测

扫描maps文件
maps文件用于显示当前app中加载的依赖库
Frida在运行时会先确定路径下是否有re.frida.server文件夹
若没有则创建该文件夹并存放frida-agent.so等文件,该so会出现在maps文件中
还有几个相关文件也会出现在maps里面的

首先是获取正常的,在没有进行frida注入的maps,因为这里是需要对应的程序的maps来进行相对数据的获取的,所以需要一个正常的maps,然后我们进行对应open函数参数的重定位,实现利用open函数来读取正常的maps文件

cp /proc/self/maps /data/data/com.oacia.apk_protect/maps

重定向frida脚本

function hoook_proc_self_maps()
{
    const openPtr = Module.findExportByName(null,"open");
    const open_old = new NativeFunction(openPtr,'int',['pointer', 'int']); //创建一个原函数指针,用于之后返回原函数
    var fakePath = "/data/data/com.oacia.apk_protect/maps";                //设置对应的fakepath
    Interceptor.replace(openPtr,new NativeCallback(function(pathnameptr,flag){
        var pathname = Memory.readUtf8String(pathnameptr);
        console.log("open:",pathname);
        if(pathname.indexOf("maps") >= 0){
            console.log("find",pathname,",redirect to",fakePath);
            var filename = Memory.allocUtf8String(fakePath);
            return open_old(filename,flag); //设置我们cp下的maps
        }else{
            return open_old(pathname,flag);//原函数
        }
    }, 'int', ['pointer', 'int']));
}

特殊情况

我这里出现的情况和原作者是不一样的,再重定向了maps文件之后其实是并没有程序退出的,反而是进行了下去的


看着这里啊,我们之前分析的时候为什么没有去分析这里出现的libcjgdtc.so????

在加载库之后其实是进行了DtcLoader.init()的加载的,这里是去加载了对应的so文件的,但是我们是没有看到的

这里在这个类的是有加载这个库的,但是我们没有看到的,但是刚刚我们看到了这里是去打开了这里的libjgdtc.so的,那么就说明这里是被之后创建出来了的

同时去看了对应位置的,也是没有的,说明是加载之后会被删除的,这里我尝试了再一次进行重定位的时候发现这一次就蹦掉了

所以应该是还是会中断的,当这个maps里面的映射的内存有某些部分被检测或者是被读取的时候,检查到了不匹配就直接中断了,于是我就又一次去复制了一下对应程序的maps,就又可以执行下去了

但是这里我是想把那个之前open 的/libjgdtc.so给dump下来的,于是我在my_hook_dlopen里面添加了对于libjgdtc.so的so_dump,但是一直报错,应该是在加载了这个so之后就直接删除了。

function my_hook_dlopen(soName = '') {
    var dlopenPtr = Module.findExportByName(null, "dlopen");
    if (dlopenPtr) {
        Interceptor.attach(dlopenPtr, {
            onEnter: function (args) {
                // 获取加载的库路径
                var pathptr = args[0];
                if (pathptr !== undefined && pathptr != null) {
                    var path = ptr(pathptr).readCString();
                    if (path.indexOf(soName) !== -1) {
                        this.is_can_hook = true;
                        console.log("dlopen hook triggered: " + path);
                    }
                    if (path.indexOf("libjgdtc.so") !== -1) {
                        console.log("Dumping libjgdtc.so...");
                        dump_so("libjgdtc.so"); // 直接进行 dump
                    }
                }
            },
            onLeave: function (retval) {
                // 如果满足条件,执行 dump 操作
                if (this.is_can_hook) {
                    console.log("Dumping library: " + soName);

                }
            }
        });
    } 
    // 找到 android_dlopen_ext 函数的地址并挂钩
    Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"), {
        onEnter: function (args) {
            // 获取加载的库路径
            var pathptr = args[0];
            if (pathptr !== undefined && pathptr != null) {
                var path = ptr(pathptr).readCString();
                // 如果路径中包含目标动态库名,则标记为可以进行 hook
                    if(path.indexOf("libjiagu_64") !== -1) {
                        this.is_can_hook = true;
                        console.log("hook hook_proc_self_maps started ......");
                        // 执行 Hook 任务
                        //...
                    }
                    if (path.indexOf("libjgdtc.so") !== -1) {
                        console.log("Dumping libjgdtc.so...");
                        dump_so("libjgdtc.so"); // 直接进行 dump
                    }
            }
        },
        onLeave: function (retval) {
            // 如果满足条件,执行 dump 操作
            if (this.is_can_hook) {
                // dump_so(soName); // Dump 目标 SO 文件
                // hook_open();
                hook_proc_self_maps()
            }
        }
    });
}

又尝试了在open函数执行之前去dump so

function hook_proc_self_maps() {
    const openPtr = Module.getExportByName(null, 'open');
    const open = new NativeFunction(openPtr, 'int', ['pointer', 'int']);//获取原函数指针
    var fakePath = "/data/data/com.oacia.apk_protect/maps";
    // var fakePath ="/data/data/com.oacia.apk_protect/maps_nonexistent"
    Interceptor.replace(openPtr, new NativeCallback(function (pathnameptr, flag) {
        var pathname = Memory.readUtf8String(pathnameptr);
        console.log("open",pathname);
        if (pathname.indexOf("maps") >= 0) {
            console.log("find ",pathname," redirect to ——>",fakePath);
            var filename = Memory.allocUtf8String(fakePath);
            return open(filename, flag);
        }
        if(pathname.indexOf("libjgdtc.so")>=0) {
            console.log("dump libjgdtc.so start .......")
            dump_so("libjgdtc.so");
        }
        var fd = open(pathnameptr, flag);
        return fd;
    }, 'int', ['pointer', 'int']));
}

还是会有报错,所以这里是怎么做到的:

在执行open(/data/app/com.oacia.apk_protecte2Q3Wqlr5MkcYny7j4QoVQ==/lib/arm64/libjgdtc.so)的时候这个正好就被加载了,而且再open完成之后就被删除了,应该是被特殊处理了

我再去通过了dlopen去看看这个libjgdtc.so到底是怎么进行加载的,想着就加载的时候就去dump下来。

function my_hook_dlopen(soName = '') {
    // 找到 android_dlopen_ext 函数的地址并挂钩
    Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"), {
        onEnter: function (args) {
            // 获取加载的库路径
            var pathptr = args[0];
            if (pathptr !== undefined && pathptr != null) {
                var path = ptr(pathptr).readCString();
                // 如果路径中包含目标动态库名,则标记为可以进行 hook
                if (path.indexOf(soName) !== -1) {
                    this.is_can_hook = true;
                    console.log("Library " + soName + " loaded, dumping...");
                    // 执行 Dump 操作
                    dump_so(path); // 传入动态库路径
                }
            }
        },
        onLeave: function (retval) {
            // 其他操作,暂时不需要处理
        }
    });
}

// 延时执行 hook 操作
setImmediate(function () {
    my_hook_dlopen("libjgdtc.so"); // 传入目标库名
});

但是能够确定这里是android_dlopen_ext来进行这个so的加载的,不是自写的加载函数,但是不确定的是是否android_dlopen_ext被重写并进行了特殊处理。也使用了很多相关的方法,但是是没用的,并且也确实是很好奇这个so里面写了什么........

最后尝试一下打印堆栈信息,看看是什么个情况:

发现是在boot下的系统文件在操作了,这些 OAT 文件是 Android 系统自带的底层文件,但是在这里调用出来的了相关的堆栈情况应该是在系统底层去完成的so文件的加载和删除,所以才dump不了。具体应该怎么去dump下这个so,希望有大牛能说说

正常继续

于是按照正常的复现分析流程继续去分析,把正常的maps文件的路径替换成一个不存在的位置,查看一下报错信息之类的

由于数据的提取的不正常而导致直接进行了程序的退出,结果连里面的dex文件也没有进行删除,这里应该是开发者的失误,正这样的结果直接可以让我们找到对应是在哪使用open函数读取的dex文件了,推出来看看

加密了全是垃圾数据,修改frida,查看再输入的时候的堆栈情况

当前我们需要确定的就是这些dex文件是在哪个so中被加载使用的。那么我们就要去定位到对应的so文件中去

如何去实现so文件的定位,也就是在android_dlopen_ext("libcjiagu_a64.so")的时候,看看open("classes.dex")这个行为是在哪个so中进行的,所以我们要去确定当前的addr,如何通过枚举每个module,来比对这个addr是否在这个module之间来确定,对应的so文件

function addr_in_so(addr){
    var ModuleS = Process.enumerateModules();
    for(var i = 0; i < ModuleS.length; i++) {
        if(addr>ModuleS[i].base && addr<ModuleS[i].base.add(ModuleS[i].size)){
            console.log(addr.toString(16),"is in",ModuleS[i].name,"offset: 0x"+(addr-ModuleS[i].base).toString(16));
        }
    }
}

function hook_proc_self_maps() {
    const openPtr = Module.getExportByName(null, 'open');
    const open = new NativeFunction(openPtr, 'int', ['pointer', 'int']);//获取原函数指针
    // var fakePath = "/data/data/com.oacia.apk_protect/maps";
    var fakePath ="/data/data/com.oacia.apk_protect/maps_nonexistent"
    Interceptor.replace(openPtr, new NativeCallback(function (pathnameptr, flag) {
        var pathname = Memory.readUtf8String(pathnameptr);
        console.log("open",pathname);
        if (pathname.indexOf("maps") >= 0) {
            console.log("find ",pathname," redirect to ——>",fakePath);
            var filename = Memory.allocUtf8String(fakePath);
            return open(filename, flag);
        }
        if(pathname.indexOf("libjgdtc.so")>=0) {
            console.log("dump libjgdtc.so start .......")
            // dump_so("libjgdtc.so");
        }
        if(pathname.indexOf("dex")>=0) {
            Thread.backtrace(this.context, Backtracer.FUZZY).map(addr_in_so);

        }
        var fd = open(pathnameptr, flag);
        return fd;
    }, 'int', ['pointer', 'int']));
}

这样就可以去打印到在对应module的时候的so文件了


可以发现的是这些dex文件所定位到到的so全是相同的位置,同时在classes3.dex的时候前半部分也是一样的,同时由于可能数据的错误直接导致中断了,所以我们有利用去怀疑这里的dex文件的加载其实是写的循环,通过循环去加载了所以的dex文件

同时我们去访问了一下对应的libjiagu_64.so中上述偏移offset的位置

这里我是直接将把里面的数据也dump出来了的,在原作者那里,数据全是0000...........,这样正好懒得dump了,但是同样的在实际环境下的话ELF文件里面藏ELF文件就很难发现了,不过又一次踩在了巨人的肩膀上。

用010打开搜索了一下ELF文件的头

有两个是真的ELF文件,但是都是被加密了的,这种就是在ELF里面藏ELF文件了,在执行过程中按照对应的偏移和文件大小进行读取的

原话:壳elf加载主elf,并且program header还被加密了,感觉这种形式很像是 自实现linker加固so,原作者的经验。

这里的文件里面给出了一个特殊的加固的方式自实现linker 加固 so

linker 加固 so

自实现Linker 加固 SO 是一种通过定制或替换系统动态链接器(如 ld-linux.so.2 或 ld-linux-armhf.so.3 等)的行为,来增加对动态共享对象(SO 文件)保护的技术。在这种加固技术中,加固工具会创建一个“壳”ELF 文件,这个壳 ELF 文件在其自身的代码中实现了一个私有的 Linker。这个私有 Linker 的功能包括解析 ELF 文件的结构、处理加密的 Program Header、以及执行各种加固和保护措施。然后,它会解密主要的 ELF 文件(主 ELF)中的程序头部或其他部分,将解密后的内容加载到内存中,最后可能通过调用`dlopen`等函数手动加载解密后的动态共享对象。这种方法的优点是能够提供额外的保护层次,防止未经授权的分析和修改。由于Program Header 等关键信息被加密,传统的工具和方法很难直接分析或执行加固后的程序,增加了破解的难度。

这里通过了dlopen进行的交叉引用来实现查看相关调用的方法

找到了第二个dlopen

复现上面说看到了类似于AOSP源码中对于Linker的相关代码

这里确实是没有这样细致的分析过AOSP的全部源码,所以确实是看不出来

既然是自定义的Linker那么就是同样需要soinfo的结构体

soinfo 结构体是 Android 和 Linux 系统中在动态链接库加载过程中使用的一个结构体,特别是在解析、加载、链接和卸载共享库时。它通常出现在 linker 的内部,表示一个共享库(如 .so 文件)的信息。soinfo 是 Android 系统中链接器(linker)的一个关键结构,用于存储有关共享库的各种信息,如符号表、重定位信息、动态节(Section)等。

所以需要导入对应的结构体

// ELF64 启用该宏
#define __LP64__ 1
// ELF32 启用该宏
//#define __work_around_b_24465209__ 1

/* 
https://android.googlesource.com/platform/bionic/+/master/linker/Android.bp
架构为 32 位 定义__work_around_b_24465209__宏arch: {
    arm: {cflags: [" D__work_around_b_24465209__"],},
    x86: {cflags: [" D__work_around_b_24465209__"],},
}
*/

// 定义 ELF 文件的基本类型,根据架构决定使用 ELF32 或 ELF64 类型
#if defined(__LP64__)
    #define ElfW(type) Elf64_ ## type
#else
    #define ElfW(type) Elf32_ ## type
#endif

// 32 位和 64 位重定位和符号表数据结构

// Elf32 和 Elf64 基本类型
typedef signed char __s8;
typedef unsigned char __u8;
typedef signed short __s16;
typedef unsigned short __u16;
typedef signed int __s32;
typedef unsigned int __u32;
typedef signed long long __s64;
typedef unsigned long long __u64;

// 32 位 ELF 基本类型
typedef __u32 Elf32_Addr;
typedef __u16 Elf32_Half;
typedef __u32 Elf32_Off;
typedef __s32 Elf32_Sword;
typedef __u32 Elf32_Word;

// 64 位 ELF 基本类型
typedef __u64 Elf64_Addr;
typedef __u16 Elf64_Half;
typedef __s16 Elf64_SHalf;
typedef __u64 Elf64_Off;
typedef __s32 Elf64_Sword;
typedef __u32 Elf64_Word;
typedef __u64 Elf64_Xword;
typedef __s64 Elf64_Sxword;

// 动态段数据结构(Elf32 和 Elf64)
typedef struct dynamic {
    Elf32_Sword d_tag;
    union {
        Elf32_Sword d_val;
        Elf32_Addr d_ptr;
    } d_un;
} Elf32_Dyn;

typedef struct {
    Elf64_Sxword d_tag; /* entry tag value */
    union {
        Elf64_Xword d_val;
        Elf64_Addr d_ptr;
    } d_un;
} Elf64_Dyn;

// 重定位数据结构(Elf32 和 Elf64)
typedef struct elf32_rel {
    Elf32_Addr r_offset;
    Elf32_Word r_info;
} Elf32_Rel;

typedef struct elf64_rel {
    Elf64_Addr r_offset; /* Location at which to apply the action */
    Elf64_Xword r_info;  /* index and type of relocation */
} Elf64_Rel;

typedef struct elf32_rela {
    Elf32_Addr r_offset;
    Elf32_Word r_info;
    Elf32_Sword r_addend;
} Elf32_Rela;

typedef struct elf64_rela {
    Elf64_Addr r_offset;  /* Location at which to apply the action */
    Elf64_Xword r_info;   /* index and type of relocation */
    Elf64_Sxword r_addend; /* Constant addend used to compute value */
} Elf64_Rela;

// 符号表数据结构(Elf32 和 Elf64)
typedef struct elf32_sym {
    Elf32_Word st_name;
    Elf32_Addr st_value;
    Elf32_Word st_size;
    unsigned char st_info;
    unsigned char st_other;
    Elf32_Half st_shndx;
} Elf32_Sym;

typedef struct elf64_sym {
    Elf64_Word st_name;     /* Symbol name, index in string tbl */
    unsigned char st_info;  /* Type and binding attributes */
    unsigned char st_other; /* No defined meaning, 0 */
    Elf64_Half st_shndx;    /* Associated section index */
    Elf64_Addr st_value;    /* Value of the symbol */
    Elf64_Xword st_size;    /* Associated symbol size */
} Elf64_Sym;

// ELF 文件头数据结构(Elf32 和 Elf64)
#define EI_NIDENT 16

typedef struct elf32_hdr {
    unsigned char e_ident[EI_NIDENT];
    Elf32_Half e_type;
    Elf32_Half e_machine;
    Elf32_Word e_version;
    Elf32_Addr e_entry; /* Entry point */
    Elf32_Off e_phoff;
    Elf32_Off e_shoff;
    Elf32_Word e_flags;
    Elf32_Half e_ehsize;
    Elf32_Half e_phentsize;
    Elf32_Half e_phnum;
    Elf32_Half e_shentsize;
    Elf32_Half e_shnum;
    Elf32_Half e_shstrndx;
} Elf32_Ehdr;

typedef struct elf64_hdr {
    unsigned char e_ident[EI_NIDENT]; /* ELF "magic number" */
    Elf64_Half e_type;
    Elf64_Half e_machine;
    Elf64_Word e_version;
    Elf64_Addr e_entry;  /* Entry point virtual address */
    Elf64_Off e_phoff;   /* Program header table file offset */
    Elf64_Off e_shoff;   /* Section header table file offset */
    Elf64_Word e_flags;
    Elf64_Half e_ehsize;
    Elf64_Half e_phentsize;
    Elf64_Half e_phnum;
    Elf64_Half e_shentsize;
    Elf64_Half e_shnum;
    Elf64_Half e_shstrndx;
} Elf64_Ehdr;

// 程序头数据结构(Elf32 和 Elf64)
typedef struct elf32_phdr {
    Elf32_Word p_type;
    Elf32_Off p_offset;
    Elf32_Addr p_vaddr;
    Elf32_Addr p_paddr;
    Elf32_Word p_filesz;
    Elf32_Word p_memsz;
    Elf32_Word p_flags;
    Elf32_Word p_align;
} Elf32_Phdr;

typedef struct elf64_phdr {
    Elf64_Word p_type;
    Elf64_Word p_flags;
    Elf64_Off p_offset;  /* Segment file offset */
    Elf64_Addr p_vaddr;  /* Segment virtual address */
    Elf64_Addr p_paddr;  /* Segment physical address */
    Elf64_Xword p_filesz; /* Segment size in file */
    Elf64_Xword p_memsz;  /* Segment size in memory */
    Elf64_Xword p_align;  /* Segment alignment, file & memory */
} Elf64_Phdr;

// 节头数据结构(Elf32 和 Elf64)
typedef struct elf32_shdr {
    Elf32_Word sh_name;
    Elf32_Word sh_type;
    Elf32_Word sh_flags;
    Elf32_Addr sh_addr;
    Elf32_Off sh_offset;
    Elf32_Word sh_size;
    Elf32_Word sh_link;
    Elf32_Word sh_info;
    Elf32_Word sh_addralign;
    Elf32_Word sh_entsize;
} Elf32_Shdr;

typedef struct elf64_shdr {
    Elf64_Word sh_name;       /* Section name, index in string tbl */
    Elf64_Word sh_type;       /* Type of section */
    Elf64_Xword sh_flags;     /* Miscellaneous section attributes */
    Elf64_Addr sh_addr;       /* Section virtual addr at execution */
    Elf64_Off sh_offset;      /* Section file offset */
    Elf64_Xword sh_size;      /* Size of section in bytes */
    Elf64_Word sh_link;       /* Index of another section */
    Elf64_Word sh_info;       /* Additional section information */
    Elf64_Xword sh_addralign; /* Section alignment */
    Elf64_Xword sh_entsize;   /* Entry size if section holds table */
} Elf64_Shdr;

// 动态链接信息结构(Android 特有)
typedef void (*linker_dtor_function_t)();
typedef void (*linker_ctor_function_t)(int, char**, char**);

#if defined(__work_around_b_24465209__)
#define SOINFO_NAME_LEN 128
#endif

// Android 中的 soinfo 结构体,用于表示动态链接库信息
struct soinfo {
    #if defined(__work_around_b_24465209__)
    char old_name_[SOINFO_NAME_LEN];
    #endif
    const ElfW(Phdr)* phdr;
    size_t phnum;
    ElfW(Addr) base;
    size_t size;
    ElfW(Dyn)* dynamic;
    soinfo* next;
    uint32_t flags_;
    const char* strtab_;
    ElfW(Sym)* symtab_;
    size_t nbucket_;
    size_t nchain_;
    uint32_t* bucket_;
    uint32_t* chain_;
    #if !defined(__LP64__)
    ElfW(Addr)** unused4; // DO NOT USE, maintained for compatibility
    #endif
    #if defined(USE_RELA)
    ElfW(Rela)* plt_rela_;
    size_t plt_rela_count_;
    ElfW(Rela)* rela_;
    size_t rela_count_;
    #else
    ElfW(Rel)* plt_rel_;
    size_t plt_rel_count_;
    ElfW(Rel)* rel_;
    size_t rel_count_;
    #endif
    linker_ctor_function_t* preinit_array_;
    size_t preinit_array_count_;
    linker_ctor_function_t* init_array_;
    size_t init_array_count_;
    linker_dtor_function_t* fini_array_;
    size_t fini_array_count_;
    linker_ctor_function_t init_func_;
    linker_dtor_function_t fini_func_;
    size_t ref_count_;
    link_map link_map_head;
    bool constructors_called;
    ElfW(Addr) load_bias;
    bool has_text_relocations;
    bool has_DT_SYMBOLIC;
};

把dlopen的第一个参数修改为soinfo * 的结构体指针去

但是当把这个结构体传入之后,其实是有点不合的,那么就说明这个自定义的Linker所使用的soinfo结构体也是被修改了的

那么我们就要去找到对应这里被修改的地方到底是怎么样的,在实际的情况下我们也许会有花费大量的时间去寻找,但是现在比较是踩在巨人的肩膀上的,这里已经被找到了

这里通过sub_3C94交叉引用到sub_49F0,然后去查看了sub_4918最后进入了sub_5E6C

来到了这

这里的0x38是program header table中的phentsize,代表的是程序头中一个条目的大小。而程序头又是多个条目组成的,这里举例一下条目,v5是程序头的偏移地址,v6的条目的偏移地址

(这里的so是我把之前ELF里面的ELF复制十六进行按照ELF格式新建的so文件)

程序头条目 1(加载段)
Elf64_Phdr phdr1 = {
    .p_type = PT_LOAD,      // 程序段类型:PT_LOAD 表示加载段
    .p_flags = PF_R | PF_W, // 可读、可写
    .p_offset = 0x1000,     // 在 ELF 文件中的偏移量
    .p_vaddr = 0x400000,    // 程序段加载到内存中的虚拟地址
    .p_paddr = 0x0,         // 物理地址(一般为 0)
    .p_filesz = 0x2000,     // 文件中的大小
    .p_memsz = 0x3000,      // 内存中的大小(包括额外的空间)
    .p_align = 0x1000       // 对齐要求:0x1000 字节对齐
};
程序头条目 2(动态段)
Elf64_Phdr phdr2 = {
    .p_type = PT_DYNAMIC,  // 程序段类型:PT_DYNAMIC 表示动态段
    .p_flags = PF_R,       // 可读
    .p_offset = 0x3000,    // 文件中的偏移量
    .p_vaddr = 0x500000,   // 程序段加载到内存中的虚拟地址
    .p_paddr = 0x0,        // 物理地址(一般为 0)
    .p_filesz = 0x500,     // 文件中的大小
    .p_memsz = 0x1000,     // 内存中的大小
    .p_align = 0x1000      // 对齐要求:0x1000 字节对齐
};
程序头条目 3(堆栈段)
Elf64_Phdr phdr3 = {
    .p_type = PT_STACK,    // 程序段类型:PT_STACK 表示堆栈段
    .p_flags = PF_R | PF_W, // 可读、可写
    .p_offset = 0x0,       // 文件中的偏移量(通常为 0,因为堆栈段不存储在文件中)
    .p_vaddr = 0x7fffffff, // 程序段加载到内存中的虚拟地址
    .p_paddr = 0x0,        // 物理地址(一般为 0)
    .p_filesz = 0x0,       // 文件中的大小(通常为 0,因为堆栈段不存储在文件中)
    .p_memsz = 0x10000,    // 内存中的大小
    .p_align = 0x1000      // 对齐要求:0x1000 字节对齐
};

而每个条目的偏移的计算公式又是这样的:

程序头表偏移量 (e_phoff)
程序头条目的大小 (e_phentsize)
程序头条目数量 (e_phnum)
第一个程序头条目的偏移量为 e_phoff + 0 * e_phentsize,即 0x40 + 0 * 0x38 = 0x40,表示第一个程序头条目位于文件偏移 0x40。
第二个程序头条目的偏移量为 e_phoff + 1 * e_phentsize,即 0x40 + 1 * 0x38 = 0x78,表示第二个程序头条目位于文件偏移 0x78。
第三个程序头条目的偏移量为 e_phoff + 2 * e_phentsize,即 0x40 + 2 * 0x38 = 0xB0,表示第三个程序头条目位于文件偏移 0xB0。

也就是程序头的偏移加上对应条目的数目*条目的大小,和上面那个一比对,就会知道

v5=程序头表偏移量 (e_phoff)
0x38=程序头条目的大小 (e_phentsize)
a2=程序头条目数量 (e_phnum)

这里我们去HOOK一下这里的位置看一下,顺便可以看看对应传入的结构体soinfo到底应该是怎么样的

function hook_5E6C(){
var module = Process.findModuleByName("libjiagu_64.so");
Interceptor.attach(module.base.add(0x5E6C), {
    // fd, buff, len
    onEnter: function (args) {
        console.log(hexdump(args[0], {
        offset: 0,// 相对偏移
        length: 0x38*0x6+0x20,//dump 的大小    6个0x38大小的条目+偏移
        header: true,
        ansi: true
        }));
        console.log(args[1])
        console.log(args[2])
        console.log(`base = ${module.base}`)
        },
    onLeave: function (ret) {
        }
        });
}

这里的HOOK不是一般的HOOK,因为这里的libjiagu_64.so不是在执行过程中就进行了加载了,同时程序在执行到module.base.add(0x5E6C)这个位置的时候肯定是被加载了的,所以我们需要HOOK的时间段是在libjiagu_64.so之后,同时又是在module.base.add(0x5E6C)这个代码执行之前,HOOK上才能dump下这个代码来,这也就是这里需要HOOK的时候的细节了,确实也是思考和试错了很久

延时dump

所以这里要进行延时dump,我们通过setTimeout来实现

    Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"), {
        onEnter: function (args) {
            // 获取加载的库路径
            var pathptr = args[0];
            if (pathptr !== undefined && pathptr != null) {
                var path = ptr(pathptr).readCString();
                console.log("android_dlopen_ext+= " + path);
                // 如果路径中包含目标动态库名,则标记为可以进行 hook
                    if(path.indexOf("libjiagu_64") !== -1) {
                        this.is_can_hook = true;
                        console.log("hook hook_proc_self_maps started ......");
                        setTimeout(function() {
                            hook_5E6C();
                        }, 10);
                        // 执行 Hook 任务
                        //...
                    }
                    if(path.indexOf("libjgdtc.so")>=0) {
                        console.log("Dumping libjgdtc.so...");
                        setTimeout(function(){
                            dump_so("libjgdtc.so")},0.000000000000000001);
                        ; // 直接进行 dump
                    }
            }
        },
        onLeave: function (retval) {
            // 如果满足条件,执行 dump 操作
            if (this.is_can_hook) {
                // dump_so(soName); // Dump 目标 SO 文件
                // hook_open();
                hook_proc_self_maps()
            }
        }
    });
}
function hook_5E6C() {
    // 查找 libjiagu_64.so 模块
    var module = Process.findModuleByName("libjiagu_64.so");

    // 如果模块为空,返回并输出错误信息
    if (module === null) {
        console.error("Module 'libjiagu_64.so' not found!");
        return;
    }

    // 定义目标地址,0x5e6c 偏移
    var targetAddr = module.base.add(0x5e6c);

    // 挂钩指定地址
    Interceptor.attach(targetAddr, {
        // 在函数入口时触发的回调
        onEnter: function (args) {
            console.log("Hooked function args:");

            // 打印 args[0] 的十六进制数据,6个0x38大小的条目 + 0x20偏移
            console.log(hexdump(args[0], {
                offset: 0, // 相对偏移
                length: 0x38 * 0x6 + 0x20, // Dump 的大小,6个0x38大小的条目 + 偏移
                header: true, // 显示头部信息
                ansi: true // 使用 ANSI 颜色
            }));

            // 打印 args[1] 和 args[2],便于查看参数
            console.log("Argument 1 (fd):", args[1]);
            console.log("Argument 2 (buff):", args[2]);
            console.log(`Base address of module: ${module.base}`);
        },

        // 在函数返回时触发的回调
        onLeave: function (ret) {
            // 在这里可以处理函数返回值,或者做一些清理工作
            // 目前不需要对返回值做处理,因此该部分为空
        }
    });

    console.log("Hooking 0x5e6c in libjiagu_64.so completed.");
}

这里我本来想着用这种方法,在硬件上cpu去处理这些代码总会有时间差异,我还想着试一试看能不能去把之前的libjgdtc.so给dump下来,我还是太年轻了,不管是长的时间,还是短的时间都不行。不过能把这个函数的参数给dump下来也挺好的

这里的  0x7c50c6f000 -  0x7c50b88000 =0xe7000 正好是对应的壳的ELF到主ELF的偏移

这里是我们HOOK函数调用的位置,这里执行的位置a1 是在偏移了232的位置,而且第二个参数也是在248的位置

而正常的情况应该是第一个参数phdr在soinfo结构体的0x0的位置,之后紧接着也就是phnum的位置,所以这个位置是被魔改了,我们补齐232个char看看有没有问题


是不对的,虽然说两个参数直接的偏移确实应该是16个字节,但是第二个参数却是base,说明这里是被魔改了

重新开寻找逻辑关系,先利用trace_Native,对于so层的函数进行跟踪,看看

start Stalker!
Stalker end!
call1:JNI_OnLoad
call2:j_interpreter_wrap_int64_t
call3:interpreter_wrap_int64_t
call4:getenv
call5:sub_13908
call6:inotify_add_watch
call7:sub_11220
call8:fopen
call9:sub_9DD8
call10:sub_E3E0
call11:strtol
call12:feof
call13:raise
call14:memset
call15:sub_C918
call16:sub_9988
call17:sub_9964
call18:sub_9AC4
call19:j_ffi_prep_cif
call20:ffi_prep_cif
call21:j_ffi_prep_cif_machdep
call22:ffi_prep_cif_machdep
call23:j_ffi_call
call24:ffi_call
call25:sub_1674C
call26:j_ffi_call_SYSV
call27:ffi_call_SYSV
call28:sub_167BC
call29:sub_1647C
call30:sub_163DC
call31:sub_9900
call32:sub_94BC
call33:inotify_init
call34:fmod
call35:strncpy
call36:_Z9__arm_a_1P7_JavaVMP7_JNIEnvPvRi
call37:sub_9E58
call38:sub_999C
call39:sub_10964
call40:j_lseek_1
call41:lseek
call42:sub_96E0
call43:sub_8000
call44:dlopen
call45:sub_60E0
call46:sub_6544
call47:sub_4B54
call48:sub_6128
call49:_ZN9__arm_c_19__arm_c_0Ev
call50:sub_A3EC
call51:sub_99CC
call52:sub_9944
call53:sub_6484
call54:sub_6590
call55:prctl
call56:sub_6698
call57:sub_9FFC
call58:j_lseek_3
call59:j_lseek_2
call60:j_lseek_0
call61:sub_9A90
call62:sub_5F20
call63:sub_6044
call64:sub_3574
call65:uncompress
call66:sub_49F0
call67:sub_5400
call68:sub_5478
call69:sub_5B08
call70:sub_5650
call71:sub_580C
call72:open
call73:atoi
call74:sub_3C94
call75:strncmp
call76:sub_4918
call77:sub_4000
call78:sub_41B4
call79:sub_35AC
call80:sigaction
call81:sub_5E6C
call82:sub_5444
call83:sub_633C
call84:sub_8130
call85:sub_4C70
call86:sub_825C
call87:sub_8B50
call88:sub_8ED4
call89:sub_8430
call90:interpreter_wrap_int64_t_bridge
call91:sub_9D60
call92:sub_166C4
call93:memcpy
call94:_Z9__arm_a_2PcmS_Rii
call95:j_ffi_prep_cif_var
call96:ffi_prep_cif_var

RC4

这里在sub_5F20的位置找到了RC4的算法过程,这里我已经对于一些变量名称之类的进行了修改

所以一眼是RC4,我们可以去HOOK一下这里的传入的Key表的密钥是什么

这里同样使用的是延迟dump,但是我觉得这样应该不是最好的dump方法,但是能dump下来就行

Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"), 
    {
        onEnter: function (args) 
        {
            // 获取加载的库路径
            var pathptr = args[0];
            if (pathptr !== undefined && pathptr != null) {
                var path = ptr(pathptr).readCString();
                console.log("android_dlopen_ext+= " + path);
                // 如果路径中包含目标动态库名,则标记为可以进行 hook
                    if(path.indexOf("libjiagu_64") !== -1) {
                        this.is_can_hook = true;
                        console.log("hook hook_proc_self_maps started ......");
                        setTimeout(function() {
                            hook_5F20();
                        }, 1);
                        // 执行 Hook 任务
                        //...
                    }
            }
        },
        onLeave: function (retval) 
        {
            // 如果满足条件,执行 dump 操作
            if (this.is_can_hook) {
                // dump_so(soName); // Dump 目标 SO 文件
                // hook_open();
                hook_proc_self_maps()
            }
        }

function hook_5F20(){
    var module = Process.getModuleByName("libjiagu_64.so");
    if (module === null) {
        console.error("Module 'libjiagu_64.so' not found!");
        return;
    }
    Interceptor.attach(module.base.add(0x5F20),{
        onEnter:function(args){
            console.log("Hooked function args:");
            console.log("Argument 0 (KEY):"+
            hexdump(args[0],{
            offset: 0, // 相对偏移
            length:256,
            header: true, // 显示头部信息
            ansi: true // 使用 ANSI 颜色
        }));

        console.log("Argument 1 (size):", args[1]);
        console.log("Argument 2 (S):", +
        hexdump(args[2],{
            offset: 0, // 相对偏移
            length:10,
            header: true, // 显示头部信息
            ansi: true // 使用 ANSI 颜色
        }));
        console.log(`Base address of module: ${module.base}`);
        },
        onLeave:function(retval){
            console.log("Return value:",retval);
        }
    })
    console.log("Hooking 0x5F20 in libjiagu_64.so completed.");
}

这里的arg[2]应该是hexdump才是,但是这里出现的值是NaN,不过这里的S表是要被0-256填充的。

key='vUV4#.#SVt'

所以大概率这个加盟的函数就是RC4进行的加密过程了,我们来总结一下当前这个so问题,通过壳的ELF文件,通过Linker实现加载程序的ELF文件,再通过加密RC4。这里也是大概的进行猜测,再往下看看

sub_5F20进行加密,但是肯定会去解密找到我们需要的加密之前的文件,找到位于下方位置的sub_6044是解密函数,这里是通过trace_Naitve来实现的代码跟踪才找到的解密函数

同样去HOOK

function hook_6044(){
    var module = Process.getModuleByName("libjiagu_64.so");
    if (module === null) {
        console.error("Module 'libjiagu_64.so' not found!");
        return;
    }
    Interceptor.attach(module.base.add(0x6044),{
        onEnter:function(args){
            this.args0 = args[0];
            console.log("Hooked function args:");
            console.log("Argument 0 (KEY):"+
            hexdump(args[0],{
            offset: 0, // 相对偏移
            length:256,
            header: true, // 显示头部信息
            ansi: true // 使用 ANSI 颜色
        }));
        console.log("Argument 1 (size):", args[1]);
        console.log("Argument 2 (S):", +
            hexdump(args[2],{
                offset: 0, // 相对偏移
                length:256,
                header: true, // 显示头部信息
                ansi: true // 使用 ANSI 颜色
            }));
        console.log(`Base address of module: ${module.base}`);
        },
        onLeave:function(retval)
        {   
            console.log(hexdump(this.args0,{
                offset: 0, // 相对偏移
                length:256,
                header: true, // 显示头部信息
                ansi: true // 使用 ANSI 颜色
            }));
            console.log("Return value:",retval);
        }
    })
    console.log("Hooking 0x6044 in libjiagu_64.so completed.");
}

得到解密之后的数据

call61:sub_9A90
call62:sub_5F20
call63:sub_6044
call64:sub_3574
call65:uncompress
call66:sub_49F0
call67:sub_5400
call68:sub_5478

uncompress函数

同时根据之前的代码跟踪,我们知道了其实解密函数sub_6044之后就是sub_3574,查看了之后其实可以发现里面直接去调用uncompress函数了。直接就去解压缩了,那么我们就去HOOK这个系统函数,看看这个的结果到底是什么

这里可以直接去HOOK这个"libz.so"下的uncompress函数,因为是系统函数,提前就加载好了,不存在之前的延时dump之类的

function HOOKuncompree(){
    var uncompressAddress = Module.findExportByName("libz.so", "uncompress"); // 假设是 zlib 库中的 uncompress

    // 检查是否成功找到 uncompress 地址
    if (uncompressAddress) {
        console.log("uncompress 地址:" + uncompressAddress);
    } else {
        console.log("无法找到 uncompress 函数");
    }

    // Hook uncompress 函数
    Interceptor.attach(uncompressAddress, {
        onEnter: function (args) {
            this.arg0=args[0];
            this.arg2=args[2];
            // 打印参数信息,参数是 uncompress 的指针
            console.log("uncompress called");

            // 打印目标缓冲区的指针地址
            console.log("dest (destination buffer): ");
            console.log(hexdump(args[0], {
                offset: 0,// 相对偏移
                length: 0x30,//dump 的大小
                header: true,
                ansi: true
              }));
            // 打印目标缓冲区的大小(从 destLen 参数)
            console.log("destLen (destination buffer length): " + args[1].toInt32());

            // 打印源数据的指针
            console.log("source (source data):");
            console.log("args[2]"+hexdump(args[2], {
                offset: 0,// 相对偏移
                length: 0x30,//dump 的大小
                header: true,
                ansi: true
              }));

            // 打印源数据的大小(从 sourceLen 参数)
            console.log("sourceLen (source data length): " + args[3].toInt32());
        },

        onLeave: function (retval) {
            console.log("onLeave");
            // 可选:函数返回时处理的代码
            console.log("arg[0]:"+hexdump(this.arg0, {
                offset: 0,// 相对偏移
                length: 0x30,//dump 的大小
                header: true,
                ansi: true
              }));
              console.log("arg[2]"+hexdump(this.arg2, {
                offset: 0,// 相对偏移
                length: 0x30,//dump 的大小
                header: true,
                ansi: true
              }));
            console.log("uncompress 返回,返回值:", retval);
        }
    });
    }

我这里顺便截取了之前的解密的结果和现在解压缩的参数。我们发现,原本的解密出来的数据多了前面的4字节的东西,而最后解压缩的结果却是四字节之后的数据,那么大概率这前面要不就是特征码,要不就是需要被解压缩的长度了。

这里是sub_8000函数的位置,这里包含了v5的参数的填充,一个是对于数据段2e270位置的数据,一个是0xB8010的16进制数据,这个数字正好是在解密函数的第二个参数的位置的size的值

同时再看看2e270位置的其实数据 是 01 18 25 E7 开头的数据,是不是和我们之前dump解密函数OnEnter出来的开头的数据又是一样,那么是不是有理由去怀疑,这个v5的v5[0]是加密的数据,而v5[1]是对应的字节大小,那么我们可以直接在对应的壳的ELF文件中去dump下这个加密数据,自己去实现解密操作,看看会出现什么

import zlib
import struct

def RC4(data, key):
    S = list(range(256))
    j = 0
    out = []

    # KSA Phase
    for i in range(256):
        j = (j + S[i] + key[i % len(key)]) % 256
        S[i], S[j] = S[j], S[i]

    # PRGA Phase
    i = j = 0
    for ch in data:
        i = (i + 1) % 256
        j = (j + S[i]) % 256
        S[i], S[j] = S[j], S[i]
        out.append(ch ^ S[(S[i] + S[j]) % 256])

    return out

def RC4decrypt(ciphertext, key):
    return RC4(ciphertext, key)

wrap_elf_start = 0x1e270
wrap_elf_size = 0xb8010
key = b"vUV4#\x91#SVt"
with open('C:/Users/19817/Desktop/libjiagu_a64.so', 'rb') as f:
    wrap_elf = f.read()

# 对密文进行解密
dec_compress_elf = RC4decrypt(wrap_elf[wrap_elf_start:wrap_elf_start + wrap_elf_size], key)
dec_elf = zlib.decompress(bytes(dec_compress_elf[4::]))
with open('wrap_elf', 'wb') as f:
    f.write(dec_elf)

这里其实 wrap_elf_start = 0x1e270 的位置为什么会减少了0x10000,不过要是是0x2e270的话,这里是错的,显示是文件头损坏的问题

这里我们直接去解密了这段数据的内容,查看一下数据是什么



前面一堆D3,然后又是一个ELF文件,于是我们直接去分割这俩部分,把另外一个ELF文件拿来看看

import zlib
import struct

def RC4(data, key):
    S = list(range(256))
    j = 0
    out = []

    # KSA Phase
    for i in range(256):
        j = (j + S[i] + key[i % len(key)]) % 256
        S[i], S[j] = S[j], S[i]

    # PRGA Phase
    i = j = 0
    for ch in data:
        i = (i + 1) % 256
        j = (j + S[i]) % 256
        S[i], S[j] = S[j], S[i]
        out.append(ch ^ S[(S[i] + S[j]) % 256])

    return out

def RC4decrypt(ciphertext, key):
    return RC4(ciphertext, key)

wrap_elf_start = 0x1e270
wrap_elf_size = 0xb8010
key = b"vUV4#\x91#SVt"
with open('C:/Users/19817/Desktop/libjiagu_a64.so', 'rb') as f:
    wrap_elf = f.read()

# 对密文进行解密
dec_compress_elf = RC4decrypt(wrap_elf[wrap_elf_start:wrap_elf_start + wrap_elf_size], key)
dec_elf = zlib.decompress(bytes(dec_compress_elf[4::]))
with open('wrap_elf', 'wb') as f:
    f.write(dec_elf)
with open('wrap_elf', 'rb') as f:
    wrap_elf = f.read()
ELF_magic = bytes([0x7F, 0x45, 0x4C, 0x46])
for i in range(len(wrap_elf) - len(ELF_magic) + 1):
    if wrap_elf[i:i + len(ELF_magic)] == ELF_magic:
        print(hex(i))
        with open('wrap_elf_part1', 'wb') as f:
            f.write(wrap_elf[0:i])
        with open('wrap_elf_part2', 'wb') as f:
            f.write(wrap_elf[i::])
        break

得到了另外一个ELF文件


这里的条目的大小和数量和之前一样,那么我们再看看有没有类似的数据,说不定能找到相关的大小

跟着调用链,我们找到了sub_5B08,这里有0x38 ,以及传入参数的6

这个循环的异或确实是很可疑的,我们只能去inlineHOOK一下对应的数据了

function inline_HOOK() {
    console.log("[inline_HOOK]");

    // 获取模块和目标地址
    var module = Process.getModuleByName("libjiagu_64.so");
    var soaddr = module.base.add(0x5B34);  // 假设偏移地址是 0x5B34

    // 修改内存权限为只读,以确保能够读取该地址
    Memory.protect(ptr(module.base.add(0x5B34)), 0x100, 'r');

    // 附加钩子
    Interceptor.attach(soaddr, {
        onEnter: function(args) {
            console.log("HOOK STARTED");
            // 读取 ARM 架构的寄存器值(比如 x23)
            var x23 = this.context.x23;  // 读取 x23 寄存器值
            console.log("x23寄存器值: " + x23);
            console.log(hexdump(ptr(x23),{
                offset: 0, // 相对偏移
                length:256,
                header: true, // 显示头部信息
                ansi: true // 使用 ANSI 颜色
            }));
        },
        onLeave: function(retval) {

            // 可以在这里处理函数返回值等操作
        }
    });
}

这里的代码都要在libjiagu_64.so加载之后,并且进行延时dump才能触发类似于以下这种

if(path.indexOf("libjiagu_64") !== -1) {
                        this.is_can_hook = true;
                        console.log("hook hook_proc_self_maps started ......");
                        // hook_uncompress_res();
                        setTimeout(function() {
                            // hook_5F20();
                            // hook_6044();
                            // hook_uncompress_res();
                            // HOOKuncompree();
                            inline_HOOK();
                        }, 1);

这里dump的数据和我们之前分割出来的不是ELF文件那部分是一模一样的

四个数据块

同时对于函数链中的sub_5B08进行分析 ,wrap_elf_part1的读取方式是第一个字节表示被异或的数字,这里是0xD3,后面的四个字节表示一个段的长度,随后读取指定长度的字节并异或,之后再读取四个字节获取到下一个段的长度,以此类推,直到读取到文件末尾,这里原作者是已经把这里的异或过程给逆向出来了的,具体的已经涉及到了向量之类的了


同时这个函数的最后会有一个数据的相加

a1[19] = v31 + v19 + v43 + v7 + 17;

我们随便找一个变量进行分析,比如v19

v19——>v20——>v6    v19是由v6赋值而来的,那么v6是什么?

v6是之前我们inlineHOOK的v4的下一个byte,同时还是四字节的

那就是对应的一个数据块的大小

a1[19] = v31 + v19 + v43 + v7 + 17;

那么就是说明这里有四个数据块,而17  == (四个对应数据块的addr)4*4+ 1(异或的D3)

结构体编写

再根据相应的位置,可以创建一个相应的结构体。这里的a1是QWORD的指针是八字节的,数组前面的0-8都被使用了所有要填充9 8=72字节,相应的后面也要填充4 8=32字节

struct deal_extra
{
  char blank[72];
  int phnum;
  int *extra_part1;
  int phdr_size;
  char blank2[32];
  int *extra_part2;
  int *extra_part3;
  int *extra_part4;
  int *main_elf;
};

所以这里作者应该是写错了,作者写的36字节的char ,不过是差了4字节,不是八字节就无所谓。这里会自动补的。

同理,回溯到之前调用这里的函数也要去补齐对应的结构体,于是到了sub_49F0的位置

看着这里把对应的数据块的指针传递出去了,那么就要去给v7创建一个结构体,看看他怎么个事

注意这里是char 指针,所以这里是298=232字节的空

struct deal_extra_B
{
  char blank[232];
  int *extra_part1;
  char blank1[8];
  int phnum;
  int *extra_part4;
  char blank2[24];
  int *extra_part2;
  char blank3[8];
  int *extra_part3;
};


这里已经被v7的结构体给上去了,而这里的函数,正是在最初判断是自定义Linker加固so的调用的地方,而且传入的参数正是v7,于是我们来看看自定义Linker的位置

可以看到的是v9作为switch的条件判断,是v7的extra_part4也就是第四个数据块的数据传入的,是用于处理动态链接库的,那么通过源码比对,extra_part4对应Section Header Table(节头表)的.dynamic

之后再来看看自定义Linker的处理动态链接库的下面的函数sub_4918

这里去处理了extra_part0-extra_part3这三个数据块的数据,我们再去看看,这三个数据块是干嘛的

这里先普及一下重定向的东西:

重定位类型(R_*)

在重定位操作中,数字如 0x4010x408 可能也与 重定位类型(如 R_386_32R_X86_64_64 等)相关,这些类型定义了如何修改程序中指定地址的值。具体的重定位类型由操作系统和体系结构决定。

举例:典型的 .rela.plt 条目结构

假设 .rela.plt 节包含重定位条目,条目可能如下所示:

mathematica复制代码Offset      Type      Symbol    Addend
0x401       R_X86_64_PC32  foo     0x0
0x101       R_X86_64_64    bar     0x0
0x408       R_X86_64_PC32  baz     0x0

而在android的源码动态的重定向的定义下


其实是有出现类似的字眼出现的,但是能够直接确定是重定向中的哪个节,还是有点难道的

“这个函数中的switch是用来处理重定位的,因为重定位主要有基址重定位和符号重定位,这两个的值分别是0x4030x402所以extra_part2extra_part3分别对应着.rela.plt(402重定位)和.rela.dyn(403重定位)”

同时再来看看sub_5E6C(),也就是我们最初找到的条目大小和num的位置,这里也可以重新附上之前创建的结构体了

这里确实就可以知道a1也就是extra_part1代表的就是程序头program header table的位置了

  • 数据块1表示program header table
  • 数据块2表示.rela.plt
  • 数据块3表示.rela.dyn
  • 数据块4表示.dynamic

分割四个数据块:

import copy
import zlib

def RC4(data, key):
    S = list(range(256))
    j = 0
    out = []

    # KSA Phase
    for i in range(256):
        j = (j + S[i] + key[i % len(key)]) % 256
        S[i], S[j] = S[j], S[i]

    # PRGA Phase
    i = j = 0
    for ch in data:
        i = (i + 1) % 256
        j = (j + S[i]) % 256
        S[i], S[j] = S[j], S[i]
        out.append(ch ^ S[(S[i] + S[j]) % 256])

    return out

def RC4decrypt(ciphertext, key):
    return RC4(ciphertext, key)

wrap_elf_start = 0x1e270
wrap_elf_size = 0xb8010
key = b"vUV4#\x91#SVt"
with open('com.oacia.apk_protect/assets/libjiagu_a64.so', 'rb') as f:
    wrap_elf = f.read()

# 对密文进行解密
dec_compress_elf = RC4decrypt(wrap_elf[wrap_elf_start:wrap_elf_start + wrap_elf_size], key)
dec_elf = zlib.decompress(bytes(dec_compress_elf[4::]))
with open('wrap_elf', 'wb') as f:
    f.write(dec_elf)
 #以上解密

 #定义结构体   
class part:
    def __init__(self):
        self.name = ""
        self.value = b''
        self.offset = 0
        self.size = 0

index = 1
extra_part = [part() for _ in range(7)]

seg = ["phdr", ".rela.plt", ".rela.dyn", ".dynamic"]
v_xor = dec_elf[0]  #异或的是第一个的0xD3

for i in range(4):
    size = int.from_bytes(dec_elf[index:index + 4], 'little')   #获取相应位置的四字节的size
    index += 4 
    extra_part[i + 1].name = seg[i]   #赋值属性
    extra_part[i + 1].value = bytes(map(lambda x: x ^ v_xor, dec_elf[index:index + size]))  #定义匿名函数,只是去异或
    extra_part[i + 1].size = size
    index += size

for p in extra_part:
    if p.value!=b'':
        filename = f"libjiagu.so_{hex(p.size)}_{p.name}"
        print(f"[{p.name}] get {filename}, size: {hex(p.size)}")
        with open(filename,'wb') as f:
            f.write(p.value)  

通过解密每个数据块,我们可以去实现恢复最初的ELF文件格式,在这里之后我们就得到了四个解密之后的数据块的数据,同时四个分别代表了的是program header table,.rela.plt  .rela.dyn  .dynamic 到最后我们就要去实现的是位置的替换填充

program header table:选中之后就直接把我们解密的替换上去

.dynamic:这里首先要去找到对应的dynamic的偏移地址,按照对应字节数据进行填补

.rela.plt .rela.dyn:这里需要重定向并且使用d_tag的宏定义来找到对应的位置

最后来实现补齐和填充,最后我们就把解密的部分重新替换到了原来的so中了


dlopen也可以在对应的交叉引用下,看到更多的信息了

免费评分

参与人数 6威望 +1 吾爱币 +26 热心值 +6 收起 理由
Tonyha7 + 2 + 1 用心讨论,共获提升!
beihai1314 + 1 + 1 我很赞同!
allspark + 1 + 1 用心讨论,共获提升!
lj0822 + 1 + 1 我很赞同!
laozhang4201 + 1 + 1 热心回复!
正己 + 1 + 20 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

推荐
Hmily 发表于 2024-12-30 15:00
底部多了几个图是插丢了还是传多了?需要处理一下。

另外之前帖子图片补充是把之前帖子内容也合并过来了吗?如果是之前帖子是不是直接没必要了,值保留这个帖子就行了?

因为最近论坛在收录精华帖,需要处理这些丢失图片的精华帖,希望帮忙尽快处理一下。
推荐
orphanping 发表于 2024-12-30 18:34
3#
正己 发表于 2024-12-30 15:19
师傅360加固分析的,我就当一个系列加一个精华了哈

点评

看看内容是不是包含之前帖子,如果包含可以取消精华改成这个给精华了,那个帖子图都丢了没法阅读,而且作者提供的原文的图也挂了,想帮他补都不行。  详情 回复 发表于 2024-12-30 15:33
4#
Hmily 发表于 2024-12-30 15:33
正己 发表于 2024-12-30 15:19
师傅360加固分析的,我就当一个系列加一个精华了哈

看看内容是不是包含之前帖子,如果包含可以取消精华改成这个给精华了,那个帖子图都丢了没法阅读,而且作者提供的原文的图也挂了,想帮他补都不行。

点评

内容重复,上一篇撤销了  详情 回复 发表于 2024-12-31 08:22
5#
 楼主| chenchenchen777 发表于 2024-12-30 16:28 |楼主
Hmily 发表于 2024-12-30 15:33
看看内容是不是包含之前帖子,如果包含可以取消精华改成这个给精华了,那个帖子图都丢了没法阅读,而且作 ...

我在编辑的位置看着最后是没有多余图片的,但是发出来就有多余的图片,之前那个文章我删掉吗

点评

另外我建议看一下之前我写的教程,如何插入md的图,可能有所帮助https://www.52pojie.cn/forum.php?mod=viewthread&tid=717627&page=1#pid51478900  详情 回复 发表于 2024-12-30 16:36
1、是你传多了,比如文章最后这个图片”image-20241230133111984.png“,我编辑你主题,看到你上传列表里有这个图,但实际你文章没引用,所以他会自动给你放到文章尾部,另外”image-20241230133111984.png“这个图  详情 回复 发表于 2024-12-30 16:35
6#
Hmily 发表于 2024-12-30 16:35
chenchenchen777 发表于 2024-12-30 16:28
我在编辑的位置看着最后是没有多余图片的,但是发出来就有多余的图片,之前那个文章我删掉吗

1、是你传多了,比如文章最后这个图片”image-20241230133111984.png“,我编辑你主题,看到你上传列表里有这个图,但实际你文章没引用,所以他会自动给你放到文章尾部,另外”image-20241230133111984.png“这个图和”image-20241230133112032.png“这个我看了其实是同一个图片,不一样的名字,但后面这个你引用了,我更确信你重复上传同一张图片但名字不同,你根据我说的排查一下。
2、之前文章需要你确认是否被包含在本文中,如果本文已经包含,直接告诉@正己 即可,他会撤销精华删除之前帖子,给你这个帖子加精华处理。
7#
Hmily 发表于 2024-12-30 16:36
chenchenchen777 发表于 2024-12-30 16:28
我在编辑的位置看着最后是没有多余图片的,但是发出来就有多余的图片,之前那个文章我删掉吗

另外我建议看一下之前我写的教程,如何插入md的图,可能有所帮助https://www.52pojie.cn/forum.php ... ;page=1#pid51478900
9#
wasm2023 发表于 2024-12-30 22:56
是不是漏掉了参考资料链接
10#
月清晖 发表于 2024-12-31 07:15
基础功扎实,思路清晰,感谢楼主分享!
期待(下)
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2025-1-4 07:46

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表