2015移動安全挑戰賽(阿里&看雪主辦)全程回顧

wyzsk發表於2020-08-19
作者: GoSSIP_SJTU · 2015/04/01 9:26

Author: 上海交通大學密碼與電腦保安實驗室軟體安全小組GoSSIP

第一題


0x1 分析

APK介面

題目下載

本次比賽的第一個題目是一個APK檔案,安裝後,需要使用者輸入特定的密碼,輸入正確會顯示破解成功。該題目的APK檔案沒有太多的保護,可以直接使用各種分析工具(如jeb等)反編譯得到Java程式碼。

獲得正確註冊碼的程式碼邏輯為: 1. 從logo.png這張圖片的偏移89473處,讀取一個對映表,768位元組編碼成UTF-8,即256箇中文表 2. 從偏移91265處讀取18個位元組編碼的UTF-8(即6箇中文字元)為最終比較的密碼。然後透過輸入的字元的轉換,轉換規則就是ASCII字元編碼,去比較是否和最終密碼相等。

0x2 巧妙的解法

我們在這裡提供一種非常愉快的解法,不需要複雜的工具和分析,大家可以參見影片

<embed>

開啟app後,我們使用adb logcat並加上這個app獨有的 lil 標籤過濾日誌輸出,發現app輸出日誌中有table,pw以及enPassword。隨意輸入字串如123456789,發現enPassword中有對應的中文輸出,根據輸出反饋,可以知道有如下對應關係

  • 1 - 麼
  • 2 - 廣
  • 3 - 亡
  • 4 - 門
  • 5 - 義
  • 6 - 之
  • 7 - 屍
  • 8 - 弓
  • 9 - 己

透過觀察Logcat輸出可知,最終目標pw應為義弓麼丸廣之,根據上述table中的對應關係,我們可以得到最終密碼為:

581026

第二題


0x1 分析

APK介面

題目下載

本次比賽的第二個題目仍然是一個獨立的APK檔案,安裝後,需要使用者輸入特定的密碼,輸入正確會顯示成功。第二題APK在Java層程式碼中並沒有關鍵邏輯,將使用者輸入直接傳給native so層中securityCheck這個native method(securityCheck方法在libcrackme.so中),由native code來決定返回正確與否。

用IDA工具開啟libcrackme.so,首先看下程式的大致流程,可以看到在securityCheck這個方法呼叫前,在init_array段和JNI_Onload函式里程式都做了些處理,而在securityCheck方法的最後有一個判斷,將使用者輸入和wojiushidaan做比較。嘗試直接輸入wojiushidaan,發現密碼錯誤,因此可以猜測前面一大段邏輯的作用就是會把這個最終的字串改掉。此時的思路是隻需知道最終判斷時候這個wojiushidaan地址上的變換後的值就行了。嘗試使用IDA除錯發現一旦attach上去,整個程式就退出,想必一定是在之前的程式碼中有反除錯的程式碼。

0x2 巧妙的解法

同上一題一樣,我們提供一種非常巧妙的解法:

注意到在最終比較之前,程式使用了android_log_print函式,當我們直接執行程式時,發現這裡固定輸出了

I/yaotong ( XXX): SecurityCheck Started...

這時候我們想,是否可以直接patch這個libcrackme.so,修改列印的內容,直接利用這個函式幫我們輸出此時真正需要比較的值。

我們選擇patch的方法是直接把這個log函式往下移,因為在0x12A4地址處正好有我們需要的列印的資料地址賦值給了R2暫存器(本來是為了給後面做比較用的),因此將程式碼段從0x1284到0x129C的地方都用NOP改寫,在0x12AC的地方呼叫log函式,同時為了不影響R1的值,把0x12A0處的R1改成R3。

下面是對比patch前和patch後的圖:

Patch前程式碼

Patch後程式碼

參考影片給出了完整的解決過程:

<embed>

透過觀察Logcat輸出可知,最終密碼為:

aiyou,bucuoo

第三題


題目下載

