好友
阅读权限 25
听众
最后登录 1970-1-1
本帖最后由 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) { // flags_是LinkerLogger成员
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查看源码
// Proxy calls to bionic loader
__attribute__((__weak__))
void* dlopen(const char* filename, int flag) {
const void* caller_addr = __builtin_return_address(0);//这个是内置函数获取函数的调用地址,可以理解为汇编 MOV R0, LR
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) {
// name和flags是我们dlopen传入的,extinfo系统根据调用者信息构造的,java层调用native不为空,直接使用dlopen为空,caller_addr 是调用方的地址,就是上面gcc内置函数获取的用来确定命名空间用的
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); // 这里获取caller的soinfo结构
android_namespace_t* ns = get_caller_namespace(caller); // 获取caller的本机命名空间
...略
if (extinfo != nullptr) {
...略
ns = extinfo->library_namespace; //如果caller的extinfo不为空则用caller的命名空间
}
}
...略
}
这里关键两个函数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(); si != nullptr; si = si->next) {
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);
// Note: start from the namespace that is stored in the LoadTask. This namespace
// is different from the current namespace when the LoadTask is for a transitive
// dependency and the lib that created the LoadTask is not found in the
// current namespace but in one of the linked namespace.
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;
} // 最终查找库走到find_library_internal
soinfo* si = task->get_soinfo();
if (is_dt_needed) {
needed_by->add_child(si);
}
// When ld_preloads is not null, the first
// ld_preloads_count libs are in fact ld_preloads.
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)) {
// 在caller命名空间和链接的命名空间内查找当前已经加载的库是否包含目标库,如果有则直接返回
task->set_soinfo(candidate);
return true;
}
// Library might still be loaded, the accurate detection
// of this fact is done by load_library.
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) {
// if a library was not found - look into linked namespaces
// preserve current dlerror in the case it fails.
DlErrorRestorer dlerror_restorer;
for (auto& linked_namespace : ns->linked_namespaces()) {
// 在链接命名空间内加载
if (find_library_in_linked_namespace(linked_namespace, task)) {
if (task->get_soinfo() == nullptr) {
// try to load the library - once namespace boundary is crossed
// we need to load a library within separate load_group
// to avoid using symbols from foreign namespace while.
//
// However, actual linking is deferred until when the global group
// is fully identified and is applied to all namespaces.
// Otherwise, the libs in the linked namespace won't get symbols from
// the global group.
if (load_library(linked_namespace.linked_namespace(), task, zip_archive_cache, load_tasks, rtld_flags, false)) {
return true;
}
} else {
// lib is already loaded
return true;
}
}
}
}
return false;
}
在当前已加载的库中查找find_loaded_library_by_soname
// Returns true if library was found and false otherwise
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;
}
// 在caller命名空间内查找
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) {
...略
// Open the file.
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 the name contains a slash, we should attempt to open it directly and not search the paths.
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;
}
// 否者尝试从 LD_LIBRARY_PATH 路径查找, 再从 default library 查找
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);
// Check if the library is accessible
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)) {
// try searching for it on default_namespace default_library_path
fd = open_library_on_paths(zip_archive_cache, name, file_offset,
g_default_namespace.get_default_library_paths(), realpath);
}
// END OF WORKAROUND
return fd;
}
当打开文件失败后load_library 就直接返回false并输出日志,否者调用另一个重载函数,因此我们查找未打开的库时dlopen 传入完整路径可以过掉第一层权限检测
另一个load_library 重载函数负责加载共享库,先检测共享库从文件加载偏移必须要进行页对齐,同时还会检测符号链接避免加载同一文件等等,其它关键代码如下
//当共享库不在临时文件系统且命名空间没有该共享库路径的访问权限时
if ((fs_stat.f_type != TMPFS_MAGIC) && (!ns->is_accessible(realpath))) {
// TODO(dimitry): workaround for http://b/26394120 - the grey-list
// TODO(dimitry) before O release: add a namespace attribute to have this enabled
// only for classloader-namespaces
const soinfo* needed_by = task->is_dt_needed() ? task->get_needed_by() : nullptr;
// 此处还在判断是否处于灰名单中,如果开启灰名单并且是灰名单中的库则弹窗提示警告
if (is_greylisted(ns, name, needed_by)) {
// print warning only if needed by non-system library
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 {
// 如果不在灰名单中且没有访问权限就直接返回失败,这也正是日志中报告的错误来源
// do not load libraries if they are not accessible for the specified namespace.
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());
// do not print this if a library is in the list of shared libraries for linked namespaces
// 如果库也不在链接的命名空间时则会打印另一条日志,这条日志需要开启linker调试日志才能查看到
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,接下来再进行其它处理
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;
}
// 先从ld_library_paths_路径查找
// file_is_in_dir是不能包含在子目录中的
for (const auto& dir : ld_library_paths_) {
if (file_is_in_dir(file, dir)) {
return true;
}
}
// 再从default_library_paths_路径查找
for (const auto& dir : default_library_paths_) {
if (file_is_in_dir(file, dir)) {
return true;
}
}
// 再从permitted_paths_路径查找
for (const auto& dir : permitted_paths_) {
if (file_is_under_dir(file, dir)) {
return true;
}
}
return false;
}
因此命名空间权限检测都是基于所描述的三个路径,这三个路径是在创建命名空间时确认,接着返回find_library_internal 函数继续分析
if (search_linked_namespaces) {
// if a library was not found - look into linked namespaces
// preserve current dlerror in the case it fails.
DlErrorRestorer dlerror_restorer;
// 在链接的命名空间内查找
for (auto& linked_namespace : ns->linked_namespaces()) {
if (find_library_in_linked_namespace(linked_namespace,
task)) {
if (task->get_soinfo() == nullptr) {
// try to load the library - once namespace boundary is crossed
// we need to load a library within separate load_group
// to avoid using symbols from foreign namespace while.
//
// However, actual linking is deferred until when the global group
// is fully identified and is applied to all namespaces.
// Otherwise, the libs in the linked namespace won't get symbols from
// the global group.
// 在链接的命名空间内加载
if (load_library(linked_namespace.linked_namespace(), task, zip_archive_cache, load_tasks, rtld_flags, false)) {
return true;
}
} else {
// lib is already loaded
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());
}
// 再次检测该库是否可以访问,可以访问则返回true
if (!namespace_link.is_accessible(soname.c_str())) {
// the library is not accessible via namespace_link
return false;
}
// if library is already loaded - return it
if (loaded) {
task->set_soinfo(candidate);
return true;
}
// returning true with empty soinfo means that the library is okay to be
// loaded in the namespace but has not yet been loaded there before.
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;
// 当类加载器为空时直接调用dlopen,而native_loader处于默认命名空间,因此可以访问私有库
return dlopen(path, RTLD_NOW);
}
std::lock_guard<std::mutex> guard(g_namespaces_mutex);
NativeLoaderNamespace ns;
// 查找当前类加载器所属的命名空间,如果没有则新建一个命名空间
if (!g_namespaces->FindNamespaceByClassLoader(env, class_loader, &ns)) {
// This is the case where the classloader was not created by ApplicationLoaders
// In this case we create an isolated not-shared namespace for it.
if (!g_namespaces->Create(env,
target_sdk_version,
class_loader,
false /* is_shared */,
false /* is_for_vendor */,
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();
// 是Android命名空间则调用另一个dlopen函数,可见这里传入了extinfo
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");
}
// 释放dex和so构造新的类加载器环境
@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";
}
// 这里释放apk同一个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 {
// 新建一个类加载器动态加载so
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;
}
// *********************************** 分割线 ***************************
/* DynTestClass.java */
package com.beichen.fakelinker;
import android.util.Log;
import java.io.File;
/**
* @AuThor beichen
* @date 2019/04/15
*/
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(); // 申明一个native函数,但我们不默认实现它
@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是上一步动态加载时保存的,其所属ClassLoader是我们自定义的DexClassLoader
specialMethod.invoke(null, this.getClass().getClassLoader());
} catch (Exception e) {
e.printStackTrace();
}
break;
case R.id.btn_call: // 负责调用刚才申明的native函数
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 ; weak
.text:0000000000000EF0 dlopen proc near ; DATA XREF: LOAD:0000000000000508↑o
.text:0000000000000EF0 ; __unwind {
.text:0000000000000EF0 mov rdx, [rsp+0] ; 获取返回地址
.text:0000000000000EF4 jmp ___loader_dlopen
.text:0000000000000EF4 ; } // starts at EF0
第一条汇编指令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);
// capstone 是一个综合反编译库,Android默认也使用的它
insn = NULL;
// 从dlopen地址开始反编译4条指令
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); // 上面看到关键指令是jmp __loader_dlopen
// 这一步得到__loader_dlopen对应的plt地址
}
break;
}
}
if (impl != fun){
// 接着再反编译plt地址
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){
// jmp, [rip + #imm32]
// 这里加6是因为rip指向的是下一条要执行的指令,而当前指令占6个字节
gpointer tmp= GSIZE_TO_POINTER(GPOINTER_TO_SIZE(impl) + 6 + op1.mem.disp);
// 取得got表地址中的值就是真正的函数实现地址
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__)
// 解析linker调试日志开关内部符号__dl_g_ld_debug_verbosity
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; // 开启调试日志
// 采用正常方式调用dlopen
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);
// 解析linker函数实现地址
// ARM 是plt跳转地址,X86是实际地址,详情看方法内部
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);
// 采用反汇编解析的方式调用__loader_dlopen
p_runtime_handle = dlopen_impl(lib, RTLD_LAZY, open); // 这里传入open函数地址意味着将caller命名空间改为默认命名空间
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");
}
// 采用导入表查找方式获取__loader_dlopen实现地址
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:/ # cat /proc/9980/maps | grep "libnative-lib"
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;
//private:
uint32_t flags_;
const char * strtab_;
ElfW(Sym) * symtab_;
...略
// version >=3
std::vector<std::string> dt_runpath_;
android_namespace_t * primary_namespace_;
android_namespace_list_t secondary_namespaces_;
uintptr_t handle_;
// version >=4
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)
免费评分
查看全部评分