读书笔记

一些坑

  1. 有的时候直接把自己签名好的apk在命令行进行安装的时候,出现[INSTALL_FAILED_TEST_ONLY]的安装 错误的提示
  • 原因:Android Studio 3.0会在debug apk的manifest文件<application>标签里自动添加<android:testOnly="true">属性

  • 解决方案:
    adb install -t app-debug.apk



看书笔记

Dalvik可执行格式与字节范围

Dalvik虚拟机

art虚拟机的可以看看这边

https://www.jianshu.com/p/20dcfcf27004
https://www.kancloud.cn/alex_wsc/androids/401771

  • 运行时为每个线程维护了一个PC计数器和一个调用栈(这个栈用来存放寄存器)

  • 执行流程

    1. init进程先完成设备的初始化工作,再读取init.rc文件并启动系统中的Zygote进程

    2. Zygote进程启动后先初始化Dalvik虚拟机,再启动system_server进程进入Zygote模式,通过socket等候命令下达。(有看到资料说art就不用socket通信了?)

    3. 在执行一个应用程序的是偶,system_server通过Binder IPC方式将命令发给Zygote

    4. Zygote收到命令后,通过fork()自身来创造一个Dalvik虚拟机的实例来执行应用程序的入口函数

    5. Dalvik虚拟机先通过loadClassFromDex()函数来装载类。每个类解析后,都会获得运行环境中的一个ClassObject类型的数据结构存储。(虚拟机使用gDvm.loadedClasses全局散列表来存储和查询所有装载进来的类)

    6. 字节码验证器使用dvmVerifyCodeFlow()函数对装入的代码进行校验,虚拟机调用FindClass()函数查找并装载main()方法类。

    7. 虚拟机调用dvmInterpret()函数来初始化解释器并解释字节码流。

smali语法相关

  1. 类型
