这篇文章上次修改于 1130 天前,可能其部分内容已经发生变化,如有疑问可询问作者。
原文 -> https://blog.csdn.net/qq_38851536/article/details/118024298
前言
这是SO逆向入门实战教程的第八篇,总共会有十三篇,十三个实战
本篇分析的是自写demo,帮助大家熟悉unidbg中对文件读写的处理,例子中主要涉及
- Sharedpreference读写
- Assets读写
- 文件读写
demo1设计
Sharedpreferences是Android平台上常用的存储方式,用来保存应用程序的各种配置信息,其本质是一个以键-值对的方式保存数据的xml文件,其文件保存在/data/data/selfPackage/shared_prefs目录下
在APP刚启动时,我们新建两个Sharedpreference 文件,分别填入两个键值对,name-lilac和location-china,代码实现
(如果对开发不感兴趣,也可以直接去百度网盘中获取demo文件的apk进行分析)
package com.roysue.readsp;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
// Used to load the 'native-lib' library on application startup.
static {
System.loadLibrary("native-lib");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//步骤1:创建SharedPreferences对象
SharedPreferences sharedPreferences1= getSharedPreferences("one", Context.MODE_PRIVATE);
SharedPreferences sharedPreferences2= getSharedPreferences("two", Context.MODE_PRIVATE);
//步骤2: 实例化SharedPreferences.Editor对象
SharedPreferences.Editor editor1 = sharedPreferences1.edit();
SharedPreferences.Editor editor2 = sharedPreferences2.edit();
//步骤3:将获取过来的值放入文件
editor1.putString("name", "lilac");
editor2.putString("location", "china");
//步骤4:提交
editor1.apply();
editor2.apply();
TextView tv = findViewById(R.id.sample_text);
Button btn = findViewById(R.id.button);
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
tv.setText(stringFromJNI(getApplicationContext()));
}
});
}
/**
* A native method that is implemented by the 'native-lib' native library,
* which is packaged with this application.
*/
public native String stringFromJNI(Context context);
}
在shared_prefs下反映为两个如下的xml
- one.xml
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="name">lilac</string>
</map>
- two.xml
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="location">china</string>
</map>
在程序逻辑中,我们打算在stringFromJni中做一件很简单的事:在1.xml中读取gt_m的值,在2.xml中读取gt_fp的值,然后拼接后打印
但在代码实现上,我们打算绕个弯
在读取1.xml的name上,我们使用JNI去调用JAVA层对SharedPreference操纵的API
//反射Context类
jclass cls_Context = env->FindClass("android/content/Context");
//反射Context类getSharedPreferences方法
jmethodID mid_getSharedPreferences = env->GetMethodID(cls_Context, "getSharedPreferences", "(Ljava/lang/String;I)Landroid/content/SharedPreferences;");
//获取Context类MODE_PRIVATE属性值
//执行反射方法
jobject obj_sharedPreferences = env->CallObjectMethod(mycontext, mid_getSharedPreferences, env->NewStringUTF("one"), 0);
jclass cls_SharedPreferences = env->FindClass("android/content/SharedPreferences");
//反射SharedPreferences类的getString方法
jmethodID mid_getString = env->GetMethodID(cls_SharedPreferences, "getString", "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;");
//参数类型转换
jstring key_name = env->NewStringUTF("name");
//参数类型转换
jstring default_value = env->NewStringUTF(" ");
//执行反射方法
jstring key_value1 = (jstring) env->CallObjectMethod(obj_sharedPreferences, mid_getString, key_name, default_value);
const char *c_key_value1 = env->GetStringUTFChars(key_value1, 0);
在读取2.xml的location上,我们换个方式
既然Sharedpreference本质上是个xml文件,那么用native中原生的open函数去读可能会更隐蔽,可是open本质上也是通过底层系统调用open的方式实现
那我们直截了当通过系统调用open打开这个xml也是一样(处理时,我懒得切割字符串,所以直接拼接two.xml整个xml吧)
完整代码
#include <jni.h>
#include <string>
#include <sys/syscall.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
__attribute__((naked))
long raw_syscall(long _number,...){
__asm__ __volatile__ ("MOV R12,SP\r\n"
"STMFD SP!,{R4-R7}\r\n"
"MOV R7,R0\r\n"
"MOV R0,R1\r\n"
"MOV R1,R2\r\n"
"MOV R2,R3\r\n"
"LDMIA R12,{R3-R6}\r\n"
"SVC 0\r\n"
"LDMFD SP!,{R4-R7}\r\n"
"mov pc,lr");
};
char* test_syscall(const char* file_path){
char *result = "";
long fd = raw_syscall(5,file_path, O_RDONLY | O_CREAT, 400);
if(fd != -1){
char buffer[0x100] = {0};
raw_syscall(3, fd, buffer, 0x100);
result = buffer;
raw_syscall(6, fd);
}
return result;
}
extern "C" JNIEXPORT jstring JNICALL
Java_com_roysue_readsp_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */, jobject mycontext) {
//反射Context类
jclass cls_Context = env->FindClass("android/content/Context");
//反射Context类getSharedPreferences方法
jmethodID mid_getSharedPreferences = env->GetMethodID(cls_Context,
"getSharedPreferences",
"(Ljava/lang/String;I)Landroid/content/SharedPreferences;");
//获取Context类MODE_PRIVATE属性值
//执行反射方法
jobject obj_sharedPreferences = env->CallObjectMethod(mycontext,
mid_getSharedPreferences, env->NewStringUTF("one"),
0);
jclass cls_SharedPreferences = env->FindClass("android/content/SharedPreferences");
//反射SharedPreferences类的getString方法
jmethodID mid_getString = env->GetMethodID(cls_SharedPreferences,
"getString",
"(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;");
//参数类型转换
jstring key_name = env->NewStringUTF("name");
//参数类型转换
jstring default_value = env->NewStringUTF(" ");
//执行反射方法
jstring key_value1 = (jstring) env->CallObjectMethod(obj_sharedPreferences,
mid_getString, key_name, default_value);
const char *c_key_value1 = env->GetStringUTFChars(key_value1, 0);
const char *path = "/data/data/com.roysue.readsp/shared_prefs/two.xml";
const char *result = test_syscall(path);
char dest[1000];
strcpy(dest,c_key_value1);
strcat(dest,result);
return env->NewStringUTF(dest);
}
结果即
lilac<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="location">china</string>
</map>
需要注意的是,因为只是一个demo,所以有些地方采用了硬编码
自己跑代码的时候,如果无法顺利跑出结果,请检查是否是路径等硬编码与本机环境不符,或者在微信联系我,很荣幸和同侪交流互动
unidbg模拟执行demo1
首先想一下我们可能遇到的问题,应该就是两个
- 补JAVA环境(one.xml的读取是通过JNI调用JAVA层API实现的)
- 补文件访问/重定向 (two.xml的读取,最终通过open系统调用实现,需要将文件重定向到本机某个位置
快开干
package com.lession8;
import com.github.unidbg.linux.android.dvm.AbstractJni;
import com.github.unidbg.Module;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.memory.Memory;
import java.io.File;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.List;
public class demo1 extends AbstractJni {
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;
demo1(){
// 防止进程名检测
emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.readSp").build(); // 创建模拟器实例,要模拟32位或者64位,在这里区分
final Memory memory = emulator.getMemory(); // 模拟器的内存操作接口
memory.setLibraryResolver(new AndroidResolver(23)); // 设置系统类库解析
vm = emulator.createDalvikVM(null);
DalvikModule dm = vm.loadLibrary(new File("C:\\Users\\pr0214\\Desktop\\DTA\\unidbg\\versions\\unidbg-2021-5-17\\unidbg-master\\unidbg-android\\src\\test\\java\\com\\lession8\\libnative-lib.so"), true);
module = dm.getModule();
vm.setJni(this);
vm.setVerbose(true);
}
public String call(){
List<Object> list = new ArrayList<>(10);
list.add(vm.getJNIEnv()); // 第一个参数是env
list.add(0); // 第二个参数,实例方法是jobject,静态方法是jclazz,直接填0,一般用不到
Object custom = null;
DvmObject<?> context = vm.resolveClass("android/content/Context").newObject(custom);// context
list.add(vm.addLocalObject(context));
Number number = module.callFunction(emulator, 0xAAC + 1, list.toArray())[0];
String result = vm.getObject(number.intValue()).getValue().toString();
return result;
}
public static void main(String[] args) throws FileNotFoundException {
demo1 test = new demo1();
System.out.println(test.call());
}
}
运行
JAVA环境缺失,在获取getSharedPreferences,对应于开发中的这一步
方法参数1是Sharedpreference的名字,参数2默认为0即可,返回SharedPreferences对象
我们补一个空的SharedPreferences返回
@Override
public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
switch (signature) {
case "android/content/Context->getSharedPreferences(Ljava/lang/String;I)Landroid/content/SharedPreferences;":
return vm.resolveClass("android/content/SharedPreferences").newObject(null);
}
return super.callObjectMethodV(vm, dvmObject, signature, vaList);
}
但我们需要思考一下,这样真的好吗?
假设APP在JNI中从五个SharedPreferences里读了十五个键值对,并且不同xml的键名有重复,如果每次取SharedPreferences时我们都返回空对象,那后面怎么区分a.xml和b.xml里键名都是name的数据呢?
先前我们说,参数1是想要获取的SharedPreferences的名字,应该把它放对象里返回,这样就有了标识性
@Override
public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
switch (signature) {
case "android/content/Context->getSharedPreferences(Ljava/lang/String;I)Landroid/content/SharedPreferences;":
return vm.resolveClass("android/content/SharedPreferences").newObject(vaList.getObject(0));
}
return super.callObjectMethodV(vm, dvmObject, signature, vaList);
}
继续运行
显然,这儿就是在获取one.xml的name所对应的值,我们应该返回lilac
@Override
public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
switch (signature) {
case "android/content/Context->getSharedPreferences(Ljava/lang/String;I)Landroid/content/SharedPreferences;":
return vm.resolveClass("android/content/SharedPreferences").newObject(vaList.getObject(0));
case "android/content/SharedPreferences->getString(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;":
return new StringObject(vm, "lilac");
}
return super.callObjectMethodV(vm, dvmObject, signature, vaList);
}
这样做固然可以,但能不能更严谨规范一些呢?当然可以
@Override
public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
switch (signature) {
case "android/content/Context->getSharedPreferences(Ljava/lang/String;I)Landroid/content/SharedPreferences;":
return vm.resolveClass("android/content/SharedPreferences").newObject(vaList.getObject(0));
case "android/content/SharedPreferences->getString(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;": {
// 如果是one.xml
if(((StringObject) dvmObject.getValue()).getValue().equals("one")){
// 如果键是name
if (vaList.getObject(0).getValue().equals("name")) {
return new StringObject(vm, "lilac");
}
}
}
}
return super.callObjectMethodV(vm, dvmObject, signature, vaList);
}
接着运行
显然,对one.xml的读取已经顺利完成了,可是千万别忘了,two.xml呢?
为什么没有做拼接?或者说,为什么unidbg没有提醒我们对文件进行重定向?two.xml 我们还没操作呢!
这是因为我们对two.xml的操作采用系统调用的方式完成,但我们没有打开unidbg中系统调用的日志显示
如下打开对arm32中系统调用的日志显示
再次运行
事实上,我建议运行真实样本时,开启所有的日志以防错过重要的环境缺失
Logger.getLogger("com.github.unidbg.linux.ARM32SyscallHandler").setLevel(Level.DEBUG);
Logger.getLogger("com.github.unidbg.unix.UnixSyscallHandler").setLevel(Level.DEBUG);
Logger.getLogger("com.github.unidbg.AbstractEmulator").setLevel(Level.DEBUG);
Logger.getLogger("com.github.unidbg.linux.android.dvm.DalvikVM").setLevel(Level.DEBUG);
Logger.getLogger("com.github.unidbg.linux.android.dvm.BaseVM").setLevel(Level.DEBUG);
Logger.getLogger("com.github.unidbg.linux.android.dvm").setLevel(Level.DEBUG);
上篇中,我们讲了两种方式补文件访问
- unidbg提供的rootfs虚拟文件系统
- 代码方式文件重定向
大家自行选择
有人可能会问,如果我不想传入文件,能不能只传入字符串,当然可以,从SimpleFileIO换成ByteArrayFileIO即可
@Override
public FileResult resolve(Emulator emulator, String pathname, int oflags) {
if ("/data/data/com.roysue.readsp/shared_prefs/two.xml".equals(pathname)) {
return FileResult.success(new ByteArrayFileIO(oflags, pathname, "mytest".getBytes()));
}
return null;
}
看一下完整代码
package com.lession8;
import com.github.unidbg.Emulator;
import com.github.unidbg.file.FileResult;
import com.github.unidbg.file.IOResolver;
import com.github.unidbg.linux.android.dvm.AbstractJni;
import com.github.unidbg.Module;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.file.ByteArrayFileIO;
import com.github.unidbg.memory.Memory;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import java.io.File;
import java.io.FileNotFoundException;
import java.util.*;
public class demo1 extends AbstractJni implements IOResolver {
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;
demo1(){
// 防止进程名检测
emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.readSp").build(); // 创建模拟器实例,要模拟32位或者64位,在这里区分
final Memory memory = emulator.getMemory(); // 模拟器的内存操作接口
memory.setLibraryResolver(new AndroidResolver(23)); // 设置系统类库解析
vm = emulator.createDalvikVM(null);
emulator.getSyscallHandler().addIOResolver(this);
DalvikModule dm = vm.loadLibrary(new File("unidbg-android\\src\\test\\java\\com\\lession8\\libnative-lib.so"), true);
module = dm.getModule();
vm.setJni(this);
vm.setVerbose(true);
}
public String call(){
List<Object> list = new ArrayList<>(10);
list.add(vm.getJNIEnv()); // 第一个参数是env
list.add(0); // 第二个参数,实例方法是jobject,静态方法是jclazz,直接填0,一般用不到
Object custom = null;
DvmObject<?> context = vm.resolveClass("android/content/Context").newObject(custom);// context
list.add(vm.addLocalObject(context));
Number number = module.callFunction(emulator, 0xAAC + 1, list.toArray())[0];
String result = vm.getObject(number.intValue()).getValue().toString();
return result;
}
@Override
public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
switch (signature) {
case "android/content/Context->getSharedPreferences(Ljava/lang/String;I)Landroid/content/SharedPreferences;":
return vm.resolveClass("android/content/SharedPreferences").newObject(vaList.getObject(0));
case "android/content/SharedPreferences->getString(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;": {
// 如果是one.xml
if(((StringObject) dvmObject.getValue()).getValue().equals("one")){
// 如果键是name
if (vaList.getObject(0).getValue().equals("name")) {
return new StringObject(vm, "lilac");
}
}
}
}
return super.callObjectMethodV(vm, dvmObject, signature, vaList);
}
public static void main(String[] args) throws FileNotFoundException {
Logger.getLogger("com.github.unidbg.linux.ARM32SyscallHandler").setLevel(Level.DEBUG);
demo1 test = new demo1();
System.out.println(test.call());
}
@Override
public FileResult resolve(Emulator emulator, String pathname, int oflags) {
if ("/data/data/com.roysue.readsp/shared_prefs/two.xml".equals(pathname)) {
return FileResult.success(new ByteArrayFileIO(oflags, pathname, "mytest".getBytes()));
}
return null;
}
}
demo2设计
各类加密算法大部分都有密钥的存在,非对称加密算法还有公钥私钥之分,所以加密算法运算时需要传入密钥,但是直接参数方式传递很容易被分析者意识到这是密钥,有没有办法更隐蔽一些呢?
比如我们把密钥放在xml中,在native中读取它,就类似于demo1
有没有更隐蔽一些些的呢,我们可以把密钥藏在资源文件的图片里
即在so里读取资源文件里的某张图片,以它的某部分或者整体的md5结果作为密钥?这是一个更好的方案
demo2就是这个方案的简单实现——native中读取资源文件的1.jpg,并求其md5值,返回JAVA层
看一下代码
JAVA层
package com.roysue.readasset;
import androidx.appcompat.app.AppCompatActivity;
import android.content.res.AssetManager;
import android.os.Bundle;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
// Used to load the 'native-lib' library on application startup.
static {
System.loadLibrary("native-lib");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Example of a call to a native method
TextView tv = findViewById(R.id.sample_text);
tv.setText(setNativeAssetManager(getAssets()));
}
public native String setNativeAssetManager(AssetManager assetManager);
}
Native层
#include <jni.h>
#include <string>
#include "android/asset_manager.h"
#include "android/asset_manager_jni.h"
#define MD5_LONG unsigned long
// 分组大小
#define MD5_CBLOCK 64
// 分块个数
#define MD5_LBLOCK (MD5_CBLOCK/4)
// 摘要长度(字节)
#define MD5_DIGEST_LENGTH 16
#define MD32_REG_T long
// 小端序
#define DATA_ORDER_IS_LITTLE_ENDIAN
// 四个初始化常量
#define INIT_DATA_A (unsigned long)0x67452301L
#define INIT_DATA_B (unsigned long)0xefcdab89L
#define INIT_DATA_C (unsigned long)0x98badcfeL
#define INIT_DATA_D (unsigned long)0x10325476L
// 循环左移以及取低64位
#define ROTATE(a,n) (((a)<<(n))|(((a)&0xffffffff)>>(32-(n))))
// 大小端序互转
#define HOST_c2l(c,l) (l =(((unsigned long)(*((c)++))) ), \
l|=(((unsigned long)(*((c)++)))<< 8), \
l|=(((unsigned long)(*((c)++)))<<16), \
l|=(((unsigned long)(*((c)++)))<<24) )
#define HOST_l2c(l,c) (*((c)++)=(unsigned char)(((l) )&0xff), \
*((c)++)=(unsigned char)(((l)>> 8)&0xff), \
*((c)++)=(unsigned char)(((l)>>16)&0xff), \
*((c)++)=(unsigned char)(((l)>>24)&0xff), \
l)
// 更新链接变量值
#define HASH_MAKE_STRING(c,s) do { \
unsigned long ll; \
ll=(c)->A; (void)HOST_l2c(ll,(s)); \
ll=(c)->B; (void)HOST_l2c(ll,(s)); \
ll=(c)->C; (void)HOST_l2c(ll,(s)); \
ll=(c)->D; (void)HOST_l2c(ll,(s)); \
} while (0)
// 四个初始化非线性函数,或者叫逻辑函数
#define F(b,c,d) ((((c) ^ (d)) & (b)) ^ (d))
#define G(b,c,d) ((((b) ^ (c)) & (d)) ^ (c))
#define H(b,c,d) ((b) ^ (c) ^ (d))
#define I(b,c,d) (((~(d)) | (b)) ^ (c))
// F函数,每隔16步/轮 换下一个
#define R0(a,b,c,d,k,s,t) { \
a+=((k)+(t)+F((b),(c),(d))); \
a=ROTATE(a,s); \
a+=b; };
#define R1(a,b,c,d,k,s,t) { \
a+=((k)+(t)+G((b),(c),(d))); \
a=ROTATE(a,s); \
a+=b;};
#define R2(a,b,c,d,k,s,t) { \
a+=((k)+(t)+H((b),(c),(d))); \
a=ROTATE(a,s); \
a+=b; };
#define R3(a,b,c,d,k,s,t) { \
a+=((k)+(t)+I((b),(c),(d))); \
a=ROTATE(a,s); \
a+=b; };
typedef struct MD5state_st1{
MD5_LONG A,B,C,D; // ABCD
MD5_LONG Nl,Nh; // 数据的bit数计数器(对2^64取余),Nh存储高32位,Nl存储低32位,这种设计是服务于32位处理器,MD5的设计就是为了服务于32位处理器的
MD5_LONG data[MD5_LBLOCK];//数据缓冲区
unsigned int num;
}MD5_CTX; // 存放MD5算法相关信息的结构体定义
unsigned char cleanse_ctr = 0;
// 初始化链接变量/幻数
int MD5_Init(MD5_CTX *c){
memset (c,0,sizeof(*c));
c->A=INIT_DATA_A;
c->B=INIT_DATA_B;
c->C=INIT_DATA_C;
c->D=INIT_DATA_D;
return 1;
}
// md5 一个分组中的全部运算
void md5_block_data_order(MD5_CTX *c, const void *data_, unsigned int num){
const unsigned char *data= static_cast<const unsigned char *>(data_);
register unsigned MD32_REG_T A,B,C,D,l;
#ifndef MD32_XARRAY
unsigned MD32_REG_T XX0, XX1, XX2, XX3, XX4, XX5, XX6, XX7,
XX8, XX9,XX10,XX11,XX12,XX13,XX14,XX15;
# define X(i) XX##i
#else
MD5_LONG XX[MD5_LBLOCK];
# define X(i) XX[i]
#endif
A=c->A;
B=c->B;
C=c->C;
D=c->D;
// 64轮
// 前16轮需要改变分组中每个分块的
for (;num--;){
HOST_c2l(data,l); X( 0)=l; HOST_c2l(data,l); X( 1)=l;
/* Round 0 */
R0(A,B,C,D,X( 0), 7,0xd76aa478L); HOST_c2l(data,l); X( 2)=l;
R0(D,A,B,C,X( 1),12,0xe8c7b756L); HOST_c2l(data,l); X( 3)=l;
R0(C,D,A,B,X( 2),17,0x242070dbL); HOST_c2l(data,l); X( 4)=l;
R0(B,C,D,A,X( 3),22,0xc1bdceeeL); HOST_c2l(data,l); X( 5)=l;
R0(A,B,C,D,X( 4), 7,0xf57c0fafL); HOST_c2l(data,l); X( 6)=l;
R0(D,A,B,C,X( 5),12,0x4787c62aL); HOST_c2l(data,l); X( 7)=l;
R0(C,D,A,B,X( 6),17,0xa8304613L); HOST_c2l(data,l); X( 8)=l;
R0(B,C,D,A,X( 7),22,0xfd469501L); HOST_c2l(data,l); X( 9)=l;
R0(A,B,C,D,X( 8), 7,0x698098d8L); HOST_c2l(data,l); X(10)=l;
R0(D,A,B,C,X( 9),12,0x8b44f7afL); HOST_c2l(data,l); X(11)=l;
R0(C,D,A,B,X(10),17,0xffff5bb1L); HOST_c2l(data,l); X(12)=l;
R0(B,C,D,A,X(11),22,0x895cd7beL); HOST_c2l(data,l); X(13)=l;
R0(A,B,C,D,X(12), 7,0x6b901122L); HOST_c2l(data,l); X(14)=l;
R0(D,A,B,C,X(13),12,0xfd987193L); HOST_c2l(data,l); X(15)=l;
R0(C,D,A,B,X(14),17,0xa679438eL);
R0(B,C,D,A,X(15),22,0x49b40821L);
/* Round 1 */
R1(A,B,C,D,X( 1), 5,0xf61e2562L);
R1(D,A,B,C,X( 6), 9,0xc040b340L);
R1(C,D,A,B,X(11),14,0x265e5a51L);
R1(B,C,D,A,X( 0),20,0xe9b6c7aaL);
R1(A,B,C,D,X( 5), 5,0xd62f105dL);
R1(D,A,B,C,X(10), 9,0x02441453L);
R1(C,D,A,B,X(15),14,0xd8a1e681L);
R1(B,C,D,A,X( 4),20,0xe7d3fbc8L);
R1(A,B,C,D,X( 9), 5,0x21e1cde6L);
R1(D,A,B,C,X(14), 9,0xc33707d6L);
R1(C,D,A,B,X( 3),14,0xf4d50d87L);
R1(B,C,D,A,X( 8),20,0x455a14edL);
R1(A,B,C,D,X(13), 5,0xa9e3e905L);
R1(D,A,B,C,X( 2), 9,0xfcefa3f8L);
R1(C,D,A,B,X( 7),14,0x676f02d9L);
R1(B,C,D,A,X(12),20,0x8d2a4c8aL);
/* Round 2 */
R2(A,B,C,D,X( 5), 4,0xfffa3942L);
R2(D,A,B,C,X( 8),11,0x8771f681L);
R2(C,D,A,B,X(11),16,0x6d9d6122L);
R2(B,C,D,A,X(14),23,0xfde5380cL);
R2(A,B,C,D,X( 1), 4,0xa4beea44L);
R2(D,A,B,C,X( 4),11,0x4bdecfa9L);
R2(C,D,A,B,X( 7),16,0xf6bb4b60L);
R2(B,C,D,A,X(10),23,0xbebfbc70L);
R2(A,B,C,D,X(13), 4,0x289b7ec6L);
R2(D,A,B,C,X( 0),11,0xeaa127faL);
R2(C,D,A,B,X( 3),16,0xd4ef3085L);
R2(B,C,D,A,X( 6),23,0x04881d05L);
R2(A,B,C,D,X( 9), 4,0xd9d4d039L);
R2(D,A,B,C,X(12),11,0xe6db99e5L);
R2(C,D,A,B,X(15),16,0x1fa27cf8L);
R2(B,C,D,A,X( 2),23,0xc4ac5665L);
/* Round 3 */
R3(A,B,C,D,X( 0), 6,0xf4292244L);
R3(D,A,B,C,X( 7),10,0x432aff97L);
R3(C,D,A,B,X(14),15,0xab9423a7L);
R3(B,C,D,A,X( 5),21,0xfc93a039L);
R3(A,B,C,D,X(12), 6,0x655b59c3L);
R3(D,A,B,C,X( 3),10,0x8f0ccc92L);
R3(C,D,A,B,X(10),15,0xffeff47dL);
R3(B,C,D,A,X( 1),21,0x85845dd1L);
R3(A,B,C,D,X( 8), 6,0x6fa87e4fL);
R3(D,A,B,C,X(15),10,0xfe2ce6e0L);
R3(C,D,A,B,X( 6),15,0xa3014314L);
R3(B,C,D,A,X(13),21,0x4e0811a1L);
R3(A,B,C,D,X( 4), 6,0xf7537e82L);
R3(D,A,B,C,X(11),10,0xbd3af235L);
R3(C,D,A,B,X( 2),15,0x2ad7d2bbL);
R3(B,C,D,A,X( 9),21,0xeb86d391L);
A = c->A += A;
B = c->B += B;
C = c->C += C;
D = c->D += D;
}
}
// 传入需要哈希的明文,支持多次调用
int MD5_Update(MD5_CTX *c, const void *data_, size_t len){
const unsigned char *data= static_cast<const unsigned char *>(data_);
unsigned char *p;
MD5_LONG l;
size_t n;
if (len==0) return 1;
// 低位
l=(c->Nl+(((MD5_LONG)len)<<3))&0xffffffffUL;
if (l < c->Nl)
c->Nh++;
// 高位
c->Nh+=(MD5_LONG)(len>>29);
c->Nl=l;
n = c->num;
if (n != 0){
p=(unsigned char *)c->data;
if (len >= MD5_CBLOCK || len+n >= MD5_CBLOCK){
memcpy (p+n,data,MD5_CBLOCK-n);
md5_block_data_order(c,p,1);
n = MD5_CBLOCK-n;
data += n;
len -= n;
c->num = 0;
memset (p,0,MD5_CBLOCK);
}else{
memcpy (p+n,data,len);
c->num += (unsigned int)len;
return 1;
}
}
n = len/MD5_CBLOCK;
if (n > 0){
md5_block_data_order(c,data,n);
n *= MD5_CBLOCK;
data += n;
len -= n;
}
if (len != 0){
p = (unsigned char *)c->data;
c->num = (unsigned int)len;
memcpy (p,data,len);
}
return 1;
}
// 得出最终结果
int MD5_Final(unsigned char *md, MD5_CTX *c){
unsigned char *p = (unsigned char *)c->data;
size_t n = c->num;
p[n] = 0x80; /* there is always room for one */
n++;
if (n > (MD5_CBLOCK-8)){
memset (p+n,0,MD5_CBLOCK-n);
n=0;
md5_block_data_order(c,p,1);
}
memset (p+n,0,MD5_CBLOCK-8-n);
p += MD5_CBLOCK-8;
#if defined(DATA_ORDER_IS_BIG_ENDIAN)
(void)HOST_l2c(c->Nh,p);
(void)HOST_l2c(c->Nl,p);
#elif defined(DATA_ORDER_IS_LITTLE_ENDIAN)
(void)HOST_l2c(c->Nl,p);
(void)HOST_l2c(c->Nh,p);
#endif
p -= MD5_CBLOCK;
md5_block_data_order(c,p,1);
c->num=0;
memset (p,0,MD5_CBLOCK);
#ifndef HASH_MAKE_STRING
#error "HASH_MAKE_STRING must be defined!"
#else
HASH_MAKE_STRING(c,md);
#endif
return 1;
}
//清除加载的各种算法,包括对称算法、摘要算法以及 PBE 算法,并清除这些算法相关的哈希表的内容
void OPENSSL_cleanse(void *ptr, size_t len){
unsigned char *p = static_cast<unsigned char *>(ptr);
size_t loop = len, ctr = cleanse_ctr;
while(loop--){
*(p++) = (unsigned char)ctr;
ctr += (17 + ((size_t)p & 0xF));
}
p= static_cast<unsigned char *>(memchr(ptr, (unsigned char) ctr, len));
if(p)
ctr += (63 + (size_t)p);
cleanse_ctr = (unsigned char)ctr;
}
// 计算图片的md5值并返回
extern "C"
JNIEXPORT jstring JNICALL
Java_com_roysue_readasset_MainActivity_setNativeAssetManager(JNIEnv *env, jobject thiz,jobject asset_manager) {
AAssetManager *nativeasset = AAssetManager_fromJava(env, asset_manager);
AAsset *assetFile = AAssetManager_open(nativeasset, "1.png", AASSET_MODE_BUFFER);
size_t fileLength = AAsset_getLength(assetFile);
char *dataBuffer = (char *) malloc(fileLength);
//read file data
AAsset_read(assetFile, dataBuffer, fileLength);
//the data has been copied to dataBuffer2, so , close it
AAsset_close(assetFile);
// 初始化MD5的上下文结构体
MD5_CTX context = {0};
MD5_Init(&context);
// 传入待处理的内容以及内容的长度
MD5_Update(&context, dataBuffer, fileLength);
// 收尾和输出
// 输出的缓冲区
unsigned char dest[16] = {0};
MD5_Final(dest, &context);
// 结果转成十六进制字符串
int i = 0;
char szMd5[33] = {0};
for(i=0; i<16; i++){
sprintf(szMd5, "%s%02x", szMd5, dest[i]);
}
//free malloc
free(dataBuffer);
// 传回Java世界
return env->NewStringUTF(szMd5);
}
运行结果
unidbg模拟执行demo2
模拟执行demo2会出现什么问题呢?先看一下IDA F5 效果
再写一下unidbg代码
package com.lession8;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Module;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.memory.Memory;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import java.io.File;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.List;
public class demo2 extends AbstractJni {
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;
demo2(){
emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.readAssets").build(); // 创建模拟器实例,要模拟32位或者64位,在这里区分
final Memory memory = emulator.getMemory(); // 模拟器的内存操作接口
memory.setLibraryResolver(new AndroidResolver(23)); // 设置系统类库解析
vm = emulator.createDalvikVM(new File("unidbg-android\\src\\test\\java\\com\\lession8\\demo2.apk"));
DalvikModule dm = vm.loadLibrary(new File("unidbg-android\\src\\test\\java\\com\\lession8\\libnative-lib.so"), true);
module = dm.getModule();
vm.setJni(this);
vm.setVerbose(true);
}
public String call(){
List<Object> list = new ArrayList<>(10);
list.add(vm.getJNIEnv()); // 第一个参数是env
list.add(0); // 第二个参数,实例方法是jobject,静态方法是jclazz,直接填0,一般用不到
Object custom = null;
DvmObject<?> assetManager = vm.resolveClass("android/content/res/AssetManager").newObject(custom);// context
list.add(vm.addLocalObject(assetManager));
Number number = module.callFunction(emulator, 0x207C + 1, list.toArray())[0];
String result = vm.getObject(number.intValue()).getValue().toString();
return result;
}
public static void main(String[] args) throws FileNotFoundException {
Logger.getLogger("com.github.unidbg.linux.ARM32SyscallHandler").setLevel(Level.DEBUG);
Logger.getLogger("com.github.unidbg.unix.UnixSyscallHandler").setLevel(Level.DEBUG);
Logger.getLogger("com.github.unidbg.AbstractEmulator").setLevel(Level.DEBUG);
Logger.getLogger("com.github.unidbg.linux.android.dvm.DalvikVM").setLevel(Level.DEBUG);
Logger.getLogger("com.github.unidbg.linux.android.dvm.BaseVM").setLevel(Level.DEBUG);
Logger.getLogger("com.github.unidbg.linux.android.dvm").setLevel(Level.DEBUG);
demo2 test = new demo2();
System.out.println("call demo2");
System.out.println(test.call());
}
}
运行
直截了当的报错,根据traceCode定位
AAssetManager_fromJava函数哪来的?为什么报错?这需要我们思考两个问题
- Android开发中,Native层如何读取Assets文件
- unidbg如何处理这情况
首先,apk资源文件的读取与demo1不同,并非简单的open了事
Assets加载相关的API,比如AAssetManager_fromJava,就是由libandroid.so这个系统SO实现,但是呢,unidbg并没有内置加载这个系统SO,我们首先看一下unidbg完整支持的系统SO
可以发现,其实并不是很多
考虑两个个问题
- 为什么unidbg不内置支持所有系统SO的加载
- 如果一个SO的依赖SO里包含unidbg尚未支持的系统SO,那该怎么办?
先讨论第一个问题
一部分原因是大部分SO中主要的依赖项,就是unidbg已经支持的这些,即已经够用了
把Android系统中全部SO都成功加载进unidbg虚拟内存中,既是很大的工作量,又会占用过多内存
另一个更主要的原因是,比如libandroid.so
,其依赖SO实在太多了,想顺利加载整个SO确确实实是个苦差事!
再看问题2
如果SO的依赖项中有unidbg不支持的系统SO,怎么办?
首先,unidbg会给予提示
其次,尽管SO加载了unidbg不支持的SO,但有可能我们的目标函数并没有使用到这个系统SO,这种情况下就不用理会,当作不存在就行
但如果目标函数使用到了这个系统SO,那就麻烦了,我们就得直面这个问题,一般有两种处理办法
- Patch/Hook 这个不支持的SO所使用的函数
- 使用unidbg VirtualModule
方法一没什么技术含量而且并不总是能用
我们主要看一下方法二
VirtualModule是unidbg为此种情况所提供的官方解决方案,并在代码中提供了两个示例
分别是对libandroid.so以及libJniGraphics的处理
我们使用一下
只用这一句即可,需要注意,一定要在样本SO加载前加载它,道理也很简单,系统SO肯定比用户SO加载早鸭
unidbg如何实现一个VirtualModule?此类问题我们在更后面的文章去讨论它
需要注意的是,VirtualModule并不是一种真正意义上的加载SO,它本质上也是Hook,只不过实现了SO中少数几个函数罢了
比如AndroidModule中,只实现了libandroid中这几个常用的导出函数
@Override
protected void onInitialize(Emulator<?> emulator, final VM vm, Map<String, unidbgPointer> symbols) {
boolean is64Bit = emulator.is64Bit();
SvcMemory svcMemory = emulator.getSvcMemory();
symbols.put("AAssetManager_fromJava", svcMemory.registerSvc(is64Bit ? new Arm64Svc() {
@Override
public long handle(Emulator<?> emulator) {
return fromJava(emulator, vm);
}
} : new ArmSvc() {
@Override
public long handle(Emulator<?> emulator) {
return fromJava(emulator, vm);
}
}));
symbols.put("AAssetManager_open", svcMemory.registerSvc(is64Bit ? new Arm64Svc() {
@Override
public long handle(Emulator<?> emulator) {
return open(emulator, vm);
}
} : new ArmSvc() {
@Override
public long handle(Emulator<?> emulator) {
return open(emulator, vm);
}
}));
symbols.put("AAsset_close", svcMemory.registerSvc(is64Bit ? new Arm64Svc() {
@Override
public long handle(Emulator<?> emulator) {
return close(emulator, vm);
}
} : new ArmSvc() {
@Override
public long handle(Emulator<?> emulator) {
return close(emulator, vm);
}
}));
symbols.put("AAsset_getBuffer", svcMemory.registerSvc(is64Bit ? new Arm64Svc() {
@Override
public long handle(Emulator<?> emulator) {
return getBuffer(emulator, vm);
}
} : new ArmSvc() {
@Override
public long handle(Emulator<?> emulator) {
return getBuffer(emulator, vm);
}
}));
symbols.put("AAsset_getLength", svcMemory.registerSvc(is64Bit ? new Arm64Svc() {
@Override
public long handle(Emulator<?> emulator) {
return getLength(emulator, vm);
}
} : new ArmSvc() {
@Override
public long handle(Emulator<?> emulator) {
return getLength(emulator, vm);
}
}));
symbols.put("AAsset_read", svcMemory.registerSvc(is64Bit ? new Arm64Svc() {
@Override
public long handle(Emulator<?> emulator) {
throw new BackendException();
}
} : new ArmSvc() {
@Override
public long handle(Emulator<?> emulator) {
return read(emulator, vm);
}
}));
}
没有评论