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 Headere_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

2016年9月25日星期日

Shadowsocks 的定制和 “不安全性” 的分析

用 Shadowsocks 有很长一段时间了,确实非常好用,速度不错而且能够满足很多工作中的需求。之前也只是一直用着没有做过多的分析,直到最近看见 ShadowsocksR 这个非官方的项目发现 Shadowsocks 稍做修改便能够实现定制化,比较方便。

SS 的工作流程图下图所示:


流程图中其实显示的很清楚了,要定制自己的 Shadowsocks 服务只需要对 # 2 处的代码进行相应的混淆,而在 # 3 处进行反混淆即可,通过看 SS 代码可以看到这部分的内容在 shadowsocks/tcprelay.py 文件定义的类 TCPRelayHandler 方法 _handle_stage_stream() 中处理,该段的代码如下所示:

def _handle_stage_stream(self, data):
    if self._is_local:
        # ss-local encrypt data and send to ss-server, _remote_sock -> ss-server
        if self._ota_enable_session:
            data = self._ota_chunk_data_gen(data)
        data = self._encryptor.encrypt(data)
        self._write_to_sock(data, self._remote_sock)
        ...

可以看到 ss-local 加密之后写进 socket 发往 ss-remote,这种情况下的 data 为明文 data,此时加密方式可以自己定义,也可以自己修改报文,伪造其他协议。使用 SSR 来免流就是用的这个方法,伪造成 HOST 为服务商的 HTTP 请求达到欺骗基站的目的。

这种情况下还需要在 ss-server 做对应的解密,找到该类的 _on_remote_read() 函数进行相应的反操作即可。

def _on_remote_read(self):
    # handle all remote read events, deal with HTTP request data.
    data = None
    ...
    if self._is_local:
        data = self._encryptor.decrypt(data)
    else:
        data = self._encryptor.encrypt(data)

另外说一点,SS 现在确实在部分情况下可以被较准确地探测,前提是:

1. 使用了 RC4 、AES-256-* 类型的流加密方式,加密之后前 16 字节为随机生成的 IV,第 17 字节表示通过 SS 的协议类型(1 - IPv4,3 - DNS,4 - IPv6),紧跟着从 18 字节开始表示目的地址,长度不定。
2. 没有开启 SS 的 AutoBan 功能,防火墙没有做其他类型的尝试数量限制。

SS Server 端的特性是,如果解密之后的明文在第 17 字节位置上的值不是(1、3、4)中的一个,那么就立马断开连接,否则就继续运行对后面的目的地址进行连接和请求。

而 breakwa11 给出的测探 SS Server 的方法并不是要破解整个密文,而是构造特定密文给 SS Server 去解密,并观察响应。前面 16 字节随便构造,这段密文总可以解密成功,虽然这里的明文完全没有办法知道是啥,不过也不需要知道就是了。根据流加密的特性,遍历 17 字节位置上的所有情况,总有解密之后为(1、3、4)中的某一个的,这时的 SS Server 不会立马断开,而是去解密 18 字节位置开始的目的地址,这样通过 SS Server 的响应差别便可以完成测探。

第一次测探确认成功之后会进行第二次测探,修改 18 字节位置(即:目的地址),虽然这里没办法之后解密之后的目的地址是什么,但是目前 IPv4 基本上全被占用的情况下(假设),即使修改目的地址仍然可以建立连接,如果第二次也不是马上断开,那么就能够确定是运行着 SS Server。

这个探测方法确实是可以成功探测到特殊情况下 SS Server 的存在,不过感觉意义不大,有点像鸡蛋里头挑骨头的做法。而且这也不是 SS 的错,只能说是这类型加密算法的特性,SS 本来就是一个加密协议这样做也没什么有问题的地方。

同时可以发现,breakwa11 在这个 comment 里也有提到,chacha20 这类使用 8 字节 IV 的加密更少被探测,因为默认的 AES-256-* 是 16 字节 IV,而 GFW 主要探测的为 16 字节 IV,所以换成 chacha20 往往 SS 速度会得到提升,因为绕过了部分区域的 QoS。(但并不是这种不能被探测,根据上面的做法修改第 10 位开始的目的地址就行了)

