type
Post
status
Published
date
Jul 14, 2026
slug
summary
• 在Android7.0及以后引入了命名空间把应用层的共享库与系统层的共享库区分开来,直接影响就是在7.0以后dlopen和dlsym函数的限制接下来将通过源码和实战来详细了解
• 关于本地命名空间介绍参考基于命名空间的动态链接—— 隔离 Android 中应用程序和系统的本地库,该文章详细的讲解了命名空间的各种细节,后面分析也是基于这个基础
• 下面文章分析都是基于Android9.0源码,其它版本可能稍有不同,但是具体思路都是一样
tags
namespace
吾爱破解
category
转载
icon
password
网址
上次编辑时间
Jul 14, 2026 07:26 AM
comment
AI 总结
1.Android命名空间介绍
- 在Android7.0及以后引入了命名空间把应用层的共享库与系统层的共享库区分开来,直接影响就是在7.0以后dlopen和dlsym函数的限制接下来将通过源码和实战来详细了解
- 关于本地命名空间介绍参考 基于命名空间的动态链接—— 隔离 Android 中应用程序和系统的本地库 ,该文章详细的讲解了命名空间的各种细节,后面分析也是基于这个基础
- 下面文章分析都是基于 Android9.0 源码,其它版本可能稍有不同,但是具体思路都是一样
- 阅读上面文章有几点需要弄清楚
- app进程分为默认命名空间和java对应的类加载器命名空间,命名空间是相互隔离开的
- app中不同的ClassLoader对应不同的命名空间
- 查找共享库和符号是在 caller namespace 和所链接的 link namespace 内查找的
- 两个命名空间创建单向链接用于将库共享到另一个命名空间
- 每个命名空间有ld\_library\_paths(通常为null,好像可以包含预加载库环境变量) default\_library\_paths(默认路径,正常调用是该库所在路径) permitted\_paths(允许库所在路径)
2.Android命名空间分析
- 在分析前先说说如何打印linker日志
- 查看linker.cpp源码有如下类似代码
- 跟踪LD\_LOG来到linker\_logger.cpp中的Log函数
- 可以看到日志是否打印有flags\_成员来控制,查找它的赋值的关键地方
- 分析发现通过设置环境变量 debug.ld.all 来控制 dlopen , dlsym , dlerror ,这里我是直接用的模拟器直接用 setprop 命令,其它日志开关就不详解了
- linker相关环境变量如下:
- debug.ld.greylist\_disabled true 关闭系统库灰名单,这样无法访问如libandroid\_runtime.so等系统私有库,在target sdk>6.0以上废弃
- debug.ld.app.${process\_name} \[dlopen,dlsym,dlerror\]开启某个进程对应的linker日志
- debug.ld.all \[dlerror,dlopen,dlsym\] 开启所有进程linker日志
- LD\_DEBUG \[0,1,2\]开启调试日志,设置并不起作用,猜想是执行时间太早,可能在zygote进程就已经执行过了,而它对应控制日志开关是 g\_ld\_debug\_verbosity 变量,因此进程内重新赋值即可打开日志,方法下面再说
- 既然7.0以上存在限制那就直接从 dlopen 函数开始,我们有时常常会用到 libandroid\_runtime.so 去获取全局JavaVM对象,下面开始代码尝试
- 正常运行得到下列输出
- 错误日志里面包含了关键提示 is not accessible for the namespace: \[name="classloader-namespace" 告诉我们没有权限访问,接下来深入源码,在此使用在线查询网站 Android源码 ,搜索 dlopen (提示:linker相关的代码都在 bionic 目录下减少查询范围)定义 
- 进入libdl.cpp查看源码
- 具体实现在 \_\_loader\_dlopen,一步步函数跟踪最终到linker.cpp中的 do\_dlopen 函数,前面都是获取一些调用方相关参数
- 这里关键两个函数 find\_containing\_library 获取调用者的 soinfo 结构,get\_caller\_namespace 获取调用者的命名空间,在Android7.0以下 dlopen 实际上返回的就是这个 soinfo 结构体,在7.0以上我们的目的还是为了获取库对应的 soinfo 结构体
- find\_containing\_library 函数源码
- solist\_get\_head 获取第一个soinfo结构体,跟踪源码得到定义的一个静态对象
- 从这里我们就可以知道只要找到这个 solist 地址就可以遍历当前进程所有soinfo结构体,而 get\_caller\_namespace 实际上就是获取 soinfo 中的成员
- 接着分析 do\_dlopen ,关键查找代码如下
- 而 find\_library 调用 find\_libraries , find\_libraries 关键代码:
- 最终查找库实现是 find\_library\_internal 函数,根据上面参数分析是要查找链接命名空间的,而其链接命名空间共享的库正是上面文章介绍的配置文件在/etc/public.libraries.txt,但是它位于/system目录下并没有权限修改,因此要想其它办法访问私有库
- 在当前已加载的库中查找 find\_loaded\_library\_by\_soname
- 这里注意,查找已加载的库只查找库名,因为 soinfo 结构体中的 soname\_ 只保存了库名, caller namespace 找不到才找 link namespace ,通过 is\_accessible 函数判断是否有访问权限
- 可见 allow\_all\_shared *libs 成员可以影响查找,否则就在共享的库中查找,这里便可以知道 查找已经加载的库不要传入完整路径,通过改变allow\_all\_shared\_libs\_成员可以直接让权限允许*
- 当 caller namespace 和 link namespace 没有加载目标库则要从文件系统装载,关键函数 load\_library 是两个重载函数,第一个函数主要查找共享库并判断权限打开文件
- open\_library 函数负责打开共享库,这里需要注意下当传入的是完整路径时可以直接打开,而只传入库名时会依次从 caller namespace 的 ld\_library\_paths , default\_library\_paths 查找对应库,如果找不到则判断是否开启灰名单,如果开启并且是灰名单列表中的库时才能打开文件,否者无法打开文件直接就加载失败返回了,基于此可以开启 linker 调试日志查看
- 当打开文件失败后 load\_library 就直接返回false并输出日志,否者调用另一个重载函数,因此我们查找未打开的库时 dlopen 传入完整路径可以过掉第一层权限检测
- 另一个 load\_library 重载函数负责加载共享库,先检测共享库从文件加载偏移必须要进行页对齐,同时还会检测符号链接避免加载同一文件等等,其它关键代码如下
- 上面代码发现这里有个灰名单判断,灰名单正包含我们想要的 libandroid\_runtime.so 分析可知在App的targetSdk大于7.0就已经废弃了,所以如果想正常使用该库可以调整targetSdk小于24,这有个缺陷就是每次启动会有个警告弹窗,我就不截图大家可以自己试试。关键判断是否有权限是 is\_accessible 函数,对应源码如下
- 因此命名空间权限检测都是基于所描述的三个路径,这三个路径是在创建命名空间时确认,接着返回 find\_library\_internal 函数继续分析
- 当前面已加载库没找到且 caller namespace 路径内也找不到时,则继续查找 link namespace
- 回到 find\_library\_internal 再次调用 load\_library 加载,注意 这次加载是用的链接命名空间 ,至此 dlopen 查找库就分析完毕
- dlsym 分析与 dlopen 类似,都有命名空间限制,在此就不分析了大家可以自行分析
- 接下来再分析下 Java 中使用 System.load 加载库, Java 层顺着 System.load 跟踪最终是走到C层的 native\_loader.cpp 中的 OpenNativeLibrary 函数,源码路径/system/core/libnativeloader/native\_loader.cpp
- 因此如果是自定义的类加载器会拥有私有的命名空间,默认的ClassLoader命名空间是由 ApplicationLoaders 创建的,分析该native——loader类也可以看到一些查找路径和环境变量,其它分析略
3.Android命名空间和一些内存相关总结
- 经过第二步分析 dlopen , dlsym 函数调用存在命名空间限制,加载库和查找符号只在自己的命名空间内查找, 但这并不意味着无法跨命名空间访问,我们只是没有权限加载库和查找符号,只要我们知道地址同样可以访问 这是C/C++代码控制不了的,而我们编写C++代码看似有 private 限制,事实上那只是编译器欺骗你在编译阶段控制权限否则编译不通过,而编译成二进制后只要你能知道地址你可以随意访问
- 通常情况下进程中只包含两个命名空间(默认命名空间,Java类加载器命名空间),匿名命名空间看源码可知就是默认命名空间, 因此访问系统私有库可以改变调用者的命名空间得到权限
- 命名空间之间的库共享通过单向链接来共享, 因此可以更改 link namespace 的访问权限得到私有库权限 , 可以更改总开关allow\_all\_shared libs ,也可以添加库到共享集合中
- 谈一下Android中C++对象内存布局,这在后面写代码和分析起作用
- 当类中不存在虚方法时则不包含虚方法指针(vptr),指自己没有定义虚方法且父类也没有定义虚方法
- 无论继承的层级有多深,一个类对象最多就一个虚方法指针,且虚指针在对象开始处0偏移
- 类中的成员如public和private分开多次定义,共有权限成员并不会合并在一起,始终遵守后定义的成员有更高的地址
- 当包含各种不同大小的成员时也不会重排成员,但是会遵守对齐要求,因此可能会扩大对象空间
4.共享库加载流程总结及绕过思路
- 基于命名空间加载库的流程
- 当传入路径不是完整路径时会在已加载的库中查找,否者跳过这一步
- 查找 caller namespace 是否已经加载,如果加载过则直接返回
- 查找 link namespace 是否有权限访问该so,如果有权限访问则继续查找已加载库中是否包含,包含则直接返回
- 当已加载库没找到时尝试在当前caller命名空间加载
- 如果传入完整路径则直接打开文件,然后进行下一步加载
- 非完整路径要在 caller namespace中的ld\_library\_paths\_和default\_library\_paths\_路径下查找
- 都没查找到则进一步判断开启灰名单,判断库在名单中且有访问权限
- 前三步负责找到文件并打开,如果没找到则直接加载失败,否者进行下一步加载
- 假设找到文件后且传入的 extinfo 为空时还要进一步判断库是否已经加载(避免符号链接重复加载同一个so)。首先查找 caller namespace 已加载列表,再查找 link namespace 同时也进行权限检测,如果找到则直接返回,否者进行下一步加载
- 如果库不是临时文件则继续判断 caller namespace 的三个路径下是否包含该库,如果包含则可以进行下一步加载
- 如果上一步 caller namespace 没有访问权限,则还要继续判断灰名单,否者抛出错误加载失败
- 前两步未加载成功则继续 link namespace 加载
- 判断 link namespace 是否已经加载过,当然走到这一步是没有加载的
- 判断 link namespace 是否有so的访问权限
- 如果有访问权限则会调用 link namespace 进行加载,权限判断又重复第二步,只是换了一个命名空间加载而已
- 私有库权限绕过
- 加载流程分析清楚后权限检测基于 caller namespace 和 link namespace
- caller namespace 可以从 调用源替换、is\_isolated\_和三个加载路径 修改
- link namespace 可以修改 allow\_all\_shared\_libs\_和添加到shared\_lib\_sonames\_集合 中
- 甚至可以改变库对应的 命名空间
- 改变其它 soinfo 等等,其它方法请自由发挥
- 注意: 查找已加载库是不查找完整路径的,非完整路径加载要先通过命名空间路径权限检测才能进行加载
5.Android命名空间代码实战
- 针对上面的分析我们先提出几点疑问和要求,接下来一步一步实现
- 真的每个类加载器拥有不同命名空间吗?
- Java层跨类加载器native函数如何查找与调用?
- 当默认命名空间使用 dlopen 和 dlsym 访问类加载器命名空间中的库和函数会怎样?
- 实现跨命名空间加载库和查找符号
- 测试多个 ClassLoader命名空间 共享库加载情况,java层关键代码
- 运行上面代码后看一下进程的 maps 文件,过滤下输出
- 可以看到确实拥有两个so,/data/data/路径是我们动态加载的,/data/app/路径是系统加载的,证明了不同 ClassLoader命名空间 拥有自己的私有库
- 测试Java跨命名空间 native 函数如何使用
- 当我们不点击注册直接点击调用时输出
- 直接报错没找到函数实现,并且还找了默认命名规则函数 Java\_com\_beichen\_fakelinker\_MainActivity\_dynRegister , Java\_com\_beichen\_fakelinker\_MainActivity\_dynRegister\_\_ ,这是JNI规则可以看到有两个默认实现名字,从前面分析命名空间可知, 在 caller namespace 内查找函数,而由于我们没有默认实现它所以找不到,这也说明一个问题跨命名空间默认实现函数也是不生效的,因为根本不会在另一个命名空间内查找
- 接着尝试跨命名空间动态注册 native 函数的情况,关键代码如下
- 再次先点击动态注册然后再点击调用输出如下
- 可见通过注册后就能在主类加载中成功调用, 这也证实知道地址可以跨命名空间调用 ,这里猜想 JNI 维护着一个函数对应表,对应着每个java函数与native函数绑定,动态注册就会建立绑定关系,调用时直接地址调用省去查找的过程
- 实现默认命名空间 dlopen , dlsym 查找 ClassLoader命名空间
- 在实现之前我们确认在调用 do\_dlopen 函数时有一个 caller\_addr 参数保存着调用者的地址,只要更改这个参数就能达到替换 caller namespace ,之前分析的 link namespace 共享库配置文件/etc/public.libraries.txt现在看下有哪些库
- 现在回过头去查看 dlopen 函数
- dlopen 函数在 libdl.so 中导出,而 libdl.so 配置在共享so中,因此我们有权限访问,而真正的 loader\_dlopen函数是在linker中导出,linker并不在共享列表中没有权限访问。\\在这dlopen函数我们可以发现它先获取返回地址再调用\_\_loader\_dlopen函数,这意味着在libdl.so中必定会引用 loader\_dlopen函数 ,因此我们有办法拿到它,把 libdl.so\\拿到IDA分析如下面(我用的x86\_64)汇编
- 第一条汇编指令
mov rdx, [rsp]就是在获取返回地址,这需要对x86函数调用及栈桢了解,可以查看 x86-64 下函数调用及栈帧原理 。第二条指令直接跳转,在这里采用了 PIC 位置无关代码可参考 位置独立代码(PIC)在共享库中 , \_\_loader\_dlopen是导入的函数,因此我们只要获取到它的地址就可以直接传递caller\_address调用
- 查找导入表方式。这里提一下 ELF文件格式 ,文件格式分为 链接视图,执行视图,链接视图程序头部表可选,执行视图节区头部表可选。链接是针对编译阶段,当你的库依赖另一个库时链接器会去读依赖库的节区表,而执行视图只要程序头部表即可,节区头部表不是必须的,并且有些节区是不会出现在内存中的(关键的SHT\_SYMTAB和SHT\_STRTAB都不会被加载) 。导入表与导出表都是存在于动态符号表中(DT\_SYMTAB),这是动态链接时所必须的,在创建 soinfo 后紧接着就会预链接和赋值 soinfo 相关成员,因此共享库的程序头部表通常不会处理, 各大安全加固厂商基本都会处理节区表,删除节区表中的符号表,因此根据节区表查找不可靠 ,当然处理动态符号表然后自己实现重定向也是可能的。下面是获取导入表符号地址步骤:
- 获取动态段(PT\_DYNAMIC)
- 获取相关的动态节区(DT\_STRTAB,DT\_SYMTAB,DT\_PLTREL 等等)
- 通过hash表(只有gnu hash表时需要暴力查找名字)查找到对应符号表索引
- 解析符号地址
- 代码就不贴了,在 fake\_linker.c 中实现
- 讲到导入导出表查找就顺便说说内部符号表查找,后面我们查找 solist 和开启linker日志都是通过内部符号表查找的, 内部符号表并不存在内存中,只能通过打开文件查找节区表来查找,如果删除掉节区符号表那就无法直接查找了,但一般未加固和系统库并不会删掉节区表 。查找内部符号流程如下:
- 获取节区头部表,查找有关节区SHT\_SYMTAB,SHT\_STRTAB
- 暴力查找符号名字
- 解析符号地址
- 代码也不贴了,都在 fake\_linker.c 中实现
- 现在采用解析\_\_loader\_dlopen符号调用 libandroid\_runtime.so 实现代码如下:
- 详细输出日志如下:
- 可以看到正常情况下是获取不到 JavaVM 地址,而修改命名空间后则能获取到,并且导入表查找与反汇编查找结果是相同的
- 实现 默认命名空间 查找 ClassLoader命名空间
- 代码输出有如下日志:
- 可见在默认命名空间内找不到就又新加载了一个,现在看下 maps 文件
- 发现已经有三个库了,由此也可见 默认命名空间并没有特殊权限,找不到就尝试加载,如果dlopen只传入库名同样也会找不到
6.简单探索soinfo
- 前面提到对 soinfo 的查找离不开 solist ,这个静态变量存储着起始 soinfo ,而 soinfo 通过链表链接起来的。分析 linker 发现 solist 是内部符号并未导出,因此需要用到前面提到的查找内部符号表
- 为了编写代码方便我们自己创建 soinfo 结构体,然后再与真实 soinfo 成员偏移进行验证
- 查看源码 soinfo 结构体保存了 程序头部表,程序头数量,基址,大小,符号表,字符串表,hash表,重定位表,init fini函数等等 ,涵盖所有需要的结构,对 soinfo 赋值是在 dlopen 内完成的,因此我们可以基于 soinfo 还原so,对于加固的分析了解它更加重要
- 紧接着验证一下偏移,看结构体是否匹配,把库导出来用IDA分析查看下偏移是否一致,我只验证了9.0,其它版本自行验证
- 然后查看一下关键成员
- 可见默认情况下是关闭灰名单的,并且开启了命名空间隔离,也看到了 ClassLoader命名空间 默认路径是自己的库路径,还有允许自己的私有目录
- 前面分析更改 link namespace的allow\_all\_shared\_libs\_成员 就能通过检测,现在来试一下
- 得到下面输出成功找到私有库
- 再来尝试更改 caller namespace中的is\_isolated\_属性
- 运行发现居然崩溃了,输出日志如下
- 这里我们更改时发生访问错误,那我们先关掉修改看一下该地址对应的 maps 映射权限
- 可以看到 0x7d72583e8290 地址对应的权限是 r--p 并没有写权限,这也提醒我们修改时要注意下权限
- soinfo 结构体保存了 strtab\_, symtab\_, plt 相关结构,也意味着我们可以根据它来进行符号查找,这里我就不实现了。
7.总结
- 在7.0及以上引入命名空间限制了用户引用系统私有库,而 caller namespace 的确定是根据 dlopen 返回地址查找 soinfo ,因此你看到其它注入框架时 为什么要修改LR寄存器为libc的基址,还有dlopen只有两个参数为什么传入三个参数等,现在你该明白它们都是一个目的修改caller namespace 。 绕过权限通过围绕caller namespace和link namespace做文章就发现有多种方法,选择合适的就好
- 共享库运行过程中 soinfo 结构体非常重要,它保存了所有需要的结构,加载和查找都离不开它,对于so加固与脱壳应用很多。
- 对于共享库符号查找, 导入或导出符号必然会存在内存中,可以根据程序头查找或根据soinfo,而内部符号存在于节区中并不加载到内存,所以内部符号要根据节区表查找,如果节区表被处理或删除那你只能根据其它特征来查找 。通常加固会处理掉节区表,现根据原来的 010editor elf模板 加上对动态节区的分析,对应加固so分析及还原节区有点帮助,模板见附件,如有问题请自行修改
- 教程中用到的源码 链接: https://pan.baidu.com/s/1u-vuu8jfNjkpxqXhVhMlGQ 提取码: yed1 ,新建一个AndroidStudio工程把源码替换掉即可

- Author:24th
- URL:https://24th.top/article/39de5b08-46db-80a2-9145-ef937d696ea3
- Copyright:All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!




