mineucore-x86-32下的中断处理

x86-32下的中断处理

1、中断的类型

来自硬件设备的处理请求称为硬中断,或者外部中断。他是异步产生的,即与CPU的执行无关;
异常是非法指令或者其他原因导致当前指令执行失败后的处理请求,也叫内部中断,他的产生方式是同步的;
应用程序主动向操作系统发出的服务请求称为软中断(trap),也叫系统调用 ,一般通过INT n指令实现,可以是同步产生,也可以是异步产生。

INT、IRET指令用于系统调用,系统调用时,存在堆栈切换和特权级切换
CALL、RET用于常规函数调用

2、中断描述符表IDT

每个中断或异常与一个中断服务例程相关联,其关联关系存储在中断描述符表(IDT)中,IDT可以位于内存的任意位置,IDT的起始地址和大小保存在中断描述符表寄存器(IDTR)中,CPU可以通过IDTR找到IDT。

image-20200221225125272

IDT中每一项为门描述符,含有段选择子和偏移。发生中断后,CPU根据中断号在IDT中找到对应的门描述符,通过门描述符中的段选择子去GDT的段描述符中找到该段的基址,加上门描述符中的偏移,从而计算出中断服务例程所在的地址。

image-20200221151425723

中断描述符表IDT是一个8字节数组,每一个表项叫做一个门描述符,“门”的含义是指当中断发生时必须先访问这些“门”,能够“开门”(即将要进行的处理需通过特权检查,符合设定的权限等约束)后,然后才能进入相应的处理程序,而门描述符则描述了“门”的属性(如特权级、段内偏移量等),在IDT中,主要有3种类型的门描述符:

1、中断门描述符(Interrupt-gate descriptor),用于硬中断和异常的处理,其类型码为110,中断门包含了一个外设中断或故障中断的处理程序所在段的选择子和段内偏移量,中断门中的DPL(Descriptor Privilege Level)为0,因此用户态的进程不能访问中断门。所有的中断处理程序都由中断门激活,并全部限制在内核态。控制权通过陷阱门进入处理程序时会清空IF标志,不允许中断嵌套的发生。

2、陷阱门描述符(Trap-gate descriptor),用于系统调用的处理。与中断门类似,其唯一的区别是,控制权通过陷阱门进入处理程序时维持IF标志位不变,也就是说,不关中断。

3、任务门描述符,Intel设置的“任务”切换的手段。

image-20200221225152925

3、对中断向量表(中断描述符表)IDT进行初始化

在保护模式下有256个中断号,0~31是保留中断号,用于处理异常和不可屏蔽中断NMI,32~255由用户定义,可以是设备中断系统调用

1)生成每个中断号对应的中断处理例程地址

vectors.S包含所有中断号对应的中断服务例程地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 由tools/vector.c产生
# handler
.text
.globl __alltraps
.globl vector0
vector0:
pushl $0
pushl $0
jmp __alltraps
.globl vector1
vector1:
pushl $0
pushl $1
jmp __alltraps
......
......
# vector table
.data
.globl __vectors
__vectors:
.long vector0
.long vector1

可以看到所有中断号对应的中断服务例程都会跳转到__alltraps进行处理

下面使用SETGATE将这些地址填充到IDT中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* *
* Set up a normal interrupt/trap gate descriptor
* - istrap: 1 for a trap (= exception) gate, 0 for an interrupt gate
* - sel: Code segment selector for interrupt/trap handler
* - off: Offset in code segment for interrupt/trap handler
* - dpl: Descriptor Privilege Level - the privilege level required
* for software to invoke this interrupt/trap gate explicitly
* using an int instruction.
* */
#define SETGATE(gate, istrap, sel, off, dpl) { \
(gate).gd_off_15_0 = (uint32_t)(off) & 0xffff; \
(gate).gd_ss = (sel); \
(gate).gd_args = 0; \
(gate).gd_rsv1 = 0; \
(gate).gd_type = (istrap) ? STS_TG32 : STS_IG32; \
(gate).gd_s = 0; \
(gate).gd_dpl = (dpl); \
(gate).gd_p = 1; \
(gate).gd_off_31_16 = (uint32_t)(off) >> 16; \
}

注意中断门的DPL是0,陷阱门的DPL是3

由上可以初始化IDT

1
2
3
4
5
6
7
8
9
10
11
12
void
idt_init(void) {
extern uintptr_t __vectors[];
int i;
for (i = 0; i < 256; i ++) {
//0为中断门,GD_KTEXT为系统段选择子,DPL_KERNEL=0
SETGATE(idt[i], 0, GD_KTEXT, __vectors[i], DPL_KERNEL);
}
SETGATE(idt[T_SWITCH_TOK], 0, GD_KTEXT, __vectors[T_SWITCH_TOK], DPL_USER);
// 加载LDT
lidt(&idt_pd);
}

4、通过中断实现在内核态和用户态的切换

需要注意的是,发生中断时,有一部分压栈操作是由硬件参与的,而且根据是否存在特权级的切换,压入不同的参数。

所以我们重新梳理下发生中断后的操作。发生中断后,若发生特权级的切换(如从用户态到内核态),将SS、ESP依次压入堆栈。接着压入EFLAGS、CS、EIP、err_code,上述这些压栈操作是由硬件实现。接着CPU根据中断号在IDT中找到相应的表项,将中断号压栈后,跳转到 __alltraps__alltraps将原来的段寄存器全部压栈,并将当前ESP作为第一个参数调用trap函数,tap函数将参数解析成trapframe结构体,根据不同的中断号做进一步处理。trap执行完后会调用__trapret,将栈上的数值弹回寄存器,硬件压栈的参数由iret弹回。所以当所有参数存在栈上时进行修改,在退出中断时,相应的寄存器的值就会修改,从而完成用户态和内核态的切换

实现内核态到用户态的切换:

此时不存在特权级的切换,发生中断时当前栈空间如下(从上到下为低地址到高地址)

1
2
3
4
5
6
7
8
9
10
uint16_t tf_gs;
uint16_t tf_fs;
uint16_t tf_es;
uint16_t tf_ds;
uint32_t tf_trapno;
/* below here defined by x86 hardware */
uint32_t tf_err;
uintptr_t tf_eip;
uint16_t tf_cs;
uint32_t tf_eflags;

中断服务例程代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
case T_SWITCH_TOU:
if (tf->tf_cs != USER_CS) {
//当前在内核态,需要建立切换到用户态所需的trapframe结构的数据switchk2u,即修改栈上的段寄存器为用户态段寄存器
switchk2u = *tf;
switchk2u.tf_cs = USER_CS;
switchk2u.tf_ds = switchk2u.tf_es = switchk2u.tf_ss = USER_DS;
switchk2u.tf_esp = (uint32_t)tf + sizeof(struct trapframe) - 8;
//设置EFLAG的I/O特权位,使得在用户态可使用in/out指令
switchk2u.tf_eflags |= (3 << 12);
//设置临时栈,指向switchk2u,这样iret返回时,CPU会从switchk2u恢复数据,
//而不是从现有栈恢复数据。
*((uint32_t *)tf - 1) = (uint32_t)&switchk2u;
}

实现用户态到内核态的切换:

此时发生了特权级的变化,发生中断时当前栈空间如下(从上到下为低地址到高地址)

1
2
3
4
5
6
7
8
9
10
11
12
uint16_t tf_gs;
uint16_t tf_fs;
uint16_t tf_es;
uint16_t tf_ds;
uint32_t tf_trapno;
/* below here defined by x86 hardware */
uint32_t tf_err;
uintptr_t tf_eip;
uint16_t tf_cs;
uint32_t tf_eflags;
uintptr_t tf_esp;
uint16_t tf_ss;

中断服务例程代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
case T_SWITCH_TOK:
if (tf->tf_cs != KERNEL_CS) {
//发出中断时,CPU处于用户态,我们希望处理完此中断后,CPU继续在内核态运行,
//修改栈上的段寄存器值为内核代码段和内核数据段
tf->tf_cs = KERNEL_CS;
tf->tf_ds = tf->tf_es = KERNEL_DS;
//设置EFLAGS,让用户态不能执行in/out指令
tf->tf_eflags &= ~(3 << 12);
switchu2k = (struct trapframe *)(tf->tf_esp - (sizeof(struct trapframe) - 8));
//设置临时栈,指向switchu2k,这样iret返回时,CPU会从switchu2k恢复数据,
//而不是从现有栈恢复数据。
memmove(switchu2k, tf, sizeof(struct trapframe) - 8);
*((uint32_t *)tf - 1) = (uint32_t)switchu2k;
}

最终演示结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
0: @ring 0
0: cs = 8
0: ds = 10
0: es = 10
0: ss = 10
+++ switch to user mode +++
1: @ring 3
1: cs = 1b
1: ds = 23
1: es = 23
1: ss = 23
+++ switch to kernel mode +++
2: @ring 0
2: cs = 8
2: ds = 10
2: es = 10
2: ss = 10

mineucore-x86-32系统启动流程

将之前的记得东西重新整理成笔记,去年10月份梳理了物理内存管理和虚拟内存管理。

1、CPU加电

CPU加电后,初始化CS与IP寄存器为约定的值,CS为0xf000,IP为0xfff0。为了兼容8086,此时CPU处于16位的实模式,所以寄存器为16位,通过代码段寄存器CS和指令指针寄存器IP,计算PC的值,计算方法为PC = CS<<4 +IP,这是一个20位的地址,所以实模式下CPU的寻址空间为1MB,计算得到的PC = 0xFFFF0,这就是CPU执行的第一条指令,该指令是一个长跳转指令,jmp F000:E05B,跳转到BIOS程序起始点,实际地址是Base+EIP = FFFF0000 + 0000FFF0 = FFFFFFF0。

2、BIOS

1、BIOS首先进行硬件自检
2、读取主引导记录(MBR)。当BIOS检查到硬件正常并与CMOS中的设置相符后,按照CMOS中对启动设备的设置顺序检测可用的启动设备。BIOS将相应启动设备的第一个扇区读入内存地址为0000:7C00H处。
3、BIOS检查MBR扇区的结束标志是不是0x55AA,若满足要求,则移交控制权给MBR。

系统的启动规范:

BIOS:BIOS-MBR、BIOS-GPT(全局唯一标识分区表)、PXE
UEFI:统一可扩展固件接口,比BIOS加入了堆引导记录的可信性进行了检查等

BIOS功能:(Basic Input Output System,即基本输入/输出系统)固话在主板上的程序
以中断调用的方式实现基本的I/O功能。
INT 10h:字符显示
INT 13h:磁盘扇区读写
INT 15h:检测内存大小
INT 16h;磁盘输入
(在intel的CPU中,BIOS只能在x86的实模式下工作)

