Lab1 : Android逆向
文章目录
- Lab1 : Android逆向
1.1 Android Programming
1.1.1 任务描述
- 创建一个安卓项目,在
com.smali.secretchallenge
包中实现SecretBootReceiver
类,使得这个应用程序能在安卓开机时自动运行,并且自动启动一个后台程序。
在com.smali.secretchallenge
中实现SecretService
类,这个被SecretBootReceiver
启动。这个类的功能包括:获取设备的GPS位置,每3秒显示把位置的经纬度显示到屏幕上。
在应用程序运行时前,需要获得位置权限。在com.smali.secretchallenge
包的MainActivity
类中需要实现权限获取的请求。
-
更改线程中的UI。(不允许在另一个线程中更改UI,只能在主线程中更改它。)
需要在单击按钮时创建一个对话框,并显示刚才在文本中输入的内容。
-
关键字private的目的是保护文件或方法不被类外访问。但由于
Java Reflection
,这并不一定有效。实现以下操作:在包
classes.jar
的PoRELab类中访问私有成员变量curStr;调用私有成员函数privateMethod。 -
生成应用程序签名。
1.1.2 BroadcastReceiver
实验中的SecretBootReceiver
继承了android.content.BroadcastReceiver
。BroadcastReceiver是安卓四大组件之一,用以接受广播信息,并启动相应处理机制。BroadcastReceiver需要重写onReceive
方法来接收以Intent对象为参数的消息。实验中创建了一个Intent(context, MainActivity.class)
并通过context.startActivity
来启动MainActivity。
记得在AndroidManifest.xml
中注册receiver:
<receiver android:name=".SecretBootReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</receiver>
1.1.3 Service
Service是一个后台运行的组件,执行长时间运行且不需要用户交互的任务。
Service包括两种状态:Started、Bound。通过startService()启动了服务,则Service处于Started状态。一旦启动,Service可以在后台无限期运行,即使启动它的组件已经被销毁。当Android的应用程序组件通过bindService()绑定了Service,则Service是Bound状态。Bound状态的服务提供了一个客户服务器接口来允许组件与服务进行交互,如发送请求,获取结果,甚至通过IPC来进行跨进程通信。
Service的生命周期:
程序中SecretService
继承Service
类,重写了onStartCommand
方法。onStartCommand
中开启了一个新线程,线程执行一个死循环,循环内部调用locationUpdates
函数获取GPS信息并输出,然后线程陷入3秒睡眠。于是该应用程序可以实现每3s一次的GPS信息输出。
locationUpdates
中使用了android.location
中的方法获取经纬度。通过android.widget.Toast
将位置信息输出到屏幕上。
这边暂时Toast使用不了,用<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
开启了权限也不行。
睡了一觉,Toast就可以了。【挠头】:
记得在AndroidManifest.xml
中注册service:
<service
android:name=".SecretService"
android:enabled="true"
android:exported="true" >
</service>
1.1.4 AppCompatActivity
Activity代表了一个具有用户界面的单一屏幕,如 Java 的窗口或者帧。
Android 系统初始化它的程序是通过Activity中的 onCreate()
回调的调用开始的。
Activity的生命周期如下图:
实验中MainActivity
类继承了AppCompatActivity
这个类,并重写onCreate
,onRequestPermissionsResult
函数。
onCreate
中先调用父类onCreate
,然后获取位置权限。获取权限后,开启一个Intent
,通过startService
启动SecretService
这个类。
1.1.5 线程中的更改UI
Android UI机制:UI操作只有一个主线程,app只有这个线程可以处理UI;长时间的代码需要放后台,不能阻塞UI。
可以使用Handler来完成子线程对主线程UI的更改。
Handler
:很多功能或操作是不能都放在Activity当中,否则会出现长时间没响应,甚至会出现ANR之类的错误(即5秒内没响应)。把那些费时费力的操作放在另外一个线程操作当中,这样就能够和主线程(UI)线程同步操作,不会出现长时间等待或没响应的操作,使得用户体验大大提高。Handler就是实现上面的功能的一个东西,它接受子线程发送来的数据,并用此数据配合主线程更新UI。
Looper
:“循环者”,它被设计用来使一个普通线程变成Looper线程,即不断循环。
AlertDialog
:弹窗。通过AlertDialog.Builder
创建一个弹窗。
通过以上三者,并使用Handler.post
进行消息传递。
结果如下:
1.1.6 私有成员变量、函数的访问
Java的反射机制:Java属于先编译再运行的语言,程序中对象的类型在编译期就确定下来了,而当程序在运行时可能需要动态加载某些类,这些类因为之前用不到,所以没有被加载到JVM。通过反射,可以在运行时动态地创建对象并调用其属性,不需要提前在编译期知道运行的对象是谁。在程序运行时动态加载类并获取类的详细信息,从而操作类或对象的属性和方法。本质是JVM得到class对象之后,再通过class对象进行反编译,从而获取对象的各种信息。
导入jar包:将classes.jar包拷贝到app/src/main/libs
中,这时候还未导入,需要在这个jar上右键,然后单机add as Library
,成功导入后如图:
可以看到这个jar包内有BuildConfig和PoRELab两个类,包名是com.pore.mylibrary
。通过Class.forName("com.pore.mylibrary.PoRELab")
创建一个Class对象,然后写入以下代码:
Method method = c.getDeclaredMethod("privateMethod", String.class, String.class);
method.setAccessible(true);
Field tag = c.getDeclaredField("curStr");
tag.setAccessible(true);
Object obj = c.newInstance();
method.invoke(obj, "hello", tag.get(obj));
即可访问curStr
变量,以及调用privateMethod
函数(参数是“”hello“)。最终结果可以在Logcat中看到:
1.1.7 生成应用程序签名
为什么要生成签名?
Android系统要求每一个Android应用程序必须要经过数字签名才能够安装到系统中,也就是说如果一个Android应用程序没有经过数字签名,是没有办法安装到系统中的!Android通过数字签名来标识应用程序的作者和在应用程序之间建立信任关系,不是用来决定最终用户可以安装哪些应用程序。这个数字签名由应用程序的作者完成,并不需要权威的数字证书签名机构认证,它只是用来让应用程序包自我认证的。
主要看了这篇博文生成签名:https://blog.csdn.net/YuEOrange/article/details/86018718。
Event Log如下:
1.1.8 其它
1. 调试问题:目前采用输出调试的方法。调用`android.util.Log`将调试信息写入log中,可以在logcat中通过设置filter来观察调试结果。
Log.d("debug", "information");
1.2 Java2Smali
1.2.1 任务描述
- 使用smali写一个选择排序的程序。
- 使用smali写一个获得主机IP的程序。
1.2.2 smali选择排序
Smali是用于Dalvik(Android虚拟机)的反汇编程序实现。汇编工具(将Smali代码汇编为dex文件)为smali.jar,与之对应的baksmali.jar则是反汇编程序。
smali、dex、java、class的关系:
Smali文件结构
一个Smali文件对应的是一个Java的类,更准确的说是一个.class文件,如果有内部类,需要写成
ClassName$InnerClassA
、ClassName$InnerClassB
…这样的形式
基本类型
类型关键字 对应Java中的类型说明 V void,只能用于返回类型 Z boolean B byte S short C char I int J long (64 bits) F float D double (64 bits)
对象
Object类型,即引用类型的对象,在引用时,使用L开头,后面紧接着的是完整的包名,比如:
java.lang.String
对应的Smali语法则是Ljava/lang/String
数组
一维数组在类型的左边加一个方括号,比如:
[I
等同于Java的int[]
方法声明及调用
官方Wiki中给出的Smali引用方法的模板如下:
Lpackage/name/ObjectName;->MethodName(III)Z
.method和.end method 类似Java大括号{}
.locals 指定方法中非参寄存器总数,出现在方法第一行
.registers 指定方法中寄存器总数
.prologue 表示代码开始
.line 表示java源码行号,用于调试
首先写一个Java版本的选择排序:
public static void select_sort(int[] a) {
for(int i=0;i<a.length;i++) {
int minIndex = i;
for(int j=0;j<a.length;j++) {
if(num[j] < num[minIndex]) {
minIndex = j;
}
}
int temp = num[i];
num[i] = num[minIndex];
num[minIndex] = temp;
}
}
翻译成smali:
.method public static select_sort([I)V
.register
.prologue
array-length v0, p0
const/4 v1, 0x1 #常量赋值
if-ge v1,v0:cond_1
const
smali和dex的相互转换需要smali-2.5.2.jar
和baksmali-2.5.2.jar
这两个包。下载地址。
smali转dex:
java -jar .\smali-2.5.2.jar a Select.smali -o sean_Select.dex
进入安卓系统:
adb shell
创建目录:
mkdir /data/local/tmp/selectSort
回到windows终端,将sean_Select.dex发送给到安卓系统/data/local/tmp/selectSort
路径上:
PS D:\YC\study\系统软件安全\lab1> adb push sean_Select.dex /data/local/tmp/selectSort
sean_Select.dex: 1 file pushed, 0 skipped. 0.1 MB/s (1048 bytes in 0.014s)
在安卓系统终端运行Select.dex:
dalvikvm -cp sean_Select.dex Select 3 4 1
结果如下:
1.2.3 smali获取主机ip
Java的获取IP的程序:
import java.net.InetAddress;
import java.net.UnknownHostException;
public class GetIP{
public static void main(String args[]) {
StringBuffer res = null;
try {
res = foo(args[0]);
} catch (UnknownHostException e) {
e.printStackTrace();
}
System.out.println(res);
}
public static StringBuffer foo(String name) throws UnknownHostException {
StringBuffer res = new StringBuffer();
InetAddress address = InetAddress.getByName(name);
String hostName = address.getHostName();
String hostAddress = address.getHostAddress();
res.append(hostname).append("=").append(hostAddress);
return res;
}
}
翻译成smali程序。因为太懒了,没有自己翻。上网找了下java转smali的方式:java->class->dex->smali。
javac GetIP.java
java -jar dx.jar --dex --output=GetIP.dex GetIP.class
java -jar baksmali.jar GetIP.dex
其中dx.jar
在android-sdk\build-tools\23.0.1\lib
里面,输入上述命令后会生成一个out目录,目录里面的就是翻译好的GetIP.smali
:
.class public LGetIP;
.super Ljava/lang/Object;
.source "GetIP.java"
# direct methods
.method public constructor <init>()V
.registers 1
.prologue
.line 4
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
return-void
.end method
.method public static foo(Ljava/lang/String;)Ljava/lang/StringBuffer;
.registers 5
.annotation system Ldalvik/annotation/Throws;
value = {
Ljava/net/UnknownHostException;
}
.end annotation
.prologue
.line 15
new-instance v0, Ljava/lang/StringBuffer;
invoke-direct {v0}, Ljava/lang/StringBuffer;-><init>()V
.line 16
invoke-static {p0}, Ljava/net/InetAddress;->getByName(Ljava/lang/String;)Ljava/net/InetAddress;
move-result-object v1
.line 17
invoke-virtual {v1}, Ljava/net/InetAddress;->getHostName()Ljava/lang/String;
move-result-object v2
.line 18
invoke-virtual {v1}, Ljava/net/InetAddress;->getHostAddress()Ljava/lang/String;
move-result-object v1
.line 19
invoke-virtual {v0, v2}, Ljava/lang/StringBuffer;->append(Ljava/lang/String;)Ljava/lang/StringBuffer;
move-result-object v2
const-string v3, "="
invoke-virtual {v2, v3}, Ljava/lang/StringBuffer;->append(Ljava/lang/String;)Ljava/lang/StringBuffer;
move-result-object v2
invoke-virtual {v2, v1}, Ljava/lang/StringBuffer;->append(Ljava/lang/String;)Ljava/lang/StringBuffer;
.line 20
return-object v0
.end method
.method public static main([Ljava/lang/String;)V
.registers 3
.prologue
.line 6
const/4 v0, 0x0
.line 8
const/4 v1, 0x0
:try_start_2
aget-object v1, p0, v1
invoke-static {v1}, LGetIP;->foo(Ljava/lang/String;)Ljava/lang/StringBuffer;
:try_end_7
.catch Ljava/net/UnknownHostException; {:try_start_2 .. :try_end_7} :catch_e
move-result-object v0
.line 12
:goto_8
sget-object v1, Ljava/lang/System;->out:Ljava/io/PrintStream;
invoke-virtual {v1, v0}, Ljava/io/PrintStream;->println(Ljava/lang/Object;)V
.line 13
return-void
.line 9
:catch_e
move-exception v1
.line 10
invoke-virtual {v1}, Ljava/net/UnknownHostException;->printStackTrace()V
goto :goto_8
.end method
windows终端:
java -jar .\smali-2.5.2.jar a GetIP.smali -o sean_GetIP.dex
adb push sean_GetIP.dex /data/local/tmp/GetIP
安卓终端:
dalvikvm -cp sean_GetIP.dex GetIP weibo.com
结果如下:
1.3 Smali2Java
1.3.1 任务描述
- 将smali文件夹中的文件汇编为Box.dex,然后运行。阅读Checker.smali代码,看懂它的意思。然后写一个能实现其功能的java代码。
- 运行Box.dex,输入StudentID。然后得到一个Encode msg,再跑一遍Box.dex,输入StudentID,会返回True。阅读并理解Encoder.smali代码,然后写一个能实现其功能的java代码。
1.3.2 step1
windows终端:
java -jar .\smali-2.5.2.jar a smali -o Box.dex
adb push Box.dex /data/local/tmp/Box
安卓终端:
阅读checker.smali,一边读一遍翻译成java:
public class Checker {
private String secret;
public Checker(){secret="key";};
private boolean checkStr1(String S1) {
char[] c1 = S1.toCharArray();
int sum = 0, first = 0, second = 0;
for(int i = 0;i < c1.length;i++) {
if(c1[i] != 0x78)
continue;
sum ++;
if(sum == 1)
first = i;
if(sum == 2)
second = i;
}
if(sum != 2 || second-first != 4 || c1[0] != 0x30 || c1[c1.length-1] != 0x39)
return false;
String S2 = S1.substring(0, first);
if(S2.contains(this.secret))
return true;
return false;
}
private int count(String S1) {
char[] c1 = S1.toCharArray();
int sum = 0;
for(int i = 0;i < c1.length;i++) {
if(c1[i] == 0x31)
sum ++;
}
return sum;
}
private int func(int n)
{
if(n<=1) return 1;
else return func(n-1)*n;
}
public boolean check(String S1) {
if(S1.length() < 0xc || S1.length() > 0x10)
return false;
String S2 = S1.substring(0,0xa);
String S3 = S1.substring(0xa,S1.length());
int x2 = this.count(S3);
int x3 = this.func(x2);
if(x2 != x3 || this.checkStr1(S2) == false)
return false;
return true;
}
}
看懂了它的意思,部分注释已经写在代码上了:返回为true的串由长度由S1和S2组成,S1=“0keyx???x9”,其中"?"可为任何非"x"的字符,S2中应包含2个"1"且S2长度不大于6。
测试结果:
1.3.3 step2
先看看跑Box.dex的效果:
然后阅读Encode.smali,一边理解一边写java:
public class Encoder{
private String algorithm;
private String charSet;
private final String[] hexDigits;
Encoder() {
String[] S1 = new String[0x10];
for(int i = 0;i < 16;i++) {
S1[i] = Integer.toHexString(i);
}
hexDigits = S1;
algorithm = "MD5";
charSet = "utf-8";
}
private String byteArrayToHexString(byte[] B1)
{
StringBuffer ans = new StringBuffer();
for(int i = 0;i < B1.length;i++) {
ans.append(byteToHexString(B1[i]));
}
return ans.toString();
}
private String byteToHexString(byte B1) {
int B = B1;
if(B1 < 0)
B &= 0xff;
StringBuilder ans = new StringBuilder();
ans.append(hexDigits[B / 0x10]).append(hexDigits[B % 0x10]);
return ans.toString();
}
private String getSalt() {
StringBuilder ans = new StringBuilder(0x10);
for(int i = 0; i < 16; i++) {
if(new Random().nextBoolean())
ans.append("1");
else
ans.append("0");
}
return ans.toString();
}
public boolean check(String S1, String S2) {
char[] C1 = new char[0x20];
char[] C2 = new char[0x10];
if(S2.length() != 0x30)
return false;
for(int i = 0;i < 0x30;i += 0x3) {
C1[i / 0x3 * 0x2] = S2.charAt(i);
C1[i / 0x3 * 0x2 + 1] = S2.charAt(i + 0x2);
C2[i / 0x3] = S2.charAt(i + 0x1);
}
String S3 = new String(C2);
StringBuilder S4 = new StringBuilder();
S4.append(S1).append(S3);
String S5 = S4.toString();
try {
MessageDigest dig = MessageDigest.getInstance(algorithm);
StringBuilder str2 = new StringBuilder();
str2.append("");
str2.append(byteArrayToHexString(dig.digest(S5.getBytes(charSet))));
String str1 = new String(C1);
return str1.equals(str2.toString());
} catch(Exception e) {
e.printStackTrace();
}
return false;
}
public String encoding(String S1) {
String S2 = getSalt();
String S6 = null;
String ans = null;
StringBuilder S3 = new StringBuilder();
S3.append(S1).append(S2);
String S4 = S3.toString();
try{
MessageDigest dig = MessageDigest.getInstance(algorithm);
StringBuilder S5 = new StringBuilder();
S5 = S5.append("");
S5.append(byteArrayToHexString(dig.digest(S4.getBytes(charSet))));
S6 = S5.toString();
}catch(Exception e) {
e.printStackTrace();
}
try{
char[] C1 = new char[0x30];
for(int i = 0;i < 0x30;i += 3) {
C1[i] = S6.charAt(i / 0x3 * 0x2);
C1[i + 1] = S2.charAt(i / 0x3);
C1[i + 2] = S6.charAt(i / 0x3 * 0x2 + 1);
}
ans = new String(C1);
}catch(Exception e) {
ans = S6;
e.printStackTrace();
}
}
}
运行结果:
1.4 Reversing and Repacking
1.4.1 任务描述
通过apk解包、反编译、重新打包、apk签名完成安卓逆向。
1.4.2 Task 1 Knock the door
首先下载解包工具apktool_2.4.1.jar
,然后命令行输入java -jar apktool_2.4.1.jar d lab.apk
解包。
解包之后可以看到文件的组织吧,但主程序是smali格式,是比较难看懂的:
反编译成java需要Jeb工具,Jeb工具要在jdk1.8环境下才能工作,所以jdk环境也要配置好。
Jeb我下载的是破解版的jeb-2.2.7.201608151620_crack_qtfreet00
,用记事本打开jeb_wincon.bat
,加上一句set JAVA_HOME=C:\Program Files\Java\jdk1.8.0_121
就配置好了。
用Jeb直接打开lab.apk
:
点击Bytecode,查看Bytecode/Hierarchy,可以看到主程序。选择要看的程序,右键->Decomplie
即可查看其反汇编的java代码。主要的方法是这两个:
public MainActivity() {
super();
this.l0 = 0;
this.l1 = 999999;
}
public void buttonClick(View arg5) {
int v5 = this.l0;
if(v5 != this.l1) {
++v5;
this.l0 = v5;
this.t2.setText(String.format("%d / %d", Integer.valueOf(v5), Integer.valueOf(this.l1)));
}
else {
this.t2.setText(2131427372);
this.t3.setText(PlayGame.getFlag(this.te.getText().toString(), this.ctx));
}
}
意思大概是点击button 1000000次才能运行PlayGame。要攻击的话,把对应的smali文件中buttonClick方法if-eq改为if-ne即可。
然后回编译java -jar apktool_2.4.1.jar b lab
,报了个错:
error: No resource identifier found for attribute 'compileSdkVersion' in package 'android'
。
不知道这是什么错。最后用java -jar apktool_2.4.1.jar b lab -p framework -o lab.apk
强制回编译了QAQ。参考文章。
然后是安装openssl。官网下载地址。装好了之后把bin路径添加到环境变量里面。
然后是给apk签名:
openssl genrsa -3 -out testkey.pem 2048
openssl req -new -x509 -key testkey.pem -out testkey.x509.pem -days 10000
openssl pkcs8 -in testkey.pem -topk8 -outform DER -out testkey.pk8 -nocrypt
把lab.apk, apksigner.jar, testkey.pk8, testkey.x509.pem,testkey.pem放到同一目录,打包成签好名的lab_signed.apk
:
java -jar apksigner.jar sign --cert testkey.x509.pem --key testkey.pk8 --in lab.apk --out lab_signed.apk
然后把新的apk传到安卓设备上:adb install lab_signed.apk
,运行:
只需要点击1次button就成功啦~~~
1.4.3 Task 2 Give me your token
Task2写在PlayGame这个类里面:
public static String getFlag(String arg8, Context arg9) {
StringBuilder v9 = new StringBuilder("pore");
StringBuilder v1 = new StringBuilder("pore");
StringBuilder v2 = new StringBuilder("pore");
StringBuilder v3 = new StringBuilder("pore");
v9.setCharAt(0, ((char)(v9.charAt(0) - 4)));
v9.setCharAt(1, ((char)v9.charAt(1)));
v9.setCharAt(2, ((char)(v9.charAt(2) + 5)));
v9.setCharAt(3, ((char)(v9.charAt(3) - 1)));
v1.setCharAt(0, ((char)(v1.charAt(0) + 4)));
v1.setCharAt(1, ((char)(v1.charAt(1) + 10)));
v1.setCharAt(2, ((char)(v1.charAt(2) - 13)));
v1.setCharAt(3, ((char)(v1.charAt(3) + 7)));
v2.setCharAt(0, ((char)(v2.charAt(0) - 4)));
v2.setCharAt(1, ((char)(v2.charAt(1) - 6)));
v2.setCharAt(2, ((char)(v2.charAt(2) - 11)));
v2.setCharAt(3, ((char)(v2.charAt(3) + 3)));
v3.setCharAt(0, ((char)(v3.charAt(0) + 2)));
v3.setCharAt(1, ((char)(v3.charAt(1) - 10)));
v3.setCharAt(2, ((char)(v3.charAt(2) + 1)));
v3.setCharAt(3, ((char)(v3.charAt(3) + 14)));
if(arg8.equals("".concat(v2.toString()).concat(v1.toString()).concat(v9.toString()).concat(v3.toString()))) {
return "You got it! Task2 finished.\nTry to call sth here";
}
return "Welcome to task2";
}
翻译以下,Flag是lightyellowdress~~淡黄的长裙哈哈哈哈。
验证一下:
1.4.4 Task 3 Call to the NPC
Task2中给的hint:Try to call sth here。应该是要我们调用下面这个函数:
public static native String skdaga(String arg0) {
}
在PlayGame.smali的return前面加两行:
invoke-static {p1}, Lcom/pore/play4fun/PlayGame;->skdaga(Ljava/lang/String;)Ljava/lang/String;
move-result-objext p0
然后再次打包QAQ…
成功拿到Flag啦啊啊啊泪目。。。
Flag是flag{SmaliIsCoolll}
哈哈。
1.5 实验难点/反思
- 安卓虚拟机搞了半天。和我机子上的linux系统冲突了。重装了2次自动好了》。。
- 安卓开发并没有真正学会,都是照着样例代码打的。只能说大概了解了各组件的生命周期以及一些安卓的语法。。。
- 不知道安卓怎么调试…只学会了用log输出调试
- 逆向那边感觉还行,都可以用工具来编译or反编译or回编译。回编译那里出了一点问题,目前是用暴力的方法回编译的,并没能真正解决问题。
- 不知道smali怎么调试,就硬跑…
- 还没有在真正理解反射那部分的原理…(自己也是才学Java
- 还没有真正理解最后那个native函数的调用。
1.6. 参考资料
[2] 一篇文章帮你搞定Android中java、class、dex、smali、jar、apk之间的转换关系
[3] Smali基础知识
[4] Dalvik 字节码
[5] 教我兄弟学Android逆向番外02 jeb工具的使用