*附上抓的部分请求,可以大致描绘 SS 的网络活动,比较简单不具体分析了。





Read More

2016年9月17日星期六

ASM 逆向学习笔记 (一)- 汇编基础


因为安卓逆向时而需要分析在 .so 文件中定义的 natvie 函数,所以对汇编的指令伪代码也需要进行学习,在《Android软件安全与逆向分析》中的第六章针对部分指令有所讲述,不过例子感觉举的不太充分,下面对部分内容做一个备忘。

Register

R0 ~ R3 :用作传入函数参数,传出函数返回值,比方说 test 函数需要两个参数一个返回值,那么即使用 R0、R1 作为函数的参数,返回值保存在 R0 里。在子程序调用之间,可以将 R0 ~ R3 用于任何用途。 被调用函数在返回之前不必恢复。如果调用函数需要再次使用 R0 ~ R3 的内容,则它必须保留这些内容。

R4 ~ R11 :用作存放函数的局部变量,作用域是单个子程序。如果被调用函数使用了这些寄存器,它在返回之前必须恢复这些寄存器的值。在程序运行过程中,编译器会自动保护R4~R11。

R12:内部调用暂时寄存器 IP。它在过程链接胶合代码(例如,交互操作胶合代码)中用于此角色。 在过程调用之间,可以将它用于任何用途。被调用函数在返回之前不必恢复 R12。

R13:栈指针 SP,不能用于任何其它用途,SP 中存放的值在退出被调用函数时必须与进入时的值相同。 每一种异常模式都有其自己独立的r13,它通常指向异常模式所专用的堆栈,也就是说五种异常模式、非异常模式(用户模式和系统模式),都有各自独立的堆栈,用不同的堆栈指针来索引。这样当ARM进入异常模式的时候,程序就可以把一般通用寄存器压入堆栈,返回时再出栈,保证了各种模式下程序的状态的完整性。

R14:链接寄存器 LR,用于保存函数的返回地址。如果其他寄存器保存了函数返回地址,则可以在调用之间将 R14 用于其它用途,程序返回时要恢复。(1)保存子程序返回地址。使用BL或BLX时,跳转指令自动把返回地址放入 R14 中;子程序通过把 R14 复制到PC来实现返回。(2)当异常发生时,异常模式的 R14 用来保存异常返回地址,将 R14 如栈可以处理嵌套中断。

R15:程序计数器 PC,用于指向当前指令地址的指针。它不能用于任何其它用途。

Command

STR:STR{条件} 源寄存器,<存储器地址>