Q1:为什么不在BIOS中直接加载操作系统?
磁盘存在文件系统,BIOS不可能识别所有文件系统,所以BIOS不管磁盘的文件系统的类型,直接从磁盘的第一个扇区读取加载程序,用加载程序识别磁盘的文件系统类型,将操作系统加载进内存

3、MBR主引导记录

MBR将自己复制到0000:0600H处,然后执行446字节(一共512字节)的启动代码
扫描分区表查找活动分区;
寻找活动分区的起始扇区;
将活动分区的引导扇区读到内存;
执行引导扇区的运行代码

4、活动分区的引导扇区

活动分区的引导扇区第一条是跳转指令,跳转到引导代码,执行加载程序
(活动分区是指磁盘主分区的一种状态。标记分区为活动分区的目的是为了让操作系统由该区启动,所以通常将操作系统的分区标记为活动分区)

5、bootloader

1、开启A20
2、初始化GDT
3、使能保护模式(分段机制在保护模式下自动使能)
4、读取配置文件,从硬盘上读取kernel,并放到内存中的固定位置
5、跳转到kernel的入口点entry point执行

6、开启A20地址线

在i8086时代,CPU的地址总线为16位,数据总线为20位,最大寻址空间为1MB,但是到了i80286/i80386,CPU的地址总线变成了32位,寻址空间为4GB,但是为了兼容i8086,CPU在加电启动时仍然是20位寻址,此时为实模式,CPU通过A20地址线模块,在实模式下将第20位的地址线限制为0,这样CPU就不能访问超过1MB的空间了。bootloader要从实模式切换到保护模式,获得更大的寻址空间,就要开启A20地址线,获得高位地址线。事实上,A20就是第21根线,用来控制是否允许对 0x10FFEF 以上的实际内存寻址。称为A20 Gate。

7、初始化GDT(全局描述符表)

使能了A20地址线,有了更大的内存空间,就需要开启分段式内存管理。GDT表由bootloader创建,每一项为段描述符,段描述符中描述了该段的起始地址base和大小limit等数据。在没有启动页机制的情况下,线性地址等于物理地址

image-20200221104935105

到了分段式内存,对内存的访问就需要知道在哪个段,以及偏移量。CS、SS等段寄存器保存的有段选择子,段选择子包含了该段在GDT中的下标,进而找到该段对应的段描述符,通过base加上偏移进行寻址。

image-20200221105607933

8、使能保护模式

开启A20、初始化GDT后,需要使能保护模式,将CR0寄存器的PE位(bit0)置为1即可使能保护模式。

1
2
3
movl %cr0, %eax
orl 0x1, %eax
movl %eax, %cr0

inlineHookS

指令修复

采用跳板的方式跳回原指令,因为arm架构的三级流水线设计,在跳回原指令的跳板中,需要有备份的2条指令,如果这两条指令涉及PC的操作,那么就需要对指令修复

B BL BX BLX指令修复

针对如下指令情况进行修复:

1
2
3
4
B imme24
BL imme24
BX PC
BLX imme24

imme24表示有符号的26位整数,因为arm32地址是4字节对齐,所以后2bit为0,因此用24bit表示26bit数据。

首先是计算跳转的真正地址,imme24是相对PC值的地址,PC是32位有符号的整数,因此要想计算真正的地址,需要将26bit数据符号扩充到32位,计算后的值要用有符号整型存储,具体操作如下:

1
2
3
4
5
6
7
int imme32;
int value;
x = (instruction & 0xFFFFFF); //取指令中的24bit立即数
topBit = x >> 23;
imme32 = topBit ? ((x << 2) | 0xFC000000) : x<<2;
value = imme32 + pc;
对于BX PC的情况,value = pc

BL、BLX链接跳转后会通过LR寄存器返回,所以修复方案是将跳转改成LDR PC的形式,并手动修改LR寄存器:

1
2
3
4
5
ADD LR, PC, #4
LDR PC, [PC, #-4]
value
LDR PC,[PC,#-4]
backAddr

LDR

针对以下指令情况修复:(这里没见过 LDR Rd,[Rm,PC] 的情况)

1
2
LDR Rd,[PC,Rm]
LDR Rd,[PC,#imme]

LDR命令第23 bit位,U标志位表示加减,0减1加。修复的主要思路是用一个临时寄存器r来保存PC的值,r不能是Rd或者Rm,将上述指令修改为:

1
2
LDR Rd,[r,Rm]
LDR Rd,[r,#imme]

具体如下:

1
2
3
4
5
6
7
8
PUSH {r}
LDR r,[PC,#8] ;r为PC的值
LDR Rd,[r,Rm] ;将LDR Rd,[PC,Rm]中的PC替换
POP {r}
ADD PC,PC,#0
pc
LDR PC,[PC,#-4]
backAddr

ADD PC,PC,#0 对PC的操作会清空三级流水线,那么PC就会跳过正在取码,译码的指令,从 LDR PC,[PC,#-4] 开始取码

ADD、MOV

针对以下指令类型修复:

1
2
3
ADD Rd,PC,Rm
ADD Rd,PC
MOV Rd,PC

修复方法和LDR类似,将PC用另外一个寄存器r替换:

1
2
3
ADD Rd,r,Rm
ADD Rd,r
MOV Rd,r

ADR

回调函数

hook函数的参数

在shellcode中,保存上下文,将通用寄存器、CPSR、LR、SP保存到栈上,并将sp赋值给R0,然后回调beforeHook函数,参数用 struct pt_regs *类型接收,获得和修改函数参数

1
2
3
4
mov     r0, sp
ldr r1,[sp,#0x38]
ldr r3, _new_function_addr_s
blx r3

hook函数返回值

很难确定函数返回时PC的值,所以通过在beforeHook回调函数中获得LR的值,对LR所在的指令进行hook,通过R0获得和修改函数返回值。

获得函数符号地址

android7后,系统限制了动态链接库的操作,只能链接公有库。快速方便的获得函数符号地址,可以将动态链接库映射到内存中,手动解析符号表,获得函数的地址。

patch libc.so

为了实现hook任意动态链接库中的函数,需要把生成的so注入到目标进程。

__attribute__((constructor)) 修饰的函数会出现在动态链接库的 .init_array节区,.init_array节区中的函数指针在动态链接库被加载时就会运行。那么可以patch libc.so的 .init_array节区,使加载libc.so时,自动加载hookso。

将这段shellcode指令放在.rodata段,修改 .init_array节区的第一个函数指针,使之指向shellcode指令,并在shellcode指令最后,调用原函数指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
STMFD	SP!,{R0-R8,LR}
LDR R0,[PC,#24] ;R0 = 0xC
MOV R1,#0
ADD R0,PC,R0
;dlopen("soName",0)
; dlopenAddr = (dlopenOff - PC)>>2 = (dlopenOff - (shellcodeOff+0x18)) >>2
BL dlopenAddr
LDMFD SP!,{R0-R8,LR}
LDR R0,[PC,#8]
ADD R0,PC,R0
BX R0
soNameOff - shellcodeOff - 5*4
oldInitAddr - shellcodeOff - 9*4
0x0

I highly doubt it. The zygote process will have already loaded libc at boot time, and app processes are forked off of the zygote. “it will be loaded automatically in every app” – no, it is automatically loaded by the zygote


内存壳学习

内存壳的保护方式主要是通过动态加载的方式保护源dex。

DexShellS的主要功能是解密释放源dex,并移交控制权,开始源dex的生命周期。

1、重写attachBaseContext,替换LoadedApk中的mClassLoader

加载App的classloader被保存在LoadedApk类的mClassLoader属性中,所以首先要找到这个mClassLoader。每一个App都有的ActivityThread对象,可以通过静态方法currentActivityThread获得,ActivityThread类有一个ArrayMap类型的变量mPackages,保存了packageName到LoadedApk对象的映射

1
2
3
4
5
6
7
8
Object currentActivityThread = RefInvoke.invokeStaticMethod("android.app.ActivityThread",
"currentActivityThread",
new Class[]{},new Object[]{});
ArrayMap mPackages = (ArrayMap)RefInvoke.getFieldObject("android.app.ActivityThread",
currentActivityThread,
"mPackages");
String packageName = this.getPackageName();
WeakReference wr = (WeakReference) mPackages.get(packageName);//get LoadedApk

获得LoadedApk进而可以获得源classloader,源classloader保存了原先加载的资源,由于双亲委派机制,我们不能放弃源classloader

1
ClassLoader mClassLoader = (ClassLoader)RefInvoke.getFieldObject("android.app.LoadedApk",wr.get(),"mClassLoader");

加载释放后的源dex,并继承于源classloader

1
DexClassLoader dexClassLoader = new DexClassLoader(apkFileName, odexPath, libPath, mClassLoader);

这个新的classloader是通过DexClassLoader加载得到的,并不能拥有onCreate等生命周期,所以还要将其赋值给LoadedApk的mClassLoader属性

1
RefInvoke.setFieldObject("android.app.LoadedApk", "mClassLoader",wr.get(),dexClassLoader);

以上就完成了替换LoadedApk中的mClassLoader的替换。因为application等属性还是DexShellS的,所以此时加载的源Dex还不能拥有正常的生命周期,需要做进一步屏蔽。

2、重写onCreate

因为此时application还是DexShellS的,所以要调用LoadedApk#makeApplication创建源Dex的application,通过查看源码可以知道:

1)LoadedApk的mApplication属性要为空,不然将返回DexShellS的application
2)修改LoadedApk#mApplicationInfo#appClass为源Dex的Applicaiton的Class
3)创建applicaiton后会新添加在mActivityThread.mAllApplications中,所以要删除DexShellS的application

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public Application makeApplication(boolean forceDefaultAppClass,Instrumentation instrumentation) {
if (mApplication != null) {
return mApplication;
}
Application app = null;
String appClass = mApplicationInfo.className;
if (forceDefaultAppClass || (appClass == null)) {
appClass = "android.app.Application";
}
try {
java.lang.ClassLoader cl = getClassLoader();
if (!mPackageName.equals("android")) {
initializeJavaContextClassLoader();
}
ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
app = mActivityThread.mInstrumentation.newApplication(
cl, appClass, appContext);
appContext.setOuterContext(app);
} catch (Exception e) {
......
}
mActivityThread.mAllApplications.add(app);
mApplication = app;
......

return app;
}

根据以上3步,具体代码实现如下:

1)LoadedApk#mApplication==NULL

1
2
3
4
5
6
7
8
//---set LoadedApk#mApplication = null
Object mcurrentActivityThread = RefInvoke.invokeStaticMethod("android.app.ActivityThread",
"currentActivityThread",new Class[]{},new Object[]{});
Object mBoundApplication = RefInvoke.getFieldObject("android.app.ActivityThread",
mcurrentActivityThread,"mBoundApplication");
Object mLoadedApk = RefInvoke.getFieldObject("android.app.ActivityThread$AppBindData",
mBoundApplication,"info");
RefInvoke.setFieldObject("android.app.LoadedApk","mApplication",mLoadedApk,null);

2)修改LoadedApk#mApplicationInfo#className

1
2
3
4
//---set LoadedApk#mApplicationInfo   ActivityThread$AppBindData#appInfo
ApplicationInfo appInfoLoadedApk = (ApplicationInfo) RefInvoke.getFieldObject("android.app.LoadedApk",
mLoadedApk,"mApplicationInfo");
appInfoLoadedApk.className = applicationClassName;

3)修改ActivityThread$AppBindData#appInfo#className

1
2
3
ApplicationInfo appInfoAT$AppBindData = (ApplicationInfo) RefInvoke.getFieldObject("android.app.ActivityThread$AppBindData",
mBoundApplication,"appInfo");
appInfoAT$AppBindData.className = applicationClassName;

4)移除ActivityThread#mAllApplications中DexShellS的application

1
2
3
4
5
6
7
//---remove the old application in the ActivityThread#mAllApplications
Object oldApplication = RefInvoke.getFieldObject("android.app.ActivityThread",
mcurrentActivityThread,
"mInitialApplication");
ArrayList<Application> mAllApplications = (ArrayList<Application>)RefInvoke.getFieldObject("android.app.ActivityThread",
mcurrentActivityThread, "mAllApplications");
mAllApplications.remove(oldApplication);

5)创建新application

1
2
Application app = (Application)RefInvoke.invokeMethod("android.app.LoadedApk","makeApplication",
mLoadedApk,new Class[]{boolean.class, Instrumentation.class},new Object[]{false,null});

6)替换ActivityThread#mInitialApplication为新application