在介紹本次比賽第三道題目之前,首先要介紹一個我們GoSSIP小組開發的基於Dalvik VM的插樁分析框架InDroid,其設計思想是直接修改AOSP上的Dalvik VM直譯器,在直譯器解釋執行Dalvik位元組碼時,插入監控的程式碼,這樣就可以獲取到所有程式執行於Dalvik上的動態資訊,如執行的指令、呼叫的方法資訊、引數返回值、各種Java物件的資料等等。InDroid只需要修改AOSP的dalvik vm部分程式碼,編譯之後,可直接將編譯生成的新libdvm.so刷入任何AOSP支援的真機裝置上(目前我們主要使用Nexus系列機型特別是Nexus4和Galaxy Nexus)。在本次比賽的第三題和第四題分析過程中,我們使用該工具進行分析,大大提高了分析效率。具體細節可以參考我們發表的CIT 2014論文DIAS: Automated Online Analysis for Android Applications

回到題目上,將第三題的APK進行反編譯後發現程式碼使用了加殼保護,對付這類加殼的APK,最方便的方法就是使用InDroid來進行動態監控,因為靜態加密的DEX一定會在執行時在Dalvik上時解密執行,這樣我們可以直接在InDroid框架裡對解釋執行過程中釋放出來的指令進行監控。在我們自己使用的工具裡,我們開發了一個動態讀取整個dex資訊的介面,執行時去讀DexFile這個結構,然後對其進行解析(解析時直接複用了Android自帶的dexdump的程式碼)。這樣,我們的插樁工具在執行程式後,能夠直接得到程式的dex資訊,同未經保護時使用dexdump後得到的結果基本一致。雖然我們得到的資訊是dalvik位元組碼,沒有直接反編譯成Java程式碼那麼友好,但由於程式不大,關鍵邏輯不多,因此對我們的分析效率影響並不大。

使用InDroid進行脫殼的演示影片:

<embed>

在得到脫殼之後的dexdump結果後,我們可以對程式碼進行靜態分析。我們發現使用者的輸入會傳遞給繼承自Class timertask的Class b,被Class b的run方法處理。在run方法中,如果sendEmptyMessage方法被呼叫時的引數為0,就會導致Class c的handleMessage這個方法中得到的messagewhat值為0,進而導致103除0跳入異常處理中,觸發成功的提示。

繼續分析這個run方法的邏輯,可以知道使用者的輸入會被傳遞到Class e的a方法中,做個類似摩爾斯譯碼的過程(其譯碼與標準的摩爾斯電碼不太一樣),然後經過下面一系列大量的混淆用的無用處理和不可能相等的比較後,將譯碼後得到的字串送入到關鍵的判斷中去。這個判斷成功的條件比較複雜:對於譯碼後得到的字串的前兩個位元組,要求使用hashcode方法的結果等於3618,並且這兩個位元組相加等於168,才會進入後面的比較。我們窮搜尋一下符合這類輸入的字串:

#!java
for ( size_t i = 33; i < 127; ++i )
{
    for ( size_t j = 33; j < 127; ++j )
    {
        String x =String.valueOf((char)j)+String.valueOf((char)i);
        if (x.hashCode()==3618 && (i+j) == 168)
        {
            System.out.println(x);
            System.out.println(j+i);
        }
    }
}

輸出為:

s5
168

也就是說只有s5滿足hashcode為3618,而相加等於168這個條件。

確定前兩個字元後,後面還有四個字元需要同Class e和Class a的Annotation值比較。因為我們做脫殼的時候直接使用了dexdump的程式碼,而dexdump即使到最新版裡也無法很好地處理Annotations:

// TODO: Annotations.

不過沒關係,我們還有動態分析工具這一利器,因為最終目的是得到getAnnotation方法的返回值,依然可以用InDroid在Dalvik執行到getAnnotation方法時監控返回值,就能得到Annotation的具體值。使用InDroid獲取具體資訊的影片如下:

<embed>

最後可知,符合程式需求的字串是

s57e1p

使用程式內部的對應表,對其進行逆變換,能夠讓程式輸入成功提示的輸入應該是:

… _____ ____. . ..___ .__.

第四題


APK介面

題目下載

本次移動安全挑戰賽的第四題和第三題一樣,是一個包含了加殼dex的APK檔案,我們使用同解決上一題一樣的方法,用InDroid得到原始dex檔案的dexdump結果:

使用InDroid進行脫殼的演示影片:

<embed>

