注:框架有風險,使用要謹慎.
Cydia Substrate是一個代碼修改平臺.它可以修改任何主進程的代碼,不管是用Java還是C/C++(native代碼)編寫的.而Xposed只支持HOOK app_process中的java函數,因此Cydia Substrate是一款強大而實用的HOOK工具.
官網地址:http://www.cydiasubstrate.com/
官方教程:http://www.cydiasubstrate.com/id/38be592b-bda7-4dd2-b049-cec44ef7a73b
SDK下載地址:http://asdk.cydiasubstrate.com/zips/cydia_substrate-r2.zip
之前講解過 xposed 的用法為啥還要整這個了,下面簡單對比兩款框架.想了解之前 xposed 篇的可以看這里:http://drops.wooyun.org/tips/7488
劣勢:
優勢:
1.安裝框架app:http://www.cydiasubstrate.com/download/com.saurik.substrate.apk
2.創建一個空的Android工程.由于創建的工程將以插件的形式被加載,所以不需要activity.將SDK中的substrate-api.jar復制到project/libs文件夾中.
3.配置Manifest文件
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<meta-data android:name="com.saurik.substrate.main"
android:value=".Main"/>
</application>
<uses-permission android:name="cydia.permission.SUBSTRATE"/>
</manifest>
4.創建一個類,類名為Main.類中包含一個static方法initialize,當插件被加載的時候,該方法中的代碼就會運行,完成一些必要的初始化工作.
#!java
import com.saurik.substrate.MS;
public class Main {
static void initialize() {
// ... code to run when extension is loaded
}
}
5.hook imei example
#!java
import com.saurik.substrate.MS;
public class Main {
static void initialize() {
MS.hookClassLoad("android.telephony.TelephonyManager",
new MS.ClassLoadHook() {
@SuppressWarnings("unchecked")
public void classLoaded(Class<?> arg0) {
Method hookimei;
try {
hookimei = arg0.getMethod("getDeviceId", null);
} catch (NoSuchMethodException e) {
// TODO Auto-generated catch block
e.printStackTrace();
hookimei = null;
}
if (hookimei != null) {
final MS.MethodPointer old1 = new MS.MethodPointer();
MS.hookMethod(arg0, hookimei, new MS.MethodHook() {
@Override
public Object invoked(Object arg0,
Object... arg1) throws Throwable {
// TODO Auto-generated method stub
System.out.println("hook imei----------->");
String imei = (String) old1.invoke(arg0,
arg1);
System.out.println("imei-------->" + imei);
imei = "999996015409998";
return imei;
}
}, old1);
}
}
});
}
}
6.在 cydia app 界面中點擊 Link Substrate Files 之后重啟手機
7.使用getimei的小程序驗證imei是否被改變
#!java
public class MainActivity extends ActionBarActivity {
private static final String tag = "MainActivity";
TextView mText ;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mText = (TextView) findViewById(R.id.text);
TelephonyManager mtelehonyMgr = (TelephonyManager) getSystemService(this.TELEPHONY_SERVICE);
Build bd = new Build();
String imei = mtelehonyMgr.getDeviceId();
String imsi = mtelehonyMgr.getSubscriberId();
//getSimSerialNumber() 獲取 SIM 序列號 getLine1Number 獲取手機號
String androidId = Secure.getString(getApplicationContext().getContentResolver(), Secure.ANDROID_ID);
String id = UUID.randomUUID().toString();
String model = bd.MODEL;
StringBuilder sb = new StringBuilder();
sb.append("imei = "+ imei);
sb.append("\nimsi = " + imsi);
sb.append("\nandroid_id = " + androidId);
sb.append("\nuuid = " + id);
sb.append("\nmodel = " + model);
if(imei!=null)
mText.setText(sb.toString());
else
mText.setText("fail");
}
8.關鍵api介紹
MS.hookClassLoad:該方法實現在指定的類被加載的時候發出通知(改變其實現方式?).因為一個類可以在任何時候被加載,所以Substrate提供了一個方法用來檢測用戶感興趣的類何時被加載.
這個api需要實現一個簡單的接口MS.ClassLoadHook
,該接口只有一個方法classLoaded
,當類被加載的時候該方法會被執行.加載的類以參數形式傳入此方法.
void hookClassLoad(String name, MS.ClassLoadHook hook);
參數 | 描述 |
---|---|
name | 包名+類名,使用java的.符號(被hook的完整類名) |
hook | MS.ClassLoadHook的一個實例,當這個類被加載的時候,它的classLoaded方法會被執行. |
#!java
MS.hookClassLoad("java.net.HttpURLConnection",
new MS.ClassLoadHook() {
public void classLoaded(Class<?> _class) {
/* do something with _class argument */
}
}
);
MS.hookMethod:該API允許開發者提供一個回調函數替換原來的方法,這個回調函數是一個實現了MS.MethodHook接口的對象,是一個典型的匿名內部類.它包含一個invoked函數.
#!java
void hookMethod(Class _class, Member member, MS.MethodHook hook, MS.MethodPointer old);
參數 | 描述 |
---|---|
_class | 加載的目標類,為classLoaded傳下來的類參數 |
member | 通過反射得到的需要hook的方法(或構造函數). 注意:不能HOOK字段 (在編譯的時候會進行檢測). |
hook | MS.MethodHook的一個實例,其包含的invoked方法會被調用,用以代替member中的代碼 |
這塊的功能 xposed 就不能實現啦.
整個流程大致如下:
**第零步:添加 ndk 支持,將 cydia 的庫和頭文件加入工程
有關 ndk 開發的基礎可以參考此文: NDK入門篇
注意要是 xxx.cy.cpp,不要忘記.cy
其實應該是動態鏈接庫名稱中的 cy 必須有,所有在 Android.md 中module 處的 .cy 必須帶上咯
LOCAL_MODULE := DumpDex2.cy
第一步:修改配置文件
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
android:installLocation="internalOnly"
>
<application android:hasCode="false">
</application>
<uses-permission android:name="cydia.permission.SUBSTRATE"/>
</manifest>
設置 android:hasCode 屬性 false,設置android:installLocation屬性internalOnly"
第二步:指定要 hook 的 lib 庫
#include <substrate.h>
MSConfig(MSFilterExecutable, "/system/bin/app_process") //MSConfig(MSFilterLibrary, "liblog.so")
// this is a macro that uses __attribute__((__constructor__))
MSInitialize {
// ... code to run when extension is loaded
}
設置要 hook 的可執行文件或者動態庫
第三步: 等待 class
static void OnResources(JNIEnv *jni, jclass resources, void *data) {
// ... code to modify the class when loaded
}
MSInitialize {
MSJavaHookClassLoad(NULL, "android/content/res/Resources", &OnResources);
}
第四步:修改實現
static jint (*_Resources$getColor)(JNIEnv *jni, jobject _this, ...);
static jint $Resources$getColor(JNIEnv *jni, jobject _this, jint rid) {
jint color = _Resources$getColor(jni, _this, rid);
return color & ~0x0000ff00 | 0x00ff0000;
}
static void OnResources(JNIEnv *jni, jclass resources, void *data) {
jmethodID method = jni->GetMethodID(resources, "getColor", "(I)I");
if (method != NULL)
MSJavaHookMethod(jni, resources, method,
&$Resources$getColor, &_Resources$getColor);
}
下面是步驟是在官網教程基礎上對小白同學的一些補充吧.
? file libprocess.so
libprocess.so: ELF 32-bit LSB shared object, ARM, version 1 (SYSV), dynamically linked (uses shared libs), not stripped
第五步
復制libsubstrate-dvm.so(注意 arm 和 x86平臺的選擇)和substrate.h到 jni 目錄下.創建SuperMathHook.cy.cpp文件
第六步
配置Android.mk文件
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE:= substrate-dvm
LOCAL_SRC_FILES := libsubstrate-dvm.so
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := SuperMathHook.cy
LOCAL_SRC_FILES := SuperMathHook.cy.cpp
LOCAL_LDLIBS := -llog
LOCAL_LDLIBS += -L$(LOCAL_PATH) -lsubstrate-dvm //-L指定庫文件的目錄,-l指定庫文件名,-I指定頭文件的目錄.
include $(BUILD_SHARED_LIBRARY)
加入 c 的 lib
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE:= substrate-dvm
LOCAL_SRC_FILES := libsubstrate-dvm.so
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE:= substrate
LOCAL_SRC_FILES := libsubstrate.so
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := CydiaN.cy
LOCAL_SRC_FILES := CydiaN.cy.cpp
LOCAL_LDLIBS := -llog
LOCAL_LDLIBS += -L$(LOCAL_PATH) -lsubstrate-dvm -lsubstrate
include $(BUILD_SHARED_LIBRARY)
strings 查看下里面的函數.
/data/data/com.jerome.jni/lib # strings libprocess.so <
/system/bin/linker
__cxa_finalize
__cxa_atexit
Jstring2CStr
malloc
memcpy
__aeabi_unwind_cpp_pr0
Java_com_jerome_jni_JNIProcess_getInfoMD5
....
網上流傳的 IDA dump 脫殼流程大致如下:
現在目標就是通過 Cydia 的模塊來自動化完成這個功能.這里咱選擇對dvmDexFileOpenPartial函數進行 hook.至于為什么要選擇這里了?這就需要分析下 android dex優化過程
Android會對每一個安裝的應用的dex文件進行優化,生成一個odex文件.相比于dex文件,odex文件多了一個optheader,依賴庫信息(dex文件所需要的本地函數庫)和輔助信息(類索引信息等).
dex的優化過程是一個獨立的功能模塊來實現的,位于http://androidxref.com/4.4.3_r1.1/xref/dalvik/dexopt/OptMain.cpp#57 其中extractAndProcessZip()函數完成優化操作.
http://androidxref.com/4.1.1/xref/dalvik/dexopt/OptMain.cpp
OptMain中的main函數就是加載dex的最原始入口
#!c
int main(int argc, char* const argv[])
{
set_process_name("dexopt");
setvbuf(stdout, NULL, _IONBF, 0);
if (argc > 1) {
if (strcmp(argv[1], "--zip") == 0)
return fromZip(argc, argv);
else if (strcmp(argv[1], "--dex") == 0)
return fromDex(argc, argv);
else if (strcmp(argv[1], "--preopt") == 0)
return preopt(argc, argv);
}
...
return 1;
}
可以看到,這里會分別對3中類型的文件做不同處理,我們關心的是dex文件,所以接下來看看fromDex函數:
#!c
static int fromDex(int argc, char* const argv[])
{
...
if (dvmPrepForDexOpt(bootClassPath, dexOptMode, verifyMode, flags) != 0) {
ALOGE("VM init failed");
goto bail;
}
vmStarted = true;
/* do the optimization */
if (!dvmContinueOptimization(fd, offset, length, debugFileName,
modWhen, crc, (flags & DEXOPT_IS_BOOTSTRAP) != 0))
{
ALOGE("Optimization failed");
goto bail;
}
...
}
這個函數先初始化了一個虛擬機,然后調用dvmContinueOptimization函數 /dalvik/vm/analysis/DexPrepare.cpp,進入這個函數:
#!c
bool dvmContinueOptimization(int fd, off_t dexOffset, long dexLength,
const char* fileName, u4 modWhen, u4 crc, bool isBootstrap)
{
...
/*
* Rewrite the file. Byte reordering, structure realigning,
* class verification, and bytecode optimization are all performed
* here.
*
* In theory the file could change size and bits could shift around.
* In practice this would be annoying to deal with, so the file
* layout is designed so that it can always be rewritten in place.
*
* This creates the class lookup table as part of doing the processing.
*/
success = rewriteDex(((u1*) mapAddr) + dexOffset, dexLength,
doVerify, doOpt, &pClassLookup, NULL);
if (success) {
DvmDex* pDvmDex = NULL;
u1* dexAddr = ((u1*) mapAddr) + dexOffset;
if (dvmDexFileOpenPartial(dexAddr, dexLength, &pDvmDex) != 0) {
ALOGE("Unable to create DexFile");
success = false;
} else {
...
}
這個函數中對Dex文件做了一些優化(如字節重排序,結構對齊等),然后重新寫入Dex文件.如果優化成功的話接下來調用dvmDexFileOpenPartial,而這個函數中調用了真正的Dex文件.在具體看看這個函數/dalvik/vm/DvmDex.cpp
#!c
/*
* Create a DexFile structure for a "partial" DEX. This is one that is in
* the process of being optimized. The optimization header isn't finished
* and we won't have any of the auxillary data tables, so we have to do
* the initialization slightly differently.
*
* Returns nonzero on error.
*/
int dvmDexFileOpenPartial(const void* addr, int len, DvmDex** ppDvmDex)
{
DvmDex* pDvmDex;
DexFile* pDexFile;
int parseFlags = kDexParseDefault;
int result = -1;
/* -- file is incomplete, new checksum has not yet been calculated
if (gDvm.verifyDexChecksum)
parseFlags |= kDexParseVerifyChecksum;
*/
pDexFile = dexFileParse((u1*)addr, len, parseFlags);
if (pDexFile == NULL) {
ALOGE("DEX parse failed");
goto bail;
}
pDvmDex = allocateAuxStructures(pDexFile);
if (pDvmDex == NULL) {
dexFileFree(pDexFile);
goto bail;
}
pDvmDex->isMappedReadOnly = false;
*ppDvmDex = pDvmDex;
result = 0;
bail:
return result;
}
這個函數的前兩個參數非常關鍵,第一個參數是dex文件的起始地址,第二個參數是dex文件的長度,有了這兩個參數,就可以從內存中將這個dex文件dump下來了,這也是在此函數下斷點的原因.該函數會調用dexFileParse()對dex文件進行解析
所以在dexFileParse函數處來進行 dump 也是可行的.但是因為這個函數的原型是
DexFile* dexFileParse(const u1* data, size_t length, int flags)
其返回值為一個結構體指針struct DexFile { ... },要 hook 這個函數得把結構體從 android 源碼中扣出來或者直接改鏡像.
找到dvmDexFileOpenPartial函數在 libdvm.so 對應的名稱
#!bash
? strings libdvm_arm.so|grep dvmDexFileOpenPartial
_Z21dvmDexFileOpenPartialPKviPP6DvmDex
? strings libdvm_arm.so|grep dexFileParse
_Z12dexFileParsePKhji
有了上述理論基礎,現在可以正式開發模塊了.大致流程如下
完整代碼
#!c
#include "substrate.h"
#include <android/log.h>
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/types.h>
#include <string.h>
#define BUFLEN 1024
#define TAG "DEXDUMP"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
//get packagename from pid
int getProcessName(char * buffer){
char path_t[256]={0};
pid_t pid=getpid();
char str[15];
sprintf(str, "%d", pid);
memset(path_t, 0 , sizeof(path_t));
strcat(path_t, "/proc/");
strcat(path_t, str);
strcat(path_t, "/cmdline");
//LOG_ERROR("zhw", "path:%s", path_t);
int fd_t = open(path_t, O_RDONLY);
if(fd_t>0){
int read_count = read(fd_t, buffer, BUFLEN);
if(read_count>0){
int processIndex=0;
for(processIndex=0;processIndex<strlen(buffer);processIndex++){
if(buffer[processIndex]==':'){
buffer[processIndex]='_';
}
}
return 1;
}
}
return 0;
}
//指定要hook 的 lib 庫
MSConfig(MSFilterLibrary,"/system/lib/libdvm.so")
//保留原來的地址 DexFile* dexFileParse(const u1* data, size_t length, int flags)
int (* oldDexFileParse)(const void * addr,int len,int flags);
//替換的函數
int myDexFileParse(const void * addr,int len,void ** dvmdex)
{
LOGD("call my dvm dex!!:%d",getpid());
{
//write to file
//char buf[200];
// 導出dex文件
char dexbuffer[64]={0};
char dexbufferNamed[128]={0};
char * bufferProcess=(char*)calloc(256,sizeof(char));
int processStatus= getProcessName(bufferProcess);
sprintf(dexbuffer, "_dump_%d", len);
strcat(dexbufferNamed,"/sdcard/");
if (processStatus==1) {
strcat(dexbufferNamed,bufferProcess);
strcat(dexbufferNamed,dexbuffer);
}else{
LOGD("FAULT pid not found\n");
}
if(bufferProcess!=NULL)
{
free(bufferProcess);
}
strcat(dexbufferNamed,".dex");
//sprintf(buf,"/sdcard/dex.%d",len);
FILE * f=fopen(dexbufferNamed,"wb");
if(!f)
{
LOGD(dexbuffer + " : error open sdcard file to write");
}
else{
fwrite(addr,1,len,f);
fclose(f);
}
}
//進行原來的調用,不影響程序運行
return oldDexFileParse(addr,len,dvmdex);
}
//Substrate entry point
MSInitialize
{
LOGD("Substrate initialized.");
MSImageRef image;
//載入lib
image = MSGetImageByName("/system/lib/libdvm.so");
if (image != NULL)
{
void * dexload=MSFindSymbol(image,"_Z21dvmDexFileOpenPartialPKviPP6DvmDex");
if(dexload==NULL)
{
LOGD("error find _Z21dvmDexFileOpenPartialPKviPP6DvmDex ");
}
else{
//替換函數
//3.MSHookFunction
MSHookFunction(dexload,(void*)&myDexFileParse,(void **)&oldDexFileParse);
}
}
else{
LOGD("ERROR FIND LIBDVM");
}
}
效果如下:
[email protected]:/sdcard $ l |grep dex
app_process_classes_3220.dex
com.ali.tg.testapp_classes_606716.dex
com.chaozh.iReaderFree_classes_4673256.dex
com.secken.app_xg_service_v2_classes_6327832.dex
更改 hook 點為 dexFileParse,上文已經講解了為啥也可以選擇這里.也分析了 dex 優化的過程,這里在分析下 dex 加載的過程.
DexClassLoader廣泛被開發者用于插件的動態加載.而PathClassLoader幾乎沒怎么見過.
因為PathClassLoader 沒有提供優化 dex 的目錄而是固定將 odex 存放到 /data/dalvik-cache 中 ,故它只能加載已經安裝到 Android 系統中的 apk 文件,也就是 /data/app 目錄下的 apk 文件.
PathClassLoader 和 DexClassLoader 父類為 BaseDexClassLoader
http://androidxref.com/4.4.2_r1/xref/libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
#!c
45 public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
http://androidxref.com/4.4.2_r1/xref/libcore/dalvik/src/main/java/dalvik/system/DexPathList.java
#!c
DexPathList(this, dexPath, libraryPath, optimizedDirectory);
260 private static DexFile loadDexFile(File file, File optimizedDirectory)
http://androidxref.com/4.4.2_r1/xref/libcore/dalvik/src/main/java/dalvik/system/DexFile.java
#!c
141 static public DexFile loadDex(String sourcePathName, String outputPathName, int flags)
調用 native 函數 native private static int openDexFileNative(String sourceName, String outputName, int flags)
#!c
294 private static int openDexFile(String sourceName, String outputName,
295 int flags) throws IOException {
296 return openDexFileNative(new File(sourceName).getCanonicalPath(),
297 (outputName == null) ? null : new File(outputName).getCanonicalPath(),
298 flags);
299 }
http://androidxref.com/4.4.2_r1/xref/dalvik/vm/native/dalvik_system_DexFile.cpp
#!c
151 static void Dalvik_dalvik_system_DexFile_openDexFileNative(const u4* args, JValue* pResult)
//249 static void Dalvik_dalvik_system_DexFile_openDexFile_bytearray(const u4* args, JValue* pResult)
http://androidxref.com/4.4.2_r1/xref/dalvik/vm/RawDexFile.cpp
#!c
109 int dvmRawDexFileOpen(const char* fileName, const char* odexOutputName, RawDexFile** ppRawDexFile, bool isBootstrap) //工具類方法打開DEX文件/Jar文件
http://androidxref.com/4.4.4_r1/xref/dalvik/vm/DvmDex.cpp
#!c
93 int dvmDexFileOpenFromFd(int fd, DvmDex** ppDvmDex) //從一個打開的DEX文件,映射到只讀共享內存并且解析內容
//146 int dvmDexFileOpenPartial(const void* addr, int len, DvmDex** ppDvmDex) //通過地址和長度打開部分DEX文件
http://androidxref.com/4.4.4_r1/xref/dalvik/libdex/DexFile.cpp
#!c
289 dexFileParse(const u1* data, size_t length, int flags) //解析dex文件
方法openDexFile里通過dvmDexFileOpenFromFd函數調用dexFileParse函數,分析Dex文件里每個類名稱和類的代碼所在索引,然后dexFileParse調用函數dexParseOptData來把類名稱寫對象pDexFile->pClassLookup里面,當然也更新了索引
#!c
//Substrate entry point
MSInitialize
{
LOGD("Cydia Init");
MSImageRef image;
//載入lib
image = MSGetImageByName("/system/lib/libdvm.so");
if (image != NULL)
{
void * dexload=MSFindSymbol(image,"_Z12dexFileParsePKhji");
if(dexload==NULL)
{
LOGD("error find _Z12dexFileParsePKhji");
}
else{
//替換函數
//3.MSHookFunction
MSHookFunction(dexload,(void*)&myDexFileParse,(void **)&oldDexFileParse);
}
}
else{
LOGD("ERROR FIND LIBDVM");
}
}
github 地址如下,里面已經有一個編譯好但是沒有簽名的 apk 了...
https://github.com/WooyunDota/DumpDex
如果提取的是 encode 版的,需要 decode 一下:
base64 -D -i com.ali.tg.testapp_606716.dex.encode.dex -o my.dex
NDK Symbol 'NULL' could not be resolved
NDK環境沒有配好,沒有找到stddef.h
jni.h頭文件找不到
也是NDK環境未配置好,或者編譯器 BUG.先強行編譯一次若問題未解決就檢查下 NDK 環境.
如果遇到一些成員 ref 到兩種頭文件中,需要配置下 include.我在使用 mkdir 的時候 mode_t 就 ref 到 ndk 和 osx 的頭文件中導致編譯失敗.解決辦法下加入了include:
android-ndk-r10d/platforms/android-17/arch-arm/usr/include/sys
Android Studio 1.3已經開始支持 NDK,完全拋棄 eclipse 的時日即將到來.
http://www.cnblogs.com/goodhacker/p/4014617.html
http://www.cnblogs.com/goodhacker/p/4014617.html
http://www.cnblogs.com/baizx/p/4254359.html
http://www.gitzx.com/android-cydiasubstrate/
https://github.com/bunnyblue/DexExtractor
DexClassLoader4.4.2動態加載分析(磁盤加載分析)