【ART HOOK系列】Android Linker Namespace(一): 基础知识

导读:

在正式开始介绍Android Linker Namespace之前,不如先来了解下Namespace是什么?

本文将引入几个比较容易接触到的例子来介绍一个广泛存在于各种编程语言中的概念,也是Android Linker Namespace的思想基础: Namespace

本文章中所有的代码文件见:https://github.com/Gstalker/codes_for_blog/tree/main/art_hook_namespace_1

一、命名空间(Namespace)解决了什么问题

1.1 如何取名:大型C语言工程面临的困难

如何取名是大型C语言工程必须解决的一个问题。整个工程中不允许出现两个相同的导出符号名,否则链接时就会出现符号名冲突。

1.1.1 一个简单的例子

假设我们现有一个项目,项目目录结构如下:

.
├── CMakeLists.txt
├── foo.c
├── include
│   └── foo.h
├── main.c
└── README.md

1 directory, 5 files

其中,foo.c和main.c都实现了一个名为add的函数

main.c

#include <stdio.h>
#include "foo.h"

unsigned add(unsigned a, unsigned b) {
    return a + b;
}

int main(){
    printf("main: 1 + 2 = %ud\n", add(1,2));
    foo_test();
    return 0;
}

foo.c

#include<stdio.h>

int add(int a, int b) {
    return a + b;
}

void foo_test(){
    printf("foo_test: 1 + 2 = %d\n", add(1,2));
}

编译,必然会报错。因为链接器发现存在一处符号冲突,有两个函数的符号名相同,不能进行链接。

1.1.2 C语言大型项目中,是如何解决取名问题的

由于C语言标准没有namespace这个概念,所以我们随便点进几个C语言头文件,都可以发现大量形如下图的奇怪名字出现

简单的说,就是尽可能把内部使用的符号加上下划线来避免和外部符号完全一样,把清爽的,不需要下划线的世界留给了用这个库文件的用户。

但这样一来,代码的美观程度就大幅下降了。而且下划线这种东西,不同字体下也许长度会不一样。这一行为虽然可以解决问题,但是并不友好。

1.2 C++ Namespace: 解决源码层面的命名冲突

在C++中,有一种语法,叫做Namespace

namespace机制提供了一个解决导出符号名字相同的问题,通过以下手段:

1.提供语言层面的namespace语法,允许语言使用者通过namespace划分符号的命名空间

2.编译时对符号进行重命名,结合原始名字和变量信息为符号名

1.2.1 改进先前的例子

同样是1.1.1节中的代码,我们用cpp重新写一次,不过这一次我们加入一下namespace这一要素看看:

.
├── CMakeLists.txt
├── foo.cpp
├── include
│   └── foo.h
├── main.cpp
└── README.md

1 directory, 5 files

main.cpp

#include <iostream>
#include <foo.h>

namespace main_cpp{
    unsigned add(unsigned a, unsigned b) {
        return a + b;
    }
}

int main() {
    using std::cout;
    cout << "main: 1 + 2 = " << main_cpp::add(1,2) << '\n';
    foo_test();
    return 0; 
}

foo.cpp

#include <iostream>

namespace foo {
    int add(int a, int b) {
        return a + b;
    }
}

void foo_test() {
    using std::cout;
    cout << "foo_test: 1 + 2 = " << foo::add(1,2) << '\n';
}

在使用namespace这一机制后,编译成功通过了

1.2.2 cpp namespace的背后:mangle

前面的两个例子,主要是为了说明namespace机制在实际应用中能解决什么问题。

cpp的namespace只是将C中加下划线,加符号名所属工程的解决方案作为作为语法和编译器功能的一部分引入到编程语言中来。这一功能叫做mangle。详细可以见:https://cloud.tencent.com/developer/article/1005044

在刚才的例子中,两个add函数,实际编译出来后在二进制文件中的符号名为:

int foo::add(int a, int b) ------> _ZN3foo3addEii

unsigned main_cpp::add(unsigned a, unsigned b) -------> _ZN8main_cpp3addEjj

事实上,没有哪一种语言能实现在同一个可执行文件中搞出来两个名字真正相同的符号。

二、概括:什么是命名空间

2.1 编程语言中的命名空间

