Android apk很容易經過逆向工程進行反編譯,從而是其代碼徹底暴露給攻擊者,使apk面臨破解,軟件邏輯修改,插入惡意代碼,替換廣告商ID等風險。咱們能夠採用如下方法對apk進行保護.html
混淆是一種用來隱藏程序意圖的技術,能夠增長代碼閱讀的難度,使攻擊者難以全面掌控app內部實現邏輯,從而增長逆向工程和破解的難度,防止知識產權被竊取。java
代碼混淆技術主要作了以下的工做:linux
已經有不少第三方的軟件能夠用來混淆咱們的Android應用,常見的有:android
這些混淆器在代碼中起做用的層次是不同的。Android編譯的大體流程以下:c++
1
|
Java Code(.java) -> Java Bytecode(.class) -> Dalvik Bytecode(classes.dex)
|
有的混淆器是在編譯以前直接做用於java源代碼,有的做用於java字節碼,有的做用於Dalvik字節碼。但基本都是針對java層做混淆。git
相對於Dalvik虛擬機層次的混淆而言,原生語言(C/C++)的代碼混淆選擇並很少,Obfuscator-LLVM工程是一個值得關注的例外。github
代碼混淆的優勢是使代碼可閱讀性變差,要全面掌控代碼邏輯難度變大;能夠壓縮代碼,使得代碼大小變小。但也存在以下缺點:算法
也就是說,代碼混淆並不能有效的保護應用自身。安全
http://www.jianshu.com/p/0c23e0a886f4bash
每個軟件在發佈時都須要開發人員對其進行簽名,而簽名使用的密鑰文件時開發人員所獨有的,破解者一般不可能擁有相同的密鑰文件,所以可使用簽名校驗的方法保護apk。Android SDK中PackageManager類的getPackageInfo()方法就能夠進行軟件簽名檢測。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public
class
getSign {
public
static
int
getSignature(PackageManager pm , String packageName){
PackageInfo pi =
null
;
int
sig =
0
;
Signature[]s =
null
;
try
{
pi = pm.getPackageInfo(packageName, PackageManager.GET_SIGNATURES);
s = pi.signatures;
sig = s[
0
].hashCode();
//s[0]是簽名證書的公鑰,此處獲取hashcode方便對比
}
catch
(Exception e){
handleException();
}
return
sig;
}
}
|
主程序代碼參考:
1
2
3
4
5
|
pm =
this
.getPackageManager();
int
s = getSign.getSignature(pm,
"com.hik.getsinature"
);
if
(s != ORIGNAL_SGIN_HASHCODE){
//對比當前和預埋簽名的hashcode是否一致
System.exit(
1
);
//不一致則強制程序退出
}
|
重編譯apk其實就是重編譯了classes.dex文件,重編譯後,生成的classes.dex文件的hash值就改變了,所以咱們能夠經過檢測安裝後classes.dex文件的hash值來判斷apk是否被重打包過。
/data/app/xxx.apk
中的classes.dex文件並計算其哈希值,將該值與軟件發佈時的classes.dex哈希值作比較來判斷客戶端是否被篡改。/data/app/xxx.apk
中的META-INF目錄下的MANIFEST.MF文件,該文件詳細記錄了apk包中全部文件的哈希值,所以能夠讀取該文件獲取到classes.dex文件對應的哈希值,將該值與軟件發佈時的classes.dex哈希值作比較就能夠判斷客戶端是否被篡改。爲了防止被破解,軟件發佈時的classes.dex哈希值應該存放在服務器端。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
private
boolean
checkcrc(){
boolean
checkResult =
false
;
long
crc = Long.parseLong(getString(R.string.crc));
//獲取字符資源中預埋的crc值
ZipFile zf;
try
{
String path = getApplicationContext().getPackageCodePath();
//獲取apk安裝路徑
zf =
new
ZipFile(path);
//將apk封裝成zip對象
ZipEntry ze = zf.getEntry(
"classes.dex"
);
//獲取apk中的classes.dex
long
CurrentCRC = ze.getCrc();
//計算當前應用classes.dex的crc值
if
(CurrentCRC != crc){
//crc值對比
checkResult =
true
;
}
}
catch
(IOException e){
handleError();
checkResult =
false
;
}
return
checkResult;
}
|
另外因爲逆向c/c++代碼要比逆向Java代碼困難不少,因此關鍵代碼部位應該使用Native C/C++來編寫。
Android so經過C/C++代碼來實現,相對於Java代碼來講其反編譯難度要大不少,但對於經驗豐富的破解者來講,仍然是很容易的事。應用的關鍵性功能或算法,都會在so中實現,若是so被逆向,應用的關鍵性代碼和算法都將會暴露。對於so的保護,能夠纔有編譯器優化技術、剝離二進制文件等方式,還可使用開源的so加固殼upx進行加固。
編譯器優化技術
爲了隱藏核心的算法或者其它複雜的邏輯,使用編譯優化技術能夠幫助混淆目標代碼,使它不會很容易的被攻擊者反編譯,從而讓攻擊者對特定代碼的理解變得更加困難。如使用LLVM混淆。
剝離二進制文件
剝離本地二進制文件是一種有效的方式,使攻擊者須要更多的時間和更高的技能水平來查看你的應用程序底層功能的實現。剝離二進制文件,就是將二進制文件的符號表刪除,使攻擊者沒法輕易調試或逆向應用。在Android上可使用GNU/Linux系統上已經使用過的技術,如sstriping或者UPX。
UPX對文件進行加殼時會把軟件版本等相關信息寫入殼內,攻擊者能夠經過靜態反彙編可查看到這些殼信息,進而尋找對應的脫殼機進行脫殼,使得攻擊難度下降。因此咱們必須在UPX源碼中刪除這些信息,從新編譯後再進行加殼,步驟以下:
若是資源文件沒有保護,則會使應用存在兩方面的安全風險:
能夠考慮將其做爲一個二進制形式進行加密存儲,而後加載,解密成字節流並把它傳遞到BitmapFactory。固然,這會增長代碼的複雜度,而且形成輕微的性能影響。
不過資源文件是全局可讀的,即便不打包在apk中,而是在首次運行時下載或者須要使用時下載,不在設備中保存,可是經過網絡數據包嗅探仍是很容易獲取到資源url地址。
應用程序能夠經過使用特定的系統API來防止調試器附加到該進程。經過阻止調試器鏈接,攻擊者干擾底層運行時的能力是有限的。攻擊者爲了從底層攻擊應用程序必須首先繞過調試限制。這進一步增長了攻擊複雜性。Android應用程序應該在manifest中設置Android:debuggable=「false」
,這樣就不會很容易在運行時被攻擊者或者惡意軟件操縱。
應用程序能夠檢測本身是否正在被調試器或其餘調試工具跟蹤。若是被追蹤,應用程序能夠執行任意數量的可能攻擊響應行爲,如丟棄加密密鑰來保護用戶數據,通知服務器管理員,或者其它類型自我保護的響應。這能夠由檢查進程狀態標誌或者使用其它技術,如比較ptrace附加的返回值,檢查父進程,黑名單調試器進程列表或經過計算運行時間的差別來反調試。
a.父進程檢測
一般,咱們在使用gdb調試時,是經過gdb 這種方式進行的。而這種方式是啓動gdb,fork出子進程後執行目標二進制文件。所以,二進制文件的父進程即爲調試器。咱們可經過檢查父進程名稱來判斷是不是由調試器fork。示例代碼以下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
#include <stdio.h>
#include <string.h>
int
main(
int
argc,
char
*argv[]) {
char
buf0[32], buf1[128];
FILE
* fin;
snprintf(buf0, 24,
"/proc/%d/cmdline"
, getppid());
fin =
fopen
(buf0,
"r"
);
fgets
(buf1, 128, fin);
fclose
(fin);
if
(!
strcmp
(buf1,
"gdb"
)) {
printf
(
"Debugger detected"
);
return
1;
}
printf
(
"All good"
);
return
0;
}
|
這裏咱們經過getppid得到父進程的PID,以後由/proc文件系統獲取父進程的命令內容,並經過比較字符串檢查父進程是否爲gdb。實際運行結果以下圖所示:
b.當前運行進程檢測
例如對android_server
進程檢測。針對這種檢測只需將android_server
更名就可繞過
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
pid_t GetPidByName(
const
charchar *as_name) {
DIR *pdir = NULL;
struct dirent *pde = NULL;
FILEFILE *pf = NULL;
char
buff[
128
];
pid_t pid;
char
szName[
128
];
// 遍歷/proc目錄下全部pid目錄
pdir = opendir(
"/proc"
);
if
(!pdir) {
perror(
"open /proc fail.\n"
);
return
-
1
;
}
while
((pde = readdir(pdir))) {
if
((pde->d_name[
0
] <
'0'
) || (pde->d_name[
0
] >
'9'
)) {
continue
;
}
sprintf(buff,
"/proc/%s/status"
, pde->d_name);
pf = fopen(buff,
"r"
);
if
(pf) {
fgets(buff, sizeof(buff), pf);
fclose(pf);
sscanf(buff,
"%*s %s"
, szName);
pid = atoi(pde->d_name);
if
(strcmp(szName, as_name) ==
0
) {
closedir(pdir);
return
pid;
}
}
}
closedir(pdir);
return
0
;
}
|
c.讀取進程狀態(/proc/pid/status)
State屬性值T 表示調試狀態,TracerPid 屬性值正在調試此進程的pid,在非調試狀況下State爲S或R, TracerPid等於0
由此,咱們即可經過檢查status文件中TracerPid的值來判斷是否有正在被調試。示例代碼以下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
#include <stdio.h>
#include <string.h>
int
main(
int
argc,
char
*argv[]) {
int
i;
scanf
(
"%d"
, &i);
char
buf1[512];
FILE
* fin;
fin =
fopen
(
"/proc/self/status"
,
"r"
);
int
tpid;
const
char
*needle =
"TracerPid:"
;
size_t
nl =
strlen
(needle);
while
(
fgets
(buf1, 512, fin)) {
if
(!
strncmp
(buf1, needle, nl)) {
sscanf
(buf1,
"TracerPid: %d"
, &tpid);
if
(tpid != 0) {
printf
(
"Debuggerdetected"
);
return
1;
}
}
}
fclose
(fin);
printf
(
"All good"
);
return
0;
}
|
實際運行結果以下圖所示:
值得注意的是,/proc目錄下包含了進程的大量信息。咱們在這裏是讀取status文件,此外,也可經過/proc/self/stat文件來得到進程相關信息,包括運行狀態。
d.讀取/proc/%d/wchan
下圖中第一個紅色框值爲非調試狀態值,第二個紅色框值爲調試狀態:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
static
int
getWchanStatus(
int
pid)
{
FILEFILE *fp= NULL;
char
filename;
char
wchaninfo = {0};
int
result = WCHAN_ELSE;
char
cmd = {0};
sprintf
(cmd,
"cat /proc/%d/wchan"
,pid);
LOGANTI(
"cmd= %s"
,cmd);
FILEFILE *ptr;
if
((ptr=popen(cmd,
"r"
)) != NULL)
{
if
(
fgets
(wchaninfo, 128, ptr) != NULL)
{
LOGANTI(
"wchaninfo= %s"
,wchaninfo);
}
}
if
(strncasecmp(wchaninfo,
"sys_epoll\0"
,
strlen
(
"sys_epoll\0"
)) == 0)
result = WCHAN_RUNNING;
else
if
(strncasecmp(wchaninfo,
"ptrace_stop\0"
,
strlen
(
"ptrace_stop\0"
)) == 0)
result = WCHAN_TRACING;
return
result;
}
|
e.ptrace 自身或者fork子進程相互ptrace
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
if
(ptrace(PTRACE_TRACEME, 0, 1, 0) < 0) {
printf
(
"DEBUGGING... Bye\n"
);
return
1;
}
void
anti_ptrace(
void
)
{
pid_t child;
child = fork();
if
(child)
wait(NULL);
else
{
pid_t parent = getppid();
if
(ptrace(PTRACE_ATTACH, parent, 0, 0) < 0)
while
(1);
sleep(1);
ptrace(PTRACE_DETACH, parent, 0, 0);
exit
(0);
}
}
|
f.設置程序運行最大時間
這種方法常常在CTF比賽中看到。因爲程序在調試時的斷點、檢查修改內存等操做,運行時間每每要遠大於正常運行時間。因此,一旦程序運行時間過長,即可能是因爲正在被調試。
具體地,在程序啓動時,經過alarm設置定時,到達時則停止程序。示例代碼以下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
void
alarmHandler(
int
sig) {
printf
(
"Debugger detected"
);
exit
(1);
}
void__attribute__((constructor))setupSig(
void
) {
signal
(SIGALRM, alarmHandler);
alarm(2);
}
int
main(
int
argc,
char
*argv[]) {
printf
(
"All good"
);
return
0;
}
|
在此例中,咱們經過__attribute__((constructor))
,在程序啓動時便設置好定時。實際運行中,當咱們使用gdb在main函數下斷點,稍候片刻後繼續執行時,則觸發了SIGALRM,進而檢測到調試器。以下圖所示:
順便一提,這種方式能夠輕易地被繞過。咱們能夠設置gdb對signal的處理方式,若是咱們選擇將SIGALRM忽略而非傳遞給程序,則alarmHandler便不會被執行,以下圖所示:
g.檢查進程打開的filedescriptor
如2.2中所說,若是被調試的進程是經過gdb 的方式啓動,那麼它即是由gdb進程fork獲得的。而fork在調用時,父進程所擁有的fd(file descriptor)會被子進程繼承。因爲gdb在每每會打開多個fd,所以若是進程擁有的fd較多,則多是繼承自gdb的,即進程在被調試。
具體地,進程擁有的fd會在/proc/self/fd/下列出。因而咱們的示例代碼以下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
#include <stdio.h>
#include <dirent.h>
int
main(
int
argc,
char
*argv[]) {
struct
dirent *dir;
DIR *d = opendir(
"/proc/self/fd"
);
while
(dir=readdir(d)) {
if
(!
strcmp
(dir->d_name,
"5"
)) {
printf
(
"Debugger detected"
);
return
1;
}
}
closedir(d);
printf
(
"All good"
);
return
0;
}
|
這裏,咱們檢查/proc/self/fd/中是否包含fd爲5。因爲fd從0開始編號,因此fd爲5則說明已經打開了6個文件。若是程序正常運行則不會打開這麼多,因此由此來判斷是否被調試。運行結果見下圖:
h.防止dump
利用Inotify機制,對/proc/pid/mem和/proc/pid/pagemap文件進行監視。inotify API提供了監視文件系統的事件機制,可用於監視個體文件,或者監控目錄。具體原理可參考:http://man7.org/linux/man-pages/man7/inotify.7.html
僞代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
|
void
__fastcall anitInotify(
int
flag)
{
MemorPagemap = flag;
charchar *pagemap =
"/proc/%d/pagemap"
;
charchar *mem =
"/proc/%d/mem"
;
pagemap_addr = (charchar *)
malloc
(0x100u);
mem_addr = (charchar *)
malloc
(0x100u);
ret =
sprintf
(pagemap_addr, &pagemap, pid_);
ret =
sprintf
(mem_addr, &mem, pid_);
if
( !MemorPagemap )
{
ret = pthread_create(&th, 0, (voidvoid *(*)(voidvoid *)) inotity_func, mem_addr);
if
( ret >= 0 )
ret = pthread_detach(th);
}
if
( MemorPagemap == 1 )
{
ret = pthread_create(&newthread, 0, (voidvoid *(*)(voidvoid *)) inotity_func, pagemap_addr);
if
(ret > 0)
ret = pthread_detach(th);
}
}
void
__fastcall __noreturn inotity_func(
const
charchar *inotity_file)
{
const
charchar *name;
// r4@1
signed
int
fd;
// r8@1
bool
flag;
// zf@3
bool
ret;
// nf@3
ssize_t length;
// r10@3
ssize_t i;
// r9@7
fd_set readfds;
// @2
char
event;
// @1
name = inotity_file;
memset
(buffer, 0, 0x400u);
fd = inotify_init();
inotify_add_watch(fd, name, 0xFFFu);
while
( 1 )
{
do
{
memset
(&readfds, 0, 0x80u);
}
while
( select(fd + 1, &readfds, 0, 0, 0) <= 0 );
length = read(fd, event, 0x400u);
flag = length == 0;
ret = length < 0;
if
( length >= 0 )
{
if
( !ret && !flag )
{
i = 0;
do
{
inotity_kill((
int
)&event);
i += *(_DWORD *)&event + 16;
}
while
( length > i );
}
}
else
{
while
( *(_DWORD *)_errno() == 4 )
{
length = read(fd, buffer, 0x400u);
flag = length == 0;
ret = length < 0;
if
( length >= 0 )
}
}
}
}
|
i.對read作hook
由於通常的內存dump都會調用到read函數,因此對read作內存hook,檢測read數據是否在本身須要保護的空間來阻止dump
j.設置單步調試陷阱
1
2
3
4
5
6
7
8
9
10
11
|
int
handler()
{
return
bsd_signal(5, 0);
}
int
set_SIGTRAP()
{
int
result;
bsd_signal(5, (
int
)handler);
result =
raise
(5);
return
result;
}
|
http://www.freebuf.com/tools/83509.html
移動應用加固技術從產生到如今,一共經歷了三代:
第一代加固技術:類加載器
以梆梆加固爲例,類加載器主要作了以下工做:
使用一代加固技術之後的apk加載流程發生了變化以下:
應用啓動之後,會首先啓動保護代碼,保護代碼會啓動反調試、完整性檢測等機制,以後再加載真實的代碼。
一代加固技術的優點在於:能夠完整的保護APK,支持反調試、完整性校驗等。
一代加固技術的缺點是加固前的classes.dex文件會被完整的導入到內存中,能夠用內存dump工具直接導出未加固的classes.dex文件。
第二代加固技術:類方法替換
第二代加固技術採用了類方法替換的技術:
採用本技術的優點爲:
使用二代加固技術之後,啓動流程增長了一個解析函數代碼的過程,以下圖所示:
第三代加固技術:虛擬機指令集
第三代加固技術是基於虛擬機執行引擎替換方式,所作主要工做以下:
三代技術的優勢以下: