C++虚表杂记

vptr虚指针被定义在对象首地址的前4个字节处
指针的类型决定了普通函数的调用,指针指向的实际类型决定了虚函数的调用
构造函数不能是虚函数,析构函数最好设置为虚函数
对象的虚表指针初始化是通过编译器在构造函数内插入代码来完成的
对象首地址为this指针
在构造函数和析构函数中调用虚函数会使多态性失效

实例化一个对象时,优先调用父类的构造函数,并以子类对象的首地址作为this指针传递给父类构造函数。在父类的构造函数中,首先会初始化子类虚表指针为父类的虚表首地址(会将虚表指针修改为当前类的虚表指针),接着在子类的构造函数中,会重新写入子类虚表指针为子类的虚表首地址
接着是子类的析构函数,和父类的析构函数,同样会重写子类虚表指针(单一继承时,父类和子类的虚表指针位置一样,即对象首地址的前4字节处),重写虚表指针是为了防止在构造或析构函数中调用子类或父类的虚函数,比如在父类的构造函数中,调用虚函数,如果不更新虚表指针为父类虚表,那么调用了子类的虚函数,但是子类构造函数还没执行,没有初始化虚表指针,这样就会访问错误,在析构函数中原理一样。

总结下构造函数和析构函数的调用顺序,若存在以下继承关系:

1
2
class Bird : public Mammal
class Bat : public Bird : public Mammal

构造:Bird -> Mammal - > Bat
析构:Bat -> Mammal - >Bird

在单一继承中,其子类对象内存首先存放的是父类的数据成员

Q:为什么析构函数要定义为虚函数

因为可以父类指针保存子类对象,如 Mammal *a = new Bird();,如果析构函数没定义为虚函数,释放对象空间时就会直接调用父类的析构函数,子类和父类对象的内存大小不同,释放内存就会产生问题

虚表在内存中的结构

1
2
3
4
5
6
7
8
9
10
11
12
13
+-------------+
|offset to top|
+-------------+
| RTTI pointer|
+--------------<-------+ vptr
| Destructor1 |
+-------------+
| Destructor2 |
+-------------+
| fun1 |
+-------------+
| fun2 |
+-------------|

offset_to_top

将对象从当前这个类型转换为该对象的实际类型的地址偏移量,是对象内存中的偏移量,不是虚表内存中的偏移量

1
2
3
4
5
继承关系:Bat : Bird, Mammal

Bat *bat = new Bat();
Mammal *m = bat;
m->walk();

用父类指针保存子类对象,这时在子类对象的内存布局,

1
2
3
4
5
6
7
 0 | class Bat
0 | class Bird (primary base)
0 | (Bird vtable pointer)
4 | int a
8 | class Mammal (base)
8 | (Mammal vtable pointer)
12 | int b

this指向0,但是Mammal对象在偏移8处,需要修正this的位置,查看汇编:

1
2
3
4
5
push    ebx             ; this
call _ZN3BatC2Ev ; Bat::Bat(void)
mov [ebp+var_20], ebx
mov eax, [ebp+var_20]
add eax, 8 ; this = this+8

此时this指向Mammal对象,调用方法:

1
2
3
4
5
6
7
8
mov     [ebp+var_1C], eax
mov eax, [ebp+var_1C]
mov eax, [eax] ; eax = Mammal vptr
add eax, 8 ; eax = vptr + 8
mov eax, [eax] ; walk func addr
sub esp, 0Ch
push [ebp+var_1C]
call eax

