Android應用安全開發之源碼安全

Android應用安全開發之源碼安全

 

0x00 簡介


Android apk很容易經過逆向工程進行反編譯,從而是其代碼徹底暴露給攻擊者,使apk面臨破解,軟件邏輯修改,插入惡意代碼,替換廣告商ID等風險。咱們能夠採用如下方法對apk進行保護.html

0x01 混淆保護


混淆是一種用來隱藏程序意圖的技術,能夠增長代碼閱讀的難度,使攻擊者難以全面掌控app內部實現邏輯,從而增長逆向工程和破解的難度,防止知識產權被竊取。java

代碼混淆技術主要作了以下的工做:linux

  1. 經過對代碼類名,函數名作替換來實現代碼混淆保護
  2. 簡單的邏輯分支混淆

已經有不少第三方的軟件能夠用來混淆咱們的Android應用,常見的有:android

  • Proguard
  • DashO
  • Dexguard
  • DexProtector
  • ApkProtect
  • Shield4j
  • Stringer
  • Allitori

這些混淆器在代碼中起做用的層次是不同的。Android編譯的大體流程以下:c++

1
Java Code(.java) -> Java Bytecode(.class) -> Dalvik Bytecode(classes.dex)

有的混淆器是在編譯以前直接做用於java源代碼,有的做用於java字節碼,有的做用於Dalvik字節碼。但基本都是針對java層做混淆。git

相對於Dalvik虛擬機層次的混淆而言,原生語言(C/C++)的代碼混淆選擇並很少,Obfuscator-LLVM工程是一個值得關注的例外。github

代碼混淆的優勢是使代碼可閱讀性變差,要全面掌控代碼邏輯難度變大;能夠壓縮代碼,使得代碼大小變小。但也存在以下缺點:算法

  1. 沒法真正保護代碼不被反編譯;
  2. 在應對動態調試逆向分析上無效;
  3. 經過驗證本地簽名的機制很容易被繞過。

也就是說,代碼混淆並不能有效的保護應用自身。安全

http://www.jianshu.com/p/0c23e0a886f4bash

0x02 二次打包防禦


2.1 Apk簽名校驗

每個軟件在發佈時都須要開發人員對其進行簽名,而簽名使用的密鑰文件時開發人員所獨有的,破解者一般不可能擁有相同的密鑰文件,所以可使用簽名校驗的方法保護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 ); //不一致則強制程序退出
}

2.2 Dex文件校驗

重編譯apk其實就是重編譯了classes.dex文件,重編譯後,生成的classes.dex文件的hash值就改變了,所以咱們能夠經過檢測安裝後classes.dex文件的hash值來判斷apk是否被重打包過。

  1. 讀取應用安裝目錄下/data/app/xxx.apk中的classes.dex文件並計算其哈希值,將該值與軟件發佈時的classes.dex哈希值作比較來判斷客戶端是否被篡改。
  2. 讀取應用安裝目錄下/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++來編寫。

0x03 SO保護


Android so經過C/C++代碼來實現,相對於Java代碼來講其反編譯難度要大不少,但對於經驗豐富的破解者來講,仍然是很容易的事。應用的關鍵性功能或算法,都會在so中實現,若是so被逆向,應用的關鍵性代碼和算法都將會暴露。對於so的保護,能夠纔有編譯器優化技術、剝離二進制文件等方式,還可使用開源的so加固殼upx進行加固。

編譯器優化技術

爲了隱藏核心的算法或者其它複雜的邏輯,使用編譯優化技術能夠幫助混淆目標代碼,使它不會很容易的被攻擊者反編譯,從而讓攻擊者對特定代碼的理解變得更加困難。如使用LLVM混淆。

剝離二進制文件

剝離本地二進制文件是一種有效的方式,使攻擊者須要更多的時間和更高的技能水平來查看你的應用程序底層功能的實現。剝離二進制文件,就是將二進制文件的符號表刪除,使攻擊者沒法輕易調試或逆向應用。在Android上可使用GNU/Linux系統上已經使用過的技術,如sstriping或者UPX。

UPX對文件進行加殼時會把軟件版本等相關信息寫入殼內,攻擊者能夠經過靜態反彙編可查看到這些殼信息,進而尋找對應的脫殼機進行脫殼,使得攻擊難度下降。因此咱們必須在UPX源碼中刪除這些信息,從新編譯後再進行加殼,步驟以下:

  1. 使用原始版本對文件進行加殼。
  2. 使用IDA反彙編加殼文件,在反彙編文件的上下文中查找UPX殼特徵字符串。
  3. 在UPX源碼中查找這些特徵字符串,並一一刪除。

https://www.nowsecure.com/resources/secure-mobile-development/coding-practices/code-complexity-and-obfuscation/

0x04 資源文件保護


