2016年12月28日星期三

Android 逆向学习笔记 (七)- ELF 简单介绍

现在越来越多的安卓 app 为了保证软件的安全性,会使用 NDK 来进行 Native 开发,最后会产生一个 .so 这样的 ELF 文件,通过动态加载这个 ELF 文件调用隐藏在 .so 文件里的方法。

不过有的时候经过处理的 ELF 文件直接拖入 IDA 会报错,像下面这样,如果强行在 IDA 里查看的话会丧失很多有用信息,比较麻烦。



可以看 IDA 载入 ELF 的时候报了两个错, 1. SHT 入口位置不对,2. SHT 表大小或者偏移量不对。归纳来说就是 IDA 加载 ELF 的 SHT( Section Header Table) 内容出了错误。不过为什么出了这样的错误对安卓程序来说没有影响呢?

这是因为 Java 加载 .so 这样的 ELF 文件时不参照 Section Header Table 里的内容,所以开发者可以索性直接全部删除这个片段的内容,加大分析难度。这个时候如果需要分析的话最好可以先修复 ELF 文件结构。

ELF Structure

ELF 的主要有下面三个索引表片段。

1. ELF Header : 在 ELF 文件的最开始,保存了文件的路线图,描述该文件的组织情况。
2. Program Header Table : 告诉系统如何创建进程映像。用来构造进程映像的目标文件必须具有程序头部表,可重定位文件(e_type = 1)不需要这个表。
3. Section Header Table : 包含了描述文件节区的信息,每个节区在表中都有一项,每一项给出诸如节区名称、节区大小这类信息。用于链接的目标文件必须包含节区头部表,其他目标文件可以有,也可以没有这个表。

下面简单介绍一下各个索引表片段的内容,使用工具有 readelf(OS X - greadelf)和 xxd。
-h 选项代表显示 ELF Header 的内容,从这里可以看到 Size of this header: 52 (bytes),即 ELF Header 的长度为 52 = 0x34,使用 xxd 查看 ELF 前 52 bytes 的内容。
这里对照上图查看:
0x00000000: 457f 464c 01     01   0001      0000 0000 0000 0000
                      .ELF          |32    |LE|Version |Padding
0x00000010: 0003    0028             0001 0000 0000 0000 0034 0000
                      e_type |e_machine |e_version |e_entry   |e_phoff
0x00000020: 118c 0002 0000 0500 0034         0020               0007            0028
                      e_shoff     |e_flags      |e_ehsize |e_phentsize |e_phnum   |e_shentsize
0x00000030: 0016         0015
                      e_shnum  |e_shstrndx

具体含义不一一说明,可以看到整个 ELF 的结构在 ELF Header 就比较清楚了,这里跳过 Program Header Table 不表,因为我比较关注的是 Section Header Table。

通过 ELF Header 里 e_shoff 可以看到 SHT 的偏移量为 0x02118c,所以这里直接往 0x2118c 看。


可以看到 SHT 节头表起始为空节里没有数据,继续往下看第一个节。


0x000211b4 : 000b 0000     000b 0000 0002 0000 0114 0000
                        s_name_off  |s_type      |s_flags     |s_addr
0x000211c4 : 0114 0000 0740 0000  0002 0000 0001 0000
                       s_offset     | s_size       |s_link       |s_info
0x000311d4 : 0004 0000    0010 0000
                       s_addralign |s_entsize

对照下图查看 Section Header Table 的结构也比较清楚了,

ELF Fix

因为 SHT 在安卓引用 .so 文件时无关紧要,所以有两种保护情况:
1. 将 ELF Header 里和 SHT 有关的内容修改成错误的内容,但是不动 SHT。
2. 直接将 ELF 中的 SHT 删除。

第一种情况因为保留了正确的 SHT 所以只需要修复 ELF Header 就行,这时有下面几个需要注意的点:
1. e_shentsize = 0x28 = 40
2. e_shnum = (total_size - e_shoff) / e_shensize
3. e_shstrndx = e_shnum - 1

如果上述无法修复,那么就需要和 SHT 被删除的情况一样处理,可以使用附件中给出的一个小工具,此时修复原理比较复杂,可以在参考链接中查看具体内容。

* 附件 Github note-7。

参考链接:
Read More

2016年12月13日星期二

ANDROID 逆向实例(四)- com.qtfreet.crackme001