1
2
RefInvoke.setFieldObject("android.app.ActivityThread","mInitialApplication",
mcurrentActivityThread,app);

7)替换mProviderMap中的application

1
2
3
4
5
6
7
8
9
ArrayMap mProviderMap = (ArrayMap) RefInvoke.getFieldObject("android.app.ActivityThread",
mcurrentActivityThread,"mProviderMap");
Iterator iterator = mProviderMap.values().iterator();
while(iterator.hasNext()){
Object providerClientRecord = iterator.next();
Object mLocalProvider = RefInvoke.getFieldObject("android.app.ActivityThread$ProviderClientRecord",
providerClientRecord,"mLocalProvider");
RefInvoke.setFieldObject("android.content.ContentProvider",
"mContext",mLocalProvider,app);

之后新的applicaiton就可以开始新的生命周期了

主要学习了app的启动流程,这种内存壳已经被完全破解了,下面的指令抽取壳也可以通过主动调用类方法去实现脱壳,将核心算法放入so文件,并混淆可以有效加强代码的保护强度,java2c不稳定,和系统版本强相关,现在vmp壳只是对部分函数做虚拟化,比如onCreate


汇编分析一个so文件中的变形RC4和变形base64算法

Prolong

1
2
3
4
5
6
7
8
9
.init_array:00003E78 ; ELF Initialization Function Table
.init_array:00003E78 ; ===========================================================================
.init_array:00003E78
.init_array:00003E78 ; Segment type: Pure data
.init_array:00003E78 AREA .init_array, DATA
.init_array:00003E78 ; ORG 0x3E78
.init_array:00003E78 DCD .datadiv_decode5009363700628197108+1
.init_array:00003E78 ; .init_array ends
.init_array:00003E78

程序在.init_array中重新初始化了一些字段,包括生成RC4的初始密钥K、状态向量S,下面说下K的生成过程:
1) so中原始存储了长度为36数据:
0x93,0x90,0x95,0xC3,0x9C,0x95,0x9C,0xC6,0x88,0x92,0x97,0x94,
0x92,0x88,0x96,0x93,0x91,0x92,0x88,0x9C,0x96,0x96,0x94,0x88
0xC6,0x9D,0x97,0xC1,0xC3,0x9D,0xC7,0x9C,0x9D,0xC0,0x9C,0x9D
2) 经过.init_array重新初始化变成了:
650f909c-7217-3647-9331-c82df8b98e98
3) 经过一系列移位变成了:
89e89b8f-d28c-1339-7463-7127c909f056
4) 然后经过一个映射表abcdef0123456789=>dbeafc2409715836
映射为了最终的K36f36b3c-a03e-4996-8759-8408e626c215

状态向量也进行了重新初始化,下面主要分析:
1、变形的RC4算法
2、变形的base64算法
3、给出解密脚本

一、变形的RC4算法

RC4包含两个向量,状态向量S[256],向量T[256],T是由用户自定义初始密钥K轮转填充到256字节,算法主要包含两部分内容
1、用T置乱重排S
2、用T和明文生成密文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
状态向量S:
uint8_t S[256] = {
0xD7,0xDF, 2,0xD4,0xFE,0x6F,0x53,0x3C,0x25,0x6C,0x99,0x97, 6,0x56,0x8F,0xDE,//; 0
0x40,0x11,0x64, 7,0x36,0x15,0x70,0xCA,0x18,0x17,0x7D,0x6A,0xDB,0x13,0x30,0x37,//; 16
0x29,0x60,0xE1,0x23,0x28,0x8A,0x50,0x8C,0xAC,0x2F,0x88,0x20,0x27, 0xF,0x7C,0x52,//; 32
0xA2,0xAB,0xFC,0xA1,0xCC,0x21,0x14,0x1F,0xC2,0xB2,0x8B,0x2C,0xB0,0x3A,0x66,0x46,//; 48
0x3D,0xBB,0x42,0xA5, 0xC,0x75,0x22,0xD8,0xC3,0x76,0x1E,0x83,0x74,0xF0,0xF6,0x1C,//; 64
0x26,0xD1,0x4F, 0xB,0xFF,0x4C,0x4D,0xC1,0x87, 3,0x5A,0xEE,0xA4,0x5D,0x9E,0xF4,//; 80
0xC8, 0xD,0x62,0x63,0x3E,0x44,0x7B,0xA3,0x68,0x32,0x1B,0xAA,0x2D, 5,0xF3,0xF7,//; 96
0x16,0x61,0x94,0xE0,0xD0,0xD3,0x98,0x69,0x78,0xE9, 0xA,0x65,0x91,0x8E,0x35,0x85,//; 112
0x7A,0x51,0x86,0x10,0x3F,0x7F,0x82,0xDD,0xB5,0x1A,0x95,0xE7,0x43,0xFD,0x9B,0x24,//; 128
0x45,0xEF,0x92,0x5C,0xE4,0x96,0xA9,0x9C,0x55,0x89,0x9A,0xEA,0xF9,0x90,0x5F,0xB8,//; 144
4,0x84,0xCF,0x67,0x93, 0,0xA6,0x39,0xA8,0x4E,0x59,0x31,0x6B,0xAD,0x5E,0x5B,//; 160
0x77,0xB1,0x54,0xDC,0x38,0x41,0xB6,0x47,0x9F,0x73,0xBA,0xF8,0xAE,0xC4,0xBE,0x34,//; 176
1,0x4B,0x2A,0x8D,0xBD,0xC5,0xC6,0xE8,0xAF,0xC9,0xF5,0xCB,0xFB,0xCD,0x79,0xCE,//; 192
0x12,0x71,0xD2,0xFA, 9,0xD5,0xBC,0x58,0x19,0x80,0xDA,0x49,0x1D,0xE6,0x2E,0xE3,//; 208
0x7E,0xB7,0x3B,0xB3,0xA0,0xB9,0xE5,0x57,0x6E,0xD9, 8,0xEB,0xC7,0xED,0x81,0xF1,//; 224
0xF2,0xBF,0xC0,0xA7,0x4A,0xD6,0x2B,0xB4,0x72,0x9D, 0xE,0x6D,0xEC,0x48,0xE2,0x33,//;
}
初始密钥K = 36f36b3c-a03e-4996-8759-8408e626c215

1、用T置乱重排S

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.text:B3E408BA loc_B3E408BA                            ; CODE XREF: sub_B3E40784+15E↓j
.text:B3E408BA LDRB.W R2, [R9,R1] ; S[i], R9是状态向量S的地址 ,R1是相当于算法中的i
.text:B3E408BE LDRB R3, [R4,R1] ; T[i], R4是向量T的地址
.text:B3E408C0 ADD R3, R2 ; T[i] + S[i]
.text:B3E408C2 ADD R0, R3 ; j + T[i] + S[i] ,R0相当于算法中的j
.text:B3E408C4 ASRS R3, R0, #0x1F
.text:B3E408C6 ADD.W R3, R0, R3,LSR#24
.text:B3E408CA BIC.W R3, R3, #0xFF
.text:B3E408CE SUBS R0, R0, R3 ; R0 = R0 % 256
.text:B3E408D0 LDRB.W R3, [R9,R0] ; R3 = S[j]
.text:B3E408D4 STRB.W R3, [R9,R1] ; S[i] = R3
.text:B3E408D8 ADDS R1, #1
.text:B3E408DA CMP.W R1, #0x100
.text:B3E408DE STRB.W R2, [R9,R0] ; S[j] = R2, R2即为原R[i], swap完成
.text:B3E408E2 BNE loc_B3E408BA ; 循环

翻译成C语言算法如下:

1
2
3
4
5
6
7
i = 0;
j = T[0] - 0x29; //j = 10
swap(S[i],S[j]); //swap(S[0],S[10])
for(i = 1;i<256;i++){
j = (j + T[i] + S[i]) % 256;
swap(S[i],S[j]);
}

T[0] - 0x29就是(T[0] + S[0])256,优化一下就是:

1
2
3
4
5
j = 0;
for(i = 0;i<256;i++){
j = (j + T[i] + S[i]) % 256;
swap(S[i],S[j]);
}

