|
本帖最后由 beichen 于 2019-5-6 12:13 编辑
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(kLogDlopen,
"dlopen(name=\"%s\", flags=0x%x, extinfo=%s, caller=\"%s\", caller_ns=%s@%p) ...",
-
跟踪LD_LOG来到linker_logger.cpp中的Log函数
void LinkerLogger::Log(uint32_t type, const char* format, ...) {
if ((flags_ & type) == 0) {
return;
}
va_list ap;
va_start(ap, format);
async_safe_format_log_va_list(ANDROID_LOG_DEBUG, "linker", format, ap);
va_end(ap);
}
- 可以看到日志是否打印有flags_成员来控制,查找它的赋值的关键地方
static CachedProperty debug_ld_all("debug.ld.all");
flags_ |= ParseProperty(debug_ld_all.Get());
- 分析发现通过设置环境变量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对象,下面开始代码尝试
#if defined(__LP64__)
lib = "/system/lib64/libandroid_runtime.so";
#else
lib = "/system/lib/libandroid_runtime.so";
#endif
p_runtime_handle = dlopen(lib, RTLD_LAZY);
p_vm = NULL;
if (p_runtime_handle != NULL) {
p_vm = dlsym(p_runtime_handle, "_ZN7android14AndroidRuntime7mJavaVME");
}
LOGD("runtime: %p", p_runtime_handle);
LOGD("mJavaVM: %p", p_vm);
-
正常运行得到下列输出
library "/system/lib64/libandroid_runtime.so" ("/system/lib64/libandroid_runtime.so") needed or dlopened by "/data/app/com.beichen.fakelinker-PpeKQnfSTDvM-Soa83VUag==/lib/x86_64/libnative-lib.so" is not accessible for the namespace: [name="classloader-namespace", ld_library_paths="", default_library_paths="/data/app/com.beichen.fakelinker-PpeKQnfSTDvM-Soa83VUag==/lib/x86_64:/data/app/com.beichen.fakelinker-PpeKQnfSTDvM-Soa83VUag==/base.apk!/lib/x86_64", permitted_paths="/data:/mnt/expand:/data/data/com.beichen.fakelinker"]
-
错误日志里面包含了关键提示 is not accessible for the namespace: [name="classloader-namespace" 告诉我们没有权限访问,接下来深入源码,在此使用在线查询网站Android源码,搜索dlopen(提示:linker相关的代码都在bionic目录下减少查询范围)定义

-
进入libdl.cpp查看源码
__attribute__((__weak__))
void* dlopen(const char* filename, int flag) {
const void* caller_addr = __builtin_return_address(0);
return __loader_dlopen(filename, flag, caller_addr);
}
-
具体实现在 __loader_dlopen,一步步函数跟踪最终到linker.cpp中的do_dlopen函数,前面都是获取一些调用方相关参数
void* do_dlopen(const char* name, int flags,
const android_dlextinfo* extinfo,
const void* caller_addr) {
std::string trace_prefix = std::string("dlopen: ") + (name == nullptr ? "(nullptr)" : name);
ScopedTrace trace(trace_prefix.c_str());
ScopedTrace loading_trace((trace_prefix + " - loading and linking").c_str());
soinfo* const caller = find_containing_library(caller_addr);
android_namespace_t* ns = get_caller_namespace(caller);
...略
if (extinfo != nullptr) {
...略
ns = extinfo->library_namespace;
}
}
...略
}
-
这里关键两个函数find_containing_library获取调用者的soinfo结构,get_caller_namespace获取调用者的命名空间,在Android7.0以下dlopen实际上返回的就是这个soinfo结构体,在7.0以上我们的目的还是为了获取库对应的soinfo结构体
-
find_containing_library函数源码
soinfo* find_containing_library(const void* p) {
ElfW(Addr) address = reinterpret_cast<ElfW(Addr)>(p);
for (soinfo* si = solist_get_head()
if (address >= si->base && address - si->base < si->size) {
return si;
}
}
return nullptr;
}
-
solist_get_head获取第一个soinfo结构体,跟踪源码得到定义的一个静态对象
static soinfo* solist;
...略
soinfo* solist_get_head() {
return solist;
}
-
从这里我们就可以知道只要找到这个solist地址就可以遍历当前进程所有soinfo结构体,而get_caller_namespace实际上就是获取soinfo中的成员
static android_namespace_t* get_caller_namespace(soinfo* caller) {
return caller != nullptr ? caller->get_primary_namespace() : g_anonymous_namespace;
}
-
接着分析do_dlopen,关键查找代码如下
...略
soinfo* si = find_library(ns, translated_name, flags, extinfo, caller);
loading_trace.End();
if (si != nullptr) {
void* handle = si->to_handle();
LD_LOG(kLogDlopen,
"... dlopen calling constructors: realpath=\"%s\", soname=\"%s\", handle=%p",
si->get_realpath(), si->get_soname(), handle);
si->call_constructors();
failure_guard.Disable();
LD_LOG(kLogDlopen,
"... dlopen successful: realpath=\"%s\", soname=\"%s\", handle=%p",
si->get_realpath(), si->get_soname(), handle);
return handle;
}
return nullptr;
-
而find_library调用find_libraries,find_libraries关键代码:
for (size_t i = 0; i<load_tasks.size(); ++i) {
LoadTask* task = load_tasks[i];
soinfo* needed_by = task->get_needed_by();
bool is_dt_needed = needed_by != nullptr && (needed_by != start_with || add_as_children);
task->set_extinfo(is_dt_needed ? nullptr : extinfo);
task->set_dt_needed(is_dt_needed);
if (!find_library_internal(const_cast<android_namespace_t*>(task->get_start_from()),
task,
&zip_archive_cache,
&load_tasks,
rtld_flags,
search_linked_namespaces || is_dt_needed)) {
return false;
}
soinfo* si = task->get_soinfo();
if (is_dt_needed) {
needed_by->add_child(si);
}
if (ld_preloads != nullptr && soinfos_count < ld_preloads_count) {
ld_preloads->push_back(si);
}
if (soinfos_count < library_names_count) {
soinfos[soinfos_count++] = si;
}
}
-
最终查找库实现是find_library_internal函数,根据上面参数分析是要查找链接命名空间的,而其链接命名空间共享的库正是上面文章介绍的配置文件在/etc/public.libraries.txt,但是它位于/system目录下并没有权限修改,因此要想其它办法访问私有库
static bool find_library_internal(android_namespace_t* ns,
LoadTask* task,
ZipArchiveCache* zip_archive_cache,
LoadTaskList* load_tasks,
int rtld_flags,
bool search_linked_namespaces) {
soinfo* candidate;
if (find_loaded_library_by_soname(ns, task->get_name(), search_linked_namespaces, &candidate)) {
task->set_soinfo(candidate);
return true;
}
TRACE("[ \"%s\" find_loaded_library_by_soname failed (*candidate=%s@%p). Trying harder...]",
task->get_name(), candidate == nullptr ? "n/a" : candidate->get_realpath(), candidate);
if (load_library(ns, task, zip_archive_cache, load_tasks, rtld_flags, search_linked_namespaces)) {
return true;
}
if (search_linked_namespaces) {
DlErrorRestorer dlerror_restorer;
for (auto& linked_namespace : ns->linked_namespaces()) {
if (find_library_in_linked_namespace(linked_namespace, task)) {
if (task->get_soinfo() == nullptr) {
if (load_library(linked_namespace.linked_namespace(), task, zip_archive_cache, load_tasks, rtld_flags, false)) {
return true;
}
} else {
return true;
}
}
}
}
return false;
}
-
在当前已加载的库中查找find_loaded_library_by_soname
static bool find_loaded_library_by_soname(android_namespace_t* ns,
const char* name,
bool search_linked_namespaces,
soinfo** candidate) {
*candidate = nullptr;
if (strchr(name, '/') != nullptr) {
return false;
}
bool found = find_loaded_library_by_soname(ns, name, candidate);
if (!found && search_linked_namespaces) {
for (auto& link : ns->linked_namespaces()) {
if (!link.is_accessible(name)) {
continue;
}
android_namespace_t* linked_ns = link.linked_namespace();
if (find_loaded_library_by_soname(linked_ns, name, candidate)) {
return true;
}
}
}
return found;
}
-
这里注意,查找已加载的库只查找库名,因为soinfo结构体中的soname_只保存了库名,caller namespace找不到才找link namespace,通过is_accessible函数判断是否有访问权限
bool is_accessible(const char* soname) const {
if (soname == nullptr) {
return false;
}
return allow_all_shared_libs_ || shared_lib_sonames_.find(soname) != shared_lib_sonames_.end();
}
-
可见allow_all_sharedlibs成员可以影响查找,否则就在共享的库中查找,这里便可以知道查找已经加载的库不要传入完整路径,通过改变allow_all_shared_libs_成员可以直接让权限允许
-
当caller namespace和link namespace没有加载目标库则要从文件系统装载,关键函数load_library是两个重载函数,第一个函数主要查找共享库并判断权限打开文件
static bool load_library(android_namespace_t* ns,
LoadTask* task,
ZipArchiveCache* zip_archive_cache,
LoadTaskList* load_tasks,
int rtld_flags,
bool search_linked_namespaces) {
...略
int fd = open_library(ns, zip_archive_cache, name, needed_by, &file_offset, &realpath);
if (fd == -1) {
DL_ERR("library \"%s\" not found", name);
return false;
}
task->set_fd(fd, true);
task->set_file_offset(file_offset);
return load_library(ns, task, load_tasks, rtld_flags, realpath, search_linked_namespaces);
}
-
open_library函数负责打开共享库,这里需要注意下当传入的是完整路径时可以直接打开,而只传入库名时会依次从caller namespace的ld_library_paths,default_library_paths查找对应库,如果找不到则判断是否开启灰名单,如果开启并且是灰名单列表中的库时才能打开文件,否者无法打开文件直接就加载失败返回了,基于此可以开启linker调试日志查看
static int open_library(android_namespace_t* ns,
ZipArchiveCache* zip_archive_cache,
const char* name, soinfo *needed_by,
off64_t* file_offset, std::string* realpath) {
TRACE("[ opening %s at namespace %s]", name, ns->get_name());
if (strchr(name, '/') != nullptr) {
int fd = -1;
if (strstr(name, kZipFileSeparator) != nullptr) {
fd = open_library_in_zipfile(zip_archive_cache, name, file_offset, realpath);
}
if (fd == -1) {
fd = TEMP_FAILURE_RETRY(open(name, O_RDONLY | O_CLOEXEC));
if (fd != -1) {
*file_offset = 0;
if (!realpath_fd(fd, realpath)) {
PRINT("warning: unable to get realpath for the library \"%s\". Will use given path.", name);
*realpath = name;
}
}
}
return fd;
}
int fd = open_library_on_paths(zip_archive_cache, name, file_offset, ns->get_ld_library_paths(), realpath);
if (fd == -1 && needed_by != nullptr) {
fd = open_library_on_paths(zip_archive_cache, name, file_offset, needed_by->get_dt_runpath(), realpath);
if (fd != -1 && !ns->is_accessible(*realpath)) {
fd = -1;
}
}
if (fd == -1) {
fd = open_library_on_paths(zip_archive_cache, name, file_offset, ns->get_default_library_paths(), realpath);
}
if (fd == -1 && ns->is_greylist_enabled() && is_greylisted(ns, name, needed_by)) {
fd = open_library_on_paths(zip_archive_cache, name, file_offset,
g_default_namespace.get_default_library_paths(), realpath);
}
return fd;
}
-
当打开文件失败后load_library就直接返回false并输出日志,否者调用另一个重载函数,因此我们查找未打开的库时dlopen传入完整路径可以过掉第一层权限检测
-
另一个load_library重载函数负责加载共享库,先检测共享库从文件加载偏移必须要进行页对齐,同时还会检测符号链接避免加载同一文件等等,其它关键代码如下
if ((fs_stat.f_type != TMPFS_MAGIC) && (!ns->is_accessible(realpath))) {
const soinfo* needed_by = task->is_dt_needed() ? task->get_needed_by() : nullptr;
if (is_greylisted(ns, name, needed_by)) {
if (needed_by == nullptr || !is_system_library(needed_by->get_realpath())) {
const soinfo* needed_or_dlopened_by = task->get_needed_by();
const char* sopath = needed_or_dlopened_by == nullptr ? "(unknown)" :
needed_or_dlopened_by->get_realpath();
DL_WARN_documented_change(__ANDROID_API_N__,
"private-api-enforced-for-api-level-24",
"library \"%s\" (\"%s\") needed or dlopened by \"%s\" "
"is not accessible by namespace \"%s\"",
name, realpath.c_str(), sopath, ns->get_name());
add_dlwarning(sopath, "unauthorized access to", name);
}
} else {
const char* needed_or_dlopened_by = task->get_needed_by() == nullptr ?
"(unknown)" :
task->get_needed_by()->get_realpath();
DL_ERR("library \"%s\" needed or dlopened by \"%s\" is not accessible for the namespace \"%s\"",
name, needed_or_dlopened_by, ns->get_name());
if (!maybe_accessible_via_namespace_links(ns, name)) {
PRINT("library \"%s\" (\"%s\") needed or dlopened by \"%s\" is not accessible for the"
" namespace: [name=\"%s\", ld_library_paths=\"%s\", default_library_paths=\"%s\","
" permitted_paths=\"%s\"]",
name, realpath.c_str(),
needed_or_dlopened_by,
ns->get_name(),
android::base::Join(ns->get_ld_library_paths(), ':').c_str(),
android::base::Join(ns->get_default_library_paths(), ':').c_str(),
android::base::Join(ns->get_permitted_paths(), ':').c_str());
}
return false;
}
}
soinfo* si = soinfo_alloc(ns, realpath.c_str(), &file_stat, file_offset, rtld_flags);
if (si == nullptr) {
return false;
}
-
上面代码发现这里有个灰名单判断,灰名单正包含我们想要的libandroid_runtime.so分析可知在App的targetSdk大于7.0就已经废弃了,所以如果想正常使用该库可以调整targetSdk小于24,这有个缺陷就是每次启动会有个警告弹窗,我就不截图大家可以自己试试。关键判断是否有权限是is_accessible函数,对应源码如下
bool android_namespace_t::is_accessible(const std::string& file) {
if (!is_isolated_) {
return true;
}
for (const auto& dir : ld_library_paths_) {
if (file_is_in_dir(file, dir)) {
return true;
}
}
for (const auto& dir : default_library_paths_) {
if (file_is_in_dir(file, dir)) {
return true;
}
}
for (const auto& dir : permitted_paths_) {
if (file_is_under_dir(file, dir)) {
return true;
}
}
return false;
}
-
因此命名空间权限检测都是基于所描述的三个路径,这三个路径是在创建命名空间时确认,接着返回find_library_internal函数继续分析
if (search_linked_namespaces) {
DlErrorRestorer dlerror_restorer;
for (auto& linked_namespace : ns->linked_namespaces()) {
if (find_library_in_linked_namespace(linked_namespace,
task)) {
if (task->get_soinfo() == nullptr) {
if (load_library(linked_namespace.linked_namespace(), task, zip_archive_cache, load_tasks, rtld_flags, false)) {
return true;
}
} else {
return true;
}
}
}
}
-
当前面已加载库没找到且caller namespace路径内也找不到时,则继续查找link namespace
static bool find_library_in_linked_namespace(const android_namespace_link_t& namespace_link, LoadTask* task) {
android_namespace_t* ns = namespace_link.linked_namespace();
soinfo* candidate;
bool loaded = false;
std::string soname;
if (find_loaded_library_by_soname(ns, task->get_name(), false, &candidate)) {
loaded = true;
soname = candidate->get_soname();
} else {
soname = resolve_soname(task->get_name());
}
if (!namespace_link.is_accessible(soname.c_str())) {
return false;
}
if (loaded) {
task->set_soinfo(candidate);
return true;
}
task->set_soinfo(nullptr);
return true;
}
-
回到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
void* OpenNativeLibrary(JNIEnv* env, int32_t target_sdk_version, const char* path, jobject class_loader, jstring library_path, bool* needs_native_bridge, std::string* error_msg) {
#if defined(__ANDROID__)
UNUSED(target_sdk_version);
if (class_loader == nullptr) {
*needs_native_bridge = false;
return dlopen(path, RTLD_NOW);
}
std::lock_guard<std::mutex> guard(g_namespaces_mutex);
NativeLoaderNamespace ns;
if (!g_namespaces->FindNamespaceByClassLoader(env, class_loader, &ns)) {
if (!g_namespaces->Create(env,
target_sdk_version,
class_loader,
false ,
false ,
library_path,
nullptr,
&ns,
error_msg)) {
return nullptr;
}
}
if (ns.is_android_namespace()) {
android_dlextinfo extinfo;
extinfo.flags = ANDROID_DLEXT_USE_NAMESPACE;
extinfo.library_namespace = ns.get_android_ns();
void* handle = android_dlopen_ext(path, RTLD_NOW, &extinfo);
if (handle == nullptr) {
*error_msg = dlerror();
}
*needs_native_bridge = false;
return handle;
} else {
void* handle = NativeBridgeLoadLibraryExt(path, RTLD_NOW, ns.get_native_bridge_ns());
if (handle == nullptr) {
*error_msg = NativeBridgeGetError();
}
*needs_native_bridge = true;
return handle;
}
#else
-
因此如果是自定义的类加载器会拥有私有的命名空间,默认的ClassLoader命名空间是由ApplicationLoaders创建的,分析该native——loader类也可以看到一些查找路径和环境变量,其它分析略
3.Android命名空间和一些内存相关总结
- 经过第二步分析dlopen,dlsym函数调用存在命名空间限制,加载库和查找符号只在自己的命名空间内查找,但这并不意味着无法跨命名空间访问,我们只是没有权限加载库和查找符号,只要我们知道地址同样可以访问这是C/C++代码控制不了的,而我们编写C++代码看似有private限制,事实上那只是编译器欺骗你在编译阶段控制权限否则编译不通过,而编译成二进制后只要你能知道地址你可以随意访问
- 通常情况下进程中只包含两个命名空间(默认命名空间,Java类加载器命名空间),匿名命名空间看源码可知就是默认命名空间,因此访问系统私有库可以改变调用者的命名空间得到权限
- 命名空间之间的库共享通过单向链接来共享,因此可以更改 link namespace 的访问权限得到私有库权限,可以更改总开关allow_all_sharedlibs,也可以添加库到共享集合中
- 谈一下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层关键代码
static {
System.loadLibrary("native-lib");
}
@RequiresApi(api = Build.VERSION_CODES.N)
public static boolean releaseDexAndLoad(Context context) {
if (!releaseFile(context, "classes.dex", context.getDataDir().getAbsolutePath() + File.separator + "dyn.dex")) {
return false;
}
String[] abis = Build.SUPPORTED_32_BIT_ABIS;
boolean isX86 = false;
for (String abi : abis) {
if (abi.contains("x86")) {
isX86 = true;
break;
}
}
String libEntryName;
if (Process.is64Bit()) {
libEntryName = isX86 ? "lib/x86_64/libnative-lib.so" : "lib/arm64-v8a/libnative-lib.so";
} else {
libEntryName = isX86 ? "lib/x86/libnative-lib.so" : "lib/armeabi-v7a/libnative-lib.so";
}
File lib = new File(context.getDataDir().getAbsolutePath() + File.separator + "libnative-lib.so");
if (!releaseFile(context, libEntryName, lib.getAbsolutePath())) {
return false;
}
return dynLoad(context, lib);
}
@RequiresApi(api = Build.VERSION_CODES.N)
public static boolean dynLoad(Context context, File lib) {
try {
DexClassLoader loader = new DexClassLoader(context.getDataDir().getAbsolutePath() + File.separator + "dyn.dex", null, null, null);
Class testClass = loader.loadClass(DynTestClass.class.getCanonicalName());
Method med = testClass.getDeclaredMethod("loadOfClassLoader", File.class, ClassLoader.class);
med.invoke(null, lib, MainActivity.class.getClassLoader());
specialMethod = testClass.getDeclaredMethod("specialLoad", ClassLoader.class);
} catch (Throwable e) {
Log.e(TAG, "dyn load dex error", e);
return false;
}
return true;
}
package com.beichen.fakelinker;
import android.util.Log;
import java.io.File;
public class DynTestClass {
public static void loadOfClassLoader(File libFile, ClassLoader loader){
try {
System.load(libFile.getAbsolutePath());
}catch (Throwable e){
Log.e("beichen", "dyn load lib error ", e);
}
}
public static native int specialLoad(ClassLoader loader);
}
-
测试Java跨命名空间native函数如何使用
public static native int dynRegister();
@Override
public void onClick(View v) {
int id = v.getId();
switch (id) {
case R.id.btn_test_dlopen:
Log.d(TAG, "dlopen output: " + hookDlopen());
break;
case R.id.btn_register:
try {
specialMethod.invoke(null, this.getClass().getClassLoader());
} catch (Exception e) {
e.printStackTrace();
}
break;
case R.id.btn_call:
Log.d(TAG, "dyn register output: " + dynRegister());
break;
case R.id.btn_call_test:
testSolist();
break;
default:
break;
}
}
-
当我们不点击注册直接点击调用时输出
8950-8950/com.beichen.fakelinker E/chen.fakelinke: No implementation found for int com.beichen.fakelinker.MainActivity.dynRegister() (tried Java_com_beichen_fakelinker_MainActivity_dynRegister and Java_com_beichen_fakelinker_MainActivity_dynRegister__)
8950-8950/com.beichen.fakelinker D/AndroidRuntime: Shutting down VM
--------- beginning of crash
8950-8950/com.beichen.fakelinker E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.beichen.fakelinker, PID: 8950
java.lang.UnsatisfiedLinkError: No implementation found for int com.beichen.fakelinker.MainActivity.dynRegister() (tried Java_com_beichen_fakelinker_MainActivity_dynRegister and Java_com_beichen_fakelinker_MainActivity_dynRegister__)
at com.beichen.fakelinker.MainActivity.dynRegister(Native Method)
at com.beichen.fakelinker.MainActivity.onClick(MainActivity.java:134)
at android.view.View.performClick(View.java:6597)
at android.view.View.performClickInternal(View.java:6574)
at android.view.View.access$3100(View.java:778)
at android.view.View$PerformClick.run(View.java:25885)
at android.os.Handler.handleCallback(Handler.java:873)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:193)
at android.app.ActivityThread.main(ActivityThread.java:6669)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)
- 直接报错没找到函数实现,并且还找了默认命名规则函数Java_com_beichen_fakelinker_MainActivity_dynRegister,Java_com_beichen_fakelinker_MainActivity_dynRegister__,这是JNI规则可以看到有两个默认实现名字,从前面分析命名空间可知,在 caller namespace 内查找函数,而由于我们没有默认实现它所以找不到,这也说明一个问题跨命名空间默认实现函数也是不生效的,因为根本不会在另一个命名空间内查找
-
接着尝试跨命名空间动态注册native函数的情况,关键代码如下
jint dyn_register(JNIEnv *env, jclass type) {
LOGD("cross native namespace dynamic invoke function success");
return 1;
}
static JNINativeMethod methods[] = {
{"dynRegister", "()I", dyn_register}
};
JNIEXPORT jint JNICALL
Java_com_beichen_fakelinker_DynTestClass_specialLoad(JNIEnv *env, jclass type, jobject loader) {
jclass java_lang_ClassLoader;
jmethodID java_lang_ClassLoader_loadClass;
jstring java_str_name;
jclass java_MainActivity;
java_lang_ClassLoader = (*env)->FindClass(env, "java/lang/ClassLoader");
java_lang_ClassLoader_loadClass = (*env)->GetMethodID(env, java_lang_ClassLoader, "loadClass",
"(Ljava/lang/String;)Ljava/lang/Class;");
java_str_name = (*env)->NewStringUTF(env, "com.beichen.fakelinker.MainActivity");
java_MainActivity = (jclass) (*env)->CallObjectMethod(env, loader,
java_lang_ClassLoader_loadClass,
java_str_name);
if (java_MainActivity == NULL) {
LOGD("not found com.beichen.fakelinker.MainActivity class");
return -1;
}
(*env)->RegisterNatives(env, java_MainActivity, methods, sizeof(methods) / sizeof(methods[0]));
LOGD("dynamic register function success");
(*env)->DeleteLocalRef(env, java_lang_ClassLoader);
(*env)->DeleteLocalRef(env, java_str_name);
(*env)->DeleteLocalRef(env, java_MainActivity);
return 0;
}
- 再次先点击动态注册然后再点击调用输出如下
9125-9125/com.beichen.fakelinker D/beichen: dynamic register function success
9125-9125/com.beichen.fakelinker D/beichen: cross native namespace dynamic invoke function success
9125-9125/com.beichen.fakelinker D/beichen: dyn register output: 1
- 可见通过注册后就能在主类加载中成功调用,这也证实知道地址可以跨命名空间调用,这里猜想JNI维护着一个函数对应表,对应着每个java函数与native函数绑定,动态注册就会建立绑定关系,调用时直接地址调用省去查找的过程
-
实现默认命名空间dlopen,dlsym查找ClassLoader命名空间
- 在实现之前我们确认在调用do_dlopen函数时有一个caller_addr参数保存着调用者的地址,只要更改这个参数就能达到替换caller namespace,之前分析的link namespace共享库配置文件/etc/public.libraries.txt现在看下有哪些库
generic_x86_64:/ # cat /etc/public.libraries.txt
# See https://android.googlesource.com/platform/ndk/+/master/docs/PlatformApis.md
libandroid.so
libaaudio.so
libc.so
libcamera2ndk.so
libdl.so
...略
- 现在回过头去查看dlopen函数
void* dlopen(const char* filename, int flag) {
const void* caller_addr = __builtin_return_address(0);
return __loader_dlopen(filename, flag, caller_addr);
}
- dlopen函数在libdl.so中导出,而libdl.so配置在共享so中,因此我们有权限访问,而真正的loader_dlopen函数是在linker中导出,linker并不在共享列表中没有权限访问。**在这dlopen函数我们可以发现它先获取返回地址再调用__loader_dlopen函数,这意味着在libdl.so中必定会引用loader_dlopen函数,因此我们有办法拿到它,把libdl.so**拿到IDA分析如下面(我用的x86_64)汇编
.text:0000000000000EF0 public dlopen
.text:0000000000000EF0 dlopen proc near
.text:0000000000000EF0
.text:0000000000000EF0 mov rdx, [rsp+0]
.text:0000000000000EF4 jmp ___loader_dlopen
.text:0000000000000EF4
-
第一条汇编指令mov rdx, [rsp] 就是在获取返回地址,这需要对x86函数调用及栈桢了解,可以查看x86-64 下函数调用及栈帧原理。第二条指令直接跳转,在这里采用了PIC位置无关代码可参考位置独立代码(PIC)在共享库中,__loader_dlopen是导入的函数,因此我们只要获取到它的地址就可以直接传递caller_address调用
-
反汇编获取地址,这也是 Frida使用的方式,capstone综合反汇编库,Android内部也是用的这个库关键代码如下(基本上抄的Frida),这个解析过程也可以对PIC加深理解:
gpointer resolve_inner_dlopen_or_dlsym(gpointer fun) {
gpointer impl;
csh capstone;
cs_err err;
gsize dlopen_address;
cs_insn *insn;
size_t count;
gaddress pic;
gaddress *pic_value;
pic_value = &pic;
impl = fun;
*pic_value = 0;
...略
#elif defined(__x86_64__)
err = cs_open(CS_ARCH_X86, CS_MODE_64, &capstone);
assert(err == CS_ERR_OK);
err = cs_option(capstone, CS_OPT_DETAIL, CS_OPT_ON);
assert(err == CS_ERR_OK);
dlopen_address = GPOINTER_TO_SIZE(impl);
insn = NULL;
count = cs_disasm(capstone, GSIZE_TO_POINTER(dlopen_address), 16, dlopen_address, 4, &insn);
for (size_t i = 0; i != count; i++) {
const cs_insn * cur = &insn[i];
const cs_x86_op * op = &cur->detail->x86.operands[0];
if (cur->id == X86_INS_JMP) {
if (op->type == X86_OP_IMM) {
impl = GSIZE_TO_POINTER(op->imm);
}
break;
}
}
if (impl != fun){
count = cs_disasm(capstone, impl, 6, GPOINTER_TO_SIZE(impl), 1, &insn);
assert(count == 1);
const cs_x86_op op1 = insn[0].detail->x86.operands[0];
if (insn[0].id == X86_INS_JMP && op1.mem.base == X86_REG_RIP){
gpointer tmp= GSIZE_TO_POINTER(GPOINTER_TO_SIZE(impl) + 6 + op1.mem.disp);
gsize addr = *(gsize *)tmp;
impl = GSIZE_TO_POINTER(addr);
}
} else{
impl = NULL;
}
...略
cs_free(insn, count);
cs_close(&capstone);
return impl;
}
-
查找导入表方式。这里提一下ELF文件格式,文件格式分为链接视图,执行视图,链接视图程序头部表可选,执行视图节区头部表可选。链接是针对编译阶段,当你的库依赖另一个库时链接器会去读依赖库的节区表,而执行视图只要程序头部表即可,节区头部表不是必须的,并且有些节区是不会出现在内存中的(关键的SHT_SYMTAB和SHT_STRTAB都不会被加载)。导入表与导出表都是存在于动态符号表中(DT_SYMTAB),这是动态链接时所必须的,在创建soinfo后紧接着就会预链接和赋值soinfo相关成员,因此共享库的程序头部表通常不会处理,各大安全加固厂商基本都会处理节区表,删除节区表中的符号表,因此根据节区表查找不可靠,当然处理动态符号表然后自己实现重定向也是可能的。下面是获取导入表符号地址步骤:
- 获取动态段(PT_DYNAMIC)
- 获取相关的动态节区(DT_STRTAB,DT_SYMTAB,DT_PLTREL 等等)
- 通过hash表(只有gnu hash表时需要暴力查找名字)查找到对应符号表索引
- 解析符号地址
-
讲到导入导出表查找就顺便说说内部符号表查找,后面我们查找solist和开启linker日志都是通过内部符号表查找的,内部符号表并不存在内存中,只能通过打开文件查找节区表来查找,如果删除掉节区符号表那就无法直接查找了,但一般未加固和系统库并不会删掉节区表。查找内部符号流程如下:
- 获取节区头部表,查找有关节区SHT_SYMTAB,SHT_STRTAB
- 暴力查找符号名字
- 解析符号地址
- 代码也不贴了,都在fake_linker.c中实现
-
现在采用解析__loader_dlopen符号调用libandroid_runtime.so实现代码如下:
typedef void *(*__dlopen_impl)(const char *filename, int flag, void *address);
typedef void *(*__dlsym_impl)(void *__handle, const char *__symbol, void *address);
__dlopen_impl dlopen_impl = NULL;
__dlsym_impl dlsym_impl = NULL;
JNIEXPORT jint JNICALL
Java_com_beichen_fakelinker_MainActivity_hookDlopen(JNIEnv *env, jobject th) {
char *lib;
void *p_runtime_handle;
void *p_vm;
gaddress ld_debug;
#if defined(__LP64__)
ld_debug = resolve_library_symbol_address("/system/bin/linker64", "__dl_g_ld_debug_verbosity", ST_INNER);
lib = "/system/lib64/libandroid_runtime.so";
#else
ld_debug = resolve_library_symbol_address("/system/bin/linker", "__dl_g_ld_debug_verbosity", ST_INNER);
lib = "/system/lib/libandroid_runtime.so";
#endif
*(int *)ld_debug = 2;
p_runtime_handle = dlopen(lib, RTLD_LAZY);
p_vm = NULL;
if (p_runtime_handle != NULL) {
p_vm = dlsym(p_runtime_handle, "_ZN7android14AndroidRuntime7mJavaVME");
}
LOGD("runtime: %p", p_runtime_handle);
LOGD("mJavaVM: %p", p_vm);
dlopen_impl = resolve_inner_dlopen_or_dlsym(dlopen);
dlsym_impl = resolve_inner_dlopen_or_dlsym(dlsym);
LOGD("dlopen orig: %p, __dlopen_impl: %p", dlopen, dlopen_impl);
LOGD("dlsym orig: %p, __dlsym_impl: %p", dlsym, dlsym_impl);
p_runtime_handle = dlopen_impl(lib, RTLD_LAZY, open);
if (p_runtime_handle != NULL) {
p_vm = dlsym_impl(p_runtime_handle, "_ZN7android14AndroidRuntime7mJavaVME", open);
LOGD("runtime: %p", p_runtime_handle);
LOGD("mJavaVM: %p", p_vm);
} else {
LOGE("__dlopen_impl open android_runtime failed, possible decompilation failed");
}
gaddress linker_dlopen = resolve_library_symbol_address("libdl.so", "__loader_dlopen",
ST_IMPORTED);
gaddress addr = *(gsize *) linker_dlopen;
LOGD("find imp address: %llx, value: %llx", linker_dlopen, addr);
return (int) GPOINTER_TO_SIZE(p_vm);
}
- 详细输出日志如下:
9791-9791/com.beichen.fakelinker I/linker: [ "/system/lib64/libandroid_runtime.so" find_loaded_library_by_soname failed (*candidate=n/a@0x0). Trying harder...]
9791-9791/com.beichen.fakelinker I/linker: [ opening /system/lib64/libandroid_runtime.so at namespace classloader-namespace]
9791-9791/com.beichen.fakelinker E/linker: library "/system/lib64/libandroid_runtime.so" ("/system/lib64/libandroid_runtime.so") needed or dlopened by "/data/app/com.beichen.fakelinker--Xkekv3ZHD_qNUdkxKf_hw==/lib/x86_64/libnative-lib.so" is not accessible for the namespace: [name="classloader-namespace", ld_library_paths="", default_library_paths="/data/app/com.beichen.fakelinker--Xkekv3ZHD_qNUdkxKf_hw==/lib/x86_64:/data/app/com.beichen.fakelinker--Xkekv3ZHD_qNUdkxKf_hw==/base.apk!/lib/x86_64", permitted_paths="/data:/mnt/expand:/data/data/com.beichen.fakelinker"]
9791-9791/com.beichen.fakelinker D/beichen: runtime: 0x0
9791-9791/com.beichen.fakelinker D/beichen: mJavaVM: 0x0
9791-9791/com.beichen.fakelinker D/beichen: dlopen orig: 0x764eb6a99ef0, __dlopen_impl: 0x764ebb803fd0
9791-9791/com.beichen.fakelinker D/beichen: dlsym orig: 0x764eb6a99f10, __dlsym_impl: 0x764ebb804190
9791-9791/com.beichen.fakelinker I/linker: [ "/system/lib64/libandroid_runtime.so" find_loaded_library_by_soname failed (*candidate=n/a@0x0). Trying harder...]
9791-9791/com.beichen.fakelinker I/linker: [ opening /system/lib64/libandroid_runtime.so at namespace (default)]
9791-9791/com.beichen.fakelinker I/linker: library "/system/lib64/libandroid_runtime.so" is already loaded under different name/path "/system/lib64/libandroid_runtime.so" - will return existing soinfo
9791-9791/com.beichen.fakelinker I/linker: SEARCH _ZN7android14AndroidRuntime7mJavaVME in /system/lib64/libandroid_runtime.so@0x764eb712d000 (gnu)
9791-9791/com.beichen.fakelinker I/linker: FOUND _ZN7android14AndroidRuntime7mJavaVME in /system/lib64/libandroid_runtime.so (0x23ece0) 8
9791-9791/com.beichen.fakelinker D/beichen: runtime: 0x4e722cd0b2ad7d47
9791-9791/com.beichen.fakelinker D/beichen: mJavaVM: 0x764eb7335ce0
9791-9791/com.beichen.fakelinker D/beichen: find imp address: 764eb6a9bf70, value: 764ebb803fd0
- 可以看到正常情况下是获取不到JavaVM地址,而修改命名空间后则能获取到,并且导入表查找与反汇编查找结果是相同的
- 实现默认命名空间查找ClassLoader命名空间
JNIEXPORT void JNICALL
Java_com_beichen_fakelinker_MainActivity_findThirdNamespace(JNIEnv *env, jobject th) {
if (dlopen_impl != NULL) {
void *third_so = dlopen_impl("/data/data/com.beichen.fakelinker/libnative-lib.so",
RTLD_LAZY, open);
void *third_sym = NULL;
if (third_so != NULL) {
third_sym = dlsym_impl(third_so, "Java_com_beichen_fakelinker_DynTestClass_specialLoad",
open);
}
LOGD("dafault namespace find classloader namespace handle: %p, sym: %p", third_so, third_sym);
third_so = dlopen_impl("libnative-lib.so", RTLD_LAZY, open);
third_sym = NULL;
if (third_so != NULL) {
third_sym = dlsym_impl(third_so, "Java_com_beichen_fakelinker_DynTestClass_specialLoad",
open);
}
LOGD("dafault namespace find classloader namespace 2 handle: %p, sym: %p", third_so, third_sym);
}
}
- 代码输出有如下日志:
9980-9980/com.beichen.fakelinker D/linker: dlopen(name="/data/data/com.beichen.fakelinker/libnative-lib.so", flags=0x1, extinfo=(null), caller="/system/lib64/libc.so", caller_ns=(default)@0x764ebb93e438) ...
9980-9980/com.beichen.fakelinker I/linker: [ "/data/data/com.beichen.fakelinker/libnative-lib.so" find_loaded_library_by_soname failed (*candidate=n/a@0x0). Trying harder...]
9980-9980/com.beichen.fakelinker I/linker: [ opening /data/data/com.beichen.fakelinker/libnative-lib.so at namespace (default)]
9980-9980/com.beichen.fakelinker I/linker: name /data/data/com.beichen.fakelinker/libnative-lib.so: allocating soinfo for ns=0x764ebb93e438
9980-9980/com.beichen.fakelinker I/linker: name /data/data/com.beichen.fakelinker/libnative-lib.so: allocated soinfo @ 0x764ebaf708d0
9980-9980/com.beichen.fakelinker W/linker: [ Linking "/data/data/com.beichen.fakelinker/libnative-lib.so" ]
- 可见在默认命名空间内找不到就又新加载了一个,现在看下maps文件
generic_x86_64:/
764e19ecb000-764e19fba000 r-xp 00000000 fc:00 15629 /data/data/com.beichen.fakelinker/libnative-lib.so
764e19fba000-764e19fc5000 r--p 000ee000 fc:00 15629 /data/data/com.beichen.fakelinker/libnative-lib.so
764e19fc5000-764e1a092000 rw-p 000f9000 fc:00 15629 /data/data/com.beichen.fakelinker/libnative-lib.so
764e1e606000-764e1e6f5000 r-xp 00000000 fc:00 15629 /data/data/com.beichen.fakelinker/libnative-lib.so
764e1e6f5000-764e1e700000 r--p 000ee000 fc:00 15629 /data/data/com.beichen.fakelinker/libnative-lib.so
764e1e700000-764e1e7cd000 rw-p 000f9000 fc:00 15629 /data/data/com.beichen.fakelinker/libnative-lib.so
764e1ea62000-764e1eb51000 r-xp 00000000 fc:00 22080 /data/app/com.beichen.fakelinker-skGrJ-BX5ehtjZ
UBWufyrQ==/lib/x86_64/libnative-lib.so
764e1eb51000-764e1eb5c000 r--p 000ee000 fc:00 22080 /data/app/com.beichen.fakelinker-skGrJ-BX5ehtjZ
UBWufyrQ==/lib/x86_64/libnative-lib.so
764e1eb5c000-764e1ec29000 rw-p 000f9000 fc:00 22080 /data/app/com.beichen.fakelinker-skGrJ-BX5ehtjZ
UBWufyrQ==/lib/x86_64/libnative-lib.so
- 发现已经有三个库了,由此也可见默认命名空间并没有特殊权限,找不到就尝试加载,如果dlopen只传入库名同样也会找不到
6.简单探索soinfo
- 前面提到对soinfo的查找离不开solist,这个静态变量存储着起始soinfo,而soinfo通过链表链接起来的。分析linker发现solist是内部符号并未导出,因此需要用到前面提到的查找内部符号表
solist = resolve_library_symbol_address("/system/bin/linker64", "__dl__ZL6solist", ST_INNER)
-
为了编写代码方便我们自己创建soinfo结构体,然后再与真实soinfo成员偏移进行验证
struct soinfo9 {
...略
ElfW(Addr) base;
size_t size;
...略
soinfo9 * next;
uint32_t flags_;
const char * strtab_;
ElfW(Sym) * symtab_;
...略
std::vector<std::string> dt_runpath_;
android_namespace_t * primary_namespace_;
android_namespace_list_t secondary_namespaces_;
uintptr_t handle_;
ElfW(Relr) * relr_;
size_t relr_count_;
};
- 查看源码soinfo结构体保存了程序头部表,程序头数量,基址,大小,符号表,字符串表,hash表,重定位表,init fini函数等等,涵盖所有需要的结构,对soinfo赋值是在dlopen内完成的,因此我们可以基于soinfo还原so,对于加固的分析了解它更加重要
- 紧接着验证一下偏移,看结构体是否匹配,把库导出来用IDA分析查看下偏移是否一致,我只验证了9.0,其它版本自行验证
// 对应方法 find_containing_library, soinfo::soinfo, get_soname, get_primary_namespace,to_handle
// arm64 base: 16, next: 40, version_: 268, soname_: 408, primary_namespace_: 512, handle_: 536
// arm base: 140, next: 164, version_: 292, soname_: 376, primary_namespace_: 428, handle_: 440
// x86 base: 140, next: 164, version_: 284, soname_: 368, primary_namespace_: 420, handle_: 432
// x64 base: 16, next: 40, version_: 268, soname_: 408, primary_namespace_: 512, handle_: 536
LOGD("base: %d, next: %d, version_: %d, soname_: %d, primary_namespace_: %d, handle_: %d",
&soinfo9::base, &soinfo9::next, &soinfo9::version_, &soinfo9::soname_,
&soinfo9::primary_namespace_, &soinfo9::handle_);
- 然后查看一下关键成员
10365-10365/com.beichen.fakelinker D/beichen: solist address: 764ebb93e7f0
10365-10365/com.beichen.fakelinker D/beichen: base: 16, next: 40, version_: 268, soname_: 408, primary_namespace_: 512, handle_: 536
10365-10365/com.beichen.fakelinker D/beichen: soname: libnative-lib.so, namespace: classloader-namespace, realpath: /data/app/com.beichen.fakelinker-AKiIcDQOmJSMf7BXfGP0Mw==/lib/x86_64/libnative-lib.so, isolated: 1, greylist: 0
10365-10365/com.beichen.fakelinker D/beichen: namespace: classloader-namespace, default path: /data/app/com.beichen.fakelinker-AKiIcDQOmJSMf7BXfGP0Mw==/lib/x86_64
10365-10365/com.beichen.fakelinker D/beichen: namespace: classloader-namespace, default path: /data/app/com.beichen.fakelinker-AKiIcDQOmJSMf7BXfGP0Mw==/base.apk!/lib/x86_64
10365-10365/com.beichen.fakelinker D/beichen: namespace: classloader-namespace, permitted path: /data
10365-10365/com.beichen.fakelinker D/beichen: namespace: classloader-namespace, permitted path: /mnt/expand
10365-10365/com.beichen.fakelinker D/beichen: namespace: classloader-namespace, permitted path: /data/data/com.beichen.fakelinker
10365-10365/com.beichen.fakelinker D/beichen: soname: libnative-lib.so, link namespace: (default), link allow_all_shared_libs: 0 link isolated: 1, link greylist: 0
10365-10365/com.beichen.fakelinker D/beichen: namespace: (default), default path: /system/lib64
10365-10365/com.beichen.fakelinker D/beichen: namespace: (default), permitted path: /system/lib64/drm
10365-10365/com.beichen.fakelinker D/beichen: namespace: (default), permitted path: /system/lib64/extractors
...略
- 可见默认情况下是关闭灰名单的,并且开启了命名空间隔离,也看到了ClassLoader命名空间默认路径是自己的库路径,还有允许自己的私有目录
- 前面分析更改link namespace的allow_all_shared_libs_成员就能通过检测,现在来试一下
do {
if (strcmp("classloader-namespace", si->primary_namespace_->name_) == 0) {
...略
for (int i = 0; i < si->primary_namespace_->linked_namespaces_.size(); ++i) {
android_namespace_link_t * link = &si->primary_namespace_->linked_namespaces_[i];
...略
link->allow_all_shared_libs_ = true;
}
}
} while ((si = si->next) != nullptr);
void * handle = dlopen("libandroid_runtime.so", RTLD_LAZY);
void * symbol = nullptr;
if (handle != nullptr) {
symbol = dlsym(handle, "_ZN7android14AndroidRuntime7mJavaVME");
}
LOGE("find handler: %p, symbol: %p", handle, symbol);
- 得到下面输出成功找到私有库
5403-5403/com.beichen.fakelinker D/linker: dlopen(name="libandroid_runtime.so", flags=0x1, extinfo=(null), caller="/data/app/com.beichen.fakelinker-Go8u3KOFkt_XiAEynQ4Png==/lib/x86_64/libnative-lib.so", caller_ns=classloader-namespace@0x7d72583e8210) ...
5403-5403/com.beichen.fakelinker D/linker: ... dlopen calling constructors: realpath="/system/lib64/libandroid_runtime.so", soname="libandroid_runtime.so", handle=0x28d7f605065a26bb
5403-5403/com.beichen.fakelinker D/linker: ... dlopen successful: realpath="/system/lib64/libandroid_runtime.so", soname="libandroid_runtime.so", handle=0x28d7f605065a26bb
5403-5403/com.beichen.fakelinker E/beichen: find handler: 0x28d7f605065a26bb, symbol: 0x7d72549b9ce0
- 再来尝试更改caller namespace中的is_isolated_属性
LOGD("namespace: %p, isolated: %p", si->primary_namespace_, &si->primary_namespace_->is_isolated_);
si->primary_namespace_->is_isolated_ = true;
-
运行发现居然崩溃了,输出日志如下
6140-6140/com.beichen.fakelinker D/beichen: namespace: 0x7d72583e8290, isolated: 0x7d72583e8298
--------- beginning of crash
6140-6140/com.beichen.fakelinker A/libc: Fatal signal 11 (SIGSEGV), code 2 (SEGV_ACCERR), fault addr 0x7d72583e8218 in tid 6140 (chen.fakelinker), pid 6140 (chen.fakelinker)
- 这里我们更改时发生访问错误,那我们先关掉修改看一下该地址对应的maps映射权限
generic_x86_64:/ # ps -A | grep beichen
u0_a67 6319 1808 3966252 109324 ep_poll 7d72546011da S com.beichen.fakelinker
generic_x86_64:/ # cat /proc/6319/maps | grep "7d72583e"
7d72583df000-7d72583e0000 rw-p 00000000 00:00 0 [anon:linker_alloc_small_objects]
7d72583e0000-7d72583e1000 r--p 00000000 00:00 0 [anon:atexit handlers]
7d72583e1000-7d72583e2000 rw-p 00000000 00:00 0 [anon:linker_alloc_vector]
7d72583e2000-7d72583e5000 rw-p 00000000 00:00 0 [anon:linker_alloc_small_objects]
7d72583e5000-7d72583e6000 rw-p 00000000 00:00 0 [anon:linker_alloc]
7d72583e6000-7d72583e7000 rw-p 00000000 00:00 0 [anon:linker_alloc_small_objects]
7d72583e7000-7d72583e8000 rw-p 00000000 00:00 0 [anon:System property context nodes]
7d72583e8000-7d72583e9000 r--p 00000000 00:00 0 [anon:linker_alloc]
7d72583e9000-7d72583ea000 rw-p 00000000 00:00 0 [anon:linker_alloc_small_objects]
7d72583ea000-7d72583eb000 rw-p 00000000 00:00 0 [anon:linker_alloc_vector]
7d72583eb000-7d72583ec000 rw-p 00000000 00:00 0 [anon:linker_alloc_small_objects]
7d72583ec000-7d72583ed000 rw-p 00000000 00:00 0 [anon:linker_alloc_vector]
7d72583ed000-7d72583ee000 rw-p 00000000 00:00 0 [anon:linker_alloc_small_objects]
7d72583ee000-7d725840e000 r--s 00000000 00:11 6237 /dev/__properties__/u:object_r:exported_default_prop:s0
- 可以看到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工程把源码替换掉即可
ELF.zip
(17.05 KB, 下载次数: 105)
|
免费评分
-
查看全部评分
|