Dex整體處理過程和上一題也類似,使用handleMessage處理最後的判斷輸入成功與否,只有sendEmptyMessage(0)後,觸發除以0的異常才能成功。不過這一題將使用者輸入轉成byte後,傳給一個native的方法:ali$aM$j方法,另外引數還包括一個常數48和Handler。看樣子逆向native庫勢在必行了。這一題的lib資料夾下檔案和上一題是一樣的,有三個檔案,其中libmobisecy.so其實是個zip檔案,解壓後是個classes.dex,直接反彙編後,類和方法的名字都在,只是裡面的程式碼都是

throw new RuntimeException();

libmobisecz.so直接就是一堆binary資料,猜測應該是執行時會被解密,透過某種方式對映到為真正的程式碼執行。因此我們的目標就是libmobisec.so這個ELF檔案。

直接用IDA開啟libmobisec.so,發現IDA會崩潰。用readelf發現正常的節區頭資料都被破壞了,因此應該這個so本身也被加過殼了,很多資料只有在動態執行時才會解開,所以直接使用動態的方法,先執行這個程式後,直接在記憶體中把這個so dump出來。

首先需要在輸入框中隨便輸入些資料後,點選確定,保證使用者輸入資料執行到native方法裡後再做dump。我們使用的方法是檢視maps後,使用dd命令把整個so都dump出來。

輸入命令:

[email protected]:/ # ps | grep crackme.a4
u0_a73    1935  126   512204 48276 ffffffff 400dc408 S crackme.a4
[email protected]:/ # cat /proc/1935/maps
5e0f2000-5e283000 r-xp 00000000 103:04 741132    /data/app-lib/crackme.a4-1/libmobisec.so
5e283000-5e466000 r-xp 00000000 00:00 0 
5e466000-5e467000 rwxp 00000000 00:00 0 
5e467000-5e479000 rw-p 00000000 00:00 0
5e479000-5e490000 r-xp 00191000 103:04 741132    /data/app-lib/crackme.a4-1/libmobisec.so
5e490000-5e491000 rwxp 001a8000 103:04 741132    /data/app-lib/crackme.a4-1/libmobisec.so
5e491000-5e492000 rw-p 001a9000 103:04 741132    /data/app-lib/crackme.a4-1/libmobisec.so
5e492000-5e493000 rwxp 001aa000 103:04 741132    /data/app-lib/crackme.a4-1/libmobisec.so
5e493000-5e4c1000 rw-p 001ab000 103:04 741132    /data/app-lib/crackme.a4-1/libmobisec.so

使用dd命令將libmobisec.so的記憶體dump出來

[email protected]:/ # dd if=/proc/1935/mem of=/sdcard/alimsc4 skip=1578049536 ibs=1 count=3993600

dd命令使用的數字都是十進位制的,skip就是libmobisec.so的起始地址,count是總長度。 為了讓IDA還能夠識別libmobisec裡的libc函式,我們還需要把libc也載入到IDA中,libc就直接從system/lib裡拖出來就行了。

adb pull /system/lib/libc.so ./

用IDA先開啟libc,調整好是在記憶體中的偏移即rebase program,再在load additional binary裡載入dd出來的libmobisec.so,透過maps裡的偏移後載入。接下來的任務就是在其中找到M\$j這個函式的地址。

一開始嘗試直接在dd出來的ELF檔案中找這個M\$j這個函式名,類似的名字會被處理成

Java_ali_00024a_M_00024j

類似下圖:

圖1

不過我沒找到這個M\$j這個名字,逆過JNI庫的都知道,如果符號表裡找不到這個函式名,說明在JNI_Onload的時候,使用RegisterNatives函式重新將一個JNI函式對映為Native函式了。

正當一籌莫展的時候,我再次想起了InDroid系統。在Dalvik中,每個方法都是一個Method的結構體,其中當這個方法是native的時候,Method的insns這個指標會指向native方法的起始地址。因此我們修改了下InDroid,讓Dalvik在執行M\$j這個方法前,去列印了M\$j方法的insns指標。這時我們得到了一個指向另一片記憶體區域的值,既不在libdvm中,也不在libmobisec中,並且這片記憶體頁被對映成了rwx,由此推斷裡面也極有可能是程式碼,我們繼而又dd出了這塊記憶體,用IDA開啟,使用ARM平臺反彙編,發現該處就一條指令,是LOAD PC到另一個地址,而這個地址恰好在libmobisec中。於是我們直接到IDA中跳到這個地址,發現正好是個壓棧指令,印證了我們的想法,此處就是M$j函式,於是在在IDA裡該地址指令處,右擊選擇create function,讓IDA識別這一段彙編指令為函式指令後,就可以透過F5檢視看反編譯的C程式碼了。