Bat虚表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.rodata:08048C68 ; `vtable for'Bat
.rodata:08048C68 _ZTV3Bat dd 0 ; offset to this
.rodata:08048C6C dd offset _ZTI3Bat ; `typeinfo for'Bat
.rodata:08048C70 off_8048C70 dd offset _ZN3BatD2Ev ; DATA XREF: Bat::Bat(void)+28↑o
.rodata:08048C70 ; Bat::~Bat()+6↑o
.rodata:08048C70 ; Bat::~Bat()
.rodata:08048C74 dd offset _ZN3BatD0Ev ; Bat::~Bat()
.rodata:08048C78 dd offset _ZN3Bat3flyEv ; Bat::fly(void)
.rodata:08048C7C dd offset _ZN3Bat4flyyEv ; Bat::flyy(void)
.rodata:08048C80 dd -8 ; offset to this
.rodata:08048C84 dd offset _ZTI3Bat ; `typeinfo for'Bat
.rodata:08048C88 off_8048C88 dd offset _ZThn8_N3BatD1Ev
.rodata:08048C88 ; DATA XREF: Bat::Bat(void)+32↑o
.rodata:08048C88 ; Bat::~Bat()+10↑o
.rodata:08048C88 ; `non-virtual thunk to'Bat::~Bat()
.rodata:08048C8C dd offset _ZThn8_N3BatD0Ev ; `non-virtual thunk to'Bat::~Bat()
.rodata:08048C90 dd offset _ZN6Mammal4walkEv ; Mammal::walk(void)

可以看到次表的offset to top值为-8,即为Bat对象到Mammal对象偏移的负值

在多继承中,由于不同的基类起点可能处于不同的位置,因此当需要将它们转化为实际类型时,this 指针的偏移量也不相同,且由于多态的特性,bat 的实际类型在编译时期是无法确定的;那必然需要一个东西帮助我们在运行时期确定 bat 的实际类型,这个东西就是offset_to_top。通过让this指针加上offset_to_top的偏移量,就可以让 this 指针指向实际类型的起始地址

https://www.cnblogs.com/xhb19960928/p/11720314.html

https://www.zhihu.com/question/23971699

https://www.freebuf.com/articles/system/123821.html

https://blog.iret.xyz/article.aspx/cpp_vfunc_reversing_2

https://www.anquanke.com/post/id/85585

https://alschwalm.com/blog/static/2017/01/24/reversing-c-virtual-functions-part-2-2/


51ff4f42e4a8218a6d27ec8cd9874f37
Here's something encrypted, password is required to continue reading.
阅读更多
android-6.0.0源码分析-zygote启动过程

常规系统启动从cpu加电,从缺省的寄存器上读取第一条地址,启动BIOS,BIOS进行硬件检查,初始化中断向量、设置寄存器等,BIOS还会从主引导扇区读取512字节的主引导记录MBR,MBR会寻找活动分区的引导记录,读取硬盘上的系统,加载到内存中。下面从操作系统的第一个进程init进程开始

时序图:
image-20200325171511529

1、init

init进程是系统启动的第一个进程,进程通过init.rc脚本来启动系统,在init.rc脚本中有一句import /init.${ro.zygote}.rc,在default.prop中定义ro.zygote=zygote32

查看这个文件:init.zygote32.rc

1
2
3
4
5
6
7
service zygote /system/bin/app_process -Xzygote /system/bin --zygote --start-system-server
class main
socket zygote stream 660 root system
onrestart write /sys/android_power/request_state wake
onrestart write /sys/power/state on
onrestart restart media
onrestart restart netd

socket关键字表示这个zygote进程需要一个名称为”zygote”的socket资源;onrestart关键字表示这个zygote进程重启时需要执行的命令

可见zygote进程是通过app_process创建创建的

2、app_main.cpp的main方法

/system/bin/app_process的源码位于frameworks\base\cmds\app_process\app_main.cpp,在app_main.cpp的main函数中主要做了:
1、处理分发各种参数
2、创建AppRuntime的实例化对象runtime 2
3、调用runtime.start(“com.android.internal.os.ZygoteInit”, args, zygote),类名、参数、true 3

3、AndroidRuntime.start

frameworks/base/core/jni/AndroidRuntime.cpp

AppRuntime的父类是AndroidRuntime,start函数主要做了三件事:

1、通过startVM启动虚拟机 4
2、startReg注册jni方法 5
3、通过jni调用com.android.internal.os.ZygoteInit的main方法 6

之后进入java层

4、ZygoteInit.main

frameworks/base/core/java/com/android/internal/os/ZygoteInit.java

ZygoteInit.main主要做了四件事:

1、创建ZygoteServer对象 7

1
ZygoteServer zygoteServer = new ZygoteServer();

2、ZygoteServer注册socket,socket文件描述符为zygote 8

1
zygoteServer.registerServerSocket(socketName);

3、启动SystemServer超级管理进程 9

SystemServer是zygote孵化的第一个进程,负责所有系统的核心服务,如ActivityManagerService

1
startSystemServer(abiList, socketName, zygoteServer);

4、开始选择循环,监听处理消息,等待ActivityManagerService通过socket的请求创建新的应用程序进程 10

1
zygoteServer.runSelectLoop(abiList);

总结

一、init进程通过执行一段脚本来启动启动Zygote进程,Zygote进程负责其他应用进程的创建,脚本调用app_process的创建虚拟机

二、在app_main.cpp的main函数中:1、实例化了AndroidRuntime对象;2、调用start方法

三、在start方法中:1、startVM开启虚拟机;2、startReg注册JNI方法;3、通过jni调用ZygoteInit的main方法,进入java世界

四、在ZygoteInit的mian方法中:
1、创建ZygoteServer对象;
2、注册socket端口,名为zygote;
3、启动SystemServer超级管理进程,SystemServer负责启动系统的其他关键服务,如AMS、PMS;
4、开启选择循环,监听消息,等待ActivityManagerService通过socket的请求创建新的应用程序进程

参考

https://blog.csdn.net/luoshengyang/article/details/6768304


mineucore-内核线程管理学习笔记

操作系统是以进程为中心设计的,所以其首要任务是为进程建立档案,进程档案用于表示、标识或描述进程,即进程控制块。下面通过实现init内核线程,学习进程从创建到运行的过程

内核线程是一种特殊的进程,内核线程和进程的区别主要有两个:

  1. 内核线程只运行在内核态,用户进程则会在用户态和内核态切换
  2. 所有内核线程共用操作系统内核内存空间,不需要为内核线程维护单独的内存空间;而每个用户进程都需要维护各自的内存空间

如何生成一个内核线程并让其运行呢?分三步

  1. 创建PCB块,设置相应属性
  2. 分配相应的资源(栈空间,虚拟内存)
  3. 被调度器执行

下面详细说如何实现这三步:

1、为内核线程创建PCB块并初始化

首先确定PCB的数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct proc_struct {
enum proc_state state; // Process state
int pid; // Process ID
int runs; // the running times of Proces
uintptr_t kstack; // Process kernel stack
volatile bool need_resched; // bool value: need to be rescheduled to release CPU?
struct proc_struct *parent; // the parent process
struct mm_struct *mm; // Process's memory management field 描述用户态进程内存空间的情况
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for current interrupt
uintptr_t cr3; // CR3 register: the base addr of Page Directroy Table(PDT)
uint32_t flags; // Process flag
char name[PROC_NAME_LEN + 1]; // Process name
list_entry_t list_link; // Process link list
list_entry_t hash_link; // Process hash list
};

调用alloc_proc完成PCB的初始化

1
2
3
4
5
6
7
8
9
10
11
12
proc->state = PROC_UNINIT;  //设置进程为uninitialized状态,已创建 未初始化
proc->pid = -1; //设置进程pid的未初始化值
proc->runs = 0;
proc->kstack = 0;
proc->need_resched = 0;
proc->parent = NULL;
proc->mm = NULL;
memset(&(proc->context), 0, sizeof(struct context));
proc->tf = NULL;
proc->cr3 = boot_cr3; //使用内核页目录表的基址,即内核线程共用一个映射内核空间的页表,这表示内核空间对所有内核线程都是“可见”的,所以更精确地说,这些内核线程都应该是从属于同一个唯一的“大内核进程”—uCore内核
proc->flags = 0;
memset(proc->name, 0, PROC_NAME_LEN);

2、为PCB分配资源

主要函数如下:(这里不贴代码了,太多了,陈述下思路)

1、setup_kstack
通过物理内存管理器申请8K内存作为内核栈

2、copy_mm(clone_flags, proc)
根据clone_flag标志复制或共享进程内存管理结构,内核线程设为NULL,由于在操作系统启动后,已经对整个核心内存空间进行了管理,通过设置页表建立了核心虚拟空间(即 boot_cr3 指向的二级页表描述的空间)。所以内核中的所有线程都不需要再建立各自的页表,只需共享这个核心虚拟空间就可以访问整个物理内存。

3、copy_thread
重点函数,设置了proc->context,context存储着线程运行时的通用寄存器的值,这里设置了init内核线程的eip和esp,即init运行时指令指针和栈指针位置。

这里的eip并不是直接指向init线程的代码,而是指向了forkret这个汇编函数,而esp则指向了中断帧。

这里中断帧完成了真正的上下文切换,包括各种段寄存器、标志寄存器等。

forkret函数做的功能就是将eip指向中断帧trapfram.eip,而trapfram.eip指向kernel_thread_entry这个汇编函数,其中trapfram.ebp为线程要执行的函数,edx为参数,在kernel_thread_entry中call ebp

总的来说,copy_thread设置进程在内核(将来也包括用户态)正常运行和调度所需的中断帧和执行上下文,context完成了通用寄存器上下文的切换,trapfram完成了处理机上下文的切换

3、线程切换(运行)

通过proc_run启动线程,proc_run 的执行过程为:

  • 保存 IF 位并且禁止中断;
  • 将 current 指针指向将要执行的进程;
  • 更新 TSS 中的栈顶指针;
  • 页表切换;
  • 调用 switch_to 进行上下文切换;
  • 当执行 proc_run 的进程恢复执行之后,需要恢复 IF 位。

主要函数时switch_to函数,当 switch_to 函数执行完“ret”指令后,就跳转到init线程执行了

1
switch_to(&(prev->context), &(next->context));

通过switch_to函数实现两个内核线程上下文的切换,这个函数是一段汇编代码,实现了通用寄存器和eip的替换,最后push eip; ret,就跳转到了next->context的eip执行,这个context被赋值成什么了呢?

在copy_thread函数中对这个context.eip进行了初始化,同时栈寄存器esp也被更新了,我们先不看这个esp有什么用,先看forkret这个函数

1
2
proc->context.eip = (uintptr_t)forkret; //设置initproc的进程上下文,上次停止执行时的下一条指令地址context.eip和上次停止执行时的堆栈地址context.esp
proc->context.esp = (uintptr_t)(proc->tf);//因为initproc还没有执行过,所以这其实就是initproc实际执行的第一条指令的堆栈指针

forkret函数是一段汇编指令,其实现在trapentry.S

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.globl __trapret
__trapret:
# restore registers from stack
popal
# restore %ds, %es, %fs and %gs
popl %gs
popl %fs
popl %es
popl %ds
# get rid of the trap number and error code
addl $0x8, %esp
iret
.globl forkrets
forkrets:
# set stack to this new process's trapframe
movl 4(%esp), %esp
jmp __trapret

这个函数更新了栈指针寄存器,然后将栈上的数据弹回寄存器,调用中断返回指令iret。这里的内核线程上下文切换参考了内核栈到用户栈的中断实现。
当发生中断时,硬件会依次压栈ss、esp、eflags、cs、eip、errno,这是硬件实现的部分,最后通过iret中断返回指令将栈上的ss、esp、eflags、cs、eip、errno弹回对应的寄存器。在内核栈到用户栈的中断实现中,当上述寄存器在栈上时,修改上述寄存器的值,当iret时,就实现了切栈的效果。
在这里我们不需要中断,但是通过iret中断返回指令实现线程切换时的上下文切换,为此我们需要建立中断帧trapframe,proc->context.esp = (uintptr_t)(proc->tf)这句代码就起作用了,

首先在 kernel_thread函数中建立临时中断帧

1
2
3
4
5
tf.tf_cs = KERNEL_CS;
tf.tf_ds = tf.tf_es = tf.tf_ss = KERNEL_DS;
tf.tf_regs.reg_ebx = (uint32_t)fn; //fn为要启动的线程的函数指针
tf.tf_regs.reg_edx = (uint32_t)arg; //arg为参数
tf.tf_eip = (uint32_t)kernel_thread_entry; //位于kern/process/entry.S

然后在copy_thread函数中,分配空间建立中断帧

1
2
3
4
5
proc->tf = (struct trapframe *)(proc->kstack + KSTACKSIZE) - 1;//在内核堆栈的顶部设置中断帧大小的一块栈空间
*(proc->tf) = *tf;
proc->tf->tf_regs.reg_eax = 0;
proc->tf->tf_esp = esp;
proc->tf->tf_eflags |= FL_IF; // Interrupt Flag 表示此内核线程在执行过程中,能响应中断,打断当前的执行

当执行iret后,cpu跳转到kernel_thread_entry执行

1
2
3
4
5
6
kernel_thread_entry:        # void kernel_thread(void)
pushl %edx # push arg
call *%ebx # call fn

pushl %eax # save the return value of fn(arg)
call do_exit # call do_exit to terminate current thread

当执行call *%ebx时,就开始执行initproc的主体了。Initprocde的主体函数很简单就是输出一段字符串,然后就返回到kernel_tread_entry函数,并进一步调用do_exit执行退出操作了,至此一个内核线程init的执行结束了。

通过ucore的实现,可以知道内核线程的启动,伴随着上下文切换,这里包括通用寄存器、段寄存器、标志寄存器等,ucore通过中断返回指令iret实现了段寄存器和标志寄存器的切换


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安卓内联hook框架

PROLOGUE

1、获得目标函数符号地址,将目标NDK库映射到内存,并解析动态符号表和动态字符串表,获得目标函数的符号地址(为了突破android7后系统对私有NDK库的访问限制)

2、hook过程有4个跳板代码组成,因为arm架构的三级流水线设计,在hook点首先要备份8字节的代码,将其修改为长跳转指令,跳转到跳板0-beforeshellcoode,在beforeshellcode中首先将处理机上下文压入栈,并将sp赋值给r0,将lr赋值给了r1,接着回调beforehook函数,由于arm32的函数调用约定,进入被调函数体后,r0~r4为前4个参数(之后的参数在栈上),所以beforehook函数就可以得到目标函数的参数,并可修改栈上的参数,beforeshellcode最后会将栈上的内容弹回原寄存器,完成了参数的输出与修改。

3、因为很难判断函数体的结尾,所以选择函数返回后的地址作为afterhook函数的hook点,通过beforehook函数获得lr寄存器的值,并在beforehook函数尾实现lr指向地址的指令的hook,将其跳转到跳板2-afterhookshellcode,在afterhook中输出或修改r0的值,即可完成函数返回值的输出和修改。

4、完成上述的hook,一共hook了两处代码,因为备份的代码可能存在涉及PC指令或相对跳转的操作,而执行完beforeshellcoode和afterhookshellcode后的PC值已经改变或者相对地址不再指向正确的地址。因此需要指令修复,修复指令分别在beforeshellcode和aftershellcode后的跳板1和跳板2中,实现跳转回到hook点

  • 对于涉及PC指令的修复,是选一个暂时用不到的寄存器Rr保存原PC的值,修改指令,将PC寄存器替换为Rr,即可完成修复。
  • 对于相对地址的跳转,则是先计算出绝对地址,使用寄存器远跳完成指令修复。

5、(存在问题2.27更新)想实现任意进程的hook,就需要将hook库注入到目标进程,Xposed通过替换app_process,将代码注入到zygote进程,这样每个app进程都存在xposed的代码。libc.so每个app也都会调用,尝试通过patch libc.so的.init_array节区,将其指向一段shellcode,shellcode加载hook库,并在返回值调用源.init_array中的函数。这样每个进程确实被注入了hook库,但是app在启动时并没有触发hook库中的JNI_Onload函数。

问题:zygote在被复制前已经加载了libc.so,而所有app都是复制的zygote,所以libc.so不是被所有app进程加载,而是被zygote加载。所以hook库没有在app启动时调用JNI_Onload中的函数。

下面具体说下实现细节:

1、获得函数符号地址

android7后,系统将阻止应用动态链接非公开NDK库,所以采用自映射目标NDK库的方法,解析ELF文件,获得.dynsym、.dynstr节区的信息和程序定义节区偏移,将其保存在ctx结构体变量中

1
void *ctx = my_dlopen(libpath);

my_dlopen内容:

1、将目标NDK库映射到内存中

1
2
fd = open(libPath, O_RDONLY)
elf = (Elf32_Ehdr *) mmap(0, size, PROT_READ, MAP_SHARED, fd, 0)

2、从ELF header获得节区头表section header table的偏移

1
shtoff = elf->e_shoff;

3、遍历节区头表找到类型为SHT_DYNSYM的节区头,即.dynsym节区头,根据节区偏移sh_off、节区大小sh_size将.dynsym节区拷贝内存中

1
2
3
Elf_Shdr *sh = (Elf_Shdr *)shtAddr;
ctx->dynsym = calloc(1, sh->sh_size);
memcpy(ctx->dynsym, ((void *) elf + sh->sh_offset), sh->sh_size);

4、遍历节区头表找到类型为SHT_STRTAB的节区头,即.dynstr节区头,根据节区偏移sh_off、节区大小sh_size将.dynstr节区拷贝内存中

1
2
ctx->dynstr = calloc(1, sh->sh_size);
memcpy(ctx->dynstr, (void *) elf + sh->sh_offset, sh->sh_size);

5、遍历节区头表找到类型为SHT_PROGBITS的节区头,即程序定义节区,计算得到偏移,动态链接库文件中 sh->sh_addr = sh->sh_offset

ctx->off = sh->sh_addr - sh->sh_offset;

通过my_dlsym获得符号地址

.dynsym节区中的符号名为.dymstr节区的下标,通过符号名找到符号的偏移,计算出符号地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void *my_dlsym(void *ctxStruct,char *symName){
if(ctxStruct==NULL){
return NULL;
}
CTXINFO *ctx = (CTXINFO*)ctxStruct;
Elf_Sym *sym =(Elf_Sym*) ctx->dynsym;
char *dynstrAddr = ctx->dynstr;
int i;
for(i=0;i<ctx->symsNum;i++,sym++){
char *name = dynstrAddr+sym->st_name;
if(strcmp(name,symName)==0){
void *res = ctx->execAddr+sym->st_value-ctx->off;
return res;
}
}
return NULL;
}

2、回调函数

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获得和修改函数返回值。

3、指令修复

采用跳板的方式跳回原指令,因为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

ADR是小范围的地址读取伪指令.ADR 指令将基于PC 相对偏移的地址值读取到寄存器中.在汇编编译源程序时,ADR 伪指令被编译器替换ADD 指令或SUB 指令来实现该ADR 伪指令的功能,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ldr r0, _start
adr r0, _start
ldr r0, =_start
_start:
b _start

0x00000000: e59f0004 ldr r0, [pc, #4] ; 0xc
0x00000004: e28f0000 add r0, pc, #0 ; 0x0
0x00000008: e59f0000 ldr r0, [pc, #0] ; 0x10
0x0000000c: eafffffe b 0xc

r0 = 0xeafffffe
r0 = 0x0000000c
r0 = 0x0000000c

4、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

内存壳学习

内存壳的保护方式主要是通过动态加载的方式保护源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 = R2 / 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/