签名校验攻防对抗基础学习
在看雪看到了逆天而行大佬的博客,于是想要跟着大佬的这个检测APP来学习一下基础的签名校验的攻防对抗,也让自己稍微了解一下签名校验是什么,基础流程是什么,都有什么手段,先说明一下最后的成果吧。


有三项没有绕过,也是我后面要继续学习的地方
下面我是先用frida来进行模拟的,先随便给这个APP签名了一下,然后看看能不能使用frida来绕过这个,在之后根据这个frida的思路去写了一个APP来对这个检测APP进行去签
检测一:路径检测
private boolean checkPath() {
String sourceDir = getApplicationInfo().sourceDir;
String publicSourceDir = getApplicationInfo().publicSourceDir;
if (sourceDir.equals(publicSourceDir)) {
Log.d(TAG, "sourceDir: " + sourceDir + "\r\nsourceDir == publicSourceDir\r\n");
int i = countSlashes(sourceDir);
if (i == 5 && sourceDir.startsWith("/data/app/") && sourceDir.endsWith("/base.apk")) {
boolean b = new File(sourceDir).getParentFile().getName().startsWith(getApplication().getPackageName());
if (b && !new File(sourceDir).getParentFile().getName().equals(getApplication().getPackageName())) {
return true;
}
return false;
}
return false;
}
return false;
}
使用frida来输出看一下
function hook_getApplication() {
Java.perform(function () {
// 获取 Context 类
var Context = Java.use("android.content.Context");
// 获取 ApplicationInfo 类
var ApplicationInfo = Java.use("android.content.pm.ApplicationInfo");
// Hook getApplicationInfo 方法
Context.getApplicationInfo.overload().implementation = function () {
var appInfo = this.getApplicationInfo(); // 调用原始 getApplicationInfo 方法
var sourceDir = appInfo.sourceDir; // 获取 sourceDir
var publicSourceDir = appInfo.publicSourceDir; // 获取 publicSourceDir
// 打印原始的 sourceDir 和 publicSourceDir
console.log("sourceDir: " + sourceDir);
console.log("publicSourceDir: " + publicSourceDir);
return appInfo;
};
var ActivityThread = Java.use("android.app.ActivityThread");
var currentApplication = ActivityThread.currentApplication(); // 获取当前的 Application 对象
var appInfo = currentApplication.getApplicationInfo(); // 调用 getApplicationInfo 方法
var sourceDir = appInfo.sourceDir;
var publicSourceDir = appInfo.publicSourceDir;
console.log("sourceDir: " + sourceDir);
console.log("publicSourceDir: " + publicSourceDir);
});
}
hook_getApplication();
得到
sourceDir: Java.Field{
holder: ApplicationInfo{b90936c com.calvin.sigcheck},
fieldType: 2,
fieldReturnType: Ljava/lang/String;,
value: /data/app/~~9-mbNDbj1rjoeRDoTEGfSA==/com.calvin.sigcheck-s6InHDy2OgAWc6fZkvaJEg==/base.apk,
}
publicSourceDir: Java.Field{
holder: ApplicationInfo{b90936c com.calvin.sigcheck},
fieldType: 2,
fieldReturnType: Ljava/lang/String;,
value: /data/app/~~9-mbNDbj1rjoeRDoTEGfSA==/com.calvin.sigcheck-s6InHDy2OgAWc6fZkvaJEg==/base.apk,
}
所以这个检测就比较简单,只需要我们在重打包的时候不要去改变他的包名就行
检测二:包签名比对
包签名检测,我们就是通过反射机制在一个比较早的时机就给他从源头修改掉这个packageInfo.signatures,这样就可以解决了
private boolean doNormalSignCheck() {
String nowSignMD5 = "";
try {
PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(), 64);
Signature[] signs = packageInfo.signatures;
if (signs != null && signs.length > 0) {
byte[] signature = signs[0].toByteArray();
String signBase64 = Base64.encodeToString(signature, 0).trim();
nowSignMD5 = md5(signBase64);
Log.d(TAG, "doNormalSignCheck: " + nowSignMD5);
}
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return "7d1e7be834bb349eb0694c524353ba3c".equals(nowSignMD5);
}
对抗代码:
function hook_Appsignature() {
Java.perform(function () {
var PM = Java.use("android.app.ApplicationPackageManager");
var Signature = Java.use("android.content.pm.Signature");
var PackageInfo = Java.use("android.content.pm.PackageInfo");
var real_sig
PM.getPackageInfo.overload('java.lang.String', 'int').implementation = function (pkg, flags) {
var pi = this.getPackageInfo(pkg, flags); // 原方法
try {
// 创建假的 Signature 数组
// if (pi.signatures.value) {
// for (var i = 0; i < pi.signatures.value.length; i++) {
// var sig1 = pi.signatures.value[i];
// real_sig[i] = sig1
// console.log("[*] 原始签名[" + i + "]: " + sig1.toCharsString());
// }
// }
var sig1 = pi.signatures.value[0].toCharsString();
//console.log("[*] 原始签名[]:" + sig1);
sig1 = "先获取真实签名填入"
var sig2 = Signature.$new(sig1); // 或者正确签名
//var fakes = sig1 + sig2
var arr = Java.array('Landroid.content.pm.Signature;', [sig2]);
pi.signatures.value = arr; // 替换签名
console.log("[*] 替换Java层签名成功");
} catch (e) {
console.log("[!] Error: " + e);
}
return pi;
};
});
}
检测三:通过调用文件META-INF信息进行比对
防御手段代码:
这种只适用于V1,其他的不行,其他的存放在APK Signing Block 中
private byte[] signatureFromAPK() {
try {
ZipFile zipFile = new ZipFile(getPackageResourcePath());
Enumeration<? extends ZipEntry> entries = zipFile.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
if (entry.getName().matches("(META-INF/.*)\\.(RSA|DSA|EC)")) {
InputStream is = zipFile.getInputStream(entry);
CertificateFactory certFactory = CertificateFactory.getInstance("X509");
X509Certificate x509Cert = (X509Certificate) certFactory.generateCertificate(is);
byte[] encoded = x509Cert.getEncoded();
zipFile.close();
return encoded;
}
}
zipFile.close();
return null;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
防御原理:这个也就是打开了(META-INF文件夹下的数据,进行比对,我们可以通过放入原始文件来进行保护
直接把原包的文件放进去就行,不过可能不是通用的,我这有核心破解,可能起到作用了
检测四:通过调用新的API进行包签名比对
防御代码:
防御原理:这个和检测二一样,就是调用了一个新的API获取包的签名罢了就是这个函数
getApkContentsSigners(),可以往上追溯他的本质,去修改那个值
private boolean useNewAPICheck() {
Signature[] signs;
String nowSignMD5 = "";
try {
if (Build.VERSION.SDK_INT >= 28) {
PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(), 134217728);
signs = packageInfo.signingInfo.getApkContentsSigners();
} else {
PackageInfo packageInfo2 = getPackageManager().getPackageInfo(getPackageName(), 64);
signs = packageInfo2.signatures;
}
String signBase64 = Base64.encodeToString(signs[0].toByteArray(), 0).trim();
nowSignMD5 = md5(signBase64);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return "7d1e7be834bb349eb0694c524353ba3c".equals(nowSignMD5);
}
攻击手法:
function hook_NewApiAppsignature() {
Java.perform(function () {
var PM = Java.use("android.app.ApplicationPackageManager");
var SigningInfo = Java.use("android.content.pm.SigningInfo");
var Signature = Java.use("android.content.pm.Signature");
SigningInfo.getApkContentsSigners.implementation = function () {
// 调用原方法获取签名数组
var signatures = this.getApkContentsSigners();
// console.log("getApkContentsSigners的值" + (signatures));
var sign = signatures[0];
var charString = sign.toCharsString();
//console.log(" 得到的签名 " + charString);
var sig1 = "正确的值"
var sig2 = Signature.$new(sig1); // 或者正确签名
//var fakes = sig1 + sig2
var arr = Java.array('Landroid.content.pm.Signature;', [sig2]);
return arr;
};
});
}
检测五:SVC签名校验
检测原理:就是利用一些syscall函数来进行校验,这样就避免了一些关键的函数被直接hook的风险
检测代码:
private byte[] signatureFromSVC() {
ZipEntry entry;
int aa = openAt(getPackageResourcePath());
this.f94fd = aa;
try {
ParcelFileDescriptor fd = ParcelFileDescriptor.adoptFd(aa);
ZipInputStream zis = new ZipInputStream(new FileInputStream(fd.getFileDescriptor()));
path = checkFD(aa);
do {
entry = zis.getNextEntry();
if (entry == null) {
zis.close();
if (fd != null) {
fd.close();
return null;
}
return null;
}
} while (!entry.getName().matches("(META-INF/.*)\\.(RSA|DSA|EC)"));
CertificateFactory certFactory = CertificateFactory.getInstance("X509");
X509Certificate x509Cert = (X509Certificate) certFactory.generateCertificate(zis);
byte[] encoded = x509Cert.getEncoded();
zis.close();
if (fd != null) {
fd.close();
}
return encoded;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
其中的几个函数如下:
__int64 __fastcall openAt(int a1, const char *a2, int a3)
{
return linux_eabi_syscall(__NR_openat, a1, a2, a3);
}
__int64 __fastcall Java_com_calvin_sigcheck_MainActivity_checkFD(_JNIEnv *a1, __int64 a2, int a3)
{
int v4; // [xsp+38h] [xbp-828h]
__int64 v7; // [xsp+50h] [xbp-810h]
char s_1[1024]; // [xsp+58h] [xbp-808h] BYREF
_BYTE s[1024]; // [xsp+458h] [xbp-408h] BYREF
__int64 v10; // [xsp+858h] [xbp-8h]
v10 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
memset(s, 0, sizeof(s));
memset(s_1, 0, sizeof(s_1));
sub_2658C(s, 1024LL, "/proc/self/fd/%d", a3);//sys_fstatat64这个调用号的函数
v4 = syscall(78LL, 4294967196LL, s, s_1, 4096LL);
if ( v4 < 1 )
{
v7 = _JNIEnv::NewStringUTF(a1, ::s);
}
else
{
s_1[v4] = 0;
__android_log_print(4, "checkFD", "fd=%d, path=%s", a3, s_1);
v7 = _JNIEnv::NewStringUTF(a1, s_1);
}
_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2));
return v7;
}+
对抗原理:对一些常用的检测的SVC进行hook,更改其参数,从而进行绕过
下面的代码有很多大佬都进行过分享,这里也是借鉴大佬们的思路来学习
这里使用frida模拟进行攻击,攻击完成,就是使用seccomp对openat进行拦截,然后再对齐进行修改参数指向私有目录,代码如下:
function hook_SVC() {
//seccomp安装初始化
let install_filter = null, syscall_thread_ptr, call_task, lock, unlock, findSoinfoByAddr, solist_get_head_ptr, get_soname, get_base, get_size, maps = [];
const MAX_STACK_TRACE_DEPTH = 10;
const Target_NR = 56;
const prctl_ptr = Module.findExportByName(null, 'prctl')
const strcpy_ptr = Module.findExportByName(null, 'strcpy')
const fopen_ptr = Module.findExportByName(null, 'fopen')
const fclose_ptr = Module.findExportByName(null, 'fclose')
const fgets_ptr = Module.findExportByName(null, 'fgets')
const strtoul_ptr = Module.findExportByName(null, 'strtoul')
const strtok_ptr = Module.findExportByName(null, 'strtok')
const malloc_ptr = Module.findExportByName(null, 'malloc')
const __android_log_print_ptr = Module.findExportByName(null, '__android_log_print')
const pthread_create_ptr = Module.findExportByName(null, 'pthread_create')
const pthread_mutex_init_ptr = Module.findExportByName(null, 'pthread_mutex_init')
const pthread_mutex_lock_ptr = Module.findExportByName(null, 'pthread_mutex_lock')
const pthread_mutex_unlock_ptr = Module.findExportByName(null, 'pthread_mutex_unlock')
const pthread_join_ptr = Module.findExportByName(null, 'pthread_join')
const syscall_ptr = Module.findExportByName(null, 'syscall')
const linker = Process.findModuleByName("linker64");
const linker_symbols = linker.enumerateSymbols()
for (let index = 0; index < linker_symbols.length; index++) {
const element = linker_symbols[index];
if (element.name == '__dl__Z15solist_get_headv') {
solist_get_head_ptr = element.address
} else if (element.name == '__dl__ZNK6soinfo10get_sonameEv') {
get_soname = new NativeFunction(element.address, "pointer", ["pointer"])
}
}
function init() {
//初始化,需要在主线程初始化且需要一个比较早的时机,frida脚本运行在它自己创建的一个线程,所以需要通过hook安装seccomp规则
syscall_thread_ptr = new NativeFunction(cm.pthread_syscall_create, "pointer", [])()
findSoinfoByAddr = new NativeFunction(cm.findSoinfoByAddr, "pointer", ["pointer"])
get_base = new NativeFunction(cm.get_base, "uint64", ["pointer"])
get_size = new NativeFunction(cm.get_size, "size_t", ["pointer"])
call_task = new NativeFunction(cm.call_task, "pointer", ["pointer", "pointer", "int"])
install_filter = new NativeFunction(cm.install_filter, "int", ['uint32'])
lock = new NativeFunction(cm.lock, "int", ["pointer"])
unlock = new NativeFunction(cm.unlock, "int", ["pointer"])
// 异常处理
Process.setExceptionHandler(function (details) {
const current_off = details.context.pc - 4;
// 判断是否是seccomp导致的异常 读取opcode 010000d4 == svc 0
if (details.message == "system error" && details.type == "system" && hex(ptr(current_off).readByteArray(4)) == "010000d4") {
// 上锁避免多线程问题
lock(syscall_thread_ptr)
// 获取x8寄存器中的调用号
const nr = details.context.x8.toString(10);
console.log("开始hook SVC,其调用号为 ", nr)
if (nr == 56) {
IO_syscall(details)
}
let loginfo = "\n=================="
// 构造线程syscall调用参数
const args = Memory.alloc(7 * 8)
args.writePointer(details.context.x8)
let args_reg_arr = {}
for (let index = 0; index < 6; index++) {
eval(`args.add(8 * (index + 1)).writePointer(details.context.x${index})`)
eval(`args_reg_arr["arg${index}"] = details.context.x${index}`)
}
// 调用线程syscall 赋值x0寄存器
details.context.x0 = call_task(syscall_thread_ptr, args, 0)
// 解锁
unlock(syscall_thread_ptr)
return true;
}
return false;
})
// openat的调用号
install_filter(Target_NR)
}
// 全局持久重定向表
var redirectTable = {};
function safeReadCString(ptrArg, maxLen) {
if (ptrArg === null || ptrArg.isNull && ptrArg.isNull()) return null;
try {
// 一些 Frida 版本支持第二个参数 length;保险起见限制一下
maxLen = maxLen || 4096;
return Memory.readUtf8String(ptrArg, maxLen);
} catch (e) {
try {
// 逐字节读取直到0
var s = "";
for (var i = 0; i < maxLen; i++) {
var b = Memory.readU8(ptrArg.add(i));
if (b === 0) break;
s += String.fromCharCode(b);
}
return s.length ? s : null;
} catch (e2) {
return null;
}
}
}
function getOrCreateRedirectPointer(origPath, redirectPath) {
if (redirectTable[origPath]) return redirectTable[origPath];
// 分配一次并保存指针,保持一下长期效果。防止失效
var p = Memory.allocUtf8String(redirectPath);
redirectTable[origPath] = p;
return p;
}
function IO_syscall(details) {
try {
// 读取一下指针
var path_ptr = details.context.x1;
if (path_ptr === null || (path_ptr.isNull && path_ptr.isNull())) {
console.log("[IO_syscall] path_ptr is NULL");
return;
}
// 读字符串
var oripaths = safeReadCString(path_ptr, 4096);
if (oripaths === null) {
console.log("[IO_syscall] cannot read original path (maybe invalid ptr)");
return;
}
console.log("[IO_syscall] 原路径:", oripaths);
// 这里写死判断(仅示范),应该需要更加细节的判断
var targetOriginal = "/data/app/~~GOgtvoJrOZCIFM76oz1_7g==/com.calvin.sigcheck-Vv73FAWzCDF3RtMK-_iDsA==/base.apk";
if (oripaths === targetOriginal) {
console.log("[IO_syscall] 命中需要重定向的路径,准备替换");
var redirectpath = "/data/data/com.calvin.sigcheck/origin.apk";
// 获取或创建持久指针
var redirectPtr = getOrCreateRedirectPointer(oripaths, redirectpath);
// 赋值到 x1
details.context.x1 = redirectPtr;
console.log("[IO_syscall] 修改后寄存器 x1 =", redirectPtr);
console.log("[IO_syscall] 重定向到:", Memory.readUtf8String(redirectPtr));
}
} catch (e) {
console.log("[IO_syscall] exception:", e);
}
}
//将原始文件加载到私有目录
function prehookapk() {
var ok = false;
Java.perform(function () {
try {
console.log("加载原始文件至私有目录");
var ActivityThread = Java.use("android.app.ActivityThread");
var currentApplication = ActivityThread.currentApplication();
if (currentApplication === null) {
console.log("[-] currentApplication is null");
ok = false;
return;
}
var context = currentApplication.getApplicationContext();
var packageName = context.getPackageName();
console.log("[+] 包名: " + packageName);
var targetPath = "/data/data/" + packageName + "/origin.apk";
var File = Java.use("java.io.File");
var targetFile = File.$new(targetPath);
var parent = targetFile.getParentFile();
if (parent !== null && !parent.exists()) {
var created = parent.mkdirs();
console.log("[*] create parent dirs: " + created);
}
var assetManager = context.getAssets();
var inputStream = null;
var outputStream = null;
try {
inputStream = assetManager.open("origin.apk");
} catch (e) {
console.log("[-] asset open failed: " + e);
ok = false;
return;
}
try {
var FileOutputStream = Java.use("java.io.FileOutputStream");
outputStream = FileOutputStream.$new(targetFile);
var BUF_SIZE = 8192;
var jsBuf = Array(BUF_SIZE).fill(0);
var buffer = Java.array('byte', jsBuf);
var bytesRead;
while ((bytesRead = inputStream.read(buffer)) !== -1) {
if (bytesRead > 0) {
outputStream.write(buffer, 0, bytesRead);
}
}
outputStream.flush();
try {
targetFile.setReadable(true, true);
targetFile.setWritable(true, true);
} catch (permErr) {
console.log("[*] set permission failed: " + permErr);
}
console.log("[+] 加载完毕: " + targetPath);
ok = true;
} catch (e) {
console.log("[-] 写入失败: " + e);
ok = false;
} finally {
try { if (outputStream !== null) outputStream.close(); } catch (e) { }
try { if (inputStream !== null) inputStream.close(); } catch (e) { }
}
} catch (outer) {
console.log("[-] 未知错误: " + outer);
ok = false;
}
});
return ok;
}
const cm = new CModule(`
#include <stdio.h>
#include <gum/gumprocess.h>
#define BPF_STMT(code, k) {(unsigned short)(code), 0, 0, k}
#define BPF_JUMP(code, k, jt, jf) {(unsigned short)(code), jt, jf, k}
#define BPF_LD 0x00
#define BPF_W 0x00
#define BPF_ABS 0x20
#define BPF_JEQ 0x10
#define BPF_JMP 0x05
#define BPF_K 0x00
#define BPF_RET 0x06
#define PR_SET_SECCOMP 22 // 表示设置seccomp
#define PR_SET_NO_NEW_PRIVS 38 // 防止获得新特权
#define SECCOMP_MODE_FILTER 2 // filter模式
#define SECCOMP_RET_TRAP 0x00030000U // 触发信号
#define SECCOMP_RET_ALLOW 0x7fff0000U // 允许信号,允许系统调用通过
#define SIGSYS 12 // 示系统调用错误的信号
#define SIG_UNBLOCK 2 // 信号拥塞接触
// 类型定义
typedef unsigned char __u8;
typedef unsigned short __u16;
typedef unsigned int __u32;
typedef unsigned long long __u64;
typedef unsigned long sigset_t;
typedef long pthread_t;
// 线程的结构体,表示线程的属性
typedef struct
{
uint32_t flags; // flag标志
void *stack_base; // 栈底
size_t stack_size; // 栈大小
size_t guard_size; // 保护区域大小,防止栈溢出
int32_t sched_policy; // 线程调度策略:普通分时调度(CFS)、先入先出、轮转调度、批处理等
int32_t sched_priority; // 线程优先级
#ifdef __LP64__
char __reserved[16];
#endif
} pthread_attr_t;
// 互斥锁
typedef struct
{
#if defined(__LP64__)
int32_t __private[10];
#else
int32_t __private[1];
#endif
} pthread_mutex_t;
// 线程系统调用相关的结构体,需要自己实现一个线程来进行模拟调用SVC
typedef struct
{
int type; // 表示任务类型
int isTask; // 表示是否在工作
void *args; // 表示任务的参数
int isReturn; // 表示任务是否已经完成
void *ret; // 存储返回的结果
pthread_t thread; // 表示当前任务的线程信息
pthread_mutex_t mutex; // 互斥锁
} thread_syscall_t;
// soinfo的结构体
typedef struct
{
const void *phdr; // 程序头标指针
size_t phnum; // 程序头的数量
uint64_t base; // 在内存中的基址
size_t size; // 内存中的大小
void *dynamic; // 动态段指针,指向 .dynamic 段,包含重定位、符号表等信息
void *next; // 下一个soinfo指针
} soinfo;
extern char *strcpy(char *__dst, const char *__src);
extern void *fopen(const char *__path, const char *__mode);
extern int fclose(void *__fp);
extern char *fgets(char *__buf, int __size, void *__fp);
extern unsigned long strtoul(const char *__s, char **__end_ptr, int __base);
extern char *strtok(char *__s, const char *__delimiter);
extern soinfo *solist_get_head();
extern int __android_log_print(int prio, const char *tag, const char *fmt, ...);
extern void *malloc(size_t __byte_count);
extern long syscall(long __number, ...);
extern int pthread_create(pthread_t *__pthread_ptr, pthread_attr_t const *__attr, void *(*__start_routine)(void *), void *);
extern int pthread_mutex_init(pthread_mutex_t *__mutex, const void *__attr);
extern int pthread_mutex_lock(pthread_mutex_t *__mutex);
extern int pthread_mutex_unlock(pthread_mutex_t *__mutex);
extern int pthread_join(pthread_t __pthread, void **__return_value_ptr);
extern void on_message(const gchar *message);
extern int prctl(int __option, ...);
// 获取soinfo结构信息
uint64_t get_base(soinfo *si)
{
return si->base;
}
size_t get_size(soinfo *si)
{
return si->size;
}
// 查找给定地址对应的动态库。如果给定地址在某个库的内存范围内,它返回该库的信息。
soinfo *findSoinfoByAddr(void *addr_v)
{
uint64_t addr = (uint64_t)addr_v;
for (soinfo *si = (soinfo *)solist_get_head(); si != NULL; si = si->next)
{
if (addr >= si->base && addr < (si->base + si->size))
{
return si;
}
}
return NULL;
}
// log日志函数
static void log(const gchar *format, ...)
{
gchar *message; // 信息
va_list args;
va_start(args, format);
message = g_strdup_vprintf(format, args);
va_end(args);
on_message(message);
g_free(message);
}
// 线程锁的具体函数,上锁与去锁
int lock(thread_syscall_t *syscall_thread)
{
return pthread_mutex_lock(&syscall_thread->mutex);
}
int unlock(thread_syscall_t *syscall_thread)
{
return pthread_mutex_unlock(&syscall_thread->mutex);
}
// 表示调用系统调用的函数
void *call_syscall(void *args)
{
void **d_args = (void **)args; // 指向参数的指针
// 调用不同类型的函数
void *ret = (void *)syscall((long)d_args[0], d_args[1], d_args[2], d_args[3], d_args[4], d_args[5], d_args[6]);
return ret;
}
// 日志打印函数,参数:优先级、tag、格式化字符串
void *call_log(void *args)
{
__android_log_print(3, "seccomp", (const char *)args);
return NULL;
}
// 处理maps文件的函数,就是单纯的复制,不过也可以在这里进行一些小操作
void *call_read_maps(void *args)
{
uint64_t addr = (uint64_t)args;
FILE *fp = fopen("/proc/self/maps", "r");
char line[1024];
char _line[1024];
uint64_t start, end;
while (fgets(line, sizeof(line), fp) != NULL)
{
strcpy(_line, line);
start = (uint64_t)strtoul(strtok(line, "-"), NULL, 16);
end = (uint64_t)strtoul(strtok(NULL, " "), NULL, 16);
if (addr >= start && addr < end)
{
break;
}
}
fclose(fp);
return (void *)_line;
}
// 线程管理函数,进行单线程操作
void *call_task(thread_syscall_t *syscall_thread, void *args, int type)
{
if (syscall_thread->isTask == 0)
{
syscall_thread->args = args;
syscall_thread->type = type;
syscall_thread->isTask = 1;
}
do
{
if (syscall_thread->isReturn)
{
syscall_thread->isReturn = 0;
return syscall_thread->ret;
}
} while (1);
}
// 负责线程的主任务循环。根据任务类型,它调用不同的处理函数,如系统调用、日志打印或读取进程映射。
void *pthread_syscall(void *args)
{
thread_syscall_t *syscall_thread = (thread_syscall_t *)args;
while (1)
{
if (syscall_thread->isTask)
{
if (syscall_thread->type == 0)
{
syscall_thread->ret = call_syscall(syscall_thread->args);
}
else if (syscall_thread->type == 1)
{
syscall_thread->ret = call_log(syscall_thread->args);
}
else if (syscall_thread->type == 2)
{
syscall_thread->ret = call_read_maps(syscall_thread->args);
}
syscall_thread->args = NULL;
syscall_thread->isReturn = 1;
syscall_thread->isTask = 0;
}
}
return NULL;
}
// 线程创建函数
// syscall线程创建
thread_syscall_t *pthread_syscall_create()
{
thread_syscall_t *syscall_thread = (thread_syscall_t *)malloc(sizeof(thread_syscall_t));
syscall_thread->type = 0;
syscall_thread->isTask = 0;
syscall_thread->args = NULL;
syscall_thread->ret = NULL;
syscall_thread->isReturn = 0;
pthread_mutex_init(&syscall_thread->mutex, NULL);
pthread_t threadId;
pthread_create(&threadId, NULL, &pthread_syscall, (void *)syscall_thread);
syscall_thread->thread = threadId;
return syscall_thread;
}
//
struct seccomp_data
{
int nr; // 系统调用号
__u32 arch; // 架构标识
__u64 instruction_pointer; // 调用指令地址
__u64 args[6]; // 系统调用参数(最多6个)
};
// 定义单个Berkeley Packet Filter指令。
struct sock_filter
{
__u16 code; // 操作码 = (指令类 | 源 | 大小)
__u8 jt; // 条件为真时跳转步数
__u8 jf; // 条件为假时跳转步数
__u32 k; // 立即数值
};
struct sock_fprog
{
unsigned short len; // 指令数量
struct sock_filter *filter; // 指令数组指针
};
// seccomp的安装,拦截特定函数的系统调用
int install_filter(__u32 nr)
{
log("install_filter(%lu)", nr);
struct sock_filter filter[] = {
/*
如果 (系统调用号 == 目标号码) {
触发SIGSYS信号 → 进入监控处理
} else {
允许系统调用正常执行
}
*/
BPF_STMT(BPF_LD + BPF_W + BPF_ABS, 0), // 这里实际就是相当于汇编,一点一点往下写的
BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, nr, 0, 1),
BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_TRAP),
BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW),
};
struct sock_fprog prog = {
.len = (unsigned short)(sizeof(filter) / sizeof(filter[0])),
.filter = filter,
};
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0))
{
on_message("prctl(NO_NEW_PRIVS)");
return 1;
}
if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog))
{
on_message("prctl(PR_SET_SECCOMP)");
return 1;
}
return 0;
}
`, {
malloc: malloc_ptr,
prctl: prctl_ptr,
fopen: fopen_ptr,
fclose: fclose_ptr,
fgets: fgets_ptr,
strtok: strtok_ptr,
strcpy: strcpy_ptr,
strtoul: strtoul_ptr,
__android_log_print: __android_log_print_ptr,
pthread_create: pthread_create_ptr,
pthread_join: pthread_join_ptr,
pthread_mutex_init: pthread_mutex_init_ptr,
pthread_mutex_lock: pthread_mutex_lock_ptr,
pthread_mutex_unlock: pthread_mutex_unlock_ptr,
syscall: syscall_ptr,
solist_get_head: solist_get_head_ptr,
on_message: new NativeCallback(messagePtr => {
const message = messagePtr.readUtf8String();
console.log(message)
}, 'void', ['pointer'])
})
//prehookapk()
Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"), {
onEnter(args) {
//hook2()
if (install_filter == null) {
if (prehookapk()) {
console.log("开始init")
init()
}
//init()
//preoriapk()
}
}
})
const byteToHex = [];
for (let n = 0; n <= 0xff; ++n) {
const hexOctet = n.toString(16).padStart(2, "0");
byteToHex.push(hexOctet);
}
function hex(arrayBuffer) {
const buff = new Uint8Array(arrayBuffer);
const hexOctets = [];
for (let i = 0; i < buff.length; ++i)
hexOctets.push(byteToHex[buff[i]]);
return hexOctets.join("");
}
function call_thread_log(str) {
call_task(syscall_thread_ptr, Memory.allocUtf8String(str), 1)
}
function call_thread_read_maps(addr) {
for (let index = 0; index < maps.length; index++) {
const element = maps[index];
if (parseInt(addr.toString()) >= element[0] && parseInt(addr.toString()) < element[1]) {
return { start: element[0], end: element[1], name: element[2] }
}
}
const map_info = call_task(syscall_thread_ptr, ptr(addr), 2).readUtf8String()
const start = parseInt("0x" + map_info.split("-")[0])
const end = parseInt("0x" + map_info.split("-")[1].split(" ")[0])
const name_arr = map_info.split(" ")
const name = name_arr.length == 2 ? name_arr[2] : ""
maps.push([start, end, name])
return { start, end, name }
}
}
这样就完成了具体的攻击,绕过了svc的检测
检测六:检测当前Application
防御手法:通过检测当前的应用命是否是和初始化的应用名一样来判断是否是没有被修改的代码
防御代码:
private boolean checkApplication() {
Application nowApplication = getmyApplication();
String nowApplicationName = nowApplication.getClass().getName();
Log.d(TAG, "checkApplication: com.calvin.sigcheck.MYapp " + nowApplicationName);
return "com.calvin.sigcheck.MYapp".equals(nowApplicationName);
}
public Application getmyApplication() {
try {
Class<?> aaaaaaa = Class.forName("android.app.ActivityThread");
Method currentActivityThreadMethod = aaaaaaa.getDeclaredMethod("currentActivityThread", new Class[0]);
Object activityThread = currentActivityThreadMethod.invoke(null, new Object[0]);
Application application = (Application) getObjectField(activityThread, "mInitialApplication");
Log.d(TAG, "ActivityThread 获取 Application: " + application.getClass().getName());
return application;
} catch (Exception e) {
Log.d(TAG, "报错!: " + e.getMessage());
return null;
}
}
攻击手法:这里还是直接修改成原来初始化的,或者直接不去动他的初始值,我没有被检测到,就没有具体进行攻击了
我这里写的是将其重定向到Application中,所以当其运行完应该再抹除这个痕迹
检测七:检测当前AppComponentFactory
防御代码:
rivate boolean checkAppComponentFactory() {
String appComponentFactory = getmyAppComponentFactory();
if (appComponentFactory != null) {
Log.d(TAG, "检测到当前AppComponentFactory:" + appComponentFactory);
return appComponentFactory.equals("androidx.core.app.CoreComponentFactory");
}
return false;
}
检测八:检测当前PM
检测原理:这个他就是通过反射拿到当前进程的PackageManager 的底层接口mPM,然后对比这个对象的真是类名是不是系统生成的标准的Binder代理类或者是我们自定义的类。
如果我们修改签名的手法是通过在mPM这一层做动态代理来改写getPackageInfo()中的PackageInfo.signatures/signingInfo,那么mPM的实际对象可能就不是原来的了,在我自己写的APP中我就是使用动态代码的反射光hi来修改签名就修改了这个值,导致被检测出来了
但是我们如果修改签名的手法是直接在PackageParser.generatePackageInfo()中膝盖PackageInfo,或者在PackageInfo.CREATOR反序列化时改字段,那么我们就不会被检测出来
检测代码:
private boolean checkPMProxy() {
String nowPMName = "";
try {
PackageManager packageManager = getPackageManager();
Field mPMField = packageManager.getClass().getDeclaredField("mPM");
mPMField.setAccessible(true);
Object mPM = mPMField.get(packageManager);
nowPMName = mPM.getClass().getName();
} catch (Exception e) {
e.printStackTrace();
}
return "android.content.pm.IPackageManager$Stub$Proxy".equals(nowPMName);
}
检测九:检测当前Creator
检测原理:这个的修改手法我们在上面提到一下,这个Creator是Parcelable.Creator<PackageInfo>的单例,系统默认的是框架里的匿名内部类,android.content.pm.PackageInfo$1,而且这个一般没有自定义字段
在我们修改签名的时候,通常就是将原始的Creator替换成自己的Ceator,这样就可以手动修改PackageInfo了
防御代码:
下面大佬写了三种不通方式的获取Creator的代码
//检测类名是否是系统内置的
private boolean checkCreator() {
String nowName = "";
try {
Object mPM = PackageInfo.CREATOR;
nowName = mPM.getClass().getName();
} catch (Exception e) {
e.printStackTrace();
}
return "android.content.pm.PackageInfo$1".equals(nowName);
}
//检测有没有自定义字段
private boolean checkCreator2() {
Field[] declaredFields = null;
try {
Object mPM = PackageInfo.CREATOR;
declaredFields = mPM.getClass().getDeclaredFields();
} catch (Exception e) {
e.printStackTrace();
}
return declaredFields.length == 0;
}
private boolean checkCreator3() {
try {
Field creatorField = PackageInfo.class.getField("CREATOR");
creatorField.setAccessible(true);
Object creator = creatorField.get(null);
int i = creator.hashCode();
Log.d(TAG, "checkCreator3: hashcode :" + i);
if (creator != null) {
ClassLoader creatorClassloader = creator.getClass().getClassLoader();
ClassLoader sysClassloader = ClassLoader.getSystemClassLoader();
if (creatorClassloader != null && sysClassloader != null) {
if (sysClassloader.getClass().getName().equals(creatorClassloader.getClass().getName())) {
return false;
}
return true;
}
return false;
}
} catch (Throwable e) {
Log.d(TAG, "checkCreator3: " + e);
}
return false;
}
检测十:内存dex校验
检测原理:这里大佬使用的方式是通过getLoadedDexPaths去获取内存中dex自带的签名值,因为我写的代码是手动将我的dex文件注入到apk中,而且还修改了一个Application的类,所以就导致会出现一个原来的dex的checksum发生变化,也会多处阿里一个dex的checksum,这样和大佬预设的值肯定会不一样
Class = _JNIEnv::FindClass(a1, "com/calvin/sigcheck/MainActivity");
if ( Class )
{
StaticMethodID = _JNIEnv::GetStaticMethodID(a1, Class, "getLoadedDexPaths", "()[J");
v9 = _JNIEnv::CallStaticObjectMethod(a1, Class, StaticMethodID);
if ( v9 )
{
ArrayLength = _JNIEnv::GetArrayLength(a1, v9);
ArrayLength_1 = ArrayLength;
if ( is_mul_ok(ArrayLength, 8uLL) )
v2 = 8LL * ArrayLength;
else
v2 = -1LL;
v7 = operator new[](v2);
_JNIEnv::GetLongArrayRegion(a1, v9, 0LL, ArrayLength_1, v7);
v6 = 0LL;
for ( i = 0; i < ArrayLength_1; ++i )
{
if ( *(v7 + 8LL * i) )
{
v4 = *(*(v7 + 8LL * i) + 8LL);
__android_log_print(4, "checkMemDex", "%x", *(v4 + 8));
v6 += *(v4 + 8);
}
}
__android_log_print(4, "checkMemDex", "%ld", v6);
if ( v6 == 0x1897A827ALL )
{
__android_log_print(4, "checkMemDex", "crc is ok");
return 1;
}
else
{
return 0;
}
}
else
{
__android_log_print(4, "checkMemDexcheckMemDex", "Failed to call getLoadedDexPaths\n");
return 0;
}
}
else
{
__android_log_print(4, "checkMemDex", "jclass1 is null");
return 0;
}
这里我想到的方式有两个,但是只是我的想法,我也会在后面测试一下
1:更换过签思路,内置一个hook框架,通过hook的方式去修改签名值,这样就不会改变checksum的值了,但同时也会增加其他的暴露风险,如hook检测等
2、既然是内存校验,那么我们应该是可以仿造一块内存的,从而让函数指向我们仿造的那一块内存处
检测十一:检测当前fd路径
这个检测也是比较常见的一个检测,就是我们进行IO重定向的话,我们这个fd路径会变成我们IO重定向之后的,这样就会和原来的不一样,我们就需要修改还原成原来的fd路径
extern "C"
JNIEXPORT jstring JNICALL
Java_com_calvin_sigcheck_MainActivity_checkFD(JNIEnv *env, jobject thiz, jint fd) {
// TODO: implement checkFD()
char fdPath[1024] = {0};
char buffer[1024] = {0};
sprintf(fdPath, "/proc/self/fd/%d", (int )fd);
int len = syscall(78, AT_FDCWD, fdPath, buffer, PATH_MAX);
if (len > 0) {
buffer[len] = '\0';
__android_log_print(ANDROID_LOG_INFO, "checkFD", "fd=%d, path=%s", (int )fd, buffer);
return env->NewStringUTF(buffer);
}
return env->NewStringUTF("失败");
}
检测十二:内存CRC校验
这个在大佬的检测APP中没有写,但是我在另外一个师傅那看到这个检测
https://bbs.kanxue.com/thread-285790.htm
这位大佬也给出了解决方案
自写去签代码:
So层
// resig_native.cpp (编译成 libkillsignture.so)
#include <jni.h>
#include <android/log.h>
#include <pthread.h>
#include <signal.h>
#include <ucontext.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <sys/prctl.h>
#include <fcntl.h>
#include <stddef.h>
#include <string.h> // 仅用于 memset
#include <stdlib.h> // malloc/free/strdup
// ===== 日志(避免在信号处理器里使用 LOG*)=====
#define TAG "resig-native"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)
// ===== seccomp/BPF 头 =====
#include <linux/filter.h>
#include <linux/seccomp.h>
#ifndef SECCOMP_RET_TRAP
# define SECCOMP_RET_TRAP 0x00030000U
#endif
#ifndef SECCOMP_RET_ALLOW
# define SECCOMP_RET_ALLOW 0x7fff0000U
#endif
#ifndef PR_SET_NO_NEW_PRIVS
# define PR_SET_NO_NEW_PRIVS 38
#endif
#ifndef PR_SET_SECCOMP
# define PR_SET_SECCOMP 22
#endif
#ifndef SECCOMP_MODE_FILTER
# define SECCOMP_MODE_FILTER 2
#endif
// ===== 全局重定向目标 =====
static const char* g_apk_path = nullptr; // 原始 APK
static const char* g_rep_path = nullptr; // 替换 APK
// ===== 工人线程与管道 =====
static int g_req_pipe[2] = {-1, -1}; // handler -> worker
static int g_resp_pipe[2] = {-1, -1}; // worker -> handler
static pthread_t g_worker;
// ===== 简单 async-signal-safe 字符串工具 =====
static inline int c_has_sub(const char* s, const char* sub) {
if (!s || !sub) return 0;
// 朴素搜索(避免调用 strstr/strlen)
for (const char* p = s; *p; ++p) {
const char* a = p;
const char* b = sub;
while (*a && *b && (*a == *b)) { ++a; ++b; }
if (*b == '\0') return 1;
}
return 0;
}
static inline long c_strlen(const char* s) {
if (!s) return 0;
long n = 0; while (s[n] != '\0') ++n; return n;
}
static inline void c_strcpy(char* dst, const char* src) {
if (!dst || !src) return;
while (*src) { *dst++ = *src++; }
*dst = '\0';
}
static inline int c_ends_with(const char* s, const char* suf) {
if (!s || !suf) return 0;
long ls = c_strlen(s), lu = c_strlen(suf);
if (lu > ls) return 0;
const char* sp = s + (ls - lu);
while (*sp && *suf && (*sp == *suf)) { ++sp; ++suf; }
return (*suf == '\0');
}
// ===== 请求结构 =====
typedef struct {
long nr;
long args[6];
} SysReq;
// ===== 工人线程:执行真实 syscall =====
static void* worker_main(void*) {
for (;;) {
SysReq req;
ssize_t n = read(g_req_pipe[0], &req, sizeof(req));
if (n != (ssize_t)sizeof(req)) continue;
long ret = syscall(req.nr,
req.args[0], req.args[1], req.args[2],
req.args[3], req.args[4], req.args[5]);
(void)write(g_resp_pipe[1], &ret, sizeof(ret));
}
return nullptr;
}
static int start_worker_thread() {
if (pipe(g_req_pipe) != 0) return -1;
if (pipe(g_resp_pipe) != 0) return -1;
pthread_attr_t attr; pthread_attr_init(&attr);
int rc = pthread_create(&g_worker, &attr, worker_main, nullptr);
pthread_attr_destroy(&attr);
return rc;
}
// ===== 安装 seccomp(仅当前线程)=====
static int install_seccomp_filter_for_current_thread() {
// 只拦 openat(56) 与 readlinkat(78)
struct sock_filter filter[] = {
// A = seccomp_data.nr
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)),
// if (A == 56) -> TRAP
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 56, 0, 3),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_TRAP),
// if (A == 78) -> TRAP
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 78, 0, 1),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_TRAP),
// 其他全部允许
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
};
struct sock_fprog prog;
prog.len = (unsigned short)(sizeof(filter) / sizeof(filter[0]));
prog.filter = filter;
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0) {
LOGE("PR_SET_NO_NEW_PRIVS failed");
return -1;
}
if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog) != 0) {
LOGE("PR_SET_SECCOMP failed");
return -1;
}
LOGI("seccomp installed (thread-local)");
return 0;
}
// ===== SIGSYS 处理器(改参 + 管道转发)=====
static void sigsys_handler(int signo, siginfo_t*, void* context) {
if (signo != SIGSYS) return;
ucontext_t* uc = (ucontext_t*)context;
// AArch64:x8=nr,x0..x5=args
long nr = uc->uc_mcontext.regs[8];
long a0 = uc->uc_mcontext.regs[0];
long a1 = uc->uc_mcontext.regs[1];
long a2 = uc->uc_mcontext.regs[2];
long a3 = uc->uc_mcontext.regs[3];
long a4 = uc->uc_mcontext.regs[4];
long a5 = uc->uc_mcontext.regs[5];
if (nr != 56 && nr != 78) return;
// openat(56):如果 pathname 含 ".apk" 则改为 g_rep_path
if (nr == 56) {
const char* pathname = (const char*)a1;
LOGI("[*] open%s",pathname);
if (pathname && g_rep_path && c_ends_with(pathname, g_apk_path)) {
LOGI("重定向到原apk:%s-->>%s",pathname,g_rep_path);
a1 = (long)g_rep_path; // 替换路径
}
}
// readlinkat(78):如果要“伪造读到 apkPath”,直接在 handler 中写 buf 并返回长度;否则转发
if (nr == 78) {
const char* pathname = (const char*)a1;
char* buf = (char*)a2;
long buflen = a3;
if (pathname && buf && g_apk_path && c_has_sub(pathname, "origin.apk")) {
// 伪造 readlinkat 结果:把 g_apk_path 拷到 buf,返回长度
long n = c_strlen(g_apk_path);
if (n >= buflen) n = buflen - 1;
for (long i = 0; i < n; ++i) buf[i] = g_apk_path[i];
if (buflen > 0) buf[n] = '\0';
uc->uc_mcontext.regs[0] = n; // 返回长度
return;
}
}
// 把(可能已改参的)syscall 请求交给工人线程
SysReq req;
req.nr = nr;
req.args[0] = a0; req.args[1] = a1; req.args[2] = a2;
req.args[3] = a3; req.args[4] = a4; req.args[5] = a5;
(void)write(g_req_pipe[1], &req, sizeof(req));
long ret = -1;
(void)read(g_resp_pipe[0], &ret, sizeof(ret));
// 把返回值填回 x0,继续执行
uc->uc_mcontext.regs[0] = ret;
}
// ===== 安装 SIGSYS =====
static int install_sigsys() {
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sigemptyset(&sa.sa_mask);
sa.sa_sigaction = sigsys_handler;
sa.sa_flags = SA_SIGINFO;
if (sigaction(SIGSYS, &sa, nullptr) != 0) {
LOGE("sigaction(SIGSYS) failed");
return -1;
}
LOGI("SIGSYS handler installed");
return 0;
}
// NI 入口:设置路径 + 启动工人线程 + 安装 SIGSYS + 安装 seccomp
extern "C"
JNIEXPORT void JNICALL
Java_com_xwaaa_hook_HookApplication_hookApkPath(
JNIEnv* env, jclass /*clazz*/, jstring jSourcePath, jstring jRepPath) {
const char* src = env->GetStringUTFChars(jSourcePath, nullptr);
const char* rep = env->GetStringUTFChars(jRepPath, nullptr);
// 保存重定向参数
g_apk_path = strdup(src ? src : "");
g_rep_path = strdup(rep ? rep : "");
LOGI("hookApkPath: src=%s, rep=%s", g_apk_path, g_rep_path);
// 1) 先起工人线程(保证该线程不受后续 seccomp 影响)
if (start_worker_thread() != 0) {
LOGE("start_worker_thread failed");
}
// 2) 安装 SIGSYS 处理器
if (install_sigsys() != 0) {
LOGE("install_sigsys failed");
}
// 3) 仅在**当前线程**安装 seccomp(避免把工人线程也拦住)
if (install_seccomp_filter_for_current_thread() != 0) {
LOGE("install_seccomp_filter_for_current_thread failed");
}
env->ReleaseStringUTFChars(jSourcePath, src);
env->ReleaseStringUTFChars(jRepPath, rep);
}
其他java层的代码写的有瑕疵,要么PM检测不过,要么Creator不过,等我在修改一些吧,而且APP的兼容性太差了,也希望大佬可以教教指点一下,给点思路和方向


后续工作:
1、考虑对Binder层进行hook测试,这样应该可以过的更多一些吧
2、植入hook框架来进行测试
3、提高兼容性,尝试改变注入dex的位置看看有没有什么更好的方法
感谢各位大佬各位师傅的文章给予的思路和方向
参考文章:
https://bbs.kanxue.com/thread-285790.htm
https://bbs.kanxue.com/thread-285647.htm
https://bbs.kanxue.com/thread-279725.htm
https://bbs.kanxue.com/thread-285339.htm
[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!
最后于 2025-11-4 20:28
被xiaowaaa编辑
,原因: 修改