這個函式本身做了一些控制流混淆,同時還有很多字串加解密的功能函式,一些簡單的如異或操作,也被展開成與和或的組合等更長更復雜的表示式形式。另外還看到一些變形過的RC4,等等。不過因為我們已經是dump出來執行過的資料,所以必要的資料都已經解密了。如下圖:

圖2

透過檢視反編譯的C程式碼,我發現程式中是直接透過JNI方法呼叫了Java中的bh類的方法a(在圖2常量中也可以看到)。 再次回到dex層檢視a方法,該方法是不斷的將輸入傳遞給不同的函式進行處理,先是cda方法,cCa方法,pa方法,xa方法,ali$aM$d方法(native),aSa方法,xa方法,ali$aM&z方法(native),cda方法,cCa方法,每一個方法都是些簡單的數學運算,編碼,以及密碼學處理等可逆的操作,結合逆向和Indroid對輸入輸出的監控,都可以輕鬆確定每個Java函式的作用,具體過程如下程式碼顯示:

invoke-static {}, LbKn;.a:()Z // [email protected]
move-result v3
invoke-static {v3}, LbKn;.b:(I)V // [email protected]
add-int/lit8 v0, v5, #int 1 // #01
invoke-static {v4, v5}, Lcd;.a:([BI)[B // [email protected]
move-result-object v1
add-int/lit8 v2, v0, #int 1 // #01
invoke-static {v1, v0}, LcC;.a:([BI)[B // [email protected]
move-result-object v0
add-int/lit8 v1, v2, #int -1 // #ff
invoke-static {v0, v2}, Lp;.a:([BI)[B // [email protected]
move-result-object v0
invoke-static {v0, v1}, Lx;.a:([BI)[B // [email protected]
move-result-object v0
add-int/lit8 v2, v1, #int -1 // #ff
invoke-static {v0, v1}, Lali$a;.M$d:([BI)[B // [email protected]
move-result-object v0
add-int/lit8 v1, v2, #int 1 // #01
invoke-static {v0, v2}, LaS;.a:([BI)[B // [email protected]
move-result-object v0
invoke-static {v0, v1}, Lx;.a:([BI)[B // [email protected]
move-result-object v0
add-int/lit8 v2, v1, #int 1 // #01
invoke-static {v0, v1}, Lali$a;.M$z:([BI)[B // [email protected]
move-result-object v0
add-int/lit8 v1, v2, #int 1 // #01
invoke-static {v0, v2}, Lcd;.a:([BI)[B // [email protected]
move-result-object v0
add-int/lit8 v2, v1, #int 1 // #01
invoke-static {v0, v1}, LcC;.a:([BI)[B // [email protected]
move-result-object v0
return-object v0

值得注意的是,其中有兩個native的方法,因為InDroid還可以監控呼叫native方法的引數以及返回值,我們發現這幾個native都沒有對輸入做複雜的處理,只有M\$d對輸入的第四個位元組做了減8的處理。

做了這些逆變換以後我們其實並沒有找到最終比較的處理,不過在解密過的資料中(圖2),不僅有之前需要呼叫的各種方法和類,還可以發現有個十分可疑的Base64的字串。

aJTCZnf6NyBPYJfbrBuLu0wOhRFbPtvqpYjiby5J81M=

並且在native的M\$z方法的反彙編程式碼中,可以看到有對這個Base64字串的長度比較,由於我們並沒有找到真正的比較函式,因此得到這個字串後,我們直接從M\$z開始向上逆推之前的變換就得到了的答案。

具體解密程式碼如下:

#!python
#!/usr/bin/env python
# encoding: utf-8

from Crypto.Cipher import AES

def Lcda(s):
    return ''.join(map(lambda x: chr((ord(x) + 3) & 0xff), s))
def de_Lcda(s):
    return ''.join(map(lambda x: chr((ord(x) - 3) & 0xff), s))

def LcCa(s, a):
    return ''.join([chr(((ord(s[i]) ^ a) + i) & 0xff) for i in xrange(len(s))])
def de_LcCa(s, a):
    return ''.join([chr(((ord(s[i]) - i) & 0xff) ^ a) for i in xrange(len(s))])

def Lpa(s):
    return s[1:] + s[0]
def de_Lpa(s):
    return s[-1] + s[:-1]

def Lxa(s):
    return s.encode("base64")[:-1]
def de_Lxa(s):
    return s.decode("base64")

def LaliaMd(s):
    return s[:3] + chr((ord(s[3]) - 8) & 0xff) + s[4:]
def de_LaliaMd(s):
    return s[:3] + chr((ord(s[3]) + 8) & 0xff) + s[4:]

def LaSa(s):
    BS = 16
    pad = lambda s: s + (BS - len(s) % BS) * chr(BS - len(s) % BS)
    cc = AES.new("qqVJwt11yyLm7hVK1iI2aw==".decode("base64"), AES.MODE_ECB)
    cipher = cc.encrypt(pad(s))
    return cipher
def de_LaSa(s):
    cc = AES.new("qqVJwt11yyLm7hVK1iI2aw==".decode("base64"), AES.MODE_ECB)
    cipher = cc.decrypt(s)
    return cipher

res = "aJTCZnf6NyBPYJfbrBuLu0wOhRFbPtvqpYjiby5J81M="

flag = de_Lcda(de_LcCa(de_Lpa(de_Lxa(de_LaliaMd(de_LaSa(de_Lxa(res))))), 49))
print flag

結果為:

alilaba2345ba

這裡還需要提一下如何尋找M\$dM\$z兩個函式在so庫中的地址的方法,不過這個方法是一些經驗的總結,原因是整個native ELF檔案的節區結構是被修改過的。這兩個方法和M\$j不太一樣,因為在dump出的libmobisec裡可以找到M\$z的函式名,證明這個方法沒有使用RegiterNatives來做變換,因此我們可以透過符號表來找這個函式與檔案頭部的偏移。方法是找M\$z和字串表的偏移,如0x03FE,然後窮搜整個檔案:

圖3

因為符號表應該會把字串表偏移作為一項,這塊區域的結構體,我們對照ELF結構發現並不是標準的符號表,但還是可以大概看出結構體的內容,包括索引,字串表偏移,以及ELF特殊的標誌數,因此推測0x57BE4偏移是M\$z函式。該地址也正好是個壓棧的指令,證明了我們的猜想。

第五題


enter image description here

題目下載

2015年移動安全挑戰賽的最後一道題目,在規定的比賽時間內,僅有來自我們GoSSIP的wbyang一名選手解決了這道問題,今天我們就來揭開這一道最高難度題目的神秘面紗。

先把名為AliCrackme_5.apk的檔案丟到JEB裡看一看:

enter image description here

dex檔案並沒有進行加殼和混淆,看上去是一個非常簡單的程式,Java程式碼部分使用函式Register("Bomb_Atlantis", input)對輸入進行判斷。所以需要分析的邏輯應該都在libcrackme.so裡的Register函式中。

接下來我們用IDA開啟這個libcrackme.so,不出所料的發現IDA完全沒法處理,應該是進行了強烈的混淆和加殼處理:

enter image description here

使用和解決前面題目相同的技巧,我們繼續使用dd的方法來去處一部分的混淆和加殼。執行一次程式後,從/proc/self/maps裡找到libcrackme.so在記憶體中的位置,使用dd命令從/proc/self/mem中提取出記憶體中的libcrackme.so,接著使用在解決第四題時使用過的技巧,將libcrackme.solibc.so一起載入到IDA裡。

用IDA開啟dump出的程式碼後,我們發現仍然有大部分的程式碼無法被IDA識別,需要手動定位到需要分析的程式碼然後手工定義(IDA快捷鍵C)程式碼,同時由於程式碼會在THUMB指令集和ARM指令集之間切換,有時候需要用快捷鍵ALT+G來將T暫存器設定為不同的值,設定正確後才能正確翻譯出程式碼。這裡我們首先遇到的問題是無法定位Register函式,同樣使用第四題中的技巧,用InDroid監控到Register函式的真實地址,就可以在該地址上開始分析。

libcrackme.so這個動態庫裡使用的一些混淆方法,對於處理了前面一些類似混淆後現在的我們來說已經不是問題(^_^)。透過分析程式碼,我們定位了幾個函式,這些函式的偏移在不同的裝置上應該是不同的。整體的邏輯其實並不複雜,首先會有一個固定的字串“Bomb_Atlantis”和一個固定的salt去進行一次md5運算,salt是動態生成的,不過由於dump記憶體的時候這些動態的值已經生成好了,所以能夠直接發現這個salt(出於一些版權原因我們不便公佈本題目的一些內部細節,因此該salt值請大家自己分析)

之後程式會將這個md5值和我們的輸入進行一些異或和計算的操作,經過幾步比較簡單、可逆的變換之後,進入一個比較複雜的函式,經過這個函式處理後直接和一個記憶體中的值進行比較,返回比較結果。

enter image description here

這裡說一個我們在做第五題時用到的分析方法——動態hook。由於libcrackme.so中並沒有對呼叫自身的上層應用進行驗證,這就導致了我們可以自己寫一個程式去載入這個so,呼叫其中的方法。這也導致了我們在載入libcrackme.so後,可以載入另一個用於hook的so,這樣我們可以hook libcrackme.so中的任意函式,從而知道任意函式的引數和返回值,這對於我們理解程式有著非常大的幫助。這裡我們使用的hook框架是著名Android安全研究人員Collin Mulliner開發的Android平臺上的一個二進位制注入框架adbi。當然這道題目並不能夠透過注入的方法將我們的so注入進去,因為源程式禁掉了ptrace這樣的系統呼叫。我們對adbi稍作修改,使之成為一個可以手動載入的動態hook框架。同時由於我們沒法透過符號表來定位函式的地址,所有的hook地址都需要硬編碼,並且要和執行這道題目程式的Android裝置記憶體對映嚴格對應。

需要指出的是adbi中存在一個小bug,hook.c這個檔案的118行應該是

h->jumpt[0]=0x60

而不是0x30,對應的thumb彙編應該是

push{r5,r6}

而不是

push{r4,r5},

這個小bug在解題過程中會造成一些影響。使用adbi來hook這道題目的函式還需要注意一點,這題的程式碼中有一些函式使用的THUMB指令集,hook這些函式時,不要忘記人工的對hook地址+1。

透過hook的方法,我們已經能夠動態的分析libcrackme.so,首先我們驗證了我們對之前幾步變換的分析結果。之後就是分析最後一個複雜的處理函式,透過靜態分析+動態除錯,我們發現這是一個類似於白盒密碼學的加密函式。我們的輸入進入函式後,首先經過幾步類似DES的預處理,之後會進行若干輪的查表,透過查詢一個巨大的表將我們的輸入進行某種加密,生成一段密文,再經過幾次簡單的處理後和最後記憶體中的一段常量(出於一些版權原因我們不便公佈本題目的一些內部細節,因此該常量請大家自己分析)進行比較。

透過動態除錯,我們能夠計算出加密演算法最後應該輸出的值,但是由於這個加密演算法的金鑰融入了整個置換表中,要找出一個逆置換表顯然不太可能。我們簡單過濾了libcrackme.so的其他函式,也沒有發現用於解密的函式,想要正常解密密文是不太現實了。不過根據對加密演算法的分析,我們發現這若干輪的置換是相互獨立的,並且每一輪的複雜度並不高,這就意味這我們可以在可以接受的時間內對演算法進行爆破。我們一開始的想法是code reuse,直接在Android裝置上爆破,但是發現速度太慢,最後只能用笨辦法,透過hook從記憶體中dump出來置換表,用C程式碼重寫了這個演算法,有驚無險地在比賽結束前半小時搜尋出結果。根據逆推演算法推出正確輸入是:

3EFoAdTxepVcVtGgdVDB6AA=   

好了,我們的2015移動安全挑戰賽全系列回顧就到此為止了!希望大家能和我們多多交流討論,歡迎大家關注我們的微博GoSSIP_SJTU,基本上每天都會有精彩的內容釋出哦。

本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章