现在很多 apk 在开发中为了保证代码不被轻易逆向出来会对关键函数采用 ndk 的方式用 c 语言和 java 代码进行交互,所以学会分析 .so 文件(加载流程、方式等)对安卓逆向来说是比较重要的。

这里借助一个 CrackMe.apk 实例来帮忙分析。反编译 apk 文件得到 smali 代码,提取关键代码如下:

# direct methods
.method static constructor ()V
    .locals 2

    .prologue
    .line 15
    sget v0, Landroid/os/Build$VERSION;->SDK_INT:I

    const/16 v1, 0x14

    if-ge v0, v1, :cond_0

    .line 16
    const-string v0, "qtfreet"

    invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V

    .line 20
    :cond_0
    return-void
.end method

...

# virtual methods
.method public native check(Ljava/lang/String;)Z
.end method

分析后可以看到调用了 .so 文件里定义的 check 函数,然后这个 check 函数又和 button 的 OnClick 事件绑定,可以判断 check 则是判断用户输入 key 是否正确的函数。

用 IDA 对 libqtfreet.so 进行分析,首先先看看有没有 JAVA_com_qtfreet_crackme001_check 这样类似的函数,如果存在的话就说明 .so 文件里面没有对 check 函数进行动态注册,JAVA_com_qtfreet_crackme001_check 就是 smali 里面调用的 check 函数原型,直接分析即可。

如果没有发现上面类似函数的话则说明 smali 调用的 check 函数进行了动态注册,此时需要找到注册的位置。

这里需要注意一个问题,就是安卓加载 .so 文件的流程。安卓对 .so 文件加载时首先会查看  .init 或 .init_array 段是否存在,如果存在那么就先运行这段的内容,如果不存在的话那么就检查是否存在 JNI_Onload 存在则执行。

另外,因为 .init 或者 .init_array 在 IDA 动态调试的时候是不会显示出来的,所以需要静态分析出这两段的偏移量然后动态调试的时候计算出绝对位置,然后在 make code(快捷键:c),这样才可以看到该段内的代码内容。

查看 .init_array 段的地址有两种办法:
1.可以使用 IDA 加载 .so 文件,新建 “Segments” 视图,这里会列出不同类型的代码段信息,如下图所示。



2.可以使用二进制工具 readelf 来查看 .so 文件的结构,在 OS X 上面可以使用 greadelf 代替。


可以看到这里两个都指示 .init_array 存在,并且偏移量为 0x20790,IDA 定位到该位置查看如图。


定义了一些东西,这个不必太在意具体是什么内容后面用到的话再看就行了, .init_array 执行完之后就跳到 JNI_Onload 里面执行,IDA 跳到 JNI_Onload 之后 F5 查看伪代码。

这里会有一些小问题,就是 jni.h 头文件里的结构体因为 IDA 里面缺乏参考所以不能正确的对应到他们的类型,需要手动修改数据类型(IDA 快捷键 Y),例如 JNI_Onload(int a1) 这里的 a1 就应该修改成 JavaVM*,后面的一些则需要修改成 JNIEnv*。修改好之后的代码如下图所示:


看到高亮的 RegisterNatives,进去第三个参数 off_21458 位置查看保存的内容。


可以看到确实是 check 函数进行动态注册的地方,并且可以知道 check 真正的函数代码在 sub_8A64+1 位置(具体为什么可以参考 RegisterNative 源码),IDA 跟进 F5。


可以看到这里最后执行了一个 memcmp 所以可以确定 sub_8A64 开始是关键的判断代码,直接将 memcmp 的返回值修改即可成功破解 crackme。如果需要逆出 key 则需要结合之前的初始化变量和函数来看,可以自己试一试逆向 key 不过多阐述了。

ps: 文中省略了一些 Anti Anti-Debug 的内容,因为这个 crackme 的反调试并不复杂而且通过单一函数实现,可以直接将反调试函数 “movs r0,r0” 掉。

* crackme.apk 可以在 Github 上找到。
Read More

2016年11月24日星期四

ANDROID 逆向实例(三)- Gamex 恶意软件

流程太多 就不写了... 其实倒不是很复杂 代码没有做混淆和加壳还是比较好分析的

自己释放了几次得到最后的恶意 apk 执行 有兴趣的可以试试看 建议分析以加强对 smali 和安卓的了解 ~

* 相关文件上传到 Github 了