通过划定名称域,分别管理域内的实体,各个域之间相对独立,这是命名空间的核心思想

而namespace机制,则是上述思想在各个具体应用场景的实现。

目前我们最容易接触到的,就是各种编程语言中显式出现(如:cpp)或者隐含的(如:rust、java)的namespace机制。

各种编程语言通过结合工程结构管理规范和namespace机制二者,创造了风格各异的项目风格,也为开发者提供了更好的结构化开发环境和符号管理机制。

2.2 安卓链接器的名称空间

同样的,Android Linker Namespace也是一个命名空间机制。

不过,它解决的是动态链接库的管理问题

2.2.1 一台安卓手机的动态链接库文件

让我们先来看一下一台安卓9的手机上的动态链接库目录下有什么东西,一个真实的例子远比一大段文字来的有效。

从下图中可以明确看出,不同目录下存在名字相同的动态链接库。

并且这些动态链接库是存在差异的

那么,安卓系统是如何管理,并在运行时加载这些不同内容但是同一名字的动态链接库的?

2.2.2 一个官方页面:链接器命名空间

从安卓开源项目官网中也可以看到对其说明,不同目录下的同名动态链接库会被分配到不同的命名空间中。

https://source.android.google.cn/devices/architecture/vndk/linker-namespace?hl=zh-cn

而这一页面所阐述的“链接器命名空间”,便是接下来两篇文章将要详细介绍并绕过的Android Linker Namespace

【Android-Debuging】安卓真机调试native层步骤

环境需求

android stdio套装
装这一套单纯是为了里头的tools文件夹,以及迟早要用到ndk,android开发等功能。所以配环境时可以考虑直接一步到位
ida
不用我多说了吧
一台部分root的手机
adb shell连接进去之后,shell能提权为root就行,不要求挂在adb root
建议200~300块去淘宝入个google pixel(代号seilfish)
刷机 & root教程https://www.jianshu.com/p/ec043ef9e0c3
记得开启ro.debuggable

利用 Magisk 模块永久修改 ro.debuggable & 获取 adb root


刷机后连国内wifi提示无网络,先本地调整手机时间到现在的真实时间(时间差距不大于5分钟),然后按照这个链接去改下配置文件就行:
修改NTP服务器配置以解决wifi检测错误问题

正片

首先装上需要被调试的程序,此处略。
随后使用adb push指令将ida的安卓调试服务文件上传至机器的/data/local/tmp文件夹
以root权限启动android_server(如果被调试对象是64位程序,那就使用android_server64)

然后使用下面的指令启动待调试程序
am start -D -n 包名/类名

如何找入口点class?在mainfest里头搜索关键词”LAUNCHER”
带有下图红框中关键词的activity即为安卓java层的起始activity。取它作为类名

启动:

然后执行下面的指令获取刚起起来的程序的pid,一会儿要用上
ps -ef | grep 包名

接着在主机这边的shell,使用下面俩个指令开放端口

adb forward tcp:23946 tcp:23946 //ida的调试端口
adb forward tcp:12345 jdwp:{pid} //{pid}处填写刚才获取的pid。jdwp协议有兴趣的可以去查下,这里不多讲

然后使用ida连接attach到目标进程。
当然,我们得先在Debug-Debuger Options里头做点设置

然后Debugger-Attach to Process,附加到进程。在下边的窗口中找到我们要调试的程序,然后选择它

顺利的话可以看到ida成功附加上来了

接下来我们需要让jvm跑起来。我们的目的只是调试native层,jvm我们不管。
需要使用jdb连接上目标程序
下面这一行指令中的PORT修改为之前adb forward绑定的jdwp对本机窗口,也即是12345
jdb -connect com.sun.jdi.SocketAttach:hostname=localhost,port={PORT}
然后就不用管jdb这边了

剩下的,根据需求自由用ida调试nativelib吧~

瓶颈

有时候还是挺难顶的,缺少开发经验导致对一些代码始终停留于看得懂但是写不出来。这对以后的安全生涯有很大影响。
补,现在就补。
个人认为做安全并不需要特别高的开发能力。但是最基本的,怎么写,如何实现,其原理如何还是得了解。

唉、、、大学这个时间点,我本因在开心的玩才对,但是为了生计,不得不这样努力下去。