<dd id="sga4y"><optgroup id="sga4y"></optgroup></dd>
<nav id="sga4y"><code id="sga4y"></code></nav>
  • <optgroup id="sga4y"></optgroup>

    原文地址:http://drops.wooyun.org/mobile/12172

    0x00 簡介


    Android apk很容易通過逆向工程進行反編譯,從而是其代碼完全暴露給攻擊者,使apk面臨破解,軟件邏輯修改,插入惡意代碼,替換廣告商ID等風險。我們可以采用以下方法對apk進行保護.

    0x01 混淆保護


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

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

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

    已經有很多第三方的軟件可以用來混淆我們的Android應用,常見的有:

    這些混淆器在代碼中起作用的層次是不一樣的。Android編譯的大致流程如下:

    #!bash
    Java Code(.java) -> Java Bytecode(.class) -> Dalvik Bytecode(classes.dex)
    

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

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

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

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

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

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

    0x02 二次打包防護


    2.1 Apk簽名校驗

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

    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哈希值應該存放在服務器端。

    #!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++來編寫。

    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。示例代碼如下

    #!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。實際運行結果如下圖所示:

    p1

    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

    p2

    由此,我們便可通過檢查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;
    }
    

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

    p3

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

    d.讀取/proc/%d/wchan

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

    p4

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

    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/下列出。于是我們的示例代碼如下:

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

    p7

    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

    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. 破解需要破解自定義的指令格式,復雜度非常高

    97av