Reference

Read More

2016年10月28日星期五

Android 逆向学习笔记 (六)- 安卓脱壳之 dvmDexFileOpenPartial

加壳

学习脱壳之前,首先先学习一下什么是加壳,加壳是在二进制的程序中植入一段代码,在运行的时候这段植入的代码会优先取得程序的控制权,做一些额外的工作(将原来的代码还原出来执行,以达到保护原来的代码直接暴露的问题)。大多数病毒就是基于此原理。

Android 作为新兴出现的平台,加壳方式不像 PC 端一样成熟,本次分析常见的 Dex 加壳和脱壳方式。在此之前,首先需要简单了解 Dex 文件的结构如下表所示。

字段名称偏移值长度描述
magic0x08'Magic'值,即魔数字段,格式如”dex/n035/0”,其中的035表示结构的版本。
checksum0x84校验码。
signature0xC20SHA-1签名。
file_size0x204Dex文件的总长度。
header_size0x244文件头长度,009版本=0x5C,035版本=0x70。
endian_tag0x284标识字节顺序的常量,根据这个常量可以判断文件是否交换了字节顺序,缺省情况下=0x78563412。
link_size0x2C4连接段的大小,如果为0就表示是静态连接。
link_off0x304连接段的开始位置,从本文件头开始算起。如果连接段的大小为0,这里也是0。
map_off0x344map数据基地址。
string_ids_size0x384字符串列表的字符串个数。
string_ids_off0x3C4字符串列表表基地址。
type_ids_size0x404类型列表里类型个数。
type_ids_off0x444类型列表基地址。
proto_ids_size0x484原型列表里原型个数。
proto_ids_off0x4C4原型列表基地址。
field_ids_size0x504字段列表里字段个数。
field_ids_off0x544字段列表基地址。
method_ids_size0x584方法列表里方法个数。
method_ids_off0x5C4方法列表基地址。
class_defs_size0x604类定义类表中类的个数。
class_defs_off0x644类定义列表基地址。
data_size0x684数据段的大小,必须以4字节对齐。
data_off0x6C4数据段基地址


可以看到 Dex 文件都是以 "dex/n035/0" 开头的,然后后面跟着一堆记录 Dex 文件基本信息的数据,这些不管了,下图有简单的展示,更加具体的 Dex 文件结构可以到给出的参考链接中查看。


举一个简单的加壳例子简单了解加壳过程,流程图如下:


可以看到此时原来的 app 被加密之后放入 “加密 Source DEX 数据” 段,在 “解壳 DEX Body” 段的后面,所以当程序执行时先执行到 “解壳 DEX Body” 部分后将 “加密 Source DEX 数据” 部分解密,然后再执行解密后的内容(即是原 app 的内容)。

加壳程序工作流程:                
1、加密源程序 APK 文件为解壳数据                
2、把解壳数据写入解壳程序 Dex 文件末尾,并在文件尾部添加解壳数据的大小。                
3、修改解壳程序 DEX 头中 checksum、 signature 和 file_size 头信息。                
4、修改源程序 AndroidMainfest.xml 文件并覆盖解壳程序 AndroidMainfest.xml 文件。
        

解壳DEX程序工作流程:                
1、读取 DEX 文件末尾数据获取借壳数据长度。                
2、从 DEX 文件读取解壳数据,解密解壳数据。以文件形式保存解密数据到 a.APK 文件                
3、通过 DexClassLoader 动态加载 a.apk。

脱壳

本次脱壳针对 classes.dex 的重加载,重加载时会动态调用到 libdvm.so 里面的 dvmDexFileOpenPartial 这个函数,这个时候只需要用 IDA 的动态调试并且下断点就行了。

android_server -> adb forward -> adb shell am start -D -n -> ida attach -> add breakpoint -> ddms -> jdb
IDA 动态调试不详细说了,另外提一下,因为这里先运行的是壳程序所以 adb 启动程序的时候需要按照 AndroidManifest.xml 里面显示的 :
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.ali.tg.testapp">
启动 com.ali.tg.testapp/.MainActivity。


在 dvmDexFileOpenPartial 下好断点之后一路点击 IDA 的绿色小箭头运行,直到在 dvmDexFileOpenPartial 这个地方停下来。



然后 Hex-View 设置和 R0 同步,可以看到熟悉的 “dex\n035\0”,开始说到的 Dex 头,然后再设置成和 R1 同步,可以看到 R1 的值。