STR R0,[R1],#8     ;将 R0 中的字数据写入以 R1 为地址的存储器中,并将新地址 R1+8 写入 R1 。
STR R0, [R1, #8]   ;将 R0 中的字数据写入以 R1+8 为地址的存储器中。

LDR:LDR{条件} 目的寄存器,<存储器地址>

LDR R0,[R1,R2,LSL#2]!     ;将存储器地址为 R1+R2×4 的字数据读入寄存器 R0,并将新地址 R1+R2×4 写入 R1。
LDR R0,[R1],R2,LSL#2      ;将存储器地址为 R1 的字数据读入寄存器 R0,幵将新地址 R1+R2×4 写入 R1。
LDR R0,0x12345678           ;将 0x12345678 地址保存的数据放入 R0
LDR{cond}  register,=[expr | label-expr] ;伪代码

B 指令向前向后跳转的情况

cond_1: ;A
cmp r0, #0
beq cond_1f ; r0==0那么向前跳转到B处执行, f 表示 forward
bne cond_1b ; 否则向后跳转到A处执行, b 表示 backward
cond_1: ;B

感叹号[!]:可以用来使指令执行后,更新基底位地址。

LDR R0, [R1, #8]! ;[R1+8]->R0 
                 ;R1+8->R1
LDR R0!, {R1-R4}  ; R1=[R0], R2=[R0+#4]

Reference

Category:
Read More

ANDROID 逆向学习笔记 (三)- IDA 动态调试 so


前面几篇所说的均是对安卓进行 smali 静态分析的内容,本节探讨一下动态的调试和分析,将会使用到的是 IDA Pro (金山 leak)和 Android Studio 的部分工具(模拟器,ddms,adb 等)。

Android Studio 的官方网址是: https://developer.android.com/studio/intro/index.html,可以找到不同系统的版本,下载并安装好了之后在 ~/Library/android/sdk/tools 路径下可以找到安卓开发调试所需要的各种小工具。
安装好之后启动 Android Studio 找到 AVD Manager 按钮,即可建立自己的 AVD(Android Virtual Device),创建的过程中根据自己需要选择对应的参数吧,创建好之后如下图所示,此时可以直接点击绿色箭头运行,或者是终端输入来运行虚拟机。

emulator -netdelay none -netspeed full -avd $AVD_NAME
从图中可以看到我创建了名为 4_2_AMD 和 4_2_X86 的两个 AVD,X86 相对于 AMD 来说速度比较快,但是有部分软件和程序不能在 X86 上模拟只能使用 AMD。

在 IDA 的安装目录的 dgbsrv 文件夹下有调试需要用到的 android_server 二进制文件,只有在安卓机器上运行了这个文件之后 IDA 才可以正常 attach,然后这里有一个坑,X86 的模拟器运行这个是会出错的,所以需要换 AMD 的模拟器来调试,执行命令如下:

adb push IDA/android_server  /data/local/tmp
adb shell
cd /data/local/tmp
chmod 755 android_server
./android_server

没有出错的情况下会提示在监听 23946 端口,这个时候使用 adb forward 将 AVD 的端口映射到本地,如果是本机运行的 IDA 即可直接 attach。不过,如果要是其他主机需要 attach 呢,我这里使用的命令如下。

adb forward tcp:23946 tcp:23946
socat -v tcp-listen:12344,fork tcp-connect:localhost:23946

IDA -> Debugger -> Attach -> remote ARMLinux/Android debugger,按要求填写即可。

如果上述步骤都成功的话会进入这个界面,点击需要调试的进程即可进行动态调试。下面使用 MSC 的第二题来做详细说明。

MSC Challenge # 2

先反编译 crackme1.apk,看一下 smali 代码,在 com/yaotong/crackme/MainActivity.smali 中可以看到按钮绑定的 onClick() 事件调用了 securityCheck() 来检查输入是否正确,而该函数定义含有 native 属性,可知该函数是在 .so 文件中定义的,尝试静态分析 .so 文件,直接把 libcrackme.so 拉入 IDA 中进行分析,关键代码如下所示。

  v5 = (_BYTE *)((int (__fastcall *)(JNIEnv *, int, _DWORD))(*v3)->GetStringUTFChars)(v3, v4, 0);
  v6 = off_628C;
  while ( 1 )
  {
    v7 = (unsigned __int8)*v6;
    if ( v7 != *v5 )
      break;
    ++v6;
    ++v5;
    v8 = 1;
    if ( !v7 )
      return v8;
  }

可以看到使用用户传入的字符串跟 off_628C 位置的数据进行了比较,双击进入 off_628C 的位置,如下所示,可以看到静态分析得到的 key 是 [wojiushidaan],输入 apk 后并不正确,说明这个字符串在动态运行的时候做了变换,所以还需要使用 IDA 进行动态的分析。

.data:0000628C ; .data         ends                    ; "wojiushidaan"

这里静态的分析有一个小 trick,使用 IDA 的时候可以通过 shift + F12 来显示所有定义的字符串内容,本例中通过 shift + F12 可以看到,检测到了 “wojiushidaan” 这个字符串,不过这里并不是真正的答案。



先启动 crackme.apk 之后再用 IDA 进行 attach,IDA 闪退,说明程序做了反调试机制,检测到程序的处于被调试状态则退出,所以猜测时 .so 文件加载时进行了特殊的处理,来看一下 .so 文件的运行情况。

因为很多情况 .so 文件在 apk 运行的开始就会进行加载,所以如果直接运行软件然后再去 IDA 找对应的进程进行 attach 这样很有可能就会直接错过 .so 文件加载时的运行情况,所以这里使用

adb shell am start -D -n com.yaotong.crackme/.MainActivity

这样会启动 crackme 但是不会往下运行,因为添加了 -D 参数,说明等待调试器连接启动程序之后刮起,后面的 com.yaotong.crackme/.MainActivity 对应着上文中的 smali 目录结构和主 Activity 的名称。

这样启动的程序就可以用 IDA 来 attach 了,然后再在 .so 文件载入时下一个断点,这样就能够实时的分析 .so 文件加载时的运行情况了。下断点的方式为,Debugger -> Debugger setup -> suspend on library load/unload。另外这里值得一提的是,因为程序处于挂起状态,如果直接操作 IDA 的话没有任何反应,所以这里需要使用 jdb 恢复运行。

ddms
jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8700

看到执行 jdb 命令之前首先执行了 ddms(即 Dalvik Debug Monitor Service),jdb 的运行在 OS X 上需要 ddms 为启动状态,在之前提到的 sdk/tools 文件夹中可以找到 ddms,启动即可。此时点击 IDA 的绿色小箭头运行即可。

此时正常运行,到加载 .so 文件时会弹出两个框,ESC 取消掉即可,然后即可进行实时调试,找出反调试的位置,去掉反调试之后继续调试找到变换后的 key。系统加载 .so,在完成装载、映射和重定向以后,就首先执行 .init 和 .init_array 段的代码,之后如果存在 JNI_OnLoad 就调用该函数。对一个 .so 进行分析,需要先看看有没有 .init_array section 和 .init section ,.so加壳一般会在初始化函数进行脱壳操作。

F8 单步调试后发现运行到 JNI_OnLoad 伪代码 33 行的这一段之后调试器会闪退,F7 跟进函数看看具体的操作。

v5 = dword_4BE442B4(v8, 0, &unk_4BE3F6A4, 0);

F7 指示跳转到 pthread_create 函数,所以猜测这里使用了 phtread_create 来检查函数是否有调试器 attach 的操作,如果存在即退出,这是一种反调试的手段,知道上述情况之后再开一个 IDA 来进行 .so 的静态分析,找到 JNI_OnLoad 函数的 33 行。

v5 = dword_62B4(handle, 0, sub_16A4, 0);

故这里的 dword_64B4 即为 pthread_create,根据 pthread_create 定义,可知第三个参数为新线程的起始地址,即新开了一个线程执行 sub_16A4 操作,并且在这个操作执行的过程中调试器闪退。刚开始看的时候可能会有一个疑问,为什么不直接静态分析 dword_64B4 位置的内容呢,因为静态分析时这个位置的内容还没有被赋值,所以直接看的话会是这样的。

.bss:000062B4 dword_62B4  % 4   # 只有在动态调试时的这个 % 4 会变成相应的地址,才可以继续分析

sub_16A4 在 F5 之后的伪代码如下所示,可以看到循环调用了 sub_130C 和 dword_62B0(3) 两条指令,猜测是不断对程序进行调试器检测的操作。

void __noreturn sub_16A4()
{
  while ( 1 )
  {
    sub_130C();
    dword_62B0(3);
  }
}

不过直接看 sub_16A4 的代码显得比较复杂,这时可以在 libc.so 文件的 fopen 函数下断点,因为检测是否 attach 调试器一般都是通过 fopen 打开对应的 pid 文件 [/proc/$pid/status],然后获取对应 TracerPid,如果该值不为 0 则说明存在 attach 的调试器。

下断点的方式:在对应函数处右键 add breakpoint, 或者是在函数的起始行将行首的小圆点,点亮。下好断点之后,点击 IDA 的运行,PC 指针会执行到断点处后停止,如图所示。
这时将 Hex View 设置与 R0 同步可以看到 R0 为 /proc/1182/status,即当前程序的 Pid 为 1182。接下来看看是在什么地方调用的 fopen 函数,将 IDA View 设置与 LR (返回地址)同步,可以看到如下。
可以看到 LR 处于 libcrackme.so 里,且相对位移为 4BE3F420 - 4BE3E000 = 1420,那么在静态分析中找到偏移为 1420 位置,可以发现其实是在 sub_130C 里,而这个 sub_130C 正是 sub_16A4 内调用的函数,故推测 sub_130C 函数为调试检测函数,将 sub_16A4 内的 [BL sub_130C] 指令替换成 [MOVS R0,R0] 即可取消该调试函数。

而之所以使用 [MOVS R0, R0](指令码:A0 E1)而不是 NOP(指令码:C0 46),是因为 ARM 的平台中并不像 X86 一样拥有 NOP 指令。

然后保存 libcrackme.so 文件,重新打包进 apk 文件、签名,此时已经可以随意 attach 调试器并且没有调试器检测,动态调试断点到 Java_com_yaotong_crackme_MainActivity_securityCheck 函数的 v6 查看结果即可。

Kill Bypass反调试

使用 [kill -19 $Pid ]来达到挂起程序的目的,然后 attach IDA 的调试器,此时因为程序处于挂起状态,反调试不起作用,直接分析即可,详情见 伪·MSC解题报告 - QEver

其他参考资料:

安卓动态调试七种武器之孔雀翎 – Ida Pro -蒸米

*文中分析的 apk 文件和部分工具可以在 AndroidRevStudy 找到
Read More

2016年9月12日星期一

ANDROID 逆向实例(一)- acce3ft.aevbbb


对于恶意软件不进行打码处理了,网址是 http://hpwxn.com/ 浏览之后可以下载两个 apk ,不过从文件大小来看应该是一个文件只不过进行了改名。

一般这种恶意软件既不会加壳也不会做复杂的混淆处理,分析起来很简单。直接反编译看 AndroidManifest.xml,找到如下一行。

<activity android:label="@string/app_name" android:name="acce3ft.aevbbb.MainActivity">

可以看到程序的 MainActivity, 直接看 acce3ft/aevbbb/MainActivity.smali 文件。

OnCreate

.method protected onCreate(Landroid/os/Bundle;)V
    .locals 4

    const/4 v3, 0x1

    invoke-super {p0, p1}, Landroid/app/Activity;->onCreate(Landroid/os/Bundle;)V

    invoke-super {p0, v3}, Landroid/app/Activity;->requestWindowFeature(I)Z

    const/high16 v0, 0x7f030000

    invoke-virtual {p0, v0}, Lacce3ft/aevbbb/MainActivity;->setContentView(I)V

    invoke-static {}, Lacce3ft/aevbbb/util/MyApplication;->b()Landroid/content/Context;

    move-result-object v0

    invoke-static {}, Lacce3ft/aevbbb/util/c;->a()Z

    move-result v1

    if-nez v1, :cond_0

    invoke-virtual {p0, v0}, Lacce3ft/aevbbb/MainActivity;->a(Landroid/content/Context;)V

    invoke-virtual {p0}, Lacce3ft/aevbbb/MainActivity;->finish()V

    :cond_0
    new-instance v1, Landroid/content/ComponentName;

    const-class v2, Lacce3ft/aevbbb/NetstateReceiver;

    invoke-direct {v1, v0, v2}, Landroid/content/ComponentName;-><init>(Landroid/content/Context;Ljava/lang/Class;)V

    invoke-virtual {v0}, Landroid/content/Context;->getPackageManager()Landroid/content/pm/PackageManager;

    move-result-object v0

    const/4 v2, 0x2

    invoke-virtual {v0, v1, v2, v3}, Landroid/content/pm/PackageManager;->setComponentEnabledSetting(Landroid/content/ComponentName;II)V

    invoke-static {}, Lacce3ft/aevbbb/util/c;->d()V

    return-void
.end method

进行了一系列的初始化操作,主要就是查看当前手机是否符合恶意软件的运行环境(权限、组件的开启情况等),需要注意的是在最后调用了 Lacce3ft/aevbbb/util/c;->d() 跟一下这个函数,该片段主要的代码结构如下

const-string v1, "CONTROL_NUMBER"

    sget-object v2, Lacce3ft/aevbbb/util/a;->g:Ljava/lang/String;

    invoke-interface {v0, v1, v2}, Landroid/content/SharedPreferences;->getString(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;

    move-result-object v1

    sput-object v1, Lacce3ft/aevbbb/util/a;->g:Ljava/lang/String;
即将 Lacce3ft/aevbbb/util/a; 类的各个参数进行重新赋值,本片段中是给 g 参数进行复制,因为重复地对该类进行赋值和读取操作,所以推测该类作用为储存恶意软件运行参数,查看该类的 smali 文件。

.method static constructor <clinit>()V
    .locals 1

    const/4 v0, 0x0

    sput v0, Lacce3ft/aevbbb/util/a;->a:I

    const-string v0, "sss"

    sput-object v0, Lacce3ft/aevbbb/util/a;->b:Ljava/lang/String;

    const-string v0, "15274907736@189.cn"

    sput-object v0, Lacce3ft/aevbbb/util/a;->c:Ljava/lang/String;

    const-string v0, "aaaa123123"

    sput-object v0, Lacce3ft/aevbbb/util/a;->d:Ljava/lang/String;

    const-string v0, "15274907736@189.cn"

    sput-object v0, Lacce3ft/aevbbb/util/a;->e:Ljava/lang/String;

    const-string v0, "15274907736"

    sput-object v0, Lacce3ft/aevbbb/util/a;->f:Ljava/lang/String;

    const-string v0, "15274907736"

确实如之前猜测一样,是参数类,邮箱账号和密码如上所示,登陆一下看看。


中招的挺多的,从邮件分析了一下恶意软件的运行机制,会从各个网银、理财等软件里向手机发起验证码,进行转账第一笔的 5 块转账,目的是为了查看是否能够走通流程,如果成功的话,会转出后续的余额,看了一下最多的一个账户余额为 7 w,到我查看的时候已经所剩无几。

OnStart


主要进行了两个操作,如果可以获取管理员权限(ROOT)即进行添加管理员操作(状态码:1),不行的话就尝试修改默认 SMS 使用的 package 为恶意软件的 package(状态码:0),执行完毕之后带着状态码进入 Lacce3ft/aevbbb/MainActivity;->startActivityForResult() 函数。

而与 startActivityForResult() 对应的, onActivityResult() 中设置了各个状态码对应的操作。几个比较明显的代码段如下。

const-string v0, "device_policy" # 检查当前是否为管理权限

invoke-virtual {p0, v0}, Lacce3ft/aevbbb/MainActivity;->getSystemService(Ljava/lang/String;)Ljava/lang/Object;

move-result-object v0

check-cast v0, Landroid/app/admin/DevicePolicyManager;

new-instance v1, Landroid/content/ComponentName;

const-class v2, Lacce3ft/aevbbb/MyAdmin;

invoke-direct {v1, p0, v2}, Landroid/content/ComponentName;->(Landroid/content/Context;Ljava/lang/Class;)V

invoke-virtual {v0, v1}, Landroid/app/admin/DevicePolicyManager;->isAdminActive(Landroid/content/ComponentName;)Z

move-result v0
---
const-string v1, "isAdminActive" # 设置当前应用的管理员权限

invoke-interface {v0, v1, v3}, Landroid/content/SharedPreferences$Editor;->putInt(Ljava/lang/String;I)Landroid/content/SharedPreferences$Editor;

invoke-interface {v0}, Landroid/content/SharedPreferences$Editor;->commit()Z 
---
const-string v1, "android.provider.Telephony.SMS_DELIVER" # 设置当前应用为默认 SMS

invoke-virtual {v0, v1}, Landroid/content/Intent;->setAction(Ljava/lang/String;)Landroid/content/Intent;

iget-object v0, p0, Lacce3ft/aevbbb/MainActivity;->b:Landroid/content/Intent;

invoke-virtual {p0, v0}, Lacce3ft/aevbbb/MainActivity;->startService(Landroid/content/Intent;)Landroid/content/ComponentName;

后续行为不做分析了,基本上千篇一律,删除并举报了邮箱。不过值得一提的是,在 360 显危镜和腾讯哈勃的 app 扫描里,该软件并没有被确定是恶意软件。

*文中分析的 apk 文件可以在 AndroidRevStudy 找到
Read More