语法含义备注
vvoid, 只用于返回值类型
Zboolean
Bbyte
Sshort
Cchar
Iint
Jlong
Ffloat64位类型,需要用两个相邻存储器
Ddouble64位类型,需要用两个相邻存储器
Ljava类类型Ljava/lang/String; 表示java/lang/String
[数组类型[[I 表示 int[][]
  1. 方法

Lpackage/name/ObjectName;->MethodName(III)Z

Lpackage/name/ObjectName;为一个类型,Method是方法名,III是三个整型类型参数,Z是返回类型

  boolean Method(int a,int b,int c){ }

  boolean res = ObjectName.Method(b,c,d);

method(I[[IILjava/lang/String;[Ljava/lang/Object;])Ljava/lang/String

  String method(int ,int[][], int, String, Object[])
  • # virtual methods 表示是虚方法

    # direct methods表示是一个实方法

  1. 指令

    具体看书吧,wps第79页

指令意义
if-eqif ( vA == vB )
if-neif ( vA != vB )
if-ltif ( vA < vB )
if-geif ( vA >= vB )
if-gtif ( vA > vB )
if-leif ( vA <= vB )
if-eqzif ( !vAA )
if-nezif ( vAA )
if-ltzif ( vAA < 0 )
if-lezif ( vAA <= 0)
if-gtzif ( vAA > 0 )
if-gezif ( vAA >= 0)
cmpl-float如果vBB大于vCC;结果为-1,如果等于,结果为0;如果小于结果为1
cmpg-float如果vBB大于vCC;结果为1,如果等于,结果为0;如果小于结果为-1
cmp-long如果vBB大于vCC,结果为1,如果等于,结果为0;如果小于,结果为-1

android常见文件格式

  1. apk
  • 生成流程
  1. dex文件结构

https://bbs.pediy.com/thread-255344.htm

dex文件结构体DexHeader意义
dex headermagic固定值 64 65 78 0a 30 33 35 00 (dex.035.)(8个字)
string_idschecksum校验和 (除掉自己和magic)(4个字)
type_idssignature签名(除掉自己和checksum magic)(20个字)
proto_idsfilesize整个Dex大小(下面都是4个字大小)
field_idsheaderSizeDexheader头大小
method_idsendianTag字节序标记,指定是大端还是小端。一般0x12345678,是小端
class_deflinkSize链接段大小,一般是0
datalinkOff链接段偏移,一般是0
link_datamapOffDexMapList的文件偏移
stringIdsSize
stringIdsOff放字符串的
typeIdsSize
typeIdsOff放类型的
protoIdsSize
protoIdsOff放方法声明的(返回类型+参数类型)
filedIdsSize
filedIdsOff放成员变量的
methodIdsSize
methodIdsOff
classDefsSize
classDefsOff
dataSize
dataOff
  • 验证与优化

问题:如果直接在虚拟机加载dex文件,而dex文件验证失败,则会导致部分资源(比如加载的native动态库)难以从内存释放。

专门验证与优化的工具dexopt,使Dalvik虚拟机在加载DEX文件时,通过制定的验证与优化选项来调用dexopt进行相应的验证和优化操作

  1. oat的文件格式
  • ART虚拟机:
    dex2oat把DEX的Dalvik字节编码编译成Native机器码后生成oat文件。Anroid 7.0后还引进了JIT(即使编译)

OAT也是个Android上的ELF文件,一个OAT文件必定包含oatdataoatexecoatlastword

  • oatdata符号指向的地址是OAT所在的.rodata段。这里存放着OAT文件头oatHeader、OAT的dex文件头OATDexFile、原始的DEX文件DexFile、OAT的DEX类OatClass等信息。

  • oatexec符号指向的地址是OAT所在的.text段,这里存放着编译生成的Native指令代码。

  • oatlastword符号指向的地址是OAT文件结束处在ELF中的文件偏移,通过它可以确定OAT文件内容在哪里结束。

oatdump命令可以把OAT文件中的DEX取出来

oatdump --oat-file=/data/dalvik-cache/arm/system@framework@boot.oat --export-dex-to=/data/local/tmp

  1. odex文件格式

ODEX与dex相比多了一下几个:

  • DexOptHeader: ODEX文件头,描述了ODEX文件的基本信息。
  • Dependences : 依赖库列表,描述ODEX文件加载时可能使用的依赖库。
  • ClassLookips:优化数据块的类索引列表信息,用于提高类搜索速度
  • RegisterMaps :优化数据块的寄存图信息,主要用于帮助Dalvik虚拟机进行精确的垃圾回收

将ODEX转换成DEX,则需要先将ODEX文件反编译为smali文件,再将smali文件编译成DEX文件。

静态分析

(主要使用工具: IDA, JEB,Androguard)

  1. 代码定位技巧
  • 入口分析法

根据AndroidManifest.xml中定义的主Activity,可以查看其所在类的OnCreate()方法。

如果要在程序组建之间传递全局变量,或者在Activity启动前做一些初始化工作,可以使用Application类。(在使用Application类时需要在程序中添加一个继承自android.app.Application类,然后重写他的onCreate()方法,最后在AndroidManifest.xml<application>中添加android:name属性,其值为android.app.Application。在该方法初始化的全局变量可以在Android的其他组件中访问。)

! Application类比程序其他所有类启动得都早,一些商业授权验证代码就会转移到该类中。因此在分析Android程序的过程中,需要首先查看程序是否有Application类,如果有,就要查看他的onCreate()是否做了一些影响逆向分析的工作

  • 信息反馈法

先运行目标程序,然后将程序运行时给出的反馈信息作为突破口在寻找关键代码。比如如果程序有弹出消息,则可以在反汇编代码中搜索Toast

  • 特征函数法

针对不同类型的APK程序,配合查阅SDK文档,找出其可能用得到的特征函数,然后定位程序的关键点。

动态分析

(常用工具: MobSF)(也可以使用JDB、JEB、IDA来进行动态调试)

  1. 动态分析技巧
  • 代码注入法

先反编译Android程序,然后在smali文件中手动添加Log调用代码,然后重新打包程序并运行,查看输出结果。

const-string v3,"SN"
invoke-static {v3,v0}, Landroid/util/Log;-> v(Ljava/lang/String;Ljava/lang/String;)I  #log.v()
  • 栈跟踪法

也属于代码注入的范围,就是追踪调用的函数。在代码里插入

  new Exception("print trace").printStackTrace();

对应的smali反汇编代码为

new-instance v0, Ljava/lang/Exception;
const-string v1, "print trace"
invoke-direct {v0,v1}, Ljava/lang/Expection;-><init>(Ljava/lang/String;)V
invoke-virtual {v0}, Ljava/lang/Execption;->printStackTrce()v

然后在Logcat窗口就可以看到输出的栈跟踪信息,或者在命令行输入adb logcat -s System.err:V *:W

  • Method Profiling

由于现在AS的Device Monitor基本上没啥用个了,所以就跳过吧

  • UI检查

Anrdoid系统提供给了一个dumpsys命令,可以使用该命令打印当前系统中运行的所有程序的组件信息

$ adb shell dumpsys activity top (top表示只打印当前栈顶部的Activity信息) 

ARM汇编

1.主要组成

  • 处理器类型声明:

可以使用.arch指定处理器构架,.cpu指定处理器的型号

  • 代码与数据声明:

一个完整的程序在编译后会有存放代码的代码段.text和用于存放数据的数据段.rodata。有些程序可能没有数据段,只有代码段。

  • 符号:

在汇编代码中可以直接引用外部符号。比如bl printf,直接引用了外部printf()函数。

在汇编代码中还可以使用.global声明全局变量。全局符号可以被程序外部引用

  • 子程序:

高级语言中的函数在汇编语言中称为子程序。

子程序由.type伪指令声明为%function的符号,如.type add,%function

子程序一般由.fnstart伪指令开始,由.fnend伪指令结束。

.global 子程序
.type 子程序名, %function
子程序名:       @@子程序名
.fnstart
    <----汇编指令---->
.fnend
  1. 寄存器

不分组寄存器 R0-R7 + 分组寄存器 R8-R14 + 程序计数器 R15 (PC) + 当前程序状态寄存器 CPSR

  • ARM寄存器有两种工作状态,即AEM状态与Thumb状态

    当处理器处于ARM状态时,会执行32位对齐的ARM指令

    当处理器处于Thumb状态时,执行的是16位对齐的Thumb指令。

不同状态的存储器命名也有一定的差异

ARMThumb
R0~R7R0~R7
CPSRCPSR
R11FP
R12IP
R13SP
R14LR
R15PC
  1. 寻址方式
  • 立即寻址
    MOV R0, #2
  • 寄存器寻址
    MOV R0, R1
  • 寄存器移位寻址
    MOV R0, R1, LSL #2  @ R0 = R1<<2 即 R0 =R1 * 4
指令操作
LSL逻辑左移,移位后对寄存器空出的低位补0
LSR逻辑右移,移位后对寄存器空出的高位补0
ASR算数右移,移位过程中算数符号位不变。如果源操作数为正数,则移位后对空出的高位补0,否则补1
ROR循环右移,移位后在移出的低位中填写移位空出的高位
RRX带扩展的循环右移,操作数右移1位,移位空出的高位用带有C标志的值填充
  • 寄存器间接寻址
    MOV R0, [ R1 ]
  • 基址寻址
    MOV R0, [R1, #-4] @ 把R1指向的地址减4后获得的地址中的值,赋值给R0
  • 多寄存器寻址
    LDMIA R0, { R1, R2, R3, R4} @ R1 = [R0], R2 = [ R0 + #4], R3 = [ R0 + #8], R4 = [ R0 + #12] 
  • 堆栈寻址

寻址指令: LDMFA/STMFALDMEA/STMEALDMFD/STMFDLDMED/STMED
STM/LDM是指令前缀、FD/ED/FA/EA是指令后缀

  STMFD SP!, { R1-R7, LR }  @将R1-R7和LR寄存器入栈,多用于保存子程序现场
  LDMFD SP!, { R1-R7, LR }  @将数据出栈,放入R1-R7寄存器,多用于子程序恢复现场
  • 快拷贝寻址
    将连续地址数据从存储器的某个位置赋值到另一个位置。
    寻址指令: LDMIA/STMIALDMDA/STMDALDMIB/STMIBLDMDB/STMDB
    STM/LDM是指令前缀、IA/DA/IB/DB是指令后缀
LDMIA R0!, { R1-R3 }    @从R0寄存器指向的存储单元中读取3个字,将其分别放到R1~R3寄存器中
STMIA R0!, { R1-R3 }    @将R1~R3寄存器内容存储到R0寄存器指向的存储单元中
  • 相对寻址
BL NEXT
    ...
NEXT:
    ...

软件壳

1. 第一代壳(动态加载型壳)

这个时期的夹克技术主要对本地的DEX文件、so库、资源文件进行加密,再运行时进行动态还原。

这样的壳通常会采取一些常见的反调试方法来阻止针对软件进行的动态调试

  • 缓存脱壳法

只要将/data/dalvik-cache目录下的ODEX文件去除,进行一次ODEX操作就可以完成脱壳

但是这种很少能起作用了

  • 内存Dump脱壳法

脱壳工具android-unpacker:通过find_magic_memory()方法读取/proc/pid/maps内存映射表,找到DEX所在的内存起始位置,然后用dump_memory()方法将内存Dump下来(peek_memory()?)

  • 动态调试脱壳法

本质还是内存Dump脱壳法,重要的是寻找合适的Dump时机,即DEX文件已经在内存中完全解密,且其中的代码还没有开始执行。

通过调试器给dvmDexFileOpenPartial()或者dexFileParse()方法设置断点,当执行到断点时,使用内存Dump脚本将其Dump下来,就可以完成脱壳。

甚至由于dexFileParse()中用了memcmp(data, DEX_OPT_MAGIC , 4)进行DEX文件头信息的判断,所以可以通过hook这个函数,获得data也就是dex文件的起始地址。

  • Hook脱壳法

和动态调试脱壳一样,不同在于Hook则是使用Hook框架,配合Hook代码实现工具自动化脱壳。

书中的例子以Cydia Substrate框架为例,使用了MSHookFunction()来进行hook

  • 系统定制脱壳法

与Hook脱壳法类似。针对第一代壳在dvmDexFileOpenPartial()或者dexFileParse()设断点,修改在源码中的实现,然后编译修改后的代码,以刷机方式实现脱壳

2. 第二代壳(代码抽取型壳)

除了第一代的技术特点,还提取了DEX文件中所有的方法实现并在本地进行加密保存。APK运行时,会调用软件壳的Native的解密方法对DEX文件中的方法进行还原。 ( 比如将DEX文件中的DexCode提取后填零,当APK运行时会在内存中进行动态解密,所有的解密方法内容指针都位于DEX文件结构体外部的内存中 ) 。

这种壳会通过在DEX方法执行前解密、执行后加密的技术手段,防止以内存Dump的方式对DEX文件进行脱壳。即便DEX已经加载在内存中,也仍然处于加密状态,但是所有DEX方法在运行时是解密的

这样的壳通常会采用多进程保护、API Hook等反调试与反内存Dump技术

  • 内存重组脱壳法

通过解析内存中的DEX文件的格式,将其重新组合成DEX文件。在APK运行后的任意时刻使用kill命令让程序暂停,然后从内存中将其重组并Dump出来。

libdvm.so → gDvm → userDexFiles → pEntries.isDex(为truepRawDexFile有效) → pRawDexFile → pDvmDex → pDexFile

https://github.com/CvvT/dumpDex

  • Hook脱壳法

不需要暂停程序的运行,重点是查找合适的Hook点

一个合适的Hook点是libdvm.so中的dvmCallMethodV()方法,这个方法是用来启动java方法的。当确定是Application类的onCreate()方法后,获取ClassObject类型的类对象指针,可以通过pDvmDex字段选取DvmDex信息,然后dump出dex文件。

  • 系统定制脱壳法

脱壳工具DexHunter:https://github.com/zyq8709/DexHunter

3. 第三代壳(代码混淆壳,就是vmp壳)

在基于LLVM Pass实现的代码混淆壳、虚拟执行壳中使用较多的技术,有指令变换、花指令混淆、指令膨胀、代码流程混淆等。

这里的第三代壳指的是对原生程序代码进行混淆的第三代壳。



注入

ptrace

- 看代码心得

  1. 将当前的hook进程attach到目标进程,然后保存当前远程进程的上下文寄存器环境
  2. 获取远程进程中的mmap函数,申请一块内存空间,获得远程进程中申请的内存空间的首地址(在调用远程函数的时候arm_lr会设为0,这样远程进程在实行要调用的远程函数时就会因为返回地址有问题而暂停,这时候就可以保存他的寄存器上下文地址;返回值可以由arm_r0中获得)
  3. 获取远程进程中的dlopen、dlsym和dlclose,为了链接我们注入模块so文件(其中,传递参数的时候就需要将加载so库的路径和目标函数名写进远程进程内存中,方便远程函数调用)
  • dlopen返回so文件在链接后的内存地址
  • dlsym返回目标函数在内存里的地址
  1. 根据dlsym的返回值,远程callptrace_call远程进程中的目标函数
  2. 恢复最初的上下文寄存器环境
  3. detach远程进程

我看的代码里没用到dlcose函数,应该可用可不用吧,一般为了安全会用它吧

  • 总结

    1. 还是要去看Linux编程,好多地方都是连蒙带猜。
    2. 就是开始call远程函数后,ARM_lr(即函数返回地址)一直都是0,在第五步之前一直没改过;让目标进程继续运行用的也ptrace_cont
    3. 用ptrace进行hook的时候是将对hook函数的一些操作写在so的入口函数里的,dlsym后的函数调用也是调用的这个入口函数。
    4. 我看的2048的那个InlineHook例子是在入口函数中,dlopen目标函数在的so文件,然后再通过dlsym获得目标hook函数的地址。最终的Hook操作是通过libsubstrate.so中的MSHookFunction函数来进行操作的,相当于是替换了目标函数。


Hook

基于异常的Hook

  • 基本原理:利用SIGILL异常机制,对想要监控的地址设置一条非法指令。当程序执行到非法指令的位置就会回调预先设置好的异常处理函数。我们的Hook操作就是在异常处理函数中进行的。

1.初期设置

  1. 静态分析,找到我们要改写的目标指令的地址
  2. 利用sigaction结构体,对信号修改处理进行一定的设置
	struct sigaction {
    void (*sa_handler)(int);  
    void (*sa_sigaction)(int, siginfo_t *, void *);这边的sa_sigaction就是自己定义的处理异常的hook函数
	sigset_t sa_mask;//用来设置在处理该信号时暂时将sa_mask指定的信号集搁置,这边需要使用sigemptyset进行置空初始化
    int sa_flags;//设置为SA_SIGINFO ,指定处理函数为sigaction的sa_sigaction函数
    void (*sa_restorer)(void);
	}
  //我们这边信号需要接收附加信息,就必须给sa_sigaction赋信号处理函数指针,同时还要给sa_flags赋SA_SIGINFO
  1. 保存目标地址的opcode,并改写为非法指令
指令集对应非法指令
ARM0xe7f***f*
Thumb0xde**
Thumb20xf7f*a***

但是我在他书上的代码写得是0xf7fXaXXX才是arm的非法指令。。。而且他代码在进行辨别的时候只区分了Thumb和arm,没看到Thumb2;而且他那边给uiArmillegalValue赋的值也是0x7f000f0,不懂到底哪个才是对的

2. 异常回调的HookHandler

  1. ExceptionHookHandler有三个参数,signum为产生异常的信号量,Ssiginfo为信号信息,context为上下文句柄。其中寄存器状态在ucontext->uc_mcontext中存着。

  2. 进行Hook操作

  3. 一开始肯定是运行到目标地址的,恢复目标地址。

    为了之后能够再一次进行这个异常Hook,我们需要在执行下一条指令的时候,再将目标地址改写为非法指令的。所以我们需要再将下一条指令改写为非法指令进行这个操作。当实行到下一条指令的时候,将目标地址改写为非法指令。

    指令地址也是有区别的,arm是+2,Thumb有些是+2有些是+4

指令集长度位[15:13]位[12:11]
16bit Thumb指令(+2)16位非111任意
16bit Thumb无条件跳转令(+2)16位11100
32bit Thumb指令(+4)32位111非00

记得在设置非法指令(即往里面写东西的时候)的时候要增加代码段的可写权限

sysconf(_SC_PAGESIZE); //获取系统页面大小,即代码段大小(?)

mprotect((void *) addr, (pagesize), PROT_READ | PROT_WRITE | PROT_EXEC);


导入表HOOK

  • 基本原理:以so文件的导入作为目标进行函数指针替换
  1. 通过静态分析找到要hook的目标函数地址

  2. 打开要寻找got表的so文件(就普通的文件读取'rb'

  3. 找到.got表的起始位置和大小(起始位置是相对于模块基址的偏移量)

    ① 在elf header中找到存放所有节的节名的.shstrtab区(我们是在节头表里找的)

    iShstrtabOffset = 
        Elf32_ElfHeader.e_shoff +    //节区偏移
        Elf32_ElfHeader.e_shstrndx * // 节区名字符串表的节区内的偏移(第几个)
        Elf32_ElfHeader.e_shentsize;//节区每个项目的大小
    

    ② 在.shstrtab节里找到.got

    • Elf32_SectionHeader.sh_name中指明了在.shstrtab区里本节名的存储偏移量(是指在.shstrtab中的)
    • Elf32_SectionHeader.sh_type == SHT_PROGBITS
    • 节中的sh_addrsh_size分别存储了节的地址和大小
  4. 获得目标模块基址后,在.got节里根据目标函数的地址,找到存放目标函数的地址(函数地址与函数地址之间+4)。修改地址权限,将新地址写进去。


Inline Hook

  • 基本原理:直接修改需要Hook的位置的指令代码,并让其跳转到桩函数中。在桩函数里会处理寄存器信息,并调用自定义的桩函数。处理完桩函数后,跳转到存有原指令的地方并执行,然后再跳转回原来的函数。
//hook点信息
typedef struct tagINLINEHOOKINFO{
    void *pHookAddr;                //hook的地址
    void *pStubShellCodeAddr;            //shellcode stub的地址
    void (*onCallBack)(struct pt_regs *);       //回调函数,跳转过去的函数地址(这边参数不一定只是寄存器的值,见机行事)
    void ** ppOldFuncAddr;             //shellcode 中存放old function的地址
    BYTE szbyBackupOpcodes[OPCODEMAXLEN];    //原来的opcodes
} INLINE_HOOK_INFO;
  1. 先通过静态分析,确定要hook的地址,对INLINE_HOOK_INFO结构体中的pHookAddronCallBack进行初始化,然后对目标地址进行hook(hook的时候要注意区分arm和thumb)

  2. InitArmHookInfo()备份原opcode到szbyBackupOpcodes

  3. 通过BuildStub()构造stub函数,以及回调老函数(这边老函数指的是被hook替换的原opcode)

    在代码里,是借助shellcode来完成这一环的。

    ① 先malloc() 分配空间并使用memcpy()把shellcode写进去,还要更改他为可读可写可执行属性PROT_READ | PROT_WRITE | PROT_EXEC ( mprotect() )

    ② 将shellcode中的_hookstub_function_addr_s这个跳到用户自定义函数的地址进行修改和填充。并且在INLINE_HOOK_INFO结构体中的pStubShellCodeAddr进行内存shellcode的代码地址备份。

  4. 通过 BuildOldFunction() 构造16比特的原opcode的指令块,并将其改为可读可写可执行属性PROT_READ | PROT_WRITE | PROT_EXEC ( mprotect() ) 。

    先将pstInlineHook->szbyBackupOpcodes中的备份原opcode写进指令块(占8个比特),然后再填充跳转指令,要跳转的地址为 pstInlineHook->pHookAddr + 8
    最后填充pstInlineHook->ppOldFuncAddr回调地址。

    填充完跳转地址后要记得用cacheflush进行刷新,防止崩溃

      LDR PC, [PC, #-4]      @ LDR PC, [PC, #-4]对应的机器码为:0xE51FF004
      addr

http://ele7enxxh.com/Android-Arm-Inline-Hook.html

由于tx给的示例没有进行指令修复这个东西,因为如果涉及到跳转指令,pc可能会有变化。指令修复等看了ele7enxxh的分析再填充。

  1. 通过RebuildHookTarget()修改目标hook地址的操作权限,然后将pstInlineHook->pStubShellCodeAddr中的地址作为跳转指令的目标地址进行填充

记得要cacheflush,防止崩溃

注意事项

  • Arm模式与Thumb模式的区别

    在对一个函数进行Inline Hook时,首先需要判断当前函数指令是Arm指令还是Thumb指令,指令使用目标地址值的bit[0]来确定目标地址的指令类型

    bit[0]的值为1时,目标程序为Thumb指令;bit[0]值为0时,目标程序为ARM指令。

  • hook目标指令的覆盖要放到最后,这样可以防止在Hook一些频繁执行的函数时出现崩溃。

    崩溃的原因: 在设置Inline Hook的过程中,程序正常运行了Hook逻辑,而桩函数或者原函数(就是原opcocde的那个指令块)未构造成功而倒是崩溃。

  • 在tx给的另一个例子中,他是通过调用’/data/local/tmp/main/libsubstrate.so’中的’MSHookFunction’的函数来进行的。



使用IDA进行最简单的动态调试脱壳

  • 基本原理:根据APP应用的启动流程可以知道,dex文件最终会被加载进内存,所以思路就是,只要在载入内存前,找到并dump出来即可

  • 准备:

    1、root好的测试机和模拟器(x86应该也可以)

    2、IDA在6.6以上版本 或者手机为5.0以下 否则会出现 pie异常(抄来的)

1. 启动ida 端口监听

  1. 将IDA目录下的android_server文件push进/data/local下(只要让文件可执行就行)

  2. 赋予权限chmod 777 /data/local/android_server

  3. 执行./data/local/android_server(如果是x86环境,IDA7.0有对应的android_x86_server)

  4. 输入adb forward tcp:23946 tcp:23946 ,让手机中的23946端口映射到PC本地23946端口,用来IDA和手机通信连接

  5. 执行adb shell am start –D –n com.example.protectapp/org.isclab.shh.protectapp.MainActivity包名/入口类 一定要写完整),让需要脱壳的程序在开启入口活动前进入等待调试连接状态。

2. IDA下断点

  1. ARM环境:Debugger->Remote ARMLinux/Android debugger

    x86环境:Debugger->Remote Linux debugger

  2. hostname:127.0.0.1 port:23946

    Debugger setup Event选项卡中选第3、4、5个

    再选择目标进程进行attach(进程号可以用ps命令查)

  3. 对加载dex文件的函数下断点

    1. dvm环境

      选择libdvm.so文件(modules模块下,也可以经过Debugger->Debugger windows->Modules List),双击进入

      选择 dvmDexFileOpenPartialPKviPP6DvmDex 函数并按F2下断点(这个函数第一个参数是dex的内存首地址,第二个是dex的长度)

    2. art环境

      选择libart.so文件,双击进入

      搜索 Openmemory 关键字的函数,并按下F2下断点(这个函数第一个参数好像也是dex的内存首地址,但是好像8.0后的so文件中没有这个函数了)

3. 运行程序到下断处

  1. 输入 adb forward tcp:8700 jdwp:12876(12876为attach目标进程的pid)

  2. 输入jdb –connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8700 jdb转发运行程序

  3. 按F9让程序继续运行(看到有的说重点再是让进程运行起来之后,再用jdb连接手机虚拟机,否则连不起来,会阻塞,会无法添加到vm,顺序很重要这就是我无法dump dex的原因吗?)

  • 有些教程说要使用到Android Device Monitor,但是这个东西感觉已经名存实亡了,点开啥都没有,更别说选中要调试的目标进程了

如果有反调试是不能运行到断点的,下别的断点 1

  1. 打开寄存器窗口,可以看到有寄存器存着dex的地址(在我看的arm环境的教程里写的直接就是.dex文件,我自己在x86运行的时候看到EAX存着的是.odex文件,但是具体看他的文件开头的魔数又是dex文件的)

4. dump dex

dump dex可以有两种方法,我觉得第二种更简单粗暴一点,但是我两种都没有成功把dex dump下来。。。

  1. 直接调试时候断点到libdvm.so加载dex时候,找到在内存中的起始点r0和大小r1,然后通过idaShift +F2运行脚本

这种只能dump安装运行时apk中classes.dex,不方便dump多次dex加载或动态加载时定位到起始地址

//这种是在dvm环境下的,因为dex的首地址和长度都在寄存器里
static main(void)
{
  auto fp, begin, end, dexbyte;
  fp = fopen("C:\\dump.dex", "wb");
  begin = r0;
  end = r0 + r1;
  for ( dexbyte = begin; dexbyte < end; dexbyte ++ )
      fputc(Byte(dexbyte), fp);
}

  1. 直接运行apk之后通过首先通过cat /proc/[pid]/maps查看目标dex文件所在的内存地址,这样就可以查找所有加载过的dex内存映射地址,不论是在什么时候加载的,只要运行了,就必会在内存中找到相应的映射地址
static main(void)
{
  auto fp, begin, end, dexbyte;
  fp = fopen("C:\\dump.dex", "wb");
  begin = 0x4b92f000;
  end = 0x4bce7000;
  for ( dexbyte = begin; dexbyte < end; dexbyte ++ )
      fputc(Byte(dexbyte), fp);
}

- 总结

  • 真的好坑啊,x86都还给老师了。而且IDA那个并不是dump错了,而是根本就没办法dump下来,脚本执行后嘛也没有,不知道是什么原因。

  • 现在基本没看到基于8.0以上的脱壳,所以可能还需要去看系统源码才能知道从哪里拦截dex文件

  • odex和dex结构啥的要重新看看,我已经忘得七七八八了;arm指令集也是

- 附

  1. 对于dump出来的文件是odex直接打开看不够清晰(我就说嘛),如用GDA工具打开里面都是类似汇编语言。这时就需要用到baksmali.jarsmali.jar来转换成dex文件,可以直接方便查阅。

  2. 这只是最简单脱壳方法,很多高级壳会动态修改dex的结构体,比如将codeoffset指向内存中的其他地址,这样的话你dump出来的dex文件其实是不完整的,因为代码段保存在了内存中的其他位置。那么针对这种反调试情况下以及其他阻碍动态调试下,dump内容和ida调试都很难进行下去

  3. 网上有这样的文章:利用开源脱壳工具DexExtractor来脱壳。它的原理就是修改系统的DexFile.cpp源码,在解析dex的函数开头处加上自己的dumpdex逻辑。 这样不论怎么绕,只要加载了dex就可以dump出来,可以研究下,传送门:http://blog.csdn.net/jiangwei0910410003/article/details/54409957

  • IDA 调试快捷键
按键作用
F2下断点
F4运行到光标处
F7单步步入调试
F8单步步过调试
Ctrl + F7运行到返回

NOP指令 00 00 A0 E1 (MOV r0, r0

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值