为什么要着重强调这两个值呢,因为通过函数原型  dvmDexFileOpenPartial(addr, len, &pDvmDex) 可以知道, addr(R0) 保存的是原来 apk classes.dex 的文件起始地址,len(R1) 保存的是原来 apk classes.dex 的文件长度。

然后依次 dump,可以将原 classes.dex 恢复出来,这里有一个 dump 的小脚本,使用 shift + F2 执行。

auto fp, dex_addr, end_addr;
fp = fopen("classes.dex", "wb");
end_addr = r0 + r1;
for (dex_addr = r0; dex_addr < end_addr; dex_addr++)
    fputc(Byte(dex_addr), fp);

然后原 Dex 就 dump 出来了,然后直接反编译成 smali 文件查看即可。这里有一个小问题需要提一下:

有时断 dexFileParse 会有更好的效果,有的壳判断系统版本,>=4.0则使用dvmRawDexFileOpenArray 函数,<4 的话才用 dvmDexFileOpenPartial。

reference

Read More

2016年10月25日星期二

Android 逆向学习笔记 (五)- ARM 和 Thumb 指令集学习

学习安卓逆向,ARM 和 Thumb 是两个绕不开的内容。Thumb 可以看作是 ARM 指令集压缩形式的子集,是针对指令长度问题而提出的,它具有 16 位的指令长度(ARM:32 位),但操作数、指令地址这两项 Thumb 均和 ARM 相同,为 32 位,所以如果想要区分当前指令是 ARM 还是 Thumb 通过指令长度就可以知道。

Thumb 不是一个完整的指令体系结构,必须和 ARM 指令集结合才能正常工作。安卓逆向总是由 CODE32 声明的 ARM 指令集开始,执行的过程中可以通过 BX 指令切换到 CODE16 声明的 Thumb 指令集。

ARM 程序分析(I)

源码:

#include 
#include 

void (*func)();
  
void sub() {
  void *p = dlopen("libxxx.so", RTLD_NOW);
  if (!p) {
    return;
  }
  else {
    // myfn是libxxx.so的一个函数,作用是打印"call myfn...!"
    func = (void (*)())dlsym(p,"myfn");
    func();
  }
}

int main()
{
  printf("RTLD_NOW=%d\n", RTLD_NOW);
  sub();
  return 0;
}

反汇编之后:



