赞
踩
本人Android逆向小菜鸡一名,且文学水平有限,明白意思但说不明白,各位看官能看明白多少算多少吧
开机后,引导芯片会从固化的 ROM(只读Read Only Memroy) 处执行预设代码,将 Bootloader 加载到 RAM(可读写Random Access Memory) 中,Bootloader 设置系统硬件参数,检查 RAM,把操作系统映像文件拷贝到RAM中去,然后跳转到它的入口处去执行。内核启动,创建第一个内核进程 idle 进程,最终创建第一个用户空间进程 init;
- init进程 :
- 1.创建和挂载启动相关的文件目录。
- 2.初始化和启动属性服务(类似于pc的注册表)。
- 3.解析 .rc 配置文件,将zygote 和 startSystemServer 设为true。
- 4.判断zygote 为true后,调用 runtime.start函数传入包名"com.android.internal.os.ZygoteInit" 与start-sytem-server 。->
-
- runtime.start函数:
- 1.创建虚拟机
- JniInvocation jni_invocation;
- 2.加载 ART 虚拟机的核心动态库,如:libart.so
- jni_invocation.Init(NULL);
- JNIEnv* env;
- 3.启动 ART 虚拟机
- if (startVm(&mJavaVM, &env, zygote, primary_zygote) != 0) {return;}
- 4.找到 com.android.internal.os.ZygoteInit 中 static void main(String argv[]) 方法
- jmethodID startMeth = env->GetStaticMethodID(startClass, "main",([Ljava/lang/String;));
- 5.执行上述查询到的方法,启动Zygote进程中的main函数
- env->CallStaticVoidMethod(startClass, startMeth, strArray);
-
- Zygote进程:
- 1.创建 Server 端 Socket,用于和其他进程通信
- ZygoteServer zygoteServer = new ZygoteServer();
- 2.启动 SystemServer 进程
- Runnable r = forkSystemServer(abiList, zygoteSocketName, zygoteServer);
- 3.等待 ActivityManagerService 的请求来创建新的应用程序进程
- caller = zygoteServer.runSelectLoop(abiList);
- -> SystemServer ,ActivityManagerService,Launcher;Launcher进程加载图标,包名,androidmanifest。
Launcher进程收到点击图标消息时,使用Binder方式通知system_server进程启动app,之后Launcher进程将自己挂起,system_server进程将收到的Binder消息发配给AMS服务,AMS服务以socket方式通知Zygote进程,Zygote fork自身创建子进程,向进程中导入ActivityThread类,执行该类的main函数,
在启动ApplicationThread(在每个ActivityThread被创建的时候, 都需要向ActivityManagerService绑定,或者说是向远程服务AMS注册自己),调用bindApplication方法传入绑定的pid,在调用makeApplication,最终调用callApplicationOnCreate方法执行,所以说Application中的OnCreate方法早于所有的aictivity OnCrete调用。
题外话:第一个activiy的启动,正常情况下会优先执行Oncreate函数;意外情况:入口点类内有static块,里面的代码会被先执行,例如System.loadLibrary函数加载so文件。
下图中的,以Proxy结尾的玩意,是Binder驱动在每个进程中的对象,通过其 使用Binder通信服务。
1.作用:初始化时间,时区,语言等;设置虚拟机运行库路径;清除虚拟机内存限制(各大厂商的dalvik限制了每个进程的堆空间最大值),设置堆利用率(0.8);
****创建looper(消息管理)线程,初始化native服务,加载Android_servers;初始化系统上下文;启动ActivityManagerService(广播,activity,content,server),PackageManagerService,WindowManagerService 等服务以及 binder 线程池。****
Binder是Android中用于进程间通信(IPC)的一种机制。在Binder通信过程中,有三个主要角色:服务端、客户端和Binder驱动。
1. 服务端:服务端创建Binder对象,实现Binder接口,并将其注册到Binder驱动中。
2.客户端:客户端通过Binder驱动获取服务端提供的Binder对象,并将请求发送给服务端。
3.Binder驱动:它负责管理所有的Binder对象,并确保它们之间的通信,存在于内核空间中。
binder通信过程:
1.Binder准备工作:Binder驱动创建一块内核缓冲区,通过Server Manager 进程的server信息,找到对应的server进程,实现 内核缓冲区&用户空间 的内存映射(内存映射:通过调用mmap系统函数,可以简单理解为内存块 1 ,2 存在内存映射关系,向1中写入数据,相当于同时向 1 与 2 写入相同数据)。
2.客户进程发送请求:客户进程通过 binder_thread_write (内核中调用copy_from_user)将请求(目标方法的标识符、参数 及 方法对象标识符,接收返回值的参数)发送给内核缓冲区,客户进程被挂起,Binder通过代理对象找到server的实体对象,再把客户进程发送过来的数据拷贝到,与步骤 1 中存在映射关系的内存中,最后通知server进程解包。
3.服务进程调用方法:收到Binder的消息后,进行解包,通过包中的信息调用函数执行,将返回结果写入接收参数。
4.服务进程返回结果:将返回包写入与内核共享的,用户空间内存中,Binder驱动通过实体对象找到代理对象(将返回包数据写入代理对象?),唤醒客户进程并通知其接受返回结果,客户进程调用 binder_thread_read (内核调用copy_to_user)获取代理对象,其中的reply保存返回值(步骤 2 中的接收返回值参数)。
Binder服务在初始化时会创建一个虚拟目录(/proc/binder/proc)与一个虚拟文件(/dev/binder设备文件)
/proc/binder/proc
每一个使用了Binder进程间通信机制的进程在该目录下都对应有一个文件,这些文件以进程ID来命名,通过他们就可以读取到各个进程的Binder线程池、Binder实体对象、Binder引用对象以及内核缓冲区的信息。
/dev/binder设备文件
可以理解为Binder驱动程序的代码,客户端在使用Binder时,第一步就是创建 IBinder 类型对象,该对象就是Binder驱动的引用;在binder初始化时指定全局变量 binder_fops,提供方法接口给开发者使用。
打开方法binder_open
内存映射binder_mmap
IO控制binder_ioctl
进程消息读写 binder_thread_read 与 binder_thread_write 等
ELF 文件标准大概包含了以下四种文件类型:
.o
文件,Windows 的 .obj
文件.exe
.so
,Windows下的 .dll
。dex文件(Dalvik EXecutable):是Android应用程序的核心可执行文件,包含了应用程序的字节码和其他资源信息。dex文件经过优化后可以被Dalvik虚拟机或ART虚拟机执行。
odex文件(Optimized DEX):是针对dex文件的优化版本,通过预先将dex文件的字节码和其他信息进行优化处理,使得在程序运行时加载和执行更加高效。odex文件通常是由dex文件通过系统的dex优化工具(dexopt)生成的。
vdex文件(DEX Optimized Virtual Machine EXecutable):是针对ART虚拟机的优化版本,是一个全新的文件格式,相比于odex文件更加高效和安全。vdex文件包含了预编译的应用程序字节码和其他信息,可直接被ART虚拟机加载和执行。vdex文件通常是由系统的dex2oat工具在应用程序安装时生成的。
应用程序在第一次启动app的时候,会在/dalvik/dalvik-cache目录下生成odex文件结构,odex 是 dex 进行优化 生成的 可执行二进制码 文件,
1.header: Dex 文件头,包含 magic 字段、adler32 校验值、SHA-1 哈希值、string_ids 的个数 以及偏移地址等。Dex 文件头结构固定,占用 0x70 个字节。
2.String_Id: 定义了字符串数据的偏移(4字节指针,指向string结构体:string大小,string数据);
3.Type_Id: 表示应用程序代码中使用到的具体类型,如整型、字符串、类和方法等,类和方法以字符串形式保存在DexStringId中,*DexTypeId为对应的DexStringId的下标。
4.Proto_id:方法声明的信息,结构体包含:4字节常数,为string_id数组的下标,通过该下标可以找到方法名称;type_id的下标,方法返回值类型;指向参数结构体的指针,结构体保存参数数量与参数类型(type_id下标方式)
5.field_id:字段的信息(类成员变量);所在类信息,所属类型(type_id下标);字段名(string_id下标)
6.method_id:方法的信息;所属类(type_id[]),方法的声明(proto_id[])(所属类与声明都是以short类型索引保存,所以一个dex中最多只能有65535个方法),方法名(string_id[]);
7.class_def:类的信息;类的相对路径(如Ljava/lang/String),访问权限(public等),父类信息(type_id[]),接口信息(绝对地址),源文件名称(string[]),注释信息(绝对地址),类中字段,方法的数量(绝对地址),类的静态数据的值(绝对地址)
8.CLass_data:类的字段信息、方法信息和相关的访问标志等。
class文件中保存的是Java字节码,需要再次进行翻译为Dalvik字节码,在生成dex文件。
区别:
1.dex文件相比于class文件格式相对更加紧凑,减少冗余,加载一个dex文件相当于加载了多个class。
2.class保存的是Java字节码,是基与栈运行的;而dex存的是Dalvik字节码,是基于寄存器的。
3.dex基于寄存器寻址更加方便,适合移动端,而基于栈寻址则需要多次出栈入栈操作。
(5条消息) Android虚拟机的几个面试技术点_安卓虚拟机面试_Mr.Louis的博客-CSDN博客
Dalvik虚拟机与java虚拟机的区别 - 简书 (jianshu.com)
dex文件解析、执行流程
Dalvik虚拟机虚拟机通过调用PathClassLoader(只能加载已安装的apk的.dex文件)或DexClassLoader(可加载所有dex文件),解析dex文件为Dalvik字节码后运行,dex文件中保存了一个程序所有执行的逻辑。
JIT即时编译:程序执行时,系统将dex加载到内存,一边执行一边将dex中的字节码转为机器码,当哪个代码段被判定为热执行函数时,会将其编为机器码并优化保存在内存中;JIT编译所有的流程不会涉及生成新的文件,即使判定为热函数,优化后的函数也是在内存中,不会落地,程序结束就都没了。
AOT预编译:程序安装时,一次性将dex编译为机器码(.odex文件),保存至 /data/dalvilk-cache 目录下(都这么说,但是我用模拟器在该文件夹下没找到odex后缀的文件)。
4.4之前
只使用jit即时编译技术,优点是储存空间占用小,每次启动都会生成新的热执行函数;缺点是内存占用较大,启动和执行速度满(相对与aot来讲)。
AOT在4.4版本被引入,但编译主要还是用的是JIT技术,5.0--7.0 AOT完全取代JIT,在App安装时就将所有dex编译为机器码(odex文件)保存到本地,程序运行时,直接运行odex文件,
优点是app的启动与运行速度大幅度提高,缺点是安装时间明显变长,且静态文件(不是内存,是手机存储odex文件)变大,导致收集储存空间不足。
引入AOT + JIT结合编译,此时的delvik虚拟机从32位演变为64位(在此之前的delvik虚拟机只支持32位程序),支持的最大堆内存由1.2GB变为10+GB;数据类型大小改变(32位int 4字节,64位的为8字节);指令集由32位的 ARMv5TE 变为 ARMv8;性能提高。
引入vdex文件取代odex,文件格式不同;vdex存储的是 机器码 + 一部分原dex文件数据(类结构,注释等),vdex是在程序安装时,art通过对dex静态分析和优化的产物,并不会将所有的dex字节码转为机器码;程序运行时,会直接执行vdex内的数据,根据判断函数执行频率将其定义为热执行函数,保存到.art文件中,手机空闲时,读取.art文件,将其中记录的函数编译为机器码保存到vdex中。
实现 ART 即时 (JIT) 编译器 | Android 开源项目 | Android Open Source Project (google.cn)
字节码叫法由来:每一条指令就是一个字节,可以表示256条不同的指令。
区别:cpu可以执行arm指令,但无法执行字节码,字节码需要转换;arm是基于寄存器,字节码基于堆栈;字节码没有地址的概念,而arm可以解引用。
软中断指令格式:SWI{cond} immed_24
通过SWI指令触发软中断,切换到特权模式,后面是一个24位立即数,和调用号差不多,通过该立即数来执行不同的内核函数。
过程:执行至SWI指令时,PC被置为指向 异常向量表地址0x08 的指针,再在该处设置指针
b mySWI,这样触发中断后就会执行我们的代码了。
安装的方式:
涉及目录:
安装流程简述:
MD5、SHA、HMAC:不可逆,但是MD5与SHA可以通过暴力碰撞破解
MD5:穷举法(知道原数据长度一个个试)&字典法(将一大堆试出来的MD5存起来,以后直接找MD5库)
过程:设原文件长度为x,填充文件长度至n*512(bit) + 448 ,第一位使用1,后面填0,在将长度x用64位记录,拼接至448之后,第一次使用标准幻数A=0X67452301L,B=0XEFCDAB89L,C=0X98BADCFEL,D=0X10325476L与第一组16 * 4字节数据进行四轮计算,生成16 字节结果,在与下一组512(bit)计算,最终生成16字节。
SHA1:咔咔一顿算,最后生成32字节,,SHA256 算出32字节,位数多了,比MD5安全。计算稍慢,SHA256最慢。
过程:和MD5差不多,SHA1是16 * 5字节为一组的,生成20字节;SHA2是 16 * 6为一组生成32字节。
HMAC:MD5与SHA算法结合体,加入密钥。客户端使用密钥与要发送的消息进行一次或多次摘要计算出结果,将结果打包发给服务端,服务端用相同的密钥与消息计算,最后校验结果。
CRC:完整性校验,和MD5差不多,计算简单多用于网络收发包,步骤:要发送的数据 :send,发送端生成的帧:key,特定的数:num,send + key 能整除 num,这里采用的是模2除法(具体过程没看懂)。
对称性加密算法有:
AES:分为AES-128(192,256),密钥长度可以是128 || 192 || 256,密钥长度不同,最优加密轮数不同,对应的分别为10 || 12 ||14轮,步骤:字节代换(查表映射)、位移、列混合(矩阵相乘)、密钥计算(密钥与矩阵异或)
DES、3DES:64位密钥加密64位明文的算法,三次使用的密钥相同就是DES,不同就是3DES,下图重复执行16轮,由于是异或运算,所以加密多少轮都可以解密回来,算法简单,计算慢,比较low。
非对称性算法有:RSA、DSA、ECC(全看不懂,QTMD);
Base64:不是加密算法,只是编码传输数据。
过程:从计算之前的二进制数中取出6位(二进制6位的值域位0~63,所以叫base64),扩充为8位,前两位补0,每8位在查表替换(A-Z + a-z + 0-9 + '+' + '/'共64个字符)
在Android系统中,SO文件的加载和链接是通过一个名为"linker"的系统组件实现的
静态注册:现在用的太少了,也太low,对于逆向而言一眼看穿,就不说了。
动态注册:
通过 Java层的System.loadLibrary("相对路径")或System.load("绝对路径")调用so文件。
- System.loadLibrary -> Runtime.loadLibrary -> PathClassLoader.findLibrary ->
- Runtime.nativeload -> Dalvilk_java_lang_Runtime_nativeLoad -> dymLoadNativeCode ->
- dlopen -> //选读
- dlsym(JNI_OnLoad) -> //重点函数:一般的解密,验证等都会写在这里面
- JNI_VERSION_1_6 -> Success
- finish :JNI_OnUnLoad(与JNI_OnLoad都是非必须要实现的)
经过几层的调用,最终调用到 load_library 函数(在这之前会判断so是否已经被加载),在load_library中执行 装载,创建soinfo,链接 三个过程
装载
创建ElfReader对象,通过 ElfReader 对象的 Load 方法将 SO 文件装载到内存。
- //标准linker的装载
- bool ElfReader::Load() {
- return ReadElfHeader() && //读取elf header
- VerifyElfHeader() && //验证elf header
- ReadProgramHeader() && //读取program header
- ReserveAddressSpace() && //分配空间
- LoadSegments() && //按照program header的指示装载segments
- FindPhdr(); //找到装载后的phdr
- }
创建soinfo
- struct soinfo {
- char name[SOINFO_NAME_LEN]; // 共享库名字
- const ElfW(Phdr)* phdr; // 指向段头表(program header)的指针
- size_t phnum; // 段头表中入口的数量
- ElfW(Addr) entry; // 共享库入口地址
- ElfW(Addr) base; // 共享库基地址
- size_t size; // 共享库大小(字节数)
- uint32_t unused1; // 保留字段
- ElfW(Dyn)* dynamic; // 指向动态段的指针
- uint32_t unused2; // 保留字段
- uint32_t unused3; // 保留字段
- soinfo* next; // 指向下一个 soinfo 结构体的指针
- uint32_t flags; // 共享库标志
- const char* strtab; // 字符串表地址
- ElfW(Sym)* symtab; // 符号表地址
- size_t nbucket; // Number of buckets in the ELF hash table.
- size_t nchain; // Number of chains in the ELF hash table.
- uint32_t* bucket; // Address of the ELF hash table.
- uint32_t* chain; // Address of the ELF hash table.
- unsigned int* gnu_nbucket; // Address of the GNU hash table size.
- unsigned int* gnu_maskwords;// GNU hash table Bloom filter size (words).
- unsigned int* gnu_shift2; // GNU hash table Bloom filter shift.
- unsigned int* gnu_bloom_filter; // GNU hash table Bloom filter address.
- ...
- };
最重要的一步就是链接,链接主要步骤是:
dl_open -> dlopen_ext -> do_dlopen,do_dlopen首先会判断该so文件是否已经加载,如未加载,会首先调用so文件中的 init 函数,中主要调用find_library (判断so是否已经加载,未加载则调用load_library加载so文件),find_library会返回一个soinfo指针,在执行soinfo结构体中的成员函数CallConstructors(如果要修改一个已经加载的so文件soinfo,可以在find_library返回时修改),CallConstructors 函数会首先调用所有依赖的 SO 的 soinfo 的 CallConstructors 函数,接着调用自己的 soinfo 的 init_func 和 init_array 指定的函数,这两个变量在在解析 dynamic section 时赋值。
so文件中可以重写init_array与init_func(老版本的liniux系统)或者创建.init_arry区段,此区段的函数指针会在JNI_OnLoad函数执行之前挨个执行,.fini_arry区段保存着C++的析构函数,会在程序正常结束时被调用,释放全局资源,这两个函数会在dlopen中被调用,本质上这是两个数组,里面存放了函数指针,里面的函数会依次执行。
之后执行回调函数JNI_OnLoad,其类似于类中的OnCreate函数,会被默认调用,所以一般的so文件都会通过重写JNI_OnLoad来实现解密,反调等等功能。
- JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved);
- JNI_OnLoad 函数的主要作用是完成以下任务:
-
- 1.获取 JNIEnv 指针:在 JNI_OnLoad 中,可以通过 vm 参数获取到当前的 JNIEnv 指针。该指针
- 可以用于后续调用 Java 方法或者访问 Java 对象。
- 2.注册本地方法:通过调用 JNIEnv 的 RegisterNatives 函数,可以将本地方法注册到 JVM 中,
- 从而使得 Java 代码能够调用本地库中的函数。
- 3.执行其他初始化操作:JNI_OnLoad 函数也可以用于执行其他与本地库相关的初始化操作,例如初
- 始化全局变量、创建线程、打开文件等。
RegisterNatives 函数
作用:将本地方法(so中的函数)注册到jvm虚拟机中,之后Java层就可以通过JNI接口直接调用。
RegisterNatives(JNIEnv *env, jclass clazz, const JNINativeMethod *methods, jint nMethods);
RegisterNatives函数第三个参数JNINativeMethod结构中保存着注册的方法名称,签名,函数指针三个指针,通过调试该函数,可以拿到Java层调用的函数对应的so中的函数地址。
以自定义的文件格式保存so文件,一定程度上的阻止IDA的静态反编译,apk运行时,通过自定义的 linker(加载so文件的流程),将so文件解析为正确的格式,返回 soinfo 结构体供其他函数调用(比如原so文件开头保存的是 header 信息,自定义后将heard信息保存到固定的偏移0x6666处或将原 header 二进制文件加密保存等)。
加密过程:加壳程序通过解析原正确格式的 so 文件,拿到各各区段信息,将区段加密,将加密后的区段重新拼接成自定义的文件格式(现在的自定义 linker 壳基本都会把加壳器单独存为一个so文件)。
解密过程:
1.将加密后的so解密为正确的so文件格式并加载到内存。//初始化 soinfo结构体信息,然后链接。
2.linker_soinfo_map修复(关键):根据 r0ysue 姐的文章,安卓7以下可以通过dlopen自身,获取系统维护的 soinfo 表(系统加载的是加密的so文件,所以得到的soinfo结构体是错误的,需要修复),遍历 soinfo表 中的so名称找到我们的so对应的 soinfo结构体,在将步骤 1 中解析出的区段信息,填充到对应的 soinfo结构体中。
3.加载该so文件的依赖;各种导入表,导出表,重定位表,依赖的函数地址等的修复。
4.主动调用so中的 init_array 和 JNI_Onload 方法(解密过程看的我是三眼懵逼,开发人员请参考佬的文章《基于linker实现so加壳技术基础》)。
总结:无论中间过程怎么样,解密后提供给 系统soinfo表 的 soinfo结构体肯定是对的,soinfo结构体中保存的是so文件的详细信息,可以通过该结构体得到正确的so文件。
Android中局部变量和全局变量的存储方式与C/C++语言类似,局部变量使用栈来保存,全局变量和动态分配的内存用堆来保存。
C中的堆与栈需要开发者自己维护,栈中保存 局部变量、参数、返回值、返回地址 等,堆中保存 全局变量 与 动态分配(编译时无法确定其大小) 的内存等。
堆栈区别:
栈存储数据由 高地址 -> 低地址 存储,内存是连续的, 由系统自动分配的,速度快,不会有碎片,操作困难;
堆存储数据由 低地址 -> 高地址 存储,内存不是连续的,开发者自己申请的,速度慢,会产生碎片,操作容易;
Java中,栈是由 jvm 管理的,方便,但是限制了灵活性。
Https:安卓7以上,且app的xml中的 android:targetSdkVersion 属性大于24时,系统不在信任用户的CA证书,无root权限将无法抓到Https的包;
有root权限,可以将抓包工具提供的证书通过 adb push 到 /etc/security/cacerts/ 文件夹下,证书名称是 CA 证书 subjectDN 的 Md5 值前四位移位取或,后缀名是 .0
,比如 00673b5b.0。
无root,
targetSdkVersion大于24时,绕过方法:重打包apk,将xml文件中的 targetSdkVersion属性调低;2.平行空间或者 VirtualApp 抓包,平行空间更稳定,但版本要在 4.0.8625 以下;
3.root手机,使用 HttpCanary v2.8.0 之后的版本,可直接导出以 .0结尾的CA证书,在安装到手机的系统证书目录。
可以在无需了解so内部算法原理的情况下,主动调用so中的函数,传入所需的参数、补全运行所需的环境,即可运行出所需要的结果。达到辅助分析、算法还原、SO
调试与逆向等等功能。
在该so文件中可能会通过JNI调用Java函数,但是在Unidbg的环境中无法找到该函数,这时候就需要我们把其中调用的Java函数给补全。
KernelSU是一种基于内核的root解决方案,主要工作在内核空间,可以提供针对内核的HOOK接口,可以对内核中的几乎任意函数进行拦截,例如,我们可以在内核模式下为任何进程添加硬件断点;我们可以在任何进程的物理内存中访问,而无人知晓;我们可以在内核空间拦截任何系统调用; 等等。
大部分需要汉化的文件 RotSeek.smali strings.xml camera_preferences.xml lol.smali defcomk.smali lol1.smali arrays.xml faa.smali ;
基于Unity(java类)与il2cpp(so文件)框架开发的游戏,使用fakerandroid可将apk还原成一个Android Studio 工程,编写 native-lib.app 的 fakeApp 等函数实现 hook 功能。
1.修改 Androidmanifest.xml 的入口点为 FakerApp,MAIN & LAUNCHER 类为 FakerActivity。
2.FakerApp 中加载 native-lib.so,并执行其中的静态导出函数 fakeDex 与 fakeApp 函数。
3.fakeApp 函数中用到的 fakeCpp:参数1 要hook的函数地址,参数2 将参数1函数重定向到你自己函数的函数名,参数3 &参数1(原因没研究)
4.Unity游戏 com.unity3d.play.UnityPlayer 中 loadNative 函数,通过字符串加载so文件,最终通过main.so中的动态注册的Native函数 load 加载so文件。
[分享]某vmp壳原理分析笔记-Android安全-看雪论坛-安全社区|安全招聘|bbs.pediy.com (kanxue.com)
参考:https://blog.csdn.net/hzwailll/article/details/85339714
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。