若是資源文件沒有保護,則會使應用存在兩方面的安全風險:

  1. 經過資源定位代碼,方便應用破解 反編譯apk得到源碼,經過資源文件或者關鍵字符串的ID定位到關鍵代碼位置,爲逆向破解應用程序提供方便.
  2. 替換資源文件,盜版應用 "if you can see something, you can copy it"。Android應用程序中的資源,好比圖片和音頻文件,容易被複制和竊取。

能夠考慮將其做爲一個二進制形式進行加密存儲,而後加載,解密成字節流並把它傳遞到BitmapFactory。固然,這會增長代碼的複雜度,而且形成輕微的性能影響。

不過資源文件是全局可讀的,即便不打包在apk中,而是在首次運行時下載或者須要使用時下載,不在設備中保存,可是經過網絡數據包嗅探仍是很容易獲取到資源url地址。

0x05 反調試技術


5.1 限制調試器鏈接

應用程序能夠經過使用特定的系統API來防止調試器附加到該進程。經過阻止調試器鏈接,攻擊者干擾底層運行時的能力是有限的。攻擊者爲了從底層攻擊應用程序必須首先繞過調試限制。這進一步增長了攻擊複雜性。Android應用程序應該在manifest中設置Android:debuggable=「false」,這樣就不會很容易在運行時被攻擊者或者惡意軟件操縱。

5.2 Trace檢查

應用程序能夠檢測本身是否正在被調試器或其餘調試工具跟蹤。若是被追蹤,應用程序能夠執行任意數量的可能攻擊響應行爲,如丟棄加密密鑰來保護用戶數據,通知服務器管理員,或者其它類型自我保護的響應。這能夠由檢查進程狀態標誌或者使用其它技術,如比較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。實際運行結果以下圖所示:

p1

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

p2

由此,咱們即可經過檢查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;
}

實際運行結果以下圖所示:

p3

值得注意的是,/proc目錄下包含了進程的大量信息。咱們在這裏是讀取status文件,此外,也可經過/proc/self/stat文件來得到進程相關信息,包括運行狀態。

d.讀取/proc/%d/wchan

下圖中第一個紅色框值爲非調試狀態值,第二個紅色框值爲調試狀態:

p4

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,進而檢測到調試器。以下圖所示:

p5

順便一提,這種方式能夠輕易地被繞過。咱們能夠設置gdb對signal的處理方式,若是咱們選擇將SIGALRM忽略而非傳遞給程序,則alarmHandler便不會被執行,以下圖所示:

p6

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個文件。若是程序正常運行則不會打開這麼多,因此由此來判斷是否被調試。運行結果見下圖:

p7

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

0x06 應用加固技術

移動應用加固技術從產生到如今,一共經歷了三代:

  • 第一代是基於類加載器的方式實現保護;
  • 第二代是基於方法替換的方式實現保護;
  • 第三代是基於虛擬機指令集的方式實現保護。

第一代加固技術:類加載器

以梆梆加固爲例,類加載器主要作了以下工做:

  1. classes.dex被完整加密,放到APK的資源中
  2. 採用動態劫持虛擬機的類載入引擎的技術
  3. 虛擬機可以載入並運行加密的classes.dex

使用一代加固技術之後的apk加載流程發生了變化以下:

p8

應用啓動之後,會首先啓動保護代碼,保護代碼會啓動反調試、完整性檢測等機制,以後再加載真實的代碼。

一代加固技術的優點在於:能夠完整的保護APK,支持反調試、完整性校驗等。

一代加固技術的缺點是加固前的classes.dex文件會被完整的導入到內存中,能夠用內存dump工具直接導出未加固的classes.dex文件。

第二代加固技術:類方法替換

第二代加固技術採用了類方法替換的技術:

  1. 將原APK中的全部方法的代碼提取出來,單獨加密
  2. 運行時動態劫持Dalvik虛擬機中解析方法的代碼,將解密後的代碼交給虛擬機執行引擎

採用本技術的優點爲:

  1. 每一個方法單獨解密,內存中無完整的解密代碼
  2. 若是某個方法沒有執行,不會解密
  3. 在內存中dump代碼的成本代價很高

使用二代加固技術之後,啓動流程增長了一個解析函數代碼的過程,以下圖所示:

p9

第三代加固技術:虛擬機指令集

第三代加固技術是基於虛擬機執行引擎替換方式,所作主要工做以下:

  1. 將原APK中的全部的代碼採用一種自定義的指令格式進行替換
  2. 運行時動態劫持Dalvik虛擬機中執行引擎,使用自定義執行引擎執行自定義的代碼
  3. 相似於PC上的VMProtect採用的技術

三代技術的優勢以下:

  1. 具備2.0的全部優勢
  2. 破解須要破解自定義的指令格式,複雜度很是高
相關文章
相關標籤/搜索