这篇文章只讲Android端签名检测,安卓发展到现在,因为国内环境没有谷歌市场,所以很多官方推荐的Api没法使用 ,所以国内的签名检测方式也是“千奇百怪” 。发展至今每种方法都有一些绕过或者对抗手段,这些方法很难说就一定准 ,但是我们能做的就是取尽可能的提高攻击者的成本,提升Apk的签名检测能力,防止灰黑产进行攻击 。基础的什么Java获取签名信息这种基础方案,这里暂时跳过 ,不在过多叙述 。这篇文章主要分为Java和Native两部分 ,分别从不同的视角取检测签名,包括如何对抗等 , 其中包含一些大厂和企业壳的核心检测签名思路 。
首先讲这块之前需要先简单讲一下IPC协议的实现 。
安卓基础架构就是CS架构。每个App都是客户端,服务端只有一个 ,客户端和服务端是不同的进程 。
这样做的,客户端一旦发生崩溃不影响服务端 ,服务端也可以根据不同的uid实现不同的鉴权操作 。当我们获取一些apk信息的时候都是客户端发送IPC协议 。
服务端接收以后进行处理,利用binder进行通讯,然后把数据写到客户端内部,客户端拿到 。这块设计很多技术点 ,比如动态代理,IPC协议 ,Binder通讯等
上面说起来可能很难懂 ,首先我们去Hook PackageInfo的构造方法 ,根据栈回溯看看如果想要生产一个PackageInfo到底需要哪些流程 。
然后讲一下,为什么推荐使用IPC协议去获取签名。由点到面中间可能会存在哪些问题 。大家在测试的时候最好在谷歌原生的系统上测试,如果是国产厂商可能会添加自己的一些拦截器 。测试版本Android 11 。从上往下看 ,一步一步讲一下 每一步到底做什么的
客户端IPC协议流程图简述:
发起人App调用获取PackageInfo-> PackageManager收到请求,判断是否存在cache,如果不存在 ->服务端代理人 ->
发起Binder协议->服务端接收 ,处理完毕写入->客户端接收。
这块我们发现,很多逻辑都是没用的,都是客户端去为了各种方便封装了很多方法,但是最终目的都是为了发送IPC协议和服务端通讯 ,
但是其中,中间每一层级都可能导致我们的程序被Hook导致我们拿到的数据是不安全的 ,比如攻击者是直接hook PackageManager 或者直接hook PackageManager代理人,然后修改对应的数据内容。导致我们的数据不安全 ,被替换 。
所以得出结论:
我们可以直接模拟IPC协议直接和服务端通讯,因为客户端这些都是不安全的 ,直接通讯绕过这些复杂的处理逻辑,但是弊端就是需要兼容不同的android版本 , 否则可能会导致解析失败。
实现代码如下:
不是的,如果这块有个细节就是 我们本质上还是需要通过binder驱动进行通讯 ,和PackageInfo.CREATOR解析 。
如果攻击者 直接hook了binder驱动 ,或者hook解析方法还是可以绕过 。比如binder也是会有一些入口,还是可能被Hook劫持 。
或者直接hook签名解析的方法,因为在getPackageInfo也是需要去解析签名信息的,可以直接hook签名信息解析这块逻辑 。
可以根据安卓源码去自实现binder驱动和解析过程 ,然后连本地的binder也不信任,直接和服务端通讯也是可以的 。
目前推荐的只有这么一种,大部分都是SO层做的检测,Java层还可以做一些代理人的动态代理,包括一些代理人的classloader,或者
PackageInfo.CREATOR的classloader之类的去做对抗 。还有一个tee检测也很不错 ,用于检测各种证书,也是容易被hook,这块不多说了 ,重点放在So层 。
So层的基础svc拦截就可以bypass掉很多小白 。
这块什么是svc就不多说了,说了很多遍了 。感兴趣可以看 https://bbs.kanxue.com/thread-277402.htm
我之前再看雪发的文章 。
这种方式主要是直接在方法里面内敛svc的方式去openat打开已安装的apk ,然后去解析证书文件 。
如果svc层不做处理 ,很难绕过检测 。解析apk签名过程如下 ,代码来自Magisk ,调用 checkSign 对返回的字符串进行判断即可 。
当我们打开fd的时候,文件很有可能会被IO重定向,重定向到一个新的文件,但是这个文件不可能是系统的,因为没权限。
所以当我们得到这个fd的时候可以,再次通过fstat去获取已经打开的这个fd 是不是一个系统文件,系统文件的gid和uid都是1000 。
确认我们打开的fd是系统文件 。因为/data/app/包名/base.apk是一个系统文件 。
对抗的话可以直接svc fstat 退出阶段,对返回结果进行bypass即可 。
当我们打开fd的时候,文件很有可能会被IO重定向,重定向到一个新的文件,我们根据fd通过readlinkat 反查fd路径,判断和传入的路径是否相等,
如果相等则认为正确 。也是在SVC Readlinkat推出阶段对查询的buff进行二次覆盖 。
这个思路主要是根据 Readlinkat的返回返回值进行检测,在创建数组的时候 数组大小采用Readlinkat返回值的大小 。
Readlinkat返回值 返回值返回的是一个查询路径结果的长度,如果攻击者只改了Readlinkat的参数,改了路径,但是返回值忘记修改,这样他的返回值就会被阶段,也就是大小不匹配,也可以检测出来 路径和传入的原始路径是否相等 。
inode是linux的文件或者目录的唯一标识符,每个文件都是独一无二的 ,如果文件被关闭,再次打开一个新的文件可能会被占用,不过一些系统apk 不会被删除和关闭 。
在maps里面 每个item类似如下 :
7041e02000-7041e4e000 r--p 00000000 fc:01 25131 /apex/com.android.conscrypt/lib64/libc++.so
这里面的倒数25131 第二项就是inode,我们当得到一个fd的时候可以获取文件的fd 信息 ,然后再去maps里面获取已经打开的文件fd信息 。
判断打开文件的inode和内存里面的inode是否相等 。
对抗的话可以直接svc fstat 退出阶段,对返回结果进行bypass即可 。
这个方法过于激进,很少部分人知道 ,很多手机在刚启动的时候会自动打开 /data/app/包名/base.apk
比如 我们可以直接遍历已经打开的fd ,然后对这个readlinkat反查路径 , 如果是base.apk的fd直接对fd进行签名解析 。相当于对已经打开的文件进行签名解析 。
因为这个apk是系统启动阶段就被打开了 ,所以不会走IO重定向的逻辑 ,相对安全 。
经过测试,有的手机App在开机启动阶段不会打开 ,只适合Apk再启动阶段打开了base.apk ,直接对已经打开的fd解析即可 。可以逃逸IO重定向 。
对抗的话 也很简单3种方案
如果有人通过已经打开的文件去遍历fd , 在readlinkat 结果处理 ,如果是base.apk直接隐藏掉 。
直接处理getdents64和getdents ,在遍历阶段直接找不到对应的fd即可
直接close,但是有的版本在close会报 fdsan异常,需要通过 android_fdsan_set_error_level 设置禁用
上述这些方案基本覆盖大多数手段 ,Hunter的核心检测思路也是上面这几种 。签名Native部分代码如下 。
经常有人问我,签名检测太难,不知道应该怎么绕过 ,有没有一种可以快速绕过全部检测的方案呢?并且我可以随便修改apk信息不被发现 。
其实很简单,在root手机上直接使用核心破解或者小米的三方lsp插件 Cemiuiler , 页面如下 。