sub:
1.    84a0:  4808        ldr  r0, [pc, #32]  ; (84c4 )
; r0 = [pc + 32 + 4] = [0x84a0 + 0x24] = [0x84c4] = 0x48
; 这里之所以多 +4 是因为 Thumb 涉及 PC 时需要 +4,而 ARM 计算涉及 PC 时需要 +8

2.    84a2:  2100        movs  r1, #0
; r1 = 0

3.    84a4:  b510        push  {r4, lr}
; r4 和 lr 入栈,r4 属于局部变量而 lr 保存了 sub 函数的返回地址

4.    84a6:  4c08        ldr  r4, [pc, #32]  ; (84c8 )
; r4 = [pc + 32 + 4] = [0x84a6 + 0x24] = [0x8aca] = [0x84c8] = 0x0c26
; 这里的 [0x8aca] 不属于标准起始,必须对齐到 (0, 4, 8, c),所以 [0x84ca] 变成 [0x84c8]

5.    84a8:  4478        add  r0, pc
; r0 = r0 + pc = 0x48 + 0x84a8 + 4 = 0x84f4 -> [0x84f4] = 'libxxx.so' 

6.    84aa:  447c        add  r4, pc
; r4 = r4 + pc = 0x0c26 + 0x84aa + 4 = 0x90d4

7.    84ac:  f7ff efc8   blx  8440 
; 执行 dlopen 函数,可以看到此时的指令长度为 (0x84ac ~ 0x84b0) = 4 bytes = 32 bit 为 ARM 指令
; 而之前指令长度可以看到尾 2 bytes = 16 bit,为 Thumb 指令

8.    84b0:  b138        cbz  r0, 84c2 
; r0 不为 0 则跳转执行 0x84c2 位置的 printf 函数,此时 r0 为上一条指令 dlopen 的返回结果

9.    84b2:  4906        ldr  r1, [pc, #24]  ; (84cc )
; r1 = [0x84b2 + 24 + 4] = [0x84b2 + 0x1c] = [0x84ce] = [0x84cc] = 0x46

10.   84b4:  4479        add  r1, pc
; r1 = 0x46 + 0x84b4 + 4= 0x84fe -> [0x84fe] = 'myfn'

11.   84b6:  f7ff efca   blx  844c 
; 切换到 ARM 指令执行 dlsym

12.   84ba:  4905        ldr  r1, [pc, #20]  ; (84d0 )
; r1 = [pc + 20 + 4] = [0x84ba + 0x18] = [0x84d2] = [0x84d0] = 0xfffffffc

13.   84bc:  5863        ldr  r3, [r4, r1]
; r3 = [r4 + r1] = [0x90d4 + 0xfffffffc] = [0x90d4 - 0x04] = [0x90d0] = 0x90f0
; 图中没有给出 [0x90d0] 的内容,原文中截图截少了,这里的 0x90f0 即是 myfn 函数的地址,下面传入 r0 并执行

14.   84be:  6018        str  r0, [r3, #0]
15.   84c0:  4780        blx  r0
16.   84c2:  bd10        pop  {r4, pc}

ARM 程序分析(II)

#include <stdio.h>
#include <string.h>
int main(int argc,char *argv[])
{
  char name[]="helloworld";
  int  keys[]={0xb,0x1f,0x19,0x19,0x49,0xb,0xb,0xb,0x31,0x53};
  char Thekeys[11];
  int i;
  for(i=0;i<10;i++)
  {
    keys[i]^=7;
    keys[i]=keys[i]/6;
    keys[i]+=22;
    keys[i]-=24;
    keys[i]^=name[i];
  }
  for(i=0;i<10;i++)
  {
    Thekeys[i]=keys[i];
  }
  Thekeys[i]=0;
  if(!strcmp(Thekeys,argv[1]))
    printf("Good Work,you have Successed!");
  else
    printf("NO,you are failed!");
  return 0;
}

反编译之后,流程图下所示,可以看到起点是 start 函数,但是没有发现名称为 main 的函数。那怎么确定改名之后的 main 函数呢?可以通过搜索 “__libc_init” ,定位到调用位置,代码类似如下:
.text:0000847C start
.text:0000847C
.text:0000847C var_14          = -0x14
.text:0000847C var_10          = -0x10
.text:0000847C var_C           = -0xC
.text:0000847C var_8           = -8
.text:0000847C
.text:0000847C                 LDR     R12, =(_GLOBAL_OFFSET_TABLE_ - 0x8498)
.text:00008480                 STMFD   SP!, {R11,LR}
.text:00008484                 LDR     R3, =(off_AF98 - 0xAFCC)
.text:00008488                 ADD     R11, SP, #4
.text:0000848C                 SUB     SP, SP, #0x10
.text:00008490                 ADD     R12, PC, R12 ; _GLOBAL_OFFSET_TABLE_
.text:00008494                 LDR     R3, [R12,R3] ; unk_AE90
.text:00008498                 STR     R3, [R11,#var_14]
.text:0000849C                 LDR     R3, =(off_AF9C - 0xAFCC)
.text:000084A0                 ADD     R0, R11, #4
.text:000084A4                 LDR     R3, [R12,R3] ; unk_AE88
.text:000084A8                 STR     R3, [R11,#var_10]
.text:000084AC                 LDR     R3, =(off_AFA0 - 0xAFCC)
.text:000084B0                 MOV     R1, #0
.text:000084B4                 LDR     R3, [R12,R3] ; unk_AE80
.text:000084B8                 STR     R3, [R11,#var_C]
.text:000084BC                 LDR     R3, =(off_AFA4 - 0xAFCC)
.text:000084C0                 LDR     R3, [R12,R3] ; unk_AE98
.text:000084C4                 STR     R3, [R11,#var_8]
.text:000084C8                 LDR     R3, =(off_AFA8 - 0xAFCC)
.text:000084CC                 LDR     R2, [R12,R3] ; sub_850C
.text:000084D0                 SUB     R3, R11, #-var_14
.text:000084D4                 BL      __libc_init
.text:000084D8                 SUB     SP, R11, #4
.text:000084DC                 LDMFD   SP!, {R11,PC}
.text:000084DC ; End of function start

根据 __libc_init 函数的定义,可以知道调用时保存在寄存器 R2 中的地址即使 main 函数的地址,此时根据 “LDR R2, [R12, R3] ; sub_850C” 这一行即可知道 main 函数为 sub_850C,跟进 F5 看看。

int __fastcall sub_850C(int a1, int a2)
{
  int *v2; // r3@1
  char *v3; // r0@1
  int v4; // t1@2
  signed int *v5; // r3@3
  char *v6; // r2@3
  signed int v7; // t1@4
  const char *v8; // r1@5
  int result; // r0@7
  int v10; // [sp+0h] [bp-60h]@3
  int v11; // [sp+4h] [bp-5Ch]@1
  signed int v12; // [sp+8h] [bp-58h]@1
  signed int v13; // [sp+Ch] [bp-54h]@1
  signed int v14; // [sp+10h] [bp-50h]@1
  signed int v15; // [sp+14h] [bp-4Ch]@1
  signed int v16; // [sp+18h] [bp-48h]@1
  signed int v17; // [sp+1Ch] [bp-44h]@1
  signed int v18; // [sp+20h] [bp-40h]@1
  signed int v19; // [sp+24h] [bp-3Ch]@1
  signed int v20; // [sp+28h] [bp-38h]@1
  int v21; // [sp+2Ch] [bp-34h]@1
  signed int v22; // [sp+30h] [bp-30h]@1
  __int16 v23; // [sp+34h] [bp-2Ch]@1
  char v24; // [sp+36h] [bp-2Ah]@1
  char v25; // [sp+37h] [bp-29h]@3
  char v26; // [sp+38h] [bp-28h]@5
  char v27; // [sp+42h] [bp-1Eh]@5
  int v28; // [sp+44h] [bp-1Ch]@1

  v21 = 1819043176;
  v22 = 1919907695;
  v23 = 'dl';
  v24 = 0;
  v11 = 11;
  v12 = 31;
  v13 = 25;
  v14 = 25;
  v15 = 73;
  v16 = 11;
  v17 = 11;
  v18 = 11;
  v2 = &v11;
  v28 = _stack_chk_guard;
  v19 = 49;
  v20 = 83;
  v3 = (char *)&v20 + 3;
  do
  {
    v4 = (unsigned __int8)(v3++)[1];
    *v2 = ((*v2 ^ 7) / 6 - 2) ^ v4;
    ++v2;
  }
  while ( v2 != &v21 );
  v5 = &v10;
  v6 = &v25;
  do
  {
    v7 = v5[1];
    ++v5;
    (v6++)[1] = v7;
  }
  while ( v5 != &v20 );
  v8 = *(const char **)(a2 + 4);
  v27 = 0;
  if ( !strcmp(&v26, v8) )
    printf("Good Work,you have Successed!");
  else
    printf("NO,you are failed!");
  result = 0;
  if ( v28 != _stack_chk_guard )
    _stack_chk_fail(0);
  return result;
}

可以看到做了一系列变换之后进行判断,不涉及其他位置的函数了,不详细分析下去了。

参考链接:
* IDA 如何识别 ARM 的 main 函数
* ARM 指令和 Thumb 指令的区别 
* 关于__stack_chk_guard_ptr的理解
* ARM 汇编版 CRACKME 分析

Read More

2016年10月11日星期二

ANDROID 逆向实例(二)- XDCTF Mobile 100

刚刚接触安卓逆向不久看见 XDCTF 有部分安卓逆向的题目,拿过来看看,试试手,第一次分析 CTF 相关的题目确实用了不少时间,不过同时也学到了不少东西。

常规手段还原出 smali 文件(这里偷了个懒,直接用的 apktool),没有混淆也没有 so 加固,只需要理出 smali 处理流程即可。根据 AndroidManifest.xml 找到 com.example.ring.myapplication.MainActivity, 故从对应的 MainActivity.smali 作为起点分析。

进行了一系列的初始化操作,然后理出比较重要的代码:进入 p 函数、绑定类 d 为点击事件。

.method protected onCreate(Landroid/os/Bundle;)V
    ...
    invoke-direct {p0}, Lcom/example/ring/myapplication/MainActivity;->p()V
    ...
    new-instance v1, Lcom/example/ring/myapplication/d;
    invoke-direct {v1, p0}, Lcom/example/ring/myapplication/d;->(Lcom/example/ring/myapplication/MainActivity;)V
    invoke-virtual {v0, v1}, Landroid/widget/Button;->setOnClickListener(Landroid/view/View$OnClickListener;)V
.end method

跟进一下 p() 函数:

.method private p()V
    ...
    const-string v1, "url.png"
    invoke-virtual {v0, v1}, Landroid/content/res/AssetManager;->open(Ljava/lang/String;)Ljava/io/InputStream;
    ...
    invoke-virtual {v0, v2, v3, v1}, Ljava/io/InputStream;->read([BII)I
    ...
    const/16 v1, 0x90
    const/4 v3, 0x0
    const/16 v4, 0x10
    invoke-static {v2, v1, v0, v3, v4}, Ljava/lang/System;->arraycopy(Ljava/lang/Object;ILjava/lang/Object;II)V
    ...
    iput-object v1, p0, Lcom/example/ring/myapplication/MainActivity;->v:Ljava/lang/String;
    ...
.end method

可以看到这里打开了 asset 目录下的 “url.png”,然后读取了 0x90 ~ 0xa0 位置的内容保存到 MainActivity->v 里,下面看下 d 类:

.method public onClick(Landroid/view/View;)V
    ...
    invoke-virtual {v0}, Landroid/widget/EditText;->getText()Landroid/text/Editable;
    move-result-object v0
    invoke-virtual {v0}, Ljava/lang/Object;->toString()Ljava/lang/String;
    move-result-object v0 
    ...
    # 下行是用来获取 MainActivity->v 的
    invoke-static {v2}, Lcom/example/ring/myapplication/MainActivity;->a(Lcom/example/ring/myapplication/MainActivity;)Ljava/lang/String;
    move-result-object v2
    invoke-static {v1, v2, v0}, Lcom/example/ring/myapplication/MainActivity;->a(Lcom/example/ring/myapplication/MainActivity;Ljava/lang/String;Ljava/lang/String;)Z
    move-result v0
    ...
    if-eqz v0, :cond_0
    ...
    const-string v2, "Congratulations!"

好的,发现成功条件了,当 MainActivity->a(String,String) 返回结果为真时程序成功,看下这里的两个 String,一个是 MainActivity->v(上面说过,其实是 url.png 的 0x90 ~ 0xa0 位置), 一个是用户输入的字符串(即为 Flag),下面先看一下 MainActivity->v :


xxd 可以看到 MainActivity->v 即是 "this_is_the_key.",然后跟进 MainActivity->a(String,String) 函数,看后续流程,由下面的代码可以看出,当用户输入的内容经过转换后和 array_0 位置的内容相同为成功条件。

.method private a(Ljava/lang/String;Ljava/lang/String;)Z
    ...
    new-instance v0, Lcom/example/ring/myapplication/c;
    invoke-direct {v0}, Lcom/example/ring/myapplication/c;->()V
    invoke-virtual {v0, p1, p2}, Lcom/example/ring/myapplication/c;->a(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
    ...
    const/16 v1, 0x20
    new-array v1, v1, [B
    fill-array-data v1, :array_0
    new-instance v2, Ljava/lang/String;    
    invoke-direct {v2, v1}, Ljava/lang/String;->([B)V      # 将 array_0 位置的内容转化成字符串
    invoke-virtual {v0, v2}, Ljava/lang/String;->equals(Ljava/lang/Object;)
    move-result v0
    return v0
.end method

在比较之前,函数初始化了类 c,然后执行 c->a(String,String) ,这里有点复杂,偷个懒使用 jeb 还原 java 代码看,这里。



可以看到做的主要的事情有:进入 c->a(String) 函数将 bytes 分为两个一组,然后组内交换顺序,那么现在的 MainActivity->v 就变成了 "htsii__sht_eek.y";另外初始化了类 a,并执行了 a->a(byte[]) 和 a->b(byte[]) 操作,看下类 a 的代码。


可以看出这里其实是以 MainActivity->v 作为了 AES 加密 Key,加密了用户输入的字符串这个操作。所以最后的比较即是:AES 密文和 array_0 位置数据的比较,写了个解密代码,运行出来了 Flag。

import java.io.*;
import javax.crypto.spec.SecretKeySpec;
import javax.crypto.Cipher;

class test  
{
 public static void main (String[] args) throws java.lang.Exception
 { 
         String key = "htsii__sht_eek.y";
         SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(), "AES");
         Cipher cipher = Cipher.getInstance("AES");// 创建密码器  
         byte[] bye = {21,-93,-68,-94,86,117,-19,-68,-92,33,50,118,16,13,1,-15,-13,3,4,103,-18,81,30,68,54,-93,44,-23,93,98,5,59};
         /*
            byte[] byteContent = content.getBytes("utf-8");  
            cipher.init(Cipher.ENCRYPT_MODE, keySpec);// 初始化   
            byte[] result = cipher.doFinal(byteContent);  
            String str2 = new String(result,"UTF-8");
            System.out.println(str2); // 加密   
            */
            
            cipher.init(Cipher.DECRYPT_MODE, keySpec);
            byte[] result2 = cipher.doFinal(bye);
            String str3 = new String(result2, "utf-8");
            System.out.println(str3);
 }
}


# LCTF{1t's_rea1ly_an_ea3y_ap4}

* 逆向的 apk 上传到了 Github 。
Read More

2016年10月10日星期一

Android 逆向学习笔记 (四)- 签名校验突破

有的时候在篡改 apk 并且重新打包之后不能直接运行,此时部分情况是因为存在软件的签名校验机制。在前面几篇的学习中可以知道,软件打包之后需要重新签名才可以安装到安卓机器上,此时重新签名一般用的都是公开的  platform.x509.pem 和 platform.pk8 , 有的软件为达到防篡改的目的会对新老签名进行比较,如果存在差异则确定被篡改,程序终止。

除了签名校验之外,还有一种情况是 classes.dex 的 MD5 检验,也可以发现被篡改的 classes.dex。怎么区分这两种不同的校验呢?拿到 apk 之后对其重新签名,如果仍然可以正常安装运行的即说明是 classes.dex 的 MD5 校验,反之则是签名校验。

一般在程序内进行的校验有两种途径,在线比对和离线比对,逆向者在意的是比对的这个过程及其参数。在线比对和离线比对的区别是:在断网情况下仍旧闪退的是离线比对,断网后先提示没有网络连接再闪退的是在线比对。

既然校验的过程是在本地程序进行的,那么在反编译得到代码之后,只要修改一下比较时的对应条件就可以简单的绕过校验了,整个校验过程有点掩耳盗铃的意思。

Signature · smali

获取当前 apk 签名的 Java 代码如下:

PackageInfo packageInfo = getPackageManager().getPackageInfo(
                    "xx.xxx.xx", PackageManager.GET_SIGNATURES);
Signature[] signs = packageInfo.signatures;

对应的 smali 代码为:

Landroid/content/pm/PackageInfo;->signatures:[Landroid/content/pm/Signature

所以对付签名校验,只要通过这段关键代码找到对应的调用位置,不断跟进,确定最终判断的位置然后修改比较条件即可。

另外有的时候会进入 native 层判断,这个时候搜索关键字 signature 然后判断跟进即可。

MD5 · smali

这类首先会在 smali 层获取当前 apk 的目录,然后再传入 native 层面进行比较,典型代码如下所示:

iget-object v1, v0,Landroid/content/pm/ApplicationInfo;->sourceDir:Ljava/lang/String;
const-string v1, "/sdcard/buyudaren.apk" # 本行目的是 bypass 校验,路径为原版 apk 路径,覆盖 v1
invoke-static {v1}, Lorg/cocos2dx/lib/Cocos2dxHelper;->nativeSetApkPath(Ljava/lang/String;)V

简单添加中间一行即可绕过 MD5 的判断,因为此时进入 native 层的是原版 apk 的路径,所以校验的也是原版 apk,此时当然没有问题。可以看到,看似复杂的进入 native 层面有的时候只是掩耳盗铃的一种方式。

如果要进入 native 层面去 Bypass 检查,在把 .so 文件拖入 IDA Pro 后搜索 “classes.dex” 即可,跟进可以发现 MD5 字符串或是进行比对的特征值。

另外有时存在 .so 和 classes.dex 文件相互验证的情况,这个时候就对应的搜索 “classes.dex” 和 “xxx.so” 两个特征字符串,找到程序运行中校验的文件路径,然后对应地修改成原版文件的路径即可。

REFERENCE



* 另外说一下,有的时候两个程序的 AndroidManifest.xml 中定义了 SharedUserId 并进行交互,这时必须两个程序的签名相同才可以正常安装。
Read More