2、用状态向量T和明文一次一密生成密文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
.text:B3E409A8 loc_B3E409A8                            ; CODE XREF: sub_B3E40784+1B0↑j
.text:B3E409A8 ; sub_B3E40784+2B4↓j
.text:B3E409A8 ADD.W R1, R10, #1 ; R10=算法中的i
.text:B3E409AC CMP R2, #0
.text:B3E409AE MOV.W R3, R1,ASR#31
.text:B3E409B2 ADD.W R3, R1, R3,LSR#24
.text:B3E409B6 BIC.W R3, R3, #0xFF ; 将R3低8位清零,0xff = 255
.text:B3E409BA SUB.W R10, R1, R3 ; 上面计算了R10 = (R10+1)%256
.text:B3E409BE LDRB.W R1, [R9,R10] ; R1 = S[R10],R9 = 状态向量S的地址
.text:B3E409C2 ADD.W R3, R12, R1 ; R12=算法中的j
.text:B3E409C6 MOV.W R4, R3,ASR#31
.text:B3E409CA ADD.W R4, R3, R4,LSR#24
.text:B3E409CE BIC.W R4, R4, #0xFF
.text:B3E409D2 SUB.W R12, R3, R4 ; 上面计算了R12 = (R12+R1)%256
.text:B3E409D6 LDRB.W R3, [R9,R12] ; 开始对S数组swap
.text:B3E409DA STRB.W R3, [R9,R10] ; S[i] = S[j]
.text:B3E409DE STRB.W R1, [R9,R12] ; S[j] = 原s[i]
.text:B3E409E2 LDRB.W R4, [R9,R10] ; R4 = swap后的S[i]
.text:B3E409E6 LDR R3, [SP,#0x238+s] ; R3 = 明文地址
.text:B3E409E8 ADD R1, R4 ; R1是原S[i]
.text:B3E409EA LDRB R3, [R3,R2] ; R3 = s[R2],取待加密明文字符
.text:B3E409EC UXTB R1, R1 ; 高24位清零,相当于模256
.text:B3E409EE LDRB.W R1, [R9,R1] ; R1 = S[ (S[i] + S[j]) % 256 ]
.text:B3E409F2 EOR.W R11, R1, R3 ; 异或,RC4结果保存在R11
.text:B3E409F6 BEQ loc_B3E40A0E ; 若字符索引index=0,则开始处理第一个字符

RC4部分对应的C语言算法如下:

1
2
3
4
5
6
7
i = (i + 1)%256;
m = S[i];
j = (j + S[i])%256;
S[i] = S[j];
S[j] = m;
n = S[i];
C = S[ (m+n) % 256 ] ^ s[index];

3、变形的RC4算法总结

从上面分析,可以知道该算法与常规RC4算法的不同:
1、使用自定义的初始密钥K
2、使用自定义的状态向量S


二、变形的base64算法

base64算法中,3个字符为一组被编码成4个base64字符,假设待编码字符索引为index,某一组的三个字符为x0、x1、x2,该组base64编码结果对应的字符为y0、y1、y2、y3。

这里需要判断当前处理的index是这一组中的第几个字符,条件为:

1
2
.text:B3E409AC CMP             R2, #0
.text:B3E409F6 BEQ loc_B3E40A0E
1
2
3
4
5
6
7
.text:B3E409F8 MOV             R1, #0xAAAAAAAB
.text:B3E40A00 UMULL.W R1, R3, R2, R1
.text:B3E40A04 LSRS R1, R3, #1 ; R1 = R1 / 3
.text:B3E40A06 ADD.W LR, R1, R1,LSL#1 ; LR = R1 + R1*2 即LR = 3*(R2/3)
.text:B3E40A0A CMP LR, R2
.text:B3E40A0C BNE loc_B3E40936 ; 若index!=0 && index%3!=0
.text:B3E40A0E loc_B3E40A0E

上面为两个条件,整合到一起就是

1
2
3
4
if(index!=0 && 3*(index/3)!=index)
goto loc_B3E40936;
else
goto loc_B3E40A0E;

这里就判断了当前index,处于小组的位置,是不是x0,若是x0,就去执行loc_B3E40A0E;若是x1、x2就去执行loc_B3E40936

但是执行loc_B3E40936后,还需要判断当前index处于的位置是x1还是x2,就有了下面的判断

1
2
3
4
5
6
7
.text:B3E40936 CMP             R2, #1                  ; 比较index和1
.text:B3E40938 ITT NE ; 不相等执行第1、2条指令
.text:B3E4093A ADDNE.W R1, LR, #1 ; R2!=1, Then R1 = LR + 1
.text:B3E4093E CMPNE R1, R2
.text:B415E940 BNE loc_B415E96A ; 不相等,说明index处于x2的位置,跳转loc_B3E4096A
.text:B415E942 ...
.text:B415E944...
1
2
3
if(3*(index/3)+1 != index)
goto loc_B3E4096A;
...

若3*(index/3)+1 != index,则说明当前index处于x2的位置,否则说明当前index处于x1的位置

1、处理第1个字符,得到第1个base64字符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
.text:B3E40A0E loc_B3E40A0E                            ; 开始处理x0
.text:B3E40A0E LDR R1, =(byte_B3E44050 - 0xB3E40A1C) ; R1 = base64字符表地址相对PC的差值
.text:B3E40A10 UXTB.W R3, R11 ; 将x0低8位,无符号扩展到32位,即高24位清零,保存到R3
.text:B3E40A14 LSRS R4, R3, #2 ; 逻辑右移2位,取高6位
.text:B3E40A16 LDR R6, [SP,#0x238+var_228] ; R6 = N,这里N是K[3] = 0x33
.text:B3E40A18 ADD R1, PC ; byte_B3E44050 ; R1=base64字符表地址
.text:B3E40A1A ADD R6, R2 ; R6 = N + index,R2是字符索引index
.text:B3E40A1C LDRB R4, [R1,R4] ; 取出第一个索引对应的base64字符,设为y0'
.text:B3E40A1E EOR.W R4, R4, #7 ; y0 = y0' ^ 7
.text:B3E40A22 STRB R4, [R0,R6] ; 保存第一个base64字符y0,R0为malloc的buf地址
.text:B3E40A24 ADDS R4, R0, R6 ; 取buf[N + index]的地址
.text:B3E40A26 MOVS R6, #0x30 ; '0'
.text:B3E40A28 AND.W R3, R6, R3,LSL#4 ; R3 = (R3<<4)& 00110000b
.text:B3E40A28 ; 即取x0低2位值
.text:B3E40A2C ADDS R6, R2, #1 ; index++
.text:B3E40A2E STRB R3, [R4,#1] ; 在buf[N + index + 1]写入x0低2位,将作为第二个base64字符y1的索引的高2位
.text:B3E40A30 CMP R6, R8 ; 判断明文字符索引index+1是否大于等于明文字符长度
.text:B3E40A30 ; 若相等,接下来就处理最后一个明文字符
.text:B3E40A32 BCS loc_B3E40A3C ; 若无符号index > len(s),则跳转,处理最后一个字符
.text:B3E40A34
.text:B3E40A34 loc_B3E40A34 ; 否则
.text:B3E40A34 ADDS R2, #1
.text:B3E40A36 CMP R2, R8
.text:B3E40A38 BCC loc_B3E409A8 ; 若index<=len(s),跳转loc_B3E409A8,继续计算下一个base64字符

初始密钥 K = 36f36b3c-a03e-4996-8759-8408e626c215

该部分算法总结:
1、取RC4加密结果C的低8位,然后取该值的高6位作为base64字符表索引
2、根据字符索引,查字符表,得到第一个base64字符,设为y0’
3、y0 = y0’ ^ 7
4、N = K[3]
5、将b写入buf[ N + index ]
6、将低2位写入buf[ N + index +1 ]
这里比常规的base64编码算法多了第三步

指令总结:
1、UXTB指令,无符号扩展到32位,即高24位清零 http://www.keil.com/support/man/docs/armasm/armasm_dom1361289924987.htm
2、取一个字符A的低2位,将其作为下一个字符索引的高2位,保存到B中 B = (A<<4) & 00110000b
3、CMP R1,R2 BCS label,无符号R1<R2,则跳转label
4、CMP R1,R2 BCC label,无符号R1>=R2,则跳转label

1
2
3
4
5
6
7
8
9
10
11
12
13
else
{
v26[v44 + v29] = byte_4050[(unsigned int)v36 >> 2] ^ 7;// 右移两位,得到第一个base64字符索引
v17 = (unsigned int)&v26[v44 + v29];
v27 = 16 * v36 & 0x30; // 获取索引2的高2位
*(_BYTE *)(v17 + 1) = v27; // v26[v44 + v29 +1] = 16 * v36 & 0x30
if ( v29 + 1 >= v24 )
{
v38 = byte_4050[v27];
*(_WORD *)(v17 + 2) = 0x3B3B;
goto LABEL_43;
}
}

2、处理第2个字符,得到第2个base64字符

下面是处理x1的过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
.text:B3E40936 loc_B3E40936                            ; CODE XREF: sub_B3E40784+288↓j
.text:B3E40936 CMP R2, #1 ; 比较index和1
.text:B3E40938 ITT NE ; 不相等执行第1、2条指令
.text:B3E4093A ADDNE.W R1, LR, #1 ; R2!=1, Then R1 = LR + 1
.text:B3E4093E CMPNE R1, R2
.text:B3E40940 BNE loc_B3E4096A
.text:B3E40942 LDR R1, [SP,#0x238+var_228] ; R1 = N
.text:B3E40944 UXTB.W R6, R11 ; R11=x1无符号扩展到32 位(高24位清0
.text:B3E40948 ADDS R3, R1, R2 ; R3 = N + index
.text:B3E4094A LDRB R4, [R0,R3] ; 取第二个base64字符,真正保存的是x0低2位
.text:B3E4094A ; R0为malloc的buf地址
.text:B3E4094C LDR R1, =(byte_B3E44050 - 0xB3E40952)
.text:B3E4094E ADD R1, PC ; byte_B3E44050 ; R1为base64字符表地址
.text:B3E40950 ORR.W R4, R4, R6,LSR#4 ; 1、取x1高4位
.text:B3E40950 ; 2、取x0低2位和上面数值进行与操作,得到一个新的6位值,作为第二个索引
.text:B3E40954 LDRB R4, [R1,R4] ; 查表,取第二个索引值对应的base64字符
.text:B3E40956 STRB R4, [R0,R3] ; 保存结果到buf[N + index]
.text:B3E40958 ADDS R4, R0, R3 ; R4 = buf[N + index]的地址
.text:B3E4095A MOVS R3, #0x3C ; '<' ; R3 = 00111100b
.text:B3E4095C AND.W R3, R3, R6,LSL#2 ; 将x1逻辑左移2位&00111100b
.text:B3E4095C ; 将x1低四位,后面作为第3个base64字符索引的高四位
.text:B3E40960 ADDS R6, R2, #1 ; index++
.text:B3E40962 CMP R6, R8
.text:B3E40964 STRB R3, [R4,#1] ; 将y2索引的高四位写入buf[N + index + 1]
.text:B3E40966 BCC loc_B3E40A34 ; 若R6<=R8,即index + 1 < len(s),跳转loc_B3E40A34,继续计算x2
.text:B3E40968 B loc_B3E40A82 ; index + 1 < len(s),当前index为最后一个字符,处理后续base64字符

该部分算法总结:
1、取x0低2位与x2的高4位,组成新的6位数值,作为base64字符表索引
2、根据字符索引,查字符表,得到第二个base64字符,设为y1
3、N = K[3]
4、将y1写入buf[ N + index ]
5、将x1低4位写入buf[ N + index +1 ]

指令总结:

1、IT(If-Then)指令:

1
2
3
4
5
.text:B3E40936 loc_B3E40936                            ; CODE XREF: sub_B3E40784+288↓j
.text:B3E40936 CMP R2, #1 ; 比较index和1
.text:B3E40938 ITT NE ; 不相等执行第1、2条指令
.text:B3E4093A ADDNE.W R1, LR, #1 ; R2!=1, Then R1 = LR + 1
.text:B3E4093E CMPNE R1, R2

IT指令用于根据特定条件来执行紧随其后的1~4条指令,其格式为:IT{x{y{z}}} {cond} 。其中x、y、z分别是执行第二、三、四条指令的条件,可取的值为T(then)或E(else),若为T,则执行cond对应的指令;若为E,则执行cond相反条件的指令。而cond对应执行第一条指令的条件。参考

2、 BCC

1
2
CMP A,B
BCC labal

A<B,则跳转到label

3、处理第3个字符,得到第3、4个base64字符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.text:B415E96A loc_B415E96A                            ; CODE XREF: sub_B415E784+1BC↑j
.text:B415E96A CMP R2, #2
.text:B415E96C ITT NE
.text:B415E96E ADDNE.W R1, LR, #2
.text:B415E972 CMPNE R1, R2
.text:B415E974 BNE loc_B415EA34
.text:B415E976 LDR R1, =(byte_B4162050 - 0xB415E984)
.text:B415E978 AND.W R4, R11, #0xC0 ; 取x2的高2位,与x1的低4位可以组成一个6位base64字符索引
.text:B415E97C LDR.W LR, [SP,#0x238+var_228] ; LR = N
.text:B415E980 ADD R1, PC ; byte_B4162050 ; R1是base64字符表地址
.text:B415E982 ADD.W R3, LR, R2 ; R3 = N + index
.text:B415E986 ADD.W LR, LR, #1 ; LR++
.text:B415E98A STR.W LR, [SP,#0x238+var_228] ; N++
.text:B415E98E LDRB R6, [R0,R3] ; R0为buf的地址,取出x1的低四位
.text:B415E990 ORR.W R6, R6, R4,LSR#6 ; 组成新的6位base64字符索引
.text:B415E994 LDRB R6, [R1,R6] ; 查表,取出base64字符
.text:B415E996 EOR.W R6, R6, #0xF ; 取出的base64字符异或0xF
.text:B415E99A STRB R6, [R0,R3] ; 写入buf
.text:B415E99C AND.W R6, R11, #0x3F ; 取x2低6位
.text:B415E9A0 ADD R3, R0 ; 取当前buf地址
.text:B415E9A2 LDRB R1, [R1,R6] ; 根据x2的低6位,查表,取第4个base64字符
.text:B415E9A4 STRB R1, [R3,#1] ; 写入buf的下一个字节
.text:B415E9A6 B loc_B415EA34

这里再次判断了index在组的位置(感觉是多余的
根据注释可以清楚的看到,生成了第3、4个base64字符,其中和常规base64算法不同的是,第3个base64字符与0xF进行了异或处理

到此第一组3个字符,生成了4个base64字符。

4、strlen(s) % 3 = 1时的处理

当base64处理的字符串长度不能被3整除,会有其他的处理

常规的base64算法的处理方式是直接补零查表

而这个变形的base64算法有点不同

我们从当剩余字符为x0时的处理方式开始分析,从上面的跳转分析,可以知道该段代码在loc_00000A3C

1
2
3
4
5
6
7
.text:B4052A3C loc_B4052A3C                            ; CODE XREF: sub_B4052784+2AE↑j
.text:B4052A3C LDRB R1, [R1,R3] ; R1 = base64字符表,将x0的低2位左移4位,当做base64字符表索引,得到y1
.text:B4052A3E MOVW R2, #0x3B3B
.text:B4052A42 STRH R2, [R4,#2] ; R4为buf地址,y2=y3=0x3b
.text:B4052A44
.text:B4052A44 loc_B4052A44 ; CODE XREF: sub_B4052784+304↓j
.text:B4052A44 STRB R1, [R4,#1] ; 写入y1

用常规base64算法生成y0、y1后,不同的是,该变形算法在后面追加了0x3B3B,为“;;”,想当于base64算法中的==

5、strlen(s) % 3 = 2时的处理

当剩余字符为x1时的处理方式的分析,从上面的跳转分析,可以知道该段代码在loc_00000A82

1
2
3
4
5
6
7
8
.text:B3A1EA82 loc_B3A1EA82                            
.text:B3A1EA82 LDRB R1, [R1,R3] ; R1为base64字符表,R3位x1低4位再左移2位的值,即y2的base64字符索引
.text:B3A1EA84 MOVS R2, #0x34 ; '4'
.text:B3A1EA86 STRB R2, [R4,#2] ; y3 = 0x34
.text:B3A1EA88 B loc_B3A1EA44

.text:B4052A44 loc_B4052A44
.text:B4052A44 STRB R1, [R4,#1] ; 写入y2

用常规base64算法生成y0、y1、y2后,不同的是,该变形算法在后面追加了0x34

至此变形base64算法分析完成

6、变形base64算法总结

设一组待编码的字符长度为L,生成编码字符为y0、y1、y2、y3

与传统base64算法不同的是:
1、字符表变成 !:#$%&()+-*/`~_[]{}?<>,.@^abcdefghijklmnopqrstuvwxyz0123456789\‘;”
2、y0 = y0 ^ 0x7
3、y2 = y2 ^ 0xF
4、若L % 3 = 2,y3 = 0x34

三、解密代码

最后处理的结果会与 {9*8ga*l!Tn?@#fj'j",0x24,"\g;; 对比

由以上分析,我们就可以写出解密代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#!/usr/bin/python
# -*- coding: UTF-8 -*-
import string
import binascii
RC4_key = "36f36b3c-a03e-4996-8759-8408e626c215"
base64_encoded = " {9*8ga*l!Tn?@#fj'j$\\g;;"
base64_table = "!:#$%&()+-*/`~_[]{}?<>,.@^abcdefghijklmnopqrstuvwxyz0123456789\\';";
base64_table_ori = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";

RC4_S = [0xD7,0xDF, 2,0xD4,0xFE,0x6F,0x53,0x3C,0x25,0x6C,0x99,0x97, 6,0x56,0x8F,0xDE,
0x40,0x11,0x64, 7,0x36,0x15,0x70,0xCA,0x18,0x17,0x7D,0x6A,0xDB,0x13,0x30,0x37,
0x29,0x60,0xE1,0x23,0x28,0x8A,0x50,0x8C,0xAC,0x2F,0x88,0x20,0x27, 0xF,0x7C,0x52,
0xA2,0xAB,0xFC,0xA1,0xCC,0x21,0x14,0x1F,0xC2,0xB2,0x8B,0x2C,0xB0,0x3A,0x66,0x46,
0x3D,0xBB,0x42,0xA5, 0xC,0x75,0x22,0xD8,0xC3,0x76,0x1E,0x83,0x74,0xF0,0xF6,0x1C,
0x26,0xD1,0x4F, 0xB,0xFF,0x4C,0x4D,0xC1,0x87, 3,0x5A,0xEE,0xA4,0x5D,0x9E,0xF4,
0xC8, 0xD,0x62,0x63,0x3E,0x44,0x7B,0xA3,0x68,0x32,0x1B,0xAA,0x2D, 5,0xF3,0xF7,
0x16,0x61,0x94,0xE0,0xD0,0xD3,0x98,0x69,0x78,0xE9, 0xA,0x65,0x91,0x8E,0x35,0x85,
0x7A,0x51,0x86,0x10,0x3F,0x7F,0x82,0xDD,0xB5,0x1A,0x95,0xE7,0x43,0xFD,0x9B,0x24,
0x45,0xEF,0x92,0x5C,0xE4,0x96,0xA9,0x9C,0x55,0x89,0x9A,0xEA,0xF9,0x90,0x5F,0xB8,
4,0x84,0xCF,0x67,0x93, 0,0xA6,0x39,0xA8,0x4E,0x59,0x31,0x6B,0xAD,0x5E,0x5B,
0x77,0xB1,0x54,0xDC,0x38,0x41,0xB6,0x47,0x9F,0x73,0xBA,0xF8,0xAE,0xC4,0xBE,0x34,
1,0x4B,0x2A,0x8D,0xBD,0xC5,0xC6,0xE8,0xAF,0xC9,0xF5,0xCB,0xFB,0xCD,0x79,0xCE,
0x12,0x71,0xD2,0xFA, 9,0xD5,0xBC,0x58,0x19,0x80,0xDA,0x49,0x1D,0xE6,0x2E,0xE3,
0x7E,0xB7,0x3B,0xB3,0xA0,0xB9,0xE5,0x57,0x6E,0xD9, 8,0xEB,0xC7,0xED,0x81,0xF1,
0xF2,0xBF,0xC0,0xA7,0x4A,0xD6,0x2B,0xB4,0x72,0x9D, 0xE,0x6D,0xEC,0x48,0xE2,0x33,
]
RC4_T = []
t = ""
tmp = ""
for i in range(len(base64_encoded)):
if base64_encoded[i]==";":
t = base64_encoded[i]
tmp +=t
continue
if i%4==0:
t = chr(ord(base64_encoded[i])^0x7)
elif i%4 == 2:
t = chr(ord(base64_encoded[i])^0xF)
else:
t = base64_encoded[i]
tmp +=t
#print tmp#'{6*?gn*k![n8@,fm'e$[g;;

transtable = string.maketrans(base64_table,base64_table_ori)
transtmp = tmp.translate(transtable)
#print transtmp#/R6KTgnKkAPn8YWfm/eDPg==

base64_decoded = transtmp.decode('base64')
cipher = []
for i in range(len(base64_decoded)):
cipher.append(int(binascii.b2a_hex(base64_decoded[i]),16))

#生成T
for i in range(256):
RC4_T.append(ord(RC4_key[i%len(RC4_key)]))

#用T置乱S
j = RC4_T[0] - 0x29
RC4_S[0],RC4_S[j] = RC4_S[j],RC4_S[0]

for i in range(1,256):
j = (j + RC4_T[i] + RC4_S[i]) % 256
RC4_S[i],RC4_S[j] = RC4_S[j],RC4_S[i]

i = 0
j = 0
res = ""
for k in range(len(cipher)):
i = (i+1)%256
m = RC4_S[i]
j = (j + m)%256
RC4_S[i],RC4_S[j] = RC4_S[j],RC4_S[i]
n = RC4_S[i]
p = RC4_S[(m+n)%256] ^ cipher[k]
res += chr(p)
print res

ubuntu-16.04 源码编译 Android-6.0.0_r1 与调试

一次过还是挺开心的,记录下编译过程

环境:ubuntu-16.04 amd64,源码是Android-6.0.0_r1

1、环境配置

1
2
3
4
5
6
7
sudo apt-get install -y git flex bison gperf build-essential libncurses5-dev:i386 \
libx11-dev:i386 libreadline6-dev:i386 libgl1-mesa-dev g++-multilib \
tofrodos python-markdown libxml2-utils xsltproc \
zlib1g-dev:i386 dpkg-dev libsdl1.2-dev libesd0-dev \
git-core gnupg zip curl zlib1g-dev gcc-multilib \
libc6-dev-i386 lib32ncurses5-dev x11proto-core-dev \
libx11-dev lib32z-dev ccache unzip m4

安装 open JDK7

Ubuntu16.04没有open JDK7的源,增加个仓库源

1
2
3
sudo add-apt-repository ppa:openjdk-r/ppa 
sudo apt-get update
sudo apt-get install openjdk-7-jdk

安装后环境变量已经配置好,java -version查看

有些工具需要JDX8才能运行,比如jadx,可以使用下面的命令切换java版本

1
2
sudo update-alternatives --config java
sudo update-alternatives --config javac

2、获得Android源码

使用repo下载文件很大,因为aosp文件夹下的.repo文件是仓库,.repo以外的文件才是需要的源码文件,.repo很大,使用repo下载的源码有几十个G。可以直接下载别人打包好的镜像文件

https://pan.baidu.com/s/1JYOgTrF6xe3cNwmzxmE5Yw

这样android-6.0.0_r1的大小为4G多

谷歌官方repo和教程
https://source.android.com/setup/build/downloading
当然也可以使用清华的repo镜像
https://mirrors.tuna.tsinghua.edu.cn/help/AOSP/

下载下来是7z压缩文件,需要额外的软件解压

1
2
sudo apt-get install p7zip-full
7z x android-6.0.0_r1.7z

解压后修改art/build/Android.common_build.mk,修改内容如下

1
2
3
4
5
6
7
# Host.
ART_HOST_CLANG := false
- ifneq ($(WITHOUT_HOST_CLANG),true)
+ ifeq ($(WITHOUT_HOST_CLANG),false)
# By default, host builds use clang for better warnings.
ART_HOST_CLANG := true
endif

否则会报unsupported reloc…的错误

If it still not works,try this in your android root path:
cp /usr/bin/ld.gold prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.11-4.6/x86_64-linux/bin/ld

3、编译

在源码根目录下:

1
2
make clobber
source build/envsetup.sh

通过lunch选择编译目标,这里编译aosp_arm-eng,选择1

1
lunch

开始编译

1
make -j8

编译好的文件在out/target/product/generic

启动模拟器

1
emulator -memory 512 -cache-size 2028

若有多个版本

1
2
3
source build/envsetup.sh
lunch
emulator

编译单独的模块

1
mmm art/runtime

重新打包系统镜像

1
make snod

emulator启动参数

-memory size
-cache-size size

https://developer.android.com/studio/run/emulator-commandline?hl=zh-cn

真正使用的命令

1
out/host/linux-x86/bin/emulator -sysdir out/target/product/generic/ -system out/target/product/generic/system.img -ramdisk out/target/product/generic/ramdisk.img -data out/target/product/generic/userdata.img -kernel /home/hzh/oldhome/learn/goldfish/arch/arm/boot/zImage -scale 1.0 -memory 512 -partition-size 1024

关闭ubuntu节能模式

1
sudo pm-powersave false

4、REFERRENCE

https://www.jianshu.com/p/367f0886e62b

http://eternalsakura13.com/2018/02/24/bianyi/


ubuntu16.04 处理wifi问题小记

在实体机上安装ubuntu后,出现了两个问题,一个是ubuntu安装后没有支持笔记本网卡的驱动,通过升级内核解决了;另一个是网卡驱动安装成功后,总是无缘无故掉线,通过关闭wifi的电源管理解决了;期间还卸载了一次双系统中的ubuntu…在此记录一下折腾的内容,方便以后查看。

一、安装网卡驱动

装了ubuntu16.04的双系统,但是没有网卡驱动,网卡型号为realtek 8822be,网上一个简单的方法就是升级linux内核,4.14的内核开始支持该网卡,中间有个些坑,记录一下

下载linux内核文件,手动安装:http://kernel.ubuntu.com/~kernel-ppa/mainline/

但是找不到4.14.0的版本,就下载了4.14.1,以为没有影响,但是安装后,发现无线网还是不能使用,所以通过别人的文件名,拼出来了下载链接,下载了4.14.0的

1
2
3
4
5
http://kernel.ubuntu.com/~kernel-ppa/mainline/v4.14/linux-headers-4.14.0-041400_4.14.0-041400.201711122031_all.deb

http://kernel.ubuntu.com/~kernel-ppa/mainline/v4.14/linux-headers-4.14.0-041400-generic_4.14.0-041400.201711122031_amd64.deb

http://kernel.ubuntu.com/~kernel-ppa/mainline/v4.14/linux-image-4.14.0-041400-generic_4.14.0-041400.201711122031_amd64.deb

安装:

1
sudo dpkg -i *.deb

重启后还是不能使用

然后去/lib/firmware/rtlwifi,发现没有rtl8822be的驱动文件

下载rtl8822be的驱动文件:
https://raw.githubusercontent.com/wkennington/linux-firmware/master/rtlwifi/rtl8822befw.bin

移动到/lib/firmware/rtlwifi目录下,重启后就可以正常使用了

二、关闭wifi的电源管理

wifi时不时会断开,关闭power management解决

一次性手动解决:

1
sudo iwconfig wlp5s0 power off

永久解决:

1
sudo vim /etc/NetworkManager/conf.d/default-wifi-powersave-on.conf

将wifi.powersave = 3修改为wifi.powersave = 2
数值解释:

1
2
3
4
NM_SETTING_WIRELESS_POWERSAVE_DEFAULT (0): use the default value
NM_SETTING_WIRELESS_POWERSAVE_IGNORE (1): don't touch existing setting
NM_SETTING_WIRELESS_POWERSAVE_DISABLE (2): disable powersave
NM_SETTING_WIRELESS_POWERSAVE_ENABLE (3): enable powersave

三、卸载双系统

通过给windows EFI启动分区分配盘符,删除其中的ubuntu引导,具体操作如下:

以管理员身份打开cmd,输入diskpart

1
2
3
4
5
list disk						;查看磁盘
select disk 1
list partition ;查看分区
select partition 1 ;启动分区一般为200多M
assign letter=F

cd进去,删除/EFI/ubuntu文件夹

1
remove letter=F	;删除原先分配的盘符

然后在磁盘管理,删除ubuntu的硬盘

备注:pppop连接命令:

1
2
3
sudo pppoeconf
sudo pon dsl-provider
sudo poff

FUPK源码阅读

1、获得完整的DEX

frameworks/base/core/java/android/app/ActivityThread.javahandleBindApplication处插桩

1
2
3
4
5
6
7
8
try {
UpkConfig config = new UpkConfig();
if (config.load() && config.mTargetPackage.equals(data.info.getPackageName())) {
Fupk upk = new Fupk(config.mTargetPackage);
upk.unpackAfter(10000);
}
} catch (Throwable t) {
}

创建Fupk的实例,创建实例过程加载so文件,然后调用其unpackAfter函数,其中调用unpackNow初始化其类变量appLoader,最后调用native函数core_Fupk::unpackAll

1
2
3
4
5
public final static String hookSo = "/data/local/tmp/libFupk3.so";

static {
System.load(Global.hookSo);
}

该so文件存在JNI_OnLoad函数,在app/src/main/cpp/main.cpp中,主要做了两部分内容:1、动态注册native函数core_Fupk::unpackAll;2、通过dlopen、dlsym函数获得libdvm.so中的接口(函数指针)

1
core_Fupk::registerNativeMethod(env)

回到core_Fupk::unpackAll,该函数首先将dump方法的函数绑定到app/src/main/cpp/DexHelper/DexDumper.cpp中的fupk_ExportMethod中

然后调用了app/src/main/cpp/DexHelper/Fupk.cpp中的对象的unpackAll方法

1
2
3
4
void ::core_Fupk::unpackAll(JNIEnv *env, jobject obj, jstring folder) {
interface->ExportMethod = fupk_ExportMethod;
Fupk upk(env, sFolder, obj);
upk.unpackAll();

在实例化Fupk对象时,通过自定义接口dlsym(libdvm, "dvmGetUserDexFiles");获得gDvm.userDexFiles返回的HashTable,从中提取DvmDex信息

1
auto dvmDex = mCookie.getCookieAt(i, name, mIgnoreCase);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
struct DvmDex {
/* pointer to the DexFile we're associated with */
DexFile* pDexFile;

/* clone of pDexFile->pHeader (it's used frequently enough) */
const DexHeader* pHeader;

/* interned strings; parallel to "stringIds" */
struct StringObject** pResStrings;

/* resolved classes; parallel to "typeIds" */
struct ClassObject** pResClasses;

/* resolved methods; parallel to "methodIds" */
struct Method** pResMethods;

/* resolved instance fields; parallel to "fieldIds" */
/* (this holds both InstField and StaticField) */
struct Field** pResFields;

/* interface method lookup cache */
struct AtomicCache* pInterfaceCache;

/* shared memory region with file contents */
bool isMappedReadOnly;
MemMapping memMap;

jobject dex_object;

/* lock ensuring mutual exclusion during updates */
pthread_mutex_t modLock; // TODO import
};

这样就获得了内存中完整的Dex,下面开始主动调用方法

1
2
DexDumper dumper(mEnv, dvmDex, mUpkObj);
dumper.rebuild();//dump方法体@cs

这个mUpkObj是内核中Fupk的实例,创建DexDumper实例后,开始主动调用其方法

1
2
3
4
5
//app/src/main/cpp/DexHelper/DexDumper.cpp
bool DexDumper::rebuild() {
jclass upkClazz = mEnv->GetObjectClass(mUpkObj);
auto loaderObject = JniInfo::GetObjectField(mEnv, mUpkObj, "appLoader", "Ljava/lang/ClassLoader;");
auto tryLoadClass_method = mEnv->GetMethodID(upkClazz, "tryLoadClass", "(Ljava/lang/String;)Ljava/lang/Class;");

2、类的主动加载

  1. 获得内核Fupk实例的Class
  2. 获得ClassLoader实例systemClassLoader
  3. 获得tryLoadClass方法id
1
auto jClazz = mEnv->CallObjectMethod(mUpkObj, tryLoadClass_method, jDotDescriptor);//通过tryLoadClass获取类对象

参数jDotDescriptor即为获得的类签名

1
2
3
4
5
6
7
8
9
// I will load the class from java(the loader may has been replaced)
public Class tryLoadClass(String className) {
try {
return FRefInvoke.getClass(appLoader, className);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
1
2
3
4
5
6
7
8
9
10
public static Class getClass(ClassLoader loader, String class_name) throws Exception{
if (loader != null) {
try {
return loader.loadClass(class_name);//主动加载@cs
} catch (Exception e){
// class not found from loader, used Class.forName instead
}
}
return Class.forName(class_name);
}

通过tryLoadClass函数,使用systemClassLoader去主动加载这个类

3、主动调用方法,dump方法体

回到native层

后面通过DexDumper::fixMethodByDvm主动调用方法

1
2
3
4
5
6
7
8
9
10
11
12
bool DexDumper::fixMethodByDvm(DexSharedData &shared, DexMethod *dexMethod,
ClassDefBuilder* builder, u4 &lastIndex) {
lastIndex = lastIndex + dexMethod->methodIdx;
auto m = builder->getMethodMap(lastIndex);

assert(m != nullptr && "Unable to fix MethodBy Dvm, this should happened");

shared.mCurMethod = dexMethod;
FupkImpl::fupkInvokeMethod(m);//调用方法@cs
shared.mCurMethod = nullptr;
return true;
}

FupkImpl::fupkInvokeMethod(m)即为

app/src/main/cpp/DexHelper/DexDumper.cpp中的fupk_ExportMethod方法

该方法插桩到了内核中的invoke函数中

在fupk_ExportMethod中,将方法指针保存在了shared变量中

1
shared->extra.append((char*)item, code_item_len);//保存方法体@cs

Android-6.0.0_r1源码分析-Java层加载Dex和类的过程

PROLOGUE

1
2
3
4
5
6
7
8
9
10
private Dynamic dynamic;

DexClassLoader dexClassLoader = new DexClassLoader(dexPath,optimizedDirectory, libraryPath, getClassLoader());
Class Clazz = dexClassLoader.loadClass("com.example.myapplication.Dynamic");
dynamic = (Dynamic) Clazz.newInstance();

dynamic.sayHello();

Method m = Clazz.getDeclaredMethod("sayHello");
m.invoke();

在android中,可以通过上面两行代码动态加载dex文件,并加载其中的类,调用类中的方法。记录一下动态加载Dex和类的过程。

动态加载的基础是ClassLoader—类加载器,ClassLoader 就是专门用来处理类加载工作的,一个运行中的 APP 不仅只有一个类加载器。

在 Java 中,只有当两个实例的类名、包名以及加载其的 ClassLoader 都相同,才会被认为是同一种类型

一、创建类加载器的实例

在Android中,ClassLoader是一个抽象类,定义为:public abstract class ClassLoader
在实际开发中,一般使用其子类DexClassLoaderPathClassLoader来创建类加载器实例,进而加载类,需要说明的是,这两个类都继承于BaseDexClassLoaderBaseDexClassLoader又继承于ClassLoader,他们之间的关系如下,图片来自gityuan

img

1
2
3
public class DexClassLoader extends BaseDexClassLoader
public class PathClassLoader extends BaseDexClassLoader
public class BaseDexClassLoader extends ClassLoader

DexClassLoader类和PathClassLoader类的区别在下面分析的时候会说

首先看他这两个类的构造函数:

DexClassLoader

1
2
3
4
5
6
/libcore/dalvik/src/main/java/dalvik/system/DexClassLoader.java

public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}

PathClassLoader

1
2
3
4
5
6
7
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String libraryPath,
ClassLoader parent) {
super(dexPath, null, libraryPath, parent);
}

这两个类的构造函数都直接使用了父类的构造函数,但是传参不同。
BaseDexClassLoader构造函数:

1
2
3
4
5
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}

使用super(parent)创建了对象,然后使用makePathElements创建了DexPathList对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
/libcore/dalvik/src/main/java/dalvik/system/DexPathList.java

public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory) {
......
this.definingContext = definingContext;
// save dexPath for BaseDexClassLoader
this.dexElements = makePathElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions);
......
}

/**
* Makes an array of dex/resource path elements, one per element of the given array.
*/
private static Element[] makePathElements(List<File> files, File optimizedDirectory,List<IOException> suppressedExceptions) {
List<Element> elements = new ArrayList<>();
/*
* Open all files and load the (direct or contained) dex files up front.
*/
for (File file : files) {
File zip = null;
File dir = new File("");
DexFile dex = null;
String path = file.getPath();
String name = file.getName();
......
......
else if (file.isFile()) {
if (name.endsWith(DEX_SUFFIX)) {
// Raw dex file (not inside a zip/jar).
dex = loadDexFile(file, optimizedDirectory);
}
}//endif if (file.isFile())

if ((zip != null) || (dex != null)) {
elements.add(new Element(dir, false, zip, dex));
}
}//endfor for (File file : files)
return elements.toArray(new Element[elements.size()]);
}


/**
* Constructs a {@code DexFile} instance, as appropriate depending
* on whether {@code optimizedDirectory} is {@code null}.
*/
private static DexFile loadDexFile(File file, File optimizedDirectory)
throws IOException {
if (optimizedDirectory == null) {
return new DexFile(file);
} else {
String optimizedPath = optimizedPathFor(file, optimizedDirectory);
return DexFile.loadDex(file.getPath(), optimizedPath, 0);
}
}


/**
* Converts a dex/jar file path and an output directory to an
* output file path for an associated optimized dex file.
*/
private static String optimizedPathFor(File path,
File optimizedDirectory) {
/*
* Get the filename component of the path, and replace the
* suffix with ".dex" if that's not already the suffix.
*/
String fileName = path.getName();
if (!fileName.endsWith(DEX_SUFFIX)) {
int lastDot = fileName.lastIndexOf(".");
if (lastDot < 0) {
fileName += DEX_SUFFIX;
} else {
StringBuilder sb = new StringBuilder(lastDot + 4);
sb.append(fileName, 0, lastDot);
sb.append(DEX_SUFFIX);
fileName = sb.toString();
}
}
File result = new File(optimizedDirectory, fileName);
return result.getPath();
}

上面4个函数

DexPathList构造函数,将整个dexPath路径分成包含多个Dex文件路径的list作为参数传给了makePathElements。

makePathElements根据dex的path,将所有的dex都封装在了数组中,然后调用loadDexFile加载dex文件。

loadDexFile创建DexFile对象。若optimizedDirectory不为NULL, 将dex文件生成在optimizedDirectory路径中,使用DexFile类的loadDex函数创建对象;若optimizedDirectory为NULL,那么会直接使用 dex 文件原有的路径来创建 DexFile对象。

optimizedPathFor主要处理了源文件后缀的问题,让其后缀为.dex。当optimizedDirectory不为NULL时,在optimizedDirectory目录创建新文件,并将目录返回。

DexClassLoader类和PathClassLoader类的区别

DexClassLoader类和PathClassLoader类的区别主要在于构造对象时optimizedDirectory是否为NULL。

由于dex文件被包含在APK或者Jar文件中,因此在装载目标类之前需要先从APK或Jar文件中解压出dex文件,optimizedDirectory 必须是一个内部存储路径,optimizedDirectory参数就是指定解压出的dex文件存放的路径,PathClassLoader类构造对象时optimizedDirectory参数为空,则说明其不需要解压提取dex文件,所以其直接加载内部的dex文件;而DexClassLoader类可以指定optimizedDirectory参数,说明其需要从外部解压提取dex文件。总结如下:

DexClassLoader:能够加载未安装的jar/apk/dex
PathClassLoader:加载Android系统类和系统中应用的类

回到makePathElements函数,将DexFile对象和路径等信息封装成了数组返回给了DexPathList构造函数

1
2
    elements.add(new Element(dir, false, zip, dex));
return elements.toArray(new Element[elements.size()]);

然后在BaseDexClassLoader函数中通过DexPathList类的实例对象pathList保存了这些DexFile对象

1
private final DexPathList pathList;

二、加载类的过程

JVM 中 ClassLoader 通过 defineClass 方法加载 jar 里面的 Class,而 Android 中这个方法被弃用了。

1
2
3
4
5
@Deprecated
protected final Class<?> defineClass(byte[] classRep, int offset, int length)
throws ClassFormatError {
throw new UnsupportedOperationException("can't load this type of class file");
}

取代的是loadClass方法。第一节中通过实例化类加载器ClassLoader,创建了DexFile对象,并把对象保存在了pathList实例对象中。在Android中,使用loadClass方法加载一个类,这个方法定义在ClassLoader类中,其派生类没有重写该方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/libcore/libart/src/main/java/java/lang/ClassLoader.java

/**
* Loads the class with the specified name. Invoking this method is
* equivalent to calling {@code loadClass(className, false)}.
*/
public Class<?> loadClass(String className) throws ClassNotFoundException {
return loadClass(className, false);
}

/**
* Loads the class with the specified name, optionally linking it after
* loading.
*/
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
Class<?> clazz = findLoadedClass(className);
if (clazz == null) {
ClassNotFoundException suppressed = null;
try {
clazz = parent.loadClass(className, false);
} catch (ClassNotFoundException e) {
suppressed = e;
}
if (clazz == null) {
try {
clazz = findClass(className);
} catch (ClassNotFoundException e) {
e.addSuppressed(suppressed);
throw e;
}
}
}
return clazz;
}

ClassLoader 双亲委派模型加载类的作用

上面第二个loadClass方法使用了双亲委派模型,loadClass在加载一个类时

1、判断当前的类加载器ClassLoader是否加载过此类,有就直接返回
2、若未加载过当前类,就去查询Parent是否加载过此类
3、如果继承路线上的ClassLoader都没有加载,则调用findClass去加载这个类
即如果一个类被位于树根的ClassLoader加载过,那么这个类在一个虚拟机的运行周期内永远不会被重新加载。

作用

一个Class的标识为包名PackageName+类名ClassName+类加载器ClassLoader

首先是共享功能,一些 Framework 层级的类一旦被顶层的 ClassLoader 加载过就缓存在内存里面,以后任何地方用到都不需要重新加载。

除此之外还有隔离功能,不同继承路线上的 ClassLoader 加载的类肯定不是同一个类,这样的限制避免了用户自己的代码冒充核心类库的类访问核心类库包可见成员的情况。这也好理解,一些系统层级的类会在系统初始化的时候被加载,比如 java.lang.String,如果在一个应用里面能够简单地用自定义的 String 类把这个系统的 String 类给替换掉,那将会有严重的安全问题。

loadClass方法调用了findClass方法,ClassLoader类的findClass方法会报异常,而BaseDexClassLoader 重载了这个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
for (Throwable t : suppressedExceptions) {
cnfe.addSuppressed(t);
}
throw cnfe;
}
return c;
}

上面在创建类构造器的过程中,已经创建好了所有的DexFile对象,并封装在了BaseDexClassLoader类中的一个实例对象pathList中:

1
private final DexPathList pathList;

BaseDexClassLoader的findClass方法直接调用了DexPathList类的findClass方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/libcore/dalvik/src/main/java/dalvik/system/DexPathList.java

/**
* Finds the named class in one of the dex files pointed at by
* this instance. This will find the one in the earliest listed
* path element. If the class is found but has not yet been
* defined, then this method will define it in the defining
* context that this instance was constructed with.
*/
public Class findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}

在DexPathList类的findClass方法中,遍历其dexElements变量中保存的DexFile对象,这个dexElements变量在DexPathList类的构方法中被初始化,由makePathElements方法生成,保存了DexFile对象。

然后接着调用了DexFile对象的loadClassBinaryName方法,我们找到DexFile类的该方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/libcore/dalvik/src/main/java/dalvik/system/DexFile.java

public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
return defineClass(name, loader, mCookie, suppressed);
}

private static Class defineClass(String name, ClassLoader loader, Object cookie,
List<Throwable> suppressed) {
Class result = null;
try {
result = defineClassNative(name, loader, cookie);
} catch (NoClassDefFoundError e) {
if (suppressed != null) {
suppressed.add(e);
}
} catch (ClassNotFoundException e) {
if (suppressed != null) {
suppressed.add(e);
}
}
return result;
}

loadClassBinaryName方法是对同类下defineClass方法的封装,在defineClass方法内,又调用了native层的defineClassNative方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/art/runtime/native/dalvik_system_DexFile.cc

static jclass DexFile_defineClassNative(JNIEnv* env, jclass, jstring javaName, jobject javaLoader,jobject cookie) {
std::unique_ptr<std::vector<const DexFile*>> dex_files = ConvertJavaArrayToNative(env, cookie);
ScopedUtfChars class_name(env, javaName);
......
......
const std::string descriptor(DotToDescriptor(class_name.c_str()));
const size_t hash(ComputeModifiedUtf8Hash(descriptor.c_str()));
for (auto& dex_file : *dex_files) {
const DexFile::ClassDef* dex_class_def = dex_file->FindClassDef(descriptor.c_str(), hash);
if (dex_class_def != nullptr) {
ScopedObjectAccess soa(env);
ClassLinker* class_linker = Runtime::Current()->GetClassLinker();
class_linker->RegisterDexFile(*dex_file);
StackHandleScope<1> hs(soa.Self());
Handle<mirror::ClassLoader> class_loader(
hs.NewHandle(soa.Decode<mirror::ClassLoader*>(javaLoader)));
mirror::Class* result = class_linker->DefineClass(soa.Self(), descriptor.c_str(), hash,class_loader, *dex_file, *dex_class_def);
}
}
return nullptr;
}

DexFile_defineClassNative方法中。可以看到通过调用Runtime类的静态成员函数Current获得了Runtime单例,在ART虚拟机进程中,存在着一个Runtime单例,用来描述ART运行时。获得了这个单例之后,就可以调用它的成员函数GetClassLinker来获得一个ClassLinker对象。ClassLinker对象是在创建ART虚拟机的过程中创建的,用来加载类以及链接类方法。然后调用了class_linker的DefineClass来获得class对象并返回,至此类加载就完成了。


NDK开发学习记录

记录式学习,内容可能不全,用到知识就会记录下来,方便之后的查阅和复习。

JNIEnv的指针会被JNI传入到本地方法的实现函数中来对Java端的代码进行操作。

一、Native函数的注册

注册native函数的具体方法不同,会导致系统在运行时采用不同的方式来寻找这些native方法。

1、Native函数静态注册

静态注册就是根据函数名来遍历Java和JNI函数之间的关联,而且要求JNI层函数的名字必须遵循特定的格式。

1
2
3
4
在java层的native方法声明:
public static native int modifyBytecode();
javah生成头文件:
javah cn.pollux.modifydalvikbytecode.MainActivity

在头文件内会有native层的方法声明

1
2
JNIEXPORT jint JNICALL Java_cn_pollux_modifydalvikbytecode_MainActivity_modifyBytecode
(JNIEnv *, jclass);

JNIEXPORT:说明该函数时导出函数,在java层可以直接调用
JNICALL

总结静态注册的流程:
1、编写包含native方法的Java类
2、使用Javah命令生成.h头文件
3、编写代码实现头文件中的方法

2、Native函数动态注册

动态注册,即通过RegisterNatives方法把C/C++中的方法映射到Java中的native方法声明。

当用System.loadLibarary()方法加载so库的时候,虚拟机会首先调用JNI_OnLoad函数,这个函数的作用是告诉虚拟机此C库使用的是哪一个JNI版本,默认最初的JNI 1.1版本。所以可以在JNI_OnLoad函数中,注册JNI函数。

在Java层声明一个native函数

1
private native void testJava(String str);

在native层的JNI_OnLoad函数中将Java层的testJava函数与Native层的testNative函数绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
jint JNI_OnLoad(JavaVM* vm, void* reserved){
JNIEnv* env = NULL;
if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
LOGE("This jni version is not supported");
return JNI_VERSION_1_6;
}
if (!registerNativeMethod(env)) {
LOGE("Unable to register native method: maybe the java has not loaded");
// I don't want the process crash, just continue
env->ExceptionClear();
}
return JNI_VERSION_1_6;
}

通过GetEnv函数获取JNIEnv结构体指针,JNIEnv结构体指向了一个函数表,该函数表指向了对应的JNI函数,我们通过这些JNI函数实现JNI编程,比如在jni.h中

1
2
3
4
5
6
typedef const struct JNINativeInterface* JNIEnv;

struct JNINativeInterface {
......
jclass (*FindClass)(JNIEnv*, const char*);
......

JNIEnv是指向JNINativeInterface结构体的指针,env是指向JNIEnv变量的指针,所以env是指向JNINativeInterface结构体指针的指针。

在看下registerNativeMethod函数内容

1
2
3
4
5
6
7
8
9
10
bool registerNativeMethod(JNIEnv *env){
auto clazz = env->FindClass("com/pollux/jnil/JNIDemo");
JNINativeMethod natives[] = {
{"testJava", "(Ljava/lang/String;)V", (void*)testNative}
};
if (env->RegisterNatives(clazz, natives,
sizeof(natives)/sizeof(JNINativeMethod))!= JNI_OK) {
env->ExceptionClear();
}
}

被动态注册的函数

1
2
3
static void testNative(JNIEnv *env, jobject, jlong handle) {
LOGI("JNI", "dynamic register");
}

RegisterNatives第二个参数为JNINativeMethod类型的数组,JNI允许通过一个函数映射表,注册给JVM,JVM通过函数映射表来查找调用相应的函数。这个函数映射表就是RegisterNatives数组,定义如下:

1
2
3
4
5
typedef struct { 
const char* name; //Java层函数名
const char* signature; //Java层函数签名
void* fnPtr; //Native层函数名(函数指针)
} JNINativeMethod;

因为Java支持重载,所以需要函数签名指定参数与返回值。

总结:

1、在Java层声明一个函数
2、在Native层的JNI_Onload函数中注册函数

二、 Java层调用Native层方法

三、Native层调用Java层代码

1、获取jclass

jclass是JNI中设定的对应于java层Class的变量,无论jclass、jmethod、jobject,出于安全的考虑,他们都是间接引用,这些变量定义在jni.h文件中

JNIEnv有三种方法获得jclass

1
2
3
jclass FindClass(const char* name)
jclass GetObjectClass(jobject obj)
jclass GetSuperclass(jclass clazz)

2、调用Java类的成员方法

关键API:

1
2
3
4
5
6
7
8
jclass FindClass(const char* name)
jobject NewObject(jclass clazz, jmethodID methodID, ...)
//获取方法ID
jmethodID GetStaticMethodID(jclass clazz, const char* name, const char* sig)
jmethodID GetMethodID(jclass clazz, const char* name, const char* sig)
//调用方法
NativeType CallStatic<type>Method(JNIEnv *env, jobject obj,jmethodID methodID, ...);
NativeType Call<type>Method(JNIEnv *env, jobject obj,jmethodID methodID, ...);

调用类的静态方法

1
2
3
jclass jclazz = env->FindClass("com/example/myapplication/JNITest");
jmethodID statictest_mid= env->GetStaticMethodID(jclazz,"static_test","()V");
env->CallStaticVoidMethod(jclazz,statictest_mid);

调用类的实例方法

1
2
3
4
5
6
7
//实例化对象
jclass jclazz = env->FindClass("com/example/myapplication/JNITest");
jmethodID construction_mid =env->GetMethodID(jclazz,"<init>", "()V");
jobject JNITest_obj = env->NewObject(jclazz,construction_mid);
//调用方法
jmethodID test_mid=env->GetMethodID(jclazz,"test", "(ILjava/lang/String;)Ljava/lang/String;");
jstring b =(jstring)env->CallObjectMethod(JNITest_obj,test_mid,4,env->NewStringUTF("bbbb"));

3、访问和设置Java类的成员变量

关键API:

1
2
3
4
jfieldID GetStaticFieldID(jclass clazz, const char* name, const char* sig)
void SetStaticIntField(jclass clazz, jfieldID fieldID, jint value)
jfieldID GetFieldID(jclass clazz, const char* name, const char* sig)
void SetIntField(jobject obj, jfieldID fieldID, jint value)

访问和设置类的静态变量

1
2
3
jfieldID fids = env->GetStaticFieldID(jclazz,"staticNum","I");
env->SetStaticIntField(jclazz,fids,34);
jint n2= env->GetStaticIntField(jclazz,fids);

访问和设置类的实例变量

1
2
3
jfieldID fid = env->GetFieldID(jclazz,"age","I");
env->SetIntField(JNITest_obj,fid,33);
jint n1 = env->GetIntField(JNITest_obj,fid);

CMakeLists.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html

# Sets the minimum version of CMake required to build the native library.

cmake_minimum_required(VERSION 3.4.1)

# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.

add_library( # Sets the name of the library.
native-lib

# Sets the library as a shared library.
SHARED

# Provides a relative path to your source file(s).
native-lib.cpp)

# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.

find_library( # Sets the name of the path variable.
log-lib

# Specifies the name of the NDK library that
# you want CMake to locate.
log)

# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.

target_link_libraries( # Specifies the target library.
native-lib

# Links the target library to the log library
# included in the NDK.
${log-lib})