直接禁用系统的签名验证,直接安装,也就是在破坏了原始签名的情况下 ,未签名,直接安装 。
**因为你没有修改apk原始的签名信息,他读取到的还是原始的签名信息,签名文件不会有任何变化 。**你二次签名本质的目的都是为了通过系统的签名检测 。
//触发hook回调,命中PackageInfo构造方法
19、at RuntimeClazz.constructor(Unknown Source:11)
//PackageInfo真正的构造方法
18、at android.content.pm.PackageInfo.<init>(PackageInfo.java:29)
17、at java.lang.reflect.Method.invoke(Native Method)
16、at com.runtime.api.rposed.RposedBridge.invokeMethod(RposedBridge.java:314)
15、at com.runtime.api.rposed.RposedBridge.-$$Nest$sminvokeMethod(Unknown Source:0)
14、at com.runtime.api.rposed.RposedBridge$HookInfo.callback(RposedBridge.java:378)
//hook拦截回调
13、at RuntimeClazz.constructor(Unknown Source:14)
12、at android.content.pm.PackageInfo$1.createFromParcel(PackageInfo.java:500)
//这块相当于是对服务端的数据去解析,把Parcel装载的数据转换成PackageInfo,再则里面进行PackageInfo的构造。
11、at android.content.pm.PackageInfo$1.createFromParcel(PackageInfo.java:497)
//读取数据类型,开始去解析服务端发送过来的数据 。
//android.os.Parcel 这个对象大家可以理解成“小盒子”,里面存放了数据包,不管是请求服务端发送协议的数据包
//还是服务端返回结果的数据包都是通过Parcel进行装载 。
10、android.os.Parcel.readTypedObject(Parcel.java:3982)
//android.content.pm.IPackageManager$Stub$Proxy 是PackageManager内部的一个代理人,也就是服务端的代理人
//代理设计模式的好处就是,代理人只有其本地的部分功能,不同的功能需要的权限也不一样 ,方便处理。
//安卓端每个服务,内部都存在一个服务端的代理人,所谓的动态代理就是通过反射的方式强制把代理人给替换成自己的实现对象 。
//伪造服务端代理人,实现非注入式hook ,比如一般的沙箱为了稳定性,不会采用类似lsplant的方式去修改函数地址的hook 。
//所以检测服务端代理人的class名称,是否包含proxy或者检测服务端代理人的classloader是否是系统的classloader。
//都是很常见的检测沙箱的方式,这个方法是PackageManager本地逻辑处理完,把请求交给服务端代理人IPackageManager
//这个方法就是核心逻辑所在,IPC协议的拼接,底层调用的binder去通讯,通讯完毕去解析 。都是在这个方法里面处理的 。
9、at android.content.pm.IPackageManager$Stub$Proxy.getPackageInfo(IPackageManager.java:4764)
//未找到cache
8、at android.content.pm.PackageManager.getPackageInfoAsUserUncached(PackageManager.java:8224)
7、at android.content.pm.PackageManager.access$100(PackageManager.java:96)
6、at android.content.pm.PackageManager$2.recompute(PackageManager.java:8236)
5、at android.content.pm.PackageManager$2.recompute(PackageManager.java:8233)
4、at android.app.PropertyInvalidatedCache.query(PropertyInvalidatedCache.java:374)
//根据uid去查询cache
3、at android.content.pm.PackageManager.getPackageInfoAsUserCached(PackageManager.java:8251)
//getPackageInfo的一个包装,根据uid再次查询
2、at android.app.ApplicationPackageManager.getPackageInfoAsUser(ApplicationPackageManager.java:202)
//当调用获取getPackageInfo时候的调用栈
1、at android.app.ApplicationPackageManager.getPackageInfo(ApplicationPackageManager.java:179)
//触发hook回调,命中PackageInfo构造方法
19、at RuntimeClazz.constructor(Unknown Source:11)
//PackageInfo真正的构造方法
18、at android.content.pm.PackageInfo.<init>(PackageInfo.java:29)
17、at java.lang.reflect.Method.invoke(Native Method)
16、at com.runtime.api.rposed.RposedBridge.invokeMethod(RposedBridge.java:314)
15、at com.runtime.api.rposed.RposedBridge.-$$Nest$sminvokeMethod(Unknown Source:0)
14、at com.runtime.api.rposed.RposedBridge$HookInfo.callback(RposedBridge.java:378)
//hook拦截回调
13、at RuntimeClazz.constructor(Unknown Source:14)
12、at android.content.pm.PackageInfo$1.createFromParcel(PackageInfo.java:500)
//这块相当于是对服务端的数据去解析,把Parcel装载的数据转换成PackageInfo,再则里面进行PackageInfo的构造。
11、at android.content.pm.PackageInfo$1.createFromParcel(PackageInfo.java:497)
//读取数据类型,开始去解析服务端发送过来的数据 。
//android.os.Parcel 这个对象大家可以理解成“小盒子”,里面存放了数据包,不管是请求服务端发送协议的数据包
//还是服务端返回结果的数据包都是通过Parcel进行装载 。
10、android.os.Parcel.readTypedObject(Parcel.java:3982)
//android.content.pm.IPackageManager$Stub$Proxy 是PackageManager内部的一个代理人,也就是服务端的代理人
//代理设计模式的好处就是,代理人只有其本地的部分功能,不同的功能需要的权限也不一样 ,方便处理。
//安卓端每个服务,内部都存在一个服务端的代理人,所谓的动态代理就是通过反射的方式强制把代理人给替换成自己的实现对象 。
//伪造服务端代理人,实现非注入式hook ,比如一般的沙箱为了稳定性,不会采用类似lsplant的方式去修改函数地址的hook 。
//所以检测服务端代理人的class名称,是否包含proxy或者检测服务端代理人的classloader是否是系统的classloader。
//都是很常见的检测沙箱的方式,这个方法是PackageManager本地逻辑处理完,把请求交给服务端代理人IPackageManager
//这个方法就是核心逻辑所在,IPC协议的拼接,底层调用的binder去通讯,通讯完毕去解析 。都是在这个方法里面处理的 。
9、at android.content.pm.IPackageManager$Stub$Proxy.getPackageInfo(IPackageManager.java:4764)
//未找到cache
8、at android.content.pm.PackageManager.getPackageInfoAsUserUncached(PackageManager.java:8224)
7、at android.content.pm.PackageManager.access$100(PackageManager.java:96)
6、at android.content.pm.PackageManager$2.recompute(PackageManager.java:8236)
5、at android.content.pm.PackageManager$2.recompute(PackageManager.java:8233)
4、at android.app.PropertyInvalidatedCache.query(PropertyInvalidatedCache.java:374)
//根据uid去查询cache
3、at android.content.pm.PackageManager.getPackageInfoAsUserCached(PackageManager.java:8251)
//getPackageInfo的一个包装,根据uid再次查询
2、at android.app.ApplicationPackageManager.getPackageInfoAsUser(ApplicationPackageManager.java:202)
//当调用获取getPackageInfo时候的调用栈
1、at android.app.ApplicationPackageManager.getPackageInfo(ApplicationPackageManager.java:179)
try {
PackageManager packageManager = getBaseContext().getPackageManager();
Object IPC_PM_Obj = RposedHelpers.getObjectField(packageManager, "mPM");
//取binder
IBinder mRemote = (IBinder) RposedHelpers.getObjectField(IPC_PM_Obj, "mRemote");
Parcel _data = Parcel.obtain();
Parcel _reply = Parcel.obtain();
_data.writeInterfaceToken("android.content.pm.IPackageManager");
_data.writeString(getPackageName());
_data.writeLong(PackageManager.GET_SIGNATURES);
_data.writeInt(android.os.Process.myUid());
//自定义flag
boolean _status = mRemote.transact(TransactCase.TRANSACTION_getPackageInfo(), _data, _reply, 0);
_reply.readException();
PackageInfo packageInfo = _reply.readTypedObject(PackageInfo.CREATOR);
_data.recycle();
_reply.recycle();
CLog.e("ipc sign info -> : "+packageInfo.signatures[0].toCharsString());
} catch (Throwable e) {
CLog.i("IPC_TEST_getPackageInfo error "+e);
}
public static int TRANSACTION_getPackageInfo() {
if(TRANSACTION_getPackageInfo == -1) {
try {
Field field = null;
try {
Class<?> pkmIPCClazz = Class.forName("android.content.pm.IPackageManager$Stub");
field = pkmIPCClazz.getDeclaredField("TRANSACTION_getPackageInfo");
} catch (Throwable e) {
CLog.e(">>>>>>>>>> getTranscationId forName error " + e.getMessage());
}
assert field != null;
field.setAccessible(true);
TRANSACTION_getPackageInfo = field.getInt(null);
} catch (Throwable e) {
e.printStackTrace();
CLog.e(">>>>>>>>>> getTranscationId error " + e.getMessage());
}
}
return TRANSACTION_getPackageInfo;
}
try {
PackageManager packageManager = getBaseContext().getPackageManager();
Object IPC_PM_Obj = RposedHelpers.getObjectField(packageManager, "mPM");
//取binder
IBinder mRemote = (IBinder) RposedHelpers.getObjectField(IPC_PM_Obj, "mRemote");
Parcel _data = Parcel.obtain();
Parcel _reply = Parcel.obtain();
_data.writeInterfaceToken("android.content.pm.IPackageManager");
_data.writeString(getPackageName());
_data.writeLong(PackageManager.GET_SIGNATURES);
_data.writeInt(android.os.Process.myUid());
//自定义flag
boolean _status = mRemote.transact(TransactCase.TRANSACTION_getPackageInfo(), _data, _reply, 0);
_reply.readException();
PackageInfo packageInfo = _reply.readTypedObject(PackageInfo.CREATOR);
_data.recycle();
_reply.recycle();
CLog.e("ipc sign info -> : "+packageInfo.signatures[0].toCharsString());
} catch (Throwable e) {
CLog.i("IPC_TEST_getPackageInfo error "+e);
}
public static int TRANSACTION_getPackageInfo() {
if(TRANSACTION_getPackageInfo == -1) {
try {
Field field = null;
try {
Class<?> pkmIPCClazz = Class.forName("android.content.pm.IPackageManager$Stub");
field = pkmIPCClazz.getDeclaredField("TRANSACTION_getPackageInfo");
} catch (Throwable e) {
CLog.e(">>>>>>>>>> getTranscationId forName error " + e.getMessage());
}
assert field != null;
field.setAccessible(true);
TRANSACTION_getPackageInfo = field.getInt(null);
} catch (Throwable e) {
e.printStackTrace();
CLog.e(">>>>>>>>>> getTranscationId error " + e.getMessage());
}
}
return TRANSACTION_getPackageInfo;
}
// Top-level block container
struct signing_block {
uint64_t block_sz;
struct id_value_pair {
uint64_t len;
struct /* v2_signature */ {
uint32_t id;
uint8_t value[0]; // size = (len - 4)
};
} id_value_pair_sequence[0];
uint64_t block_sz_; // *MUST* be same as block_sz
char magic[16]; // "APK Sig Block 42"
};
struct len_prefixed {
uint32_t len;
};
// Generic length prefixed raw data
struct len_prefixed_value : public len_prefixed {
uint8_t value[0];
};
// V2 Signature Block
struct v2_signature {
uint32_t id; // 0x7109871a
uint32_t signer_sequence_len;
struct signer : public len_prefixed {
struct signed_data : public len_prefixed {
uint32_t digest_sequence_len;
struct : public len_prefixed {
uint32_t algorithm;
len_prefixed_value digest;
} digest_sequence[0];
uint32_t certificate_sequence_len;
len_prefixed_value certificate_sequence[0];
uint32_t attribute_sequence_len;
struct attribute : public len_prefixed {
uint32_t id;
uint8_t value[0]; // size = (len - 4)
} attribute_sequence[0];
} signed_data;
uint32_t signature_sequence_len;
struct : public len_prefixed {
uint32_t id;
len_prefixed_value signature;
} signature_sequence[0];
len_prefixed_value public_key;
} signer_sequence[0];
};
// End of central directory record
struct EOCD {
uint32_t magic; // 0x6054b50
uint8_t pad[8]; // 8 bytes of irrelevant data
uint32_t central_dir_sz; // size of central directory
uint32_t central_dir_off; // offset of central directory
uint16_t comment_sz; // size of comment
char comment[0];
} __attribute__((packed));
/*
* A v2/v3 signed APK has the format as following
*
* +---------------+
* | zip content |
* +---------------+
* | signing block |
* +---------------+
* | central dir |
* +---------------+
* | EOCD |
* +---------------+
*
* Scan from end of file to find EOCD, and figure our way back to the
* offset of the signing block. Next, directly extract the certificate
* from the v2 signature block.
*
* All structures above are mostly just for documentation purpose.
*
* This method extracts the first certificate of the first signer
* within the APK v2 signature block.
*/
static string read_certificate(int fd) {
uint32_t size4;
uint64_t size8;
// Find EOCD
for (int i = 0;; i++) {
// i is the absolute offset to end of file
uint16_t comment_sz = 0;
lseek(fd, -((off_t) sizeof(comment_sz)) - i, SEEK_END);
read(fd, &comment_sz, sizeof(comment_sz));
if (comment_sz == i) {
// Double check if we actually found the structure
lseek(fd, -((off_t) sizeof(EOCD)), SEEK_CUR);
uint32_t magic = 0;
read(fd, &magic, sizeof(magic));
if (magic == EOCD_MAGIC) {
break;
}
}
if (i == 0xffff) {
// Comments cannot be longer than 0xffff (overflow), abort
return {};
}
}
// We are now at EOCD + sizeof(magic)
// Seek and read central_dir_off to find start of central directory
uint32_t central_dir_off = 0;
{
constexpr off_t off = offsetof(EOCD, central_dir_off) - sizeof(EOCD::magic);
lseek(fd, off, SEEK_CUR);
}
read(fd, ¢ral_dir_off, sizeof(central_dir_off));
// Next, find the start of the APK signing block
{
constexpr int off = sizeof(signing_block::block_sz_) + sizeof(signing_block::magic);
lseek(fd, (off_t) (central_dir_off - off), SEEK_SET);
}
read(fd, &size8, sizeof(size8)); // size8 = block_sz_
char magic[sizeof(signing_block::magic)] = {0};
read(fd, magic, sizeof(magic));
if (memcmp(magic, APK_SIGNING_BLOCK_MAGIC, sizeof(magic)) != 0) {
// Invalid signing block magic, abort
return {};
}
uint64_t signing_blk_sz = 0;
lseek(fd, (off_t) (central_dir_off - size8 - sizeof(signing_blk_sz)), SEEK_SET);
read(fd, &signing_blk_sz, sizeof(signing_blk_sz));
if (signing_blk_sz != size8) {
// block_sz != block_sz_, invalid signing block format, abort
return {};
}
// Finally, we are now at the beginning of the id-value pair sequence
for (;;) {
read(fd, &size8, sizeof(size8)); // id-value pair length
if (size8 == signing_blk_sz) {
// Outside of the id-value pair sequence; actually reading block_sz_
break;
}
uint32_t id;
read(fd, &id, sizeof(id));
if (id == SIGNATURE_SCHEME_V2_MAGIC) {
read(fd, &size4, sizeof(size4)); // signer sequence length
read(fd, &size4, sizeof(size4)); // signer length
read(fd, &size4, sizeof(size4)); // signed data length
read(fd, &size4, sizeof(size4)); // digest sequence length
lseek(fd, (off_t) (size4), SEEK_CUR); // skip all digests
read(fd, &size4, sizeof(size4)); // cert sequence length
read(fd, &size4, sizeof(size4)); // cert length
string cert;
cert.resize(size4);
read(fd, (void *) cert.data(), size4);
return cert;
} else {
// Skip this id-value pair
lseek(fd, (off_t) (size8 - sizeof(id)), SEEK_CUR);
}
}
return {};
}
string checkSign(JNIEnv * env,const char* apkPath){
int fd1 = static_cast<int >(raw_syscall(__NR_openat, AT_FDCWD,
reinterpret_cast<const char *>(apkPath),
O_RDONLY | O_CLOEXEC,
0640
));
if(fd1==-1){
return {};
}
std::string md5Result = Base64Utils::VTEncode(read_certificate(fd1));
close(fd1);
return md5Result;
}
string checkSign(JNIEnv * env,int fd1){
if(fd1 == -1){
return {};
}
std::string md5Result = Base64Utils::VTEncode(read_certificate(fd1));
return md5Result;
}
// Top-level block container
struct signing_block {
uint64_t block_sz;
struct id_value_pair {
uint64_t len;
struct /* v2_signature */ {
uint32_t id;
uint8_t value[0]; // size = (len - 4)
};
} id_value_pair_sequence[0];
uint64_t block_sz_; // *MUST* be same as block_sz
char magic[16]; // "APK Sig Block 42"
};
struct len_prefixed {
uint32_t len;
};
// Generic length prefixed raw data
struct len_prefixed_value : public len_prefixed {
uint8_t value[0];
};
// V2 Signature Block
struct v2_signature {
uint32_t id; // 0x7109871a
uint32_t signer_sequence_len;
struct signer : public len_prefixed {
struct signed_data : public len_prefixed {
uint32_t digest_sequence_len;
struct : public len_prefixed {
uint32_t algorithm;
len_prefixed_value digest;
} digest_sequence[0];
uint32_t certificate_sequence_len;
len_prefixed_value certificate_sequence[0];
uint32_t attribute_sequence_len;
struct attribute : public len_prefixed {
uint32_t id;
uint8_t value[0]; // size = (len - 4)
} attribute_sequence[0];
} signed_data;
uint32_t signature_sequence_len;
struct : public len_prefixed {
uint32_t id;
len_prefixed_value signature;
} signature_sequence[0];
len_prefixed_value public_key;
} signer_sequence[0];
};
// End of central directory record
struct EOCD {
uint32_t magic; // 0x6054b50
uint8_t pad[8]; // 8 bytes of irrelevant data
uint32_t central_dir_sz; // size of central directory
uint32_t central_dir_off; // offset of central directory
uint16_t comment_sz; // size of comment
char comment[0];
} __attribute__((packed));
/*
* A v2/v3 signed APK has the format as following
*
* +---------------+
* | zip content |
* +---------------+
* | signing block |
* +---------------+
* | central dir |
* +---------------+
* | EOCD |
* +---------------+
*
* Scan from end of file to find EOCD, and figure our way back to the
* offset of the signing block. Next, directly extract the certificate
* from the v2 signature block.
*
* All structures above are mostly just for documentation purpose.
*
* This method extracts the first certificate of the first signer
* within the APK v2 signature block.
*/
static string read_certificate(int fd) {
uint32_t size4;
uint64_t size8;
// Find EOCD
for (int i = 0;; i++) {
// i is the absolute offset to end of file
uint16_t comment_sz = 0;
lseek(fd, -((off_t) sizeof(comment_sz)) - i, SEEK_END);
[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!
最后于 2023-9-24 11:34
被珍惜Any编辑
,原因: