學習吾愛破解論壇正己的《安卓逆向這檔事》的筆記,視頻和配套工具可以去課程主頁獲取:https://www.52pojie.cn/thread-1731181-1-1.html。
工具
- 教程Demo
- MT管理器
- 雷電模擬器
- Jadx-gui
- 演算法助手
校驗
是開發者在數據傳送時採用的一種校正數據的一種方式。常見的校驗有:
- 應用簽名校驗(最常見)
- dex CRC 校驗
- APK 完整性校驗
- 路徑文件校驗
應用簽名校驗
應用簽名
通過對 Apk 進行簽名,開發者可以證明對 Apk 的所有權和控制權,可用於安裝和更新其應用。而在 Android 設備上的安裝 Apk ,如果是一個沒有被簽名的 Apk,則會被拒絕安裝。在安裝 Apk 的時候,軟體包管理器也會驗證 Apk 是否已經被正確簽名,並且通過簽名證書和數據摘要驗證是否合法沒有被篡改。只有確認安全無篡改的情況下,才允許安裝在設備上。
簡單來說,APK 簽名的主要作用有兩個:
- 證明 APK 的所有者。
- 允許 Android 市場和設備校驗 APK 的正確性。
Android 目前支持以下四種應用簽名方案:
- v1 方案:基於 JAR 簽名。
- v2 方案:APK 簽名方案 v2(在 Android 7.0 中引入)
- v3 方案:APK 簽名方案 v3(在 Android 9 中引入)
- v4 方案:APK 簽名方案 v4(在 Android 11 中引入)
V1 簽名
V1 簽名的機制主要就 META-INF 目錄下的三個文件:
- MANIFEST.MF:這是摘要文件。程序遍歷Apk包中的所有文件(entry),對非文件夾非簽名文件的文件,逐個用 SHA1(安全哈希演算法)生成摘要信息,再用 Base64 進行編碼。如果你改變了 apk 包中的文件,那麼在 apk 安裝校驗時,改變後的文件摘要信息與 MANIFEST.MF 的檢驗信息不同,於是程序就不能成功安裝。
- ANDROID.SF:這是對摘要的簽名文件。對前一步生成的 MANIFEST.MF,使用 SHA1-RSA 演算法,用開發者的私鑰進行簽名。在安裝時只能使用公鑰才能解密它。解密之後,將它與未加密的摘要信息(即,MANIFEST.MF 文件)進行對比,如果相符,則表明內容沒有被異常修改。
- ANDROID.RSA:文件中保存了公鑰、所採用的加密演算法等信息。
在某些情況下,直接對 apk 進行 V1 簽名可以繞過 apk 的簽名校驗。
V2 方案會將 APK 文件視為 blob,並對整個文件進行簽名檢查。對 APK 進行的任何修改(包括對 ZIP 元數據進行的修改)都會使 APK 簽名作廢。這種形式的 APK 驗證不僅速度要快得多,而且能夠發現更多種未經授權的修改。
簽名校驗
如何判斷是否有簽名校驗?不做任何修改,直接簽名安裝,應用閃退則說明大概率有簽名校驗。
一般來說,普通的簽名校驗會導致軟體的閃退,黑屏,卡啟動頁等。當然,以上都算是比較好的,有一些比較狠的作者,則會直接 rm -rf /
,把基帶都格掉的一鍵變磚。
常見簽名校驗特徵:
- kill/killProcess:可以殺死當前應用活動的進程,這一操作將會把所有該進程內的資源(包括線程全部清理掉).當然,由於ActivityManager時刻監聽著進程,一旦發現進程被非正常Kill,它將會試圖去重啟這個進程。這就是為什麼,有時候當我們試圖這樣去結束掉應用時,發現它又自動重新啟動的原因.
- system.exit:殺死了整個進程,這時候活動所佔的資源也會被釋放。
- finish:僅僅針對Activity,當調用finish()時,只是將活動推向後台,並沒有立即釋放內存,活動的資源並沒有被清理
三角校驗:so 檢測 dex,動態載入的 dex(在軟體運行時會解壓釋放一段dex文件,檢測完後就刪除)檢測 so,dex 檢測動態載入的 dex。
隱式簽名校驗:有一些則比較隱晦,在發現apk被修改後,會偷偷修改apk的部分功能,例如在某些多開定位軟體中,會暗改ip的經緯網等,跟實際產生一定的偏差。
普通獲取簽名校驗代碼:
private boolean SignCheck() {
String trueSignMD5 = "d0add9987c7c84aeb7198c3ff26ca152";
String nowSignMD5 = "";
try {
// 得到簽名的MD5
PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(),PackageManager.GET_SIGNATURES);
Signature[] signs = packageInfo.signatures;
String signBase64 = Base64Util.encodeToString(signs[0].toByteArray());
nowSignMD5 = MD5Utils.MD5(signBase64);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return trueSignMD5.equals(nowSignMD5);
}
簽名校驗對抗
- 核心破解插件,不簽名安裝應用(對於部分對比簽名文件的應用,不重新簽名就可以過掉簽名校驗)
- 一鍵過簽名工具,例如MT、NP、ARMPro、CNFIX、Modex的去除簽名校驗功能
- 具體分析簽名校驗邏輯(手撕簽名校驗)
- PM 代理(過時)
- IO重定向--VA&SVC:ptrace+seccomp
- 去作者家嚴刑拷打拿到
.jks
文件和密碼
手撕簽名校驗
定位
閃退攔截
演算法助手開啟」攔截應用退出「、」防止應用閃退「,打開簽名修改過的 app,可以在日誌里找到攔截日誌,通過調用堆棧定位到對應的簽名校驗代碼。
讀取應用簽名監聽
演算法助手開啟」讀取應用簽名監聽「,同理可以定位到簽名校驗代碼。
去除
把比對的簽名數據替換成修改後的簽名,或者修改判斷語句。
PM 代理
思路源自:Android中Hook 應用簽名方法
PMS
PackageManagerService(簡稱PMS),是 Android 系統核心服務之一,處理包管理相關的工作,常見的比如安裝、卸載應用等。
實現方法以及原理解析
HOOK PMS代碼:
package com.zj.hookpms;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import android.content.Context;
import android.content.pm.PackageManager;
import android.util.Log;
public class ServiceManagerWraper {
public final static String ZJ = "ZJ595";
public static void hookPMS(Context context, String signed, String appPkgName, int hashCode) {
try {
// 獲取全局的ActivityThread對象
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Method currentActivityThreadMethod =
activityThreadClass.getDeclaredMethod("currentActivityThread");
Object currentActivityThread = currentActivityThreadMethod.invoke(null);
// 獲取ActivityThread裡面原始的sPackageManager
Field sPackageManagerField = activityThreadClass.getDeclaredField("sPackageManager");
sPackageManagerField.setAccessible(true);
Object sPackageManager = sPackageManagerField.get(currentActivityThread);
// 準備好代理對象, 用來替換原始的對象
Class<?> iPackageManagerInterface = Class.forName("android.content.pm.IPackageManager");
Object proxy = Proxy.newProxyInstance(
iPackageManagerInterface.getClassLoader(),
new Class<?>[]{iPackageManagerInterface},
new PmsHookBinderInvocationHandler(sPackageManager, signed, appPkgName, 0));
// 1. 替換掉ActivityThread裡面的 sPackageManager 欄位
sPackageManagerField.set(currentActivityThread, proxy);
// 2. 替換 ApplicationPackageManager裡面的 mPM對象
PackageManager pm = context.getPackageManager();
Field mPmField = pm.getClass().getDeclaredField("mPM");
mPmField.setAccessible(true);
mPmField.set(pm, proxy);
} catch (Exception e) {
Log.d(ZJ, "hook pms error:" + Log.getStackTraceString(e));
}
}
public static void hookPMS(Context context) {
String Sign = "原包的簽名信息";
hookPMS(context, Sign, "com.zj.hookpms", 0);
}
}
用法
將編譯好 PM 代理的 dex 文件添加到 apk 中,然後將對應的簽名取出來調用 hookPMS 方法替換掉原有的簽名。
IO 重定向
IO 重定向,可以實現:
- 讓文件只讀,不可寫
- 禁止訪問文件
- 過 Root 檢測,Xposed 檢測
- 路徑替換
- 過簽名檢測(讀取原包)
- 風控對抗(例:一個文件記錄 App 啟動的次數)
具體實現:
代碼及原理:
using namespace std;
string packname;
string origpath;
string fakepath;
int (*orig_open)(const char *pathname, int flags, ...);
int (*orig_openat)(int,const char *pathname, int flags, ...);
FILE *(*orig_fopen)(const char *filename, const char *mode);
static long (*orig_syscall)(long number, ...);
int (*orig__NR_openat)(int,const char *pathname, int flags, ...);
void* (*orig_dlopen_CI)(const char *filename, int flag);
void* (*orig_dlopen_CIV)(const char *filename, int flag, const void *extinfo);
void* (*orig_dlopen_CIVV)(const char *name, int flags, const void *extinfo, void *caller_addr);
static inline bool needs_mode(int flags) {
return ((flags & O_CREAT) == O_CREAT) || ((flags & O_TMPFILE) == O_TMPFILE);
}
bool startsWith(string str, string sub){
return str.find(sub)==0;
}
bool endsWith(string s,string sub){
return s.rfind(sub)==(s.length()-sub.length());
}
bool isOrigAPK(string path){
if(path==origpath){
return true;
}
return false;
}
//該函數的功能是在打開一個文件時進行攔截,並在滿足特定條件時將文件路徑替換為另一個路徑
//fake_open 函數有三個參數:
//pathname:一個字元串,表示要打開的文件的路徑。
//flags:一個整數,表示打開文件的方式,例如只讀、只寫、讀寫等。
//mode(可選參數):一個整數,表示打開文件時應用的許可權模式。
int fake_open(const char *pathname, int flags, ...) {
mode_t mode = 0;
if (needs_mode(flags)) {
va_list args;
va_start(args, flags);
mode = static_cast<mode_t>(va_arg(args, int));
va_end(args);
}
//LOGI("open, path: %s, flags: %d, mode: %d",pathname, flags ,mode);
string cpp_path= pathname;
if(isOrigAPK(cpp_path)){
LOGI("libc_open, redirect: %s, --->: %s",pathname, fakepath.data());
return orig_open("/data/user/0/com.zj.wuaipojie/files/base.apk", flags, mode);
}
return orig_open(pathname, flags, mode);
}
//該函數的功能是在打開一個文件時進行攔截,並在滿足特定條件時將文件路徑替換為另一個路徑
//fake_openat 函數有四個參數:
//fd:一個整數,表示要打開的文件的文件描述符。
//pathname:一個字元串,表示要打開的文件的路徑。
//flags:一個整數,表示打開文件的方式,例如只讀、只寫、讀寫等。
//mode(可選參數):一個整數,表示打開文件時應用的許可權模式。
//openat 函數的作用類似於 open 函數,但是它使用文件描述符來指定文件路徑,而不是使用文件路徑本身。這樣,就可以在打開文件時使用相對路徑,而不必提供完整的文件路徑。
//例如,如果要打開相對於當前目錄的文件,可以使用 openat 函數,而不是 open 函數,因為 open 函數只能使用絕對路徑。
//
int fake_openat(int fd, const char *pathname, int flags, ...) {
mode_t mode = 0;
if (needs_mode(flags)) {
va_list args;
va_start(args, flags);
mode = static_cast<mode_t>(va_arg(args, int));
va_end(args);
}
LOGI("openat, fd: %d, path: %s, flags: %d, mode: %d",fd ,pathname, flags ,mode);
string cpp_path= pathname;
if(isOrigAPK(cpp_path)){
LOGI("libc_openat, redirect: %s, --->: %s",pathname, fakepath.data());
return orig_openat(fd,fakepath.data(), flags, mode);
}
return orig_openat(fd,pathname, flags, mode);
}
FILE *fake_fopen(const char *filename, const char *mode) {
string cpp_path= filename;
if(isOrigAPK(cpp_path)){
return orig_fopen(fakepath.data(), mode);
}
return orig_fopen(filename, mode);
}
//該函數的功能是在執行系統調用時進行攔截,並在滿足特定條件時修改系統調用的參數。
//syscall 函數是一個系統調用,是程序訪問內核功能的方法之一。使用 syscall 函數可以調用大量的系統調用,它們用於實現操作系統的各種功能,例如打開文件、創建進程、分配內存等。
//
static long fake_syscall(long number, ...) {
void *arg[7];
va_list list;
va_start(list, number);
for (int i = 0; i < 7; ++i) {
arg[i] = va_arg(list, void *);
}
va_end(list);
if (number == __NR_openat){
const char *cpp_path = static_cast<const char *>(arg[1]);
LOGI("syscall __NR_openat, fd: %d, path: %s, flags: %d, mode: %d",arg[0] ,arg[1], arg[2], arg[3]);
if (isOrigAPK(cpp_path)){
LOGI("syscall __NR_openat, redirect: %s, --->: %s",arg[1], fakepath.data());
return orig_syscall(number,arg[0], fakepath.data() ,arg[2],arg[3]);
}
}
return orig_syscall(number, arg[0], arg[1], arg[2], arg[3], arg[4], arg[5], arg[6]);
}
//函數的功能是獲取當前應用的包名、APK 文件路徑以及庫文件路徑,並將這些信息保存在全局變數中
//函數調用 GetObjectClass 和 GetMethodID 函數來獲取 context 對象的類型以及 getPackageName 方法的 ID。然後,函數調用 CallObjectMethod 函數來調用 getPackageName 方法,獲取當前應用的包名。最後,函數使用 GetStringUTFChars 函數將包名轉換為 C 字元串,並將包名保存在 packname 全局變數中
//接著,函數使用 fakepath 全局變數保存了 /data/user/0/<packname>/files/base.apk 這樣的路徑,其中 <packname> 是當前應用的包名。
//然後,函數再次調用 GetObjectClass 和 GetMethodID 函數來獲取 context 對象的類型以及 getApplicationInfo 方法的 ID。然後,函數調用 CallObjectMethod 函數來調用 getApplicationInfo 方法,獲取當前應用的 ApplicationInfo 對象。
//它先調用 GetObjectClass 函數獲取 ApplicationInfo 對象的類型,然後調用 GetFieldID 函數獲取 sourceDir 欄位的 ID。接著,函數使用 GetObjectField 函數獲取 sourceDir 欄位的值,並使用 GetStringUTFChars 函數將其轉換為 C 字元串。最後,函數將 C 字元串保存在 origpath 全局變數中,表示當前應用的 APK 文件路徑。
//最後,函數使用 GetFieldID 和 GetObjectField 函數獲取 nativeLibraryDir 欄位的值,並使用 GetStringUTFChars 函數將其轉換為 C 字元串。函數最後調用 LOGI 函數列印庫文件路徑,但是並沒有將其保存在全局變數中。
extern "C" JNIEXPORT void JNICALL
Java_com_zj_wuaipojie_util_SecurityUtil_hook(JNIEnv *env, jclass clazz, jobject context) {
jclass conext_class = env->GetObjectClass(context);
jmethodID methodId_pack = env->GetMethodID(conext_class, "getPackageName",
"()Ljava/lang/String;");
auto packname_js = reinterpret_cast<jstring>(env->CallObjectMethod(context, methodId_pack));
const char *pn = env->GetStringUTFChars(packname_js, 0);
packname = string(pn);
env->ReleaseStringUTFChars(packname_js, pn);
//LOGI("packname: %s", packname.data());
fakepath= "/data/user/0/"+ packname +"/files/base.apk";
jclass conext_class2 = env->GetObjectClass(context);
jmethodID methodId_pack2 = env->GetMethodID(conext_class2,"getApplicationInfo","()Landroid/content/pm/ApplicationInfo;");
jobject application_info = env->CallObjectMethod(context,methodId_pack2);
jclass pm_clazz = env->GetObjectClass(application_info);
jfieldID package_info_id = env->GetFieldID(pm_clazz,"sourceDir","Ljava/lang/String;");
auto sourceDir_js = reinterpret_cast<jstring>(env->GetObjectField(application_info,package_info_id));
const char *sourceDir = env->GetStringUTFChars(sourceDir_js, 0);
origpath = string(sourceDir);
LOGI("sourceDir: %s", sourceDir);
jfieldID package_info_id2 = env->GetFieldID(pm_clazz,"nativeLibraryDir","Ljava/lang/String;");
auto nativeLibraryDir_js = reinterpret_cast<jstring>(env->GetObjectField(application_info,package_info_id2));
const char *nativeLibraryDir = env->GetStringUTFChars(nativeLibraryDir_js, 0);
LOGI("nativeLibraryDir: %s", nativeLibraryDir);
//LOGI("%s", "Start Hook");
//啟動hook
void *handle = dlopen("libc.so",RTLD_NOW);
auto pagesize = sysconf(_SC_PAGE_SIZE);
auto addr = ((uintptr_t)dlsym(handle,"open") & (-pagesize));
auto addr2 = ((uintptr_t)dlsym(handle,"openat") & (-pagesize));
auto addr3 = ((uintptr_t)fopen) & (-pagesize);
auto addr4 = ((uintptr_t)syscall) & (-pagesize);
//解除部分機型open被保護
mprotect((void*)addr, pagesize, PROT_READ | PROT_WRITE | PROT_EXEC);
mprotect((void*)addr2, pagesize, PROT_READ | PROT_WRITE | PROT_EXEC);
mprotect((void*)addr3, pagesize, PROT_READ | PROT_WRITE | PROT_EXEC);
mprotect((void*)addr4, pagesize, PROT_READ | PROT_WRITE | PROT_EXEC);
DobbyHook((void *)dlsym(handle,"open"), (void *)fake_open, (void **)&orig_open);
DobbyHook((void *)dlsym(handle,"openat"), (void *)fake_openat, (void **)&orig_openat);
DobbyHook((void *)fopen, (void *)fake_fopen, (void**)&orig_fopen);
DobbyHook((void *)syscall, (void *)fake_syscall, (void **)&orig_syscall);
}
只需要在需要重定向的地方調用:
sget-object p10, Lcom/zj/wuaipojie/util/ContextUtils;->INSTANCE:Lcom/zj/wuaipojie/util/ContextUtils;
invoke-virtual {p10}, Lcom/zj/wuaipojie/util/ContextUtils;->getContext()Landroid/content/Context;
move-result-object p10
invoke-static {p10}, Lcom/zj/wuaipojie/util/SecurityUtil;->hook(Landroid/content/Context;)V
通過系統自帶的api去獲取簽名很容易被偽造,不妨試試通過SVC的方式去獲取(參考MT開源的方法)。
root 檢測
原理
fun isDeviceRooted(): Boolean {
return checkRootMethod1() || checkRootMethod2() || checkRootMethod3()
}
fun checkRootMethod1(): Boolean {
val buildTags = android.os.Build.TAGS
return buildTags != null && buildTags.contains("test-keys")
}
fun checkRootMethod2(): Boolean {
val paths = arrayOf("/system/app/Superuser.apk", "/sbin/su", "/system/bin/su", "/system/xbin/su", "/data/local/xbin/su", "/data/local/bin/su", "/system/sd/xbin/su",
"/system/bin/failsafe/su", "/data/local/su", "/su/bin/su")
for (path in paths) {
if (File(path).exists()) return true
}
return false
}
fun checkRootMethod3(): Boolean {
var process: Process? = null
return try {
process = Runtime.getRuntime().exec(arrayOf("/system/xbin/which", "su"))
val bufferedReader = BufferedReader(InputStreamReader(process.inputStream))
bufferedReader.readLine() != null
} catch (t: Throwable) {
false
} finally {
process?.destroy()
}
}
定義了一個 isDeviceRooted()
函數,該函數調用了三個檢測 root 的方法:checkRootMethod1()
、checkRootMethod2()
和 checkRootMethod3()
。
checkRootMethod1()
方法檢查設備的 build tags
是否包含 test-keys
。這通常是用於測試的設備,因此如果檢測到這個標記,則可以認為設備已被 root。
checkRootMethod2()
方法檢查設備是否存在一些特定的文件,這些文件通常被用於執行 root 操作。如果檢測到這些文件,則可以認為設備已被 root。
checkRootMethod3()
方法使用 Runtime.exec()
方法來執行 which su
命令,然後檢查命令的輸出是否不為空。如果輸出不為空,則可以認為設備已被 root。
反制手段
- 演算法助手、對話框取消等插件一鍵 hook
- 分析具體的檢測代碼並去除
- 利用 IO 重定向使文件不可讀
- 修改 Andoird 源碼,去除常見指紋
模擬器檢測
fun isEmulator(): Boolean {
return Build.FINGERPRINT.startsWith("generic") || Build.FINGERPRINT.startsWith("unknown") || Build.MODEL.contains("google_sdk") Build.MODEL.contains("Emulator") || Build.MODEL.contains("Android SDK built for x86") || Build.MANUFACTURER.contains("Genymotion") || Build.HOST.startsWith("Build") || Build.PRODUCT == "google_sdk"
}
通過檢測系統的 Build
對象來判斷當前設備是否為模擬器。具體方法是檢測 Build.FINGERPRINT
屬性是否包含字元串 "generic"
。
具體分析見模擬器檢測對抗。
反調試檢測
安卓系統自帶調試檢測函數
fun checkForDebugger() {
if (Debug.isDebuggerConnected()) {
// 如果調試器已連接,則終止應用程序
System.exit(0)
}
}
debuggable 屬性
public boolean getAppCanDebug(Context context)//上下文對象為xxActivity.this
{
boolean isDebug = context.getApplicationInfo() != null &&
(context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
return isDebug;
}
ptrace檢測
int ptrace_protect()//ptrace附加自身線程 會導致此進程TracerPid 變為父進程的TracerPid 即zygote
{
return ptrace(PTRACE_TRACEME,0,0,0);;//返回-1即為已經被調試
}
每個進程同時刻只能被1個調試進程ptrace ,主動ptrace本進程可以使得其他調試器無法調試
調試進程名檢測
int SearchObjProcess()
{
FILE* pfile=NULL;
char buf[0x1000]={0};
pfile=popen("ps","r");
if(NULL==pfile)
{
//LOGA("SearchObjProcess popen打開命令失敗!\n");
return -1;
}
// 獲取結果
//LOGA("popen方案:\n");
while(fgets(buf,sizeof(buf),pfile))
{
char* strA=NULL;
char* strB=NULL;
char* strC=NULL;
char* strD=NULL;
strA=strstr(buf,"android_server");//通過查找匹配子串判斷
strB=strstr(buf,"gdbserver");
strC=strstr(buf,"gdb");
strD=strstr(buf,"fuwu");
if(strA || strB ||strC || strD)
{
return 1;
// 執行到這裡,判定為調試狀態
}
}
pclose(pfile);
return 0;
}
frida 檢測
本文鏈接:https://linuxstory.org/android-reverse-signature-verification-confrontation/
原文鏈接:https://www.52pojie.cn/thread-1731181-1-1.html
Linux Story 整理,對原文有刪節、補充;轉載請註明,否則將追究相關責任!