Android apk很容易通過逆向工程進行反編譯,從而是其代碼完全暴露給攻擊者,使apk面臨破解,軟件邏輯修改,插入惡意代碼,替換廣告商ID等風險。我們可以采用以下方法對apk進行保護.
混淆是一種用來隱藏程序意圖的技術,可以增加代碼閱讀的難度,使攻擊者難以全面掌控app內部實現邏輯,從而增加逆向工程和破解的難度,防止知識產權被竊取。
代碼混淆技術主要做了如下的工作:
已經有很多第三方的軟件可以用來混淆我們的Android應用,常見的有:
這些混淆器在代碼中起作用的層次是不一樣的。Android編譯的大致流程如下:
#!bash
Java Code(.java) -> Java Bytecode(.class) -> Dalvik Bytecode(classes.dex)
有的混淆器是在編譯之前直接作用于java源代碼,有的作用于java字節碼,有的作用于Dalvik字節碼。但基本都是針對java層作混淆。
相對于Dalvik虛擬機層次的混淆而言,原生語言(C/C++)的代碼混淆選擇并不多,Obfuscator-LLVM工程是一個值得關注的例外。
代碼混淆的優點是使代碼可閱讀性變差,要全面掌控代碼邏輯難度變大;可以壓縮代碼,使得代碼大小變小。但也存在如下缺點:
也就是說,代碼混淆并不能有效的保護應用自身。
http://www.jianshu.com/p/0c23e0a886f4
每一個軟件在發布時都需要開發人員對其進行簽名,而簽名使用的密鑰文件時開發人員所獨有的,破解者通常不可能擁有相同的密鑰文件,因此可以使用簽名校驗的方法保護apk。Android SDK中PackageManager類的getPackageInfo()方法就可以進行軟件簽名檢測。
#!java
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;
}
}
主程序代碼參考:
#!java
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哈希值應該存放在服務器端。
#!java
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
#!cpp
#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
改名就可繞過
#!java
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的值來判斷是否有正在被調試。示例代碼如下:
#!cpp
#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
下圖中第一個紅色框值為非調試狀態值,第二個紅色框值為調試狀態:
#!cpp
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
#!cpp
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設置定時,到達時則中止程序。示例代碼如下:
#!cpp
#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
具體地,進程擁有的fd會在/proc/self/fd/下列出。于是我們的示例代碼如下:
#!cpp
#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
偽代碼:
#!cpp
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;[email protected]
??????signed?int?fd;[email protected]
??????bool?flag;[email protected]
??????bool?ret;[email protected]
??????ssize_t?length;[email protected]
??????ssize_t?i;[email protected]
??????fd_set?readfds;[email protected]
??????char?event;[email protected]
??????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.設置單步調試陷阱
#!cpp
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文件。
第二代加固技術:類方法替換
第二代加固技術采用了類方法替換的技術:
采用本技術的優勢為:
使用二代加固技術以后,啟動流程增加了一個解析函數代碼的過程,如下圖所示:
第三代加固技術:虛擬機指令集
第三代加固技術是基于虛擬機執行引擎替換方式,所做主要工作如下:
三代技術的優點如下: