NDK 应用程序签名检查
NDK application Signature Check
我在应用程序中有一些安全密钥。我想安全地存储它。我喜欢将它存储在本地共享库中(可能由某些代码生成)。之后,我希望通过一种方法返回它,该方法将检查原始 APK 的签名。因此,除了受信任的应用程序之外,没有人可以使用此文件。我知道,ndk 库也可以反编译,但这比 java .class 文件更难对本机代码进行逆向工程。
问题:
- 有没有办法从本机代码 (c/c++) 中提取原始 apk 的签名?
- 如何确保库是从受信任的应用程序调用的?
我将在这里尝试回答您的第一个问题:
您的应用程序的签名存储在您的 APK 的 DEX(Dalvik 可执行文件)文件中。 DEX 文件具有以下结构:
- 页眉
- 数据部分(包含字符串、代码指令、字段等)
- 数组 方法标识符、class 标识符等
所以,这是DEX文件头的开头:
- DEX_FILE_MAGIC 常量 - ubyte[8]
- 您应用程序的 Adler-32 校验和(DEX_FILE_MAGIC 和校验和本身除外)- uint
- 您的应用程序的 SHA-1 签名(DEX_FILE_MAGIC、校验和和哈希本身除外)- ubyte[20]
因此,要计算 apk 的签名,您应该从偏移量 32 开始计算 DEX 文件的 SHA-1 签名。
要从本机代码访问您的 apk 的 DEX 文件,您可以读取存储在 /proc/self/maps:
中的进程内存
FILE *fp;
fp = fopen("/proc/self/maps", "r");
proc/$ID/maps 文件中的每一行具有以下结构:
- 地址
- 权限
- 偏移量
- 设备
- 索引节点
- 路径名
在这里您可以找到对 proc/$ID/maps 文件结构的更好描述:Understanding Linux /proc/id/maps
要检测 DEX 文件在进程内存中的位置,您应该检查 proc/self/maps 文件每一行中的 'pathname' 列。当找到DEX文件对应的行时,应该得到DEX文件区域的起始地址和结束地址:
while (fgets(line, 2048, fp) != NULL) {
// search for '.dex'
if (strstr(line, ".dex") != NULL) {
// get starting and ending addresses of the DEX file region
因此,当您获得 apk 字节码的起始地址和结束地址时,您将能够计算出 apk 的签名。
TL;DR 可以找到示例 here.
我以另一种方式在本机层(C 代码)上获得签名:
- 获取APK的路径;
- 从 APK 中提取
'META-INF/CERT.RSA'
;
- 解析
'META-INF/CERT.RSA'
获取APK路径的代码:
static char *getPackageName() {
const size_t BUFFER_SIZE = 256;
char buffer[BUFFER_SIZE] = "";
int fd = open("/proc/self/cmdline", O_RDONLY);
if (fd > 0) {
ssize_t r = read(fd, buffer, BUFFER_SIZE - 1);
close(fd);
if (r > 0) {
return strdup(buffer);
}
}
return NULL;
}
static const char *getFilenameExt(const char *filename) {
const char *dot = strrchr(filename, '.');
if (!dot || dot == filename) return "";
return dot + 1;
}
char *pathHelperGetPath() {
char *package = getPackageName();
if (NULL == package) {
return NULL;
}
FILE *fp = fopen("/proc/self/maps", "r");
if (NULL == fp) {
free(package);
return NULL;
}
const size_t BUFFER_SIZE = 256;
char buffer[BUFFER_SIZE] = "";
char path[BUFFER_SIZE] = "";
bool find = false;
while (fgets(buffer, BUFFER_SIZE, fp)) {
if (sscanf(buffer, "%*llx-%*llx %*s %*s %*s %*s %s", path) == 1) {
if (strstr(path, package)) {
char *bname = basename(path);
NSV_LOGI("check basename[%s]", bname);
if (strcasecmp(getFilenameExt(bname), "apk") == 0) {
find = true;
break;
}
}
}
}
fclose(fp);
free(package);
if (find) {
return strdup(path);
}
return NULL;
}
为了方便,我们可以使用zlib since 3 API in Android and I use minizip。
下面META-INF/CERT.RSA
的提取代码:
//return MZ_ERROR
static int32_t unzipHelperGetCertFileInfo(void *handle, mz_zip_file **file_info) {
int32_t err = MZ_OK;
err = mz_zip_goto_first_entry(handle);
if (err != MZ_OK && err != MZ_END_OF_LIST) {
NSV_LOGE("Error %d going to first entry in zip file\n", err);
return err;
}
while (err == MZ_OK) {
err = mz_zip_entry_get_info(handle, file_info);
if (err != MZ_OK) {
NSV_LOGE("Error %d getting entry info in zip file\n", err);
*file_info = NULL;
break;
}
if (NULL != (*file_info)->filename && strcasecmp((*file_info)->filename,"META-INF/CERT.RSA") == 0) {
return MZ_OK;
}
err = mz_zip_goto_next_entry(handle);
if (err != MZ_OK && err != MZ_END_OF_LIST) {
*file_info = NULL;
NSV_LOGE("Error %d going to next entry in zip file\n", err);
return err;
}
}
*file_info = NULL;
if (err == MZ_END_OF_LIST) {
return MZ_OK;
}
return err;
}
static void unzipHelperPrintFileInfo(const mz_zip_file *file_info) {
uint32_t ratio = 0;
struct tm tmu_date;
const char *string_method = NULL;
char crypt = ' ';
usleep(500);
NSV_LOGI(" Length Method Size Attribs Ratio Date Time CRC-32 Name\n");
usleep(500);
NSV_LOGI(" ------ ------- ---- ------- ----- ---- ---- ------ ----\n");
ratio = 0;
if (file_info->uncompressed_size > 0)
ratio = (uint32_t)((file_info->compressed_size * 100) / file_info->uncompressed_size);
// Display a '*' if the file is encrypted
if (file_info->flag & MZ_ZIP_FLAG_ENCRYPTED)
crypt = '*';
switch (file_info->compression_method)
{
case MZ_COMPRESS_METHOD_RAW:
string_method = "Stored";
break;
case MZ_COMPRESS_METHOD_DEFLATE:
string_method = "Deflate";
break;
case MZ_COMPRESS_METHOD_BZIP2:
string_method = "BZip2";
break;
case MZ_COMPRESS_METHOD_LZMA:
string_method = "LZMA";
break;
default:
string_method = "Unknown";
}
mz_zip_time_t_to_tm(file_info->modified_date, &tmu_date);
usleep(500);
NSV_LOGI(" %7"PRIu64" %6s%c %7"PRIu64" %8"PRIx32" %3"PRIu32"%% %2.2"PRIu32"-%2.2"PRIu32\
"-%2.2"PRIu32" %2.2"PRIu32":%2.2"PRIu32" %8.8"PRIx32" %s\n",
file_info->uncompressed_size, string_method, crypt,
file_info->compressed_size, file_info->external_fa, ratio,
(uint32_t)tmu_date.tm_mon + 1, (uint32_t)tmu_date.tm_mday,
(uint32_t)tmu_date.tm_year % 100,
(uint32_t)tmu_date.tm_hour, (uint32_t)tmu_date.tm_min,
file_info->crc, file_info->filename);
}
unsigned char *unzipHelperGetCertificateDetails(const char *fullApkPath, size_t *len) {
unsigned char *result = NULL;
int32_t err = 0;
int32_t read_file = 0;
void *handle = NULL;
void *file_stream = NULL;
void *split_stream = NULL;
void *buf_stream = NULL;
char *password = NULL;
int64_t disk_size = 0;
int16_t mode = MZ_OPEN_MODE_READ;
int32_t err_close = 0;
if (mz_os_file_exists(fullApkPath) != MZ_OK) {
NSV_LOGE("file %s doesn't exit\n", fullApkPath);
}
mz_stream_os_create(&file_stream);
mz_stream_buffered_create(&buf_stream);
mz_stream_split_create(&split_stream);
mz_stream_set_base(split_stream, file_stream);
mz_stream_split_set_prop_int64(split_stream, MZ_STREAM_PROP_DISK_SIZE, disk_size);
err = mz_stream_open(split_stream, fullApkPath, mode);
mz_zip_file *file_info = NULL;
if (err != MZ_OK) {
NSV_LOGE("Error opening file %s\n", fullApkPath);
} else {
handle = mz_zip_open(split_stream, mode);
if (handle == NULL) {
NSV_LOGE("Error opening zip %s\n", fullApkPath);
err = MZ_FORMAT_ERROR;
} else {
err = unzipHelperGetCertFileInfo(handle, &file_info);
if (err == MZ_OK && NULL != file_info) {
unzipHelperPrintFileInfo(file_info);
//unzip
err = mz_zip_entry_read_open(handle, 0, password);
if (err != MZ_OK) {
NSV_LOGW("Error %d opening entry in zip file\n", err);
} else {
result = calloc(file_info->uncompressed_size, sizeof(unsigned char));
if (NULL != result) {
read_file = mz_zip_entry_read(handle, result,
(uint32_t) (file_info->uncompressed_size));
if (read_file < 0) {
free(result);
result = NULL;
err = read_file;
NSV_LOGW("Error %d reading entry in zip file\n", err);
} else {
NSV_LOGI("read %d from zip file\n", read_file);
*len = (size_t) read_file;
}
}
}
}
}
err_close = mz_zip_close(handle);
if (err_close != MZ_OK) {
NSV_LOGE("Error in closing %s (%d)\n", fullApkPath, err_close);
err = err_close;
}
mz_stream_close(split_stream);
}
mz_stream_split_delete(&split_stream);
mz_stream_buffered_delete(&buf_stream);
mz_stream_os_delete(&file_stream);
return result;
}
为了解析 META-INF/CERT.RSA
,我使用了来自 one public 存储库 的部分代码。它太大了,无法在 Whosebug 上发布,因此可以找到工作示例的完整源代码 here。
更新:
这是一个我们如何从签名中获取 MD5 的示例(使用 mbed TLS):
size_t len_in = 0;
size_t len_out = 0;
content = unzipHelperGetCertificateDetails(path, &len_in);
LOGI("unzipHelperGetCertificateDetails finishes\n");
if (!content) {
return;
}
LOGI("pkcs7HelperGetSignature starts\n");
unsigned char *res = pkcs7HelperGetSignature(content, len_in, &len_out);
LOGI("pkcs7HelperGetSignature finishes, len_out:[%zu]\n", len_out);
if (NULL == res) {
return;
}
LOGI("calculating md5\n");
unsigned char md5sum[16] = {""};
mbedtls_md5((unsigned const char *) res, len_out, md5sum);
char md5string[33];
for (int i = 0; i < 16; ++i) {
sprintf(&md5string[i * 2], "%02x", (unsigned int) md5sum[i]);
}
LOGI("md5:[%s]\n", md5string);
我在应用程序中有一些安全密钥。我想安全地存储它。我喜欢将它存储在本地共享库中(可能由某些代码生成)。之后,我希望通过一种方法返回它,该方法将检查原始 APK 的签名。因此,除了受信任的应用程序之外,没有人可以使用此文件。我知道,ndk 库也可以反编译,但这比 java .class 文件更难对本机代码进行逆向工程。
问题:
- 有没有办法从本机代码 (c/c++) 中提取原始 apk 的签名?
- 如何确保库是从受信任的应用程序调用的?
我将在这里尝试回答您的第一个问题:
您的应用程序的签名存储在您的 APK 的 DEX(Dalvik 可执行文件)文件中。 DEX 文件具有以下结构:
- 页眉
- 数据部分(包含字符串、代码指令、字段等)
- 数组 方法标识符、class 标识符等
所以,这是DEX文件头的开头:
- DEX_FILE_MAGIC 常量 - ubyte[8]
- 您应用程序的 Adler-32 校验和(DEX_FILE_MAGIC 和校验和本身除外)- uint
- 您的应用程序的 SHA-1 签名(DEX_FILE_MAGIC、校验和和哈希本身除外)- ubyte[20]
因此,要计算 apk 的签名,您应该从偏移量 32 开始计算 DEX 文件的 SHA-1 签名。
要从本机代码访问您的 apk 的 DEX 文件,您可以读取存储在 /proc/self/maps:
中的进程内存FILE *fp;
fp = fopen("/proc/self/maps", "r");
proc/$ID/maps 文件中的每一行具有以下结构:
- 地址
- 权限
- 偏移量
- 设备
- 索引节点
- 路径名
在这里您可以找到对 proc/$ID/maps 文件结构的更好描述:Understanding Linux /proc/id/maps
要检测 DEX 文件在进程内存中的位置,您应该检查 proc/self/maps 文件每一行中的 'pathname' 列。当找到DEX文件对应的行时,应该得到DEX文件区域的起始地址和结束地址:
while (fgets(line, 2048, fp) != NULL) {
// search for '.dex'
if (strstr(line, ".dex") != NULL) {
// get starting and ending addresses of the DEX file region
因此,当您获得 apk 字节码的起始地址和结束地址时,您将能够计算出 apk 的签名。
TL;DR 可以找到示例 here.
我以另一种方式在本机层(C 代码)上获得签名:
- 获取APK的路径;
- 从 APK 中提取
'META-INF/CERT.RSA'
; - 解析
'META-INF/CERT.RSA'
获取APK路径的代码:
static char *getPackageName() {
const size_t BUFFER_SIZE = 256;
char buffer[BUFFER_SIZE] = "";
int fd = open("/proc/self/cmdline", O_RDONLY);
if (fd > 0) {
ssize_t r = read(fd, buffer, BUFFER_SIZE - 1);
close(fd);
if (r > 0) {
return strdup(buffer);
}
}
return NULL;
}
static const char *getFilenameExt(const char *filename) {
const char *dot = strrchr(filename, '.');
if (!dot || dot == filename) return "";
return dot + 1;
}
char *pathHelperGetPath() {
char *package = getPackageName();
if (NULL == package) {
return NULL;
}
FILE *fp = fopen("/proc/self/maps", "r");
if (NULL == fp) {
free(package);
return NULL;
}
const size_t BUFFER_SIZE = 256;
char buffer[BUFFER_SIZE] = "";
char path[BUFFER_SIZE] = "";
bool find = false;
while (fgets(buffer, BUFFER_SIZE, fp)) {
if (sscanf(buffer, "%*llx-%*llx %*s %*s %*s %*s %s", path) == 1) {
if (strstr(path, package)) {
char *bname = basename(path);
NSV_LOGI("check basename[%s]", bname);
if (strcasecmp(getFilenameExt(bname), "apk") == 0) {
find = true;
break;
}
}
}
}
fclose(fp);
free(package);
if (find) {
return strdup(path);
}
return NULL;
}
为了方便,我们可以使用zlib since 3 API in Android and I use minizip。
下面META-INF/CERT.RSA
的提取代码:
//return MZ_ERROR
static int32_t unzipHelperGetCertFileInfo(void *handle, mz_zip_file **file_info) {
int32_t err = MZ_OK;
err = mz_zip_goto_first_entry(handle);
if (err != MZ_OK && err != MZ_END_OF_LIST) {
NSV_LOGE("Error %d going to first entry in zip file\n", err);
return err;
}
while (err == MZ_OK) {
err = mz_zip_entry_get_info(handle, file_info);
if (err != MZ_OK) {
NSV_LOGE("Error %d getting entry info in zip file\n", err);
*file_info = NULL;
break;
}
if (NULL != (*file_info)->filename && strcasecmp((*file_info)->filename,"META-INF/CERT.RSA") == 0) {
return MZ_OK;
}
err = mz_zip_goto_next_entry(handle);
if (err != MZ_OK && err != MZ_END_OF_LIST) {
*file_info = NULL;
NSV_LOGE("Error %d going to next entry in zip file\n", err);
return err;
}
}
*file_info = NULL;
if (err == MZ_END_OF_LIST) {
return MZ_OK;
}
return err;
}
static void unzipHelperPrintFileInfo(const mz_zip_file *file_info) {
uint32_t ratio = 0;
struct tm tmu_date;
const char *string_method = NULL;
char crypt = ' ';
usleep(500);
NSV_LOGI(" Length Method Size Attribs Ratio Date Time CRC-32 Name\n");
usleep(500);
NSV_LOGI(" ------ ------- ---- ------- ----- ---- ---- ------ ----\n");
ratio = 0;
if (file_info->uncompressed_size > 0)
ratio = (uint32_t)((file_info->compressed_size * 100) / file_info->uncompressed_size);
// Display a '*' if the file is encrypted
if (file_info->flag & MZ_ZIP_FLAG_ENCRYPTED)
crypt = '*';
switch (file_info->compression_method)
{
case MZ_COMPRESS_METHOD_RAW:
string_method = "Stored";
break;
case MZ_COMPRESS_METHOD_DEFLATE:
string_method = "Deflate";
break;
case MZ_COMPRESS_METHOD_BZIP2:
string_method = "BZip2";
break;
case MZ_COMPRESS_METHOD_LZMA:
string_method = "LZMA";
break;
default:
string_method = "Unknown";
}
mz_zip_time_t_to_tm(file_info->modified_date, &tmu_date);
usleep(500);
NSV_LOGI(" %7"PRIu64" %6s%c %7"PRIu64" %8"PRIx32" %3"PRIu32"%% %2.2"PRIu32"-%2.2"PRIu32\
"-%2.2"PRIu32" %2.2"PRIu32":%2.2"PRIu32" %8.8"PRIx32" %s\n",
file_info->uncompressed_size, string_method, crypt,
file_info->compressed_size, file_info->external_fa, ratio,
(uint32_t)tmu_date.tm_mon + 1, (uint32_t)tmu_date.tm_mday,
(uint32_t)tmu_date.tm_year % 100,
(uint32_t)tmu_date.tm_hour, (uint32_t)tmu_date.tm_min,
file_info->crc, file_info->filename);
}
unsigned char *unzipHelperGetCertificateDetails(const char *fullApkPath, size_t *len) {
unsigned char *result = NULL;
int32_t err = 0;
int32_t read_file = 0;
void *handle = NULL;
void *file_stream = NULL;
void *split_stream = NULL;
void *buf_stream = NULL;
char *password = NULL;
int64_t disk_size = 0;
int16_t mode = MZ_OPEN_MODE_READ;
int32_t err_close = 0;
if (mz_os_file_exists(fullApkPath) != MZ_OK) {
NSV_LOGE("file %s doesn't exit\n", fullApkPath);
}
mz_stream_os_create(&file_stream);
mz_stream_buffered_create(&buf_stream);
mz_stream_split_create(&split_stream);
mz_stream_set_base(split_stream, file_stream);
mz_stream_split_set_prop_int64(split_stream, MZ_STREAM_PROP_DISK_SIZE, disk_size);
err = mz_stream_open(split_stream, fullApkPath, mode);
mz_zip_file *file_info = NULL;
if (err != MZ_OK) {
NSV_LOGE("Error opening file %s\n", fullApkPath);
} else {
handle = mz_zip_open(split_stream, mode);
if (handle == NULL) {
NSV_LOGE("Error opening zip %s\n", fullApkPath);
err = MZ_FORMAT_ERROR;
} else {
err = unzipHelperGetCertFileInfo(handle, &file_info);
if (err == MZ_OK && NULL != file_info) {
unzipHelperPrintFileInfo(file_info);
//unzip
err = mz_zip_entry_read_open(handle, 0, password);
if (err != MZ_OK) {
NSV_LOGW("Error %d opening entry in zip file\n", err);
} else {
result = calloc(file_info->uncompressed_size, sizeof(unsigned char));
if (NULL != result) {
read_file = mz_zip_entry_read(handle, result,
(uint32_t) (file_info->uncompressed_size));
if (read_file < 0) {
free(result);
result = NULL;
err = read_file;
NSV_LOGW("Error %d reading entry in zip file\n", err);
} else {
NSV_LOGI("read %d from zip file\n", read_file);
*len = (size_t) read_file;
}
}
}
}
}
err_close = mz_zip_close(handle);
if (err_close != MZ_OK) {
NSV_LOGE("Error in closing %s (%d)\n", fullApkPath, err_close);
err = err_close;
}
mz_stream_close(split_stream);
}
mz_stream_split_delete(&split_stream);
mz_stream_buffered_delete(&buf_stream);
mz_stream_os_delete(&file_stream);
return result;
}
为了解析 META-INF/CERT.RSA
,我使用了来自 one public 存储库 的部分代码。它太大了,无法在 Whosebug 上发布,因此可以找到工作示例的完整源代码 here。
更新:
这是一个我们如何从签名中获取 MD5 的示例(使用 mbed TLS):
size_t len_in = 0;
size_t len_out = 0;
content = unzipHelperGetCertificateDetails(path, &len_in);
LOGI("unzipHelperGetCertificateDetails finishes\n");
if (!content) {
return;
}
LOGI("pkcs7HelperGetSignature starts\n");
unsigned char *res = pkcs7HelperGetSignature(content, len_in, &len_out);
LOGI("pkcs7HelperGetSignature finishes, len_out:[%zu]\n", len_out);
if (NULL == res) {
return;
}
LOGI("calculating md5\n");
unsigned char md5sum[16] = {""};
mbedtls_md5((unsigned const char *) res, len_out, md5sum);
char md5string[33];
for (int i = 0; i < 16; ++i) {
sprintf(&md5string[i * 2], "%02x", (unsigned int) md5sum[i]);
}
LOGI("md5:[%s]\n", md5string);