天天看點

顫抖了嗎?九步逆向破解銀行安全令牌顫抖了嗎?九步逆向破解銀行安全令牌

顫抖了嗎?九步逆向破解銀行安全令牌

寫在前面

    作者曆經千辛萬苦,逆向了一個銀行動态密碼的APP,過程艱辛曲折。最後發現,其實生成算法也不算很複雜,其中主要使用了 android_id 系統時間戳 作為生成變量。看起來高大上的動态密碼,在逆向工程師抽絲剝繭的分析下,亦不過如此哇~

正文

    我這次要對全巴西最大的銀行之一開刀,我也經常會使用到這家銀行所提供的服務。他使用包括使用者密碼在内的多種途徑來驗證使用者的身份。我逆向的動态密碼(OPT)也是其中一種,并且我把他移植到了Arduino-compatible 平台上。

免責聲明和更多的免責聲明

在文章中我已經去掉了敏感的資訊,以保護無辜的人的利益。而且這項研究的結果也不足以讓我能夠黑掉别人的銀行賬戶。即使一個擁有root權限的第三方惡意應用,在沒有足夠的賬戶資訊的前提下,都不能模拟動态密碼的生成。而且,這次研究并沒有發現任何代碼層面的漏洞,這家銀行的生成算法甚至比google的認證算法還要安全,甚至可以說這篇文章是對該銀行APP安全性的一次褒獎。他的動态生成算法,完全符合TOTP規範。把資料安全做到了極緻。

下面就是免責聲明 balabala一大堆,請原諒小編就不翻譯了,想看的可以去原文位址觀看

顫抖了嗎?九步逆向破解銀行安全令牌顫抖了嗎?九步逆向破解銀行安全令牌

通常對于新使用者,他們會得到一張密保卡,但是密保卡這東西是極其不友善的。另外一個更好的方法就是使用android app生成一個動态密碼,大膽的猜測,這個密碼的生成可能把手機号,或者是pin值作為參數。在我每次刷機,或者更換手機的時候,都要重新綁定一遍這個安全服務。雖然過程比較簡單,但還是令我不爽。是以我決定,逆向這個android APP做一個自己的動态密碼生成工具。

Activating the application 激活應用

在閱讀源碼之前,我還是喜歡先把應用下載下傳下來看看他長啥樣,下面是三個階段的截圖。

顫抖了嗎?九步逆向破解銀行安全令牌顫抖了嗎?九步逆向破解銀行安全令牌
顫抖了嗎?九步逆向破解銀行安全令牌顫抖了嗎?九步逆向破解銀行安全令牌
顫抖了嗎?九步逆向破解銀行安全令牌顫抖了嗎?九步逆向破解銀行安全令牌
顫抖了嗎?九步逆向破解銀行安全令牌顫抖了嗎?九步逆向破解銀行安全令牌
顫抖了嗎?九步逆向破解銀行安全令牌顫抖了嗎?九步逆向破解銀行安全令牌

第一張是安裝時的截圖,看一下他所需要的權限,這可不是開發人員為何好玩才加上去的。他們其中有些甚至有可能影響到動态密碼的生成。第二張是激活界面,需要填四個數字,這四個數字隻能通過給銀行打電話被告知。第三幅圖就是,成功激活後生成的動态密碼。

The toolset 工具相關

逆向android應用需要用到的幾個小工具,在下面列出來了,其實就是網上常用的幾種工具。

Android SDK

提供adb這個強大的指令行工具,提取apk檔案,和擷取手機資訊全靠它。

dex2jar

這個工具可以吧dex轉換成jar包的形式

JD, JD-GUI

這個就是··java反編譯工具,直接出源碼

Eclipse

這個就不多說了,地球人都知道。

第一步:取得APK檔案

這個相當簡單的嘛~。可以直接在play裡面下,也可以使用ADB在手機中把他抓出來。

查找包名

$ ./adb shell pm list packages | grep mybank
package:com.mybank
           

确定路徑

$ ./adb shell pm path com.mybank
package:/data/app/com.mybank-1.apk
           

下載下傳

$ ./adb pull /data/app/com.mybank-1.apk
2950 KB/s (15613144 bytes in 5.168s)
           

第二步:解壓縮APK檔案

APK直接可以被解壓縮,其實他就是一個壓縮封包件,隻是字尾不同。解壓縮以後的classes.dex檔案中包含了java源代碼資訊。

解壓縮

$ unzip com.mybank-1.apk
(file list omitted for brevity)
           

把dex轉換成jar檔案

$ mv classes.dex com.mybank-1.dex
$ ./d2j-dex2jar.sh com.mybank-1.dex
dex2jar com.mybank-1.dex -> com.mybank-1-dex2jar.jar
           

第三步:瞅代碼

把jar包放進JD—GUI裡面,就可以看到源代碼了。

顫抖了嗎?九步逆向破解銀行安全令牌顫抖了嗎?九步逆向破解銀行安全令牌

很輕易的就可以發現幾個比較特殊的包,br.com.mybank.integrador.token, br.com.othercompany.token , com.mybank.varejo.token毫無疑問核心代碼就在裡面,隻不過代碼應該被混淆了。

第四步:通過異常字元串反混淆

代碼中經常的許多字元串名都被混淆了(其實是加密了,不過加密的秘鑰在代碼中能找到~,作者說是混淆就是混淆把~),這比較蛋疼。要知道,代碼中的字元串會對逆向起到很大的幫助。

public void trocaPINcomLogin(int paramInt, boolean paramBoolean, Perfil paramPerfil)
{
    if (paramPerfil == null)
        throw new IllegalArgumentException(a.a("1p5/eEf/sl3kbeUcP509qg=="));
    if (!this.jdField_a_of_type_U.jdField_a_of_type_JavaUtilHashtable.contains(paramPerfil))
        throw new RuntimeException(a.a("86jcmKgr/ZshQu9aGVbuGscy2nHW4UEWqudRoUXhImQ=") + a.a("7u8KqqwqUD3a7FM339fp6pRrxUtQrHDMyqvZ6A2MurQ="));
    if ((this.jdField_a_of_type_BrComOtherCompanyTokenParamsGerenciador.isPinObrigatorio()) && (!paramBoolean))
        throw new RuntimeException(a.a("aMsL/5kjkXKD4K1SvpTuuJZUS0U0fL19UT2GxjJ/QzQ="));
    Configuracao localConfiguracao = paramPerfil.getConfiguracao();
    if ((localConfiguracao.a().a()) && (paramPerfil != this.jdField_a_of_type_BrComOtherCompanyTokenPerfil))
        throw new RuntimeException(a.a("ASszutKFJW3iqDb7X/+vqAcYxTLXN2SJOIs0ne596Pu3ZoRxjiiscwhV6fT70efX"));
    localConfiguracao.a().a(paramInt);
    localConfiguracao.a().a(paramBoolean);
    this.jdField_a_of_type_U.a(paramPerfil);
    if (!paramPerfil.equals(this.jdField_a_of_type_BrComOtherCompanyTokenPerfil))
        a(paramPerfil);
}
           

不過幸運的是,在抛出異常的語句中,我們可以找到一些蛛絲馬迹,我們通過觀察可以發現,混淆字元串的函數是a.a。根據這些資訊的提示,我們可以猜測a.a是一個解密有關的類。順理成章,我們直接去a函數中分析解密所使用的代碼。

這是分析完a類之後的一些額外收獲

  • p類是一個base64解密的類。
  • b類,實作了AES的功能。搜尋這個類之中的一些字元串,我發現它是網絡上的一個開源實作 Paulo Barreto's JAES中的内容。
  • a類中的private static byte[]是混淆所使用的秘鑰,可以通過一個簡短的程式來反混淆。

不過不幸的是,a.a不單單是JAES的AES加密的包裝,其中也包含着自己實作的一些加密。

不過這都不是事兒,我還是把a.a中的解密函數用python實作了。

</pre><pre name="code" class="python">def decodeExceptionString(str):
    aesKey = <data from previous step>
    xorKey = <data from decompiled method>
    blockSize = 16
    
    aes = AES(aesKey)
    stringBytes = Base64.decode(str)
    outputString = ""
    
    for blockStart in xrange(0, len(stringBytes), blockSize):
        encryptedBlock = stringBytes[blockStart:blockStart+blockSize]
        plaintextBlock = aes.decrypt(encryptedBlock)
        outputString += plaintextBlock ^ xorKey
        xorKey = encryptedBlock
    
    return outputString
           

簡而言之,除了AES和混淆秘鑰,這個類還實作了CBC(密碼段連結)。

試驗一下上述代碼的功能

$ ./decode "ASszutKFJW3iqDb7X/+vqAcYxTLXN2SJOIs0ne596Pu3ZoRxjiiscwhV6fT70efX"
N?o é possível alterar PIN sem estar logado.
           

這段葡萄牙語的意思是,it is not possible to change PIN without being logged in。看來代碼運作的還不錯。

第五步:逆向核心代碼–随機密碼生成過程

解決了,字元串混淆的問題,接下來個就要弄清楚随機密碼的生成過程了~找啊找啊找啊找~~~~找了好久,我終于發現了一個切入點,br.com.othercompany.token.dispositivo.OTP這個類。下面是它抛出的一些異常,反混淆之後我們可以看到原文。

public String calculate() throws TokenException {
    int i = (int)Math.max(Math.min((this.a.getConfiguracao().getAjusteTemporal() + Calendar.getInstance().getTime().getTime() - 1175385600000L) / 36000L, 2147483647L), -2147483648L);
    a();
    if (i < 0)
        throw new TokenException("Janela negativa"), i);
    int j = (0x3 & this.a.getConfiguracao().getAlgoritmos().a) >> 0;
    switch (j)
    {
    default:
        throw new TokenException("Algoritmo inválido:" + j, i);
    case 0:
        return a(i);
    case 1:
    }
    return o.a(this.a.getConfiguracao().getChave().a(20), i);
}
           

很容易讀懂,變量i是一個時間戳,從2007年4月11日到現在的秒數除以36,36就是每個動态密碼的存活時間。

至于為什麼是2007年4月11日,我就不知道了,大概是程式員他老婆的生日 : )

他還引入了一個修正函數getAjusteTemporal(),為了解決各地區的時差問題。上文代碼中的o.a函數是用于生成動态密碼,他的兩個參數一個是剛才說到的時間戳,還有一個是遺傳byte數組(應該是一個密鑰)。

第六步:尋找密鑰

看一下,生成語句的這段調用,this.a.getConfiguracao().getChave().a(20) this.a 是一個Perfil (profile) 對象,getConfiguracao() 傳回一個Configuracao (settings) 對象getChave()傳回一個z類,a(int)傳回一個byte數組,這個數組就是key。

z類中的字元串,也經過了混淆,但是比較簡單,反混淆過程就不提了.檢視一下c類中的a(int)函數,是傳回一個byte數組,長度截取到參數值。Perfil (profile) 對象反而是由PersistenciaDB類建立的,這個類中也包含了很多被混淆的字元串。

  • a = a.a("DwYyIlrWxIS9ruNMCKH/PQ==");
  • b = a.a("SceoTjidi0XqlgRUo9hcDw==");
  • c = a.a("yrYBlcp8nEfVKUT9WSqTqA==");
  • d = a.a("jUTzBfsP/AO/Kx/1+VQ3CQ==");
  • e = a.a("Y56SnU/pIKROPCLHu7oFuw==") + b + a.a("38oyp4eW3xqT3TaMfWZ5RA==") + "_id" + a.a("3Q+FCEVH2PxZ31ms4WHHwNB40EbmtWzHPhwoaB1nM7lGr+9zZzuVpx5iZ4YR+KUw") + c + a.a("bYYIl6LtqthcUCCFFb7JCRSC8zr5hKIFXe5JHFCCkZA=") + d + a.a("ENCtPBu4RtFta2XI1GsQag==") + a.a("ImPhDy43f+Nr4G5ofkZz+g==");

幸好a.a的機制我們前面研究過,翻譯出的原文如下。

  • a = "token.db";
  • b = "perfis";
  • c = "nome";
  • d = "data";
  • e = "create table perfis (_id integer primary key autoincrement, nome text not null, data blob not null);";

竟然是一條,SQL語句,真是很有趣,原來它使用資料庫來存儲配置資訊。其中還有一個檔案名token.db,很可能是一個SQLite資料庫。緊接着通過研究PersistenciaDB類中的 carregar(load)函數,我們可以确定這一想法,他通過SQLiteDatabase類來通路這個資料庫。

不過我接下來發現資料blob(binary large object)在carragar函數中被aa.a(和上文中的a.a不是一碼事)這個函數加密了,這個函數接收blob資料,和一串16個字元的密鑰。

在研究aa.a這個函數之前。我們先研究一下解密Blob的密鑰,他作為carregar的一個參數傳遞進來。由PersistenciaUtils這個類産生。下面是這個類的入口。

public class PersistenciaUtils {
  public static byte[] getChave(Context paramContext, byte[] paramArrayOfByte) {
    try {
      byte[] arrayOfByte = MessageDigest.getInstance("SHA-1").digest(getId(paramContext).getBytes());
      return arrayOfByte;
    } catch (NoSuchAlgorithmException localNoSuchAlgorithmException) {
    }
    return new byte[20];
  }
  public static String getId(Context paramContext) {
    String str = Settings.System.getString(paramContext.getContentResolver(), "android_id");
    if (str == null)
      str = "<their default id>";
    return str;
  }
}
           

從代碼可以看出,先取得android_id的SHA-1摘要,如果擷取失敗就是用一個預設的16進制hash字元串。

使用adb看看我的android_id

$ ./adb shell
[email protected]:/ $ content query --uri content://settings/secure --projection name:value --where "name='android_id'"
Row: 0 name=android_id, value=0123456789abcdef
[email protected]:/ $ exit
           

a.aa把blob分成很多段,96位的頭,16位的随機數,16位的标簽和為加密資料預留的空間。

進一步研究aa這個類,我們的到了,如下的資訊。

  • 類a實作了 EAX AEAD(Authenticated Encryption with Associated Data)
  • 類f實作了 CMAC (Cipher-based Message Authentication Code)
  • 類h實作了 CTR (counter) mode  廣告計費的功能
  • 以上三個類的算法實作都取自JAES庫
  • 類l實作了 SHA-1 雜湊演算法  ,有趣的是類PersistenciaUtil并沒有使用它,而是使用了MessageDigest這個函數來替代它。
  • 類m實作了 HMAC (keyed-Hash Message Authentication Code) 算法
  • 類n,包裝了l和m,提供了HMAC-SHA1接口

最重要的我發現,aa.a函數是通過CMAC标簽進行加密的,寫出python的解密代碼。

def decodeBlob(datablob, android_id):
    header = datablob[:96]
    nonce = datablob[96:112]
    tag = datablob[112:128]
    cryptotext = datablob[128:]
    key1 = SHA1(android_id)[:16]
    aes = AES(key1)
    cmac = CMAC(aes)
    cmac.update(header)
    key2 = cmac.getTag()
    eax = EAX(key2, aes)
    (validTag, plaintext) = eax.checkAndDecrypt(cryptotext, tag)
    if validTag:
        return plaintext
           

如果EAX驗證成功,aa.a傳回解密的内容給PersistenciaDB使用。

再來看PersistenciaDB這個類,其中的a方法可以解析明文資料并把它變成一個perfil對象,并反序列化之,把其變成一個包含着bool,short,byte的數組。

其中以下3個,是關鍵的資料,根據偏移量反推出偏移。

  • pin = int(blob[82:86])
  • key = blob[38:70]
  • timeOffset = long(blob[90:98])

這就是密鑰所需要的三個資料,隻要三個都正确,程式就可以正常運作。

第七步:深度了解代碼

上文中得到的秘鑰在OPT 類中被截取到20個位元組,并和時間戳一起傳送到o.a方法中,這個方法引用了上文提到的很多類,是以,比較輕松的就寫出了python代碼~

def generateToken(key, timestamp):
    message = [0] * 8
    for i in xrange(7, 0, -1):
        message[i] = timestamp & 0xFF
        timestamp >>= 8
    
    hmacSha1 = HMAC_SHA1(key)
    hmacSha1.update(message)
    hash = hmacSha1.getHash()
    
    k = 0xF & hash[-1]
    m = ((0x7F & hash[k]) << 24 | (0xFF & hash[(k + 1)]) << 16 | (0xFF & hash[(k + 2)]) << 8 | 0xFF & hash[(k + 3)]) % 1000000;
    return "%06d" % m
           

基本時間戳是一個占8位元組的長整形,手動把它轉換成大端的byte數組,接着使用HMAC-SHA1,取得hash最後四位作為整數讀取的索引。使用這個整形,mod 1000000,就是我們的随機密碼了,簡單的超乎我的想象~~

馬上我在google的TOTP認證的實作代碼中,發現了很相似的一段。

public String generateResponseCode(byte[] challenge)
        throws GeneralSecurityException {
    byte[] hash = signer.sign(challenge);
    // Dynamically truncate the hash
    // OffsetBits are the low order bits of the last byte of the hash
    int offset = hash[hash.length - 1] & 0xF;
    // Grab a positive integer value starting at the given offset.
    int truncatedHash = hashToInt(hash, offset) & 0x7FFFFFFF;
    int pinValue = truncatedHash % (int) Math.pow(10, codeLength);
    return padOutput(pinValue);
}
           

其實他們使用了基本相同的算法。

第九步:克隆

現在一個德州出品的Stellaris LaunchPad正躺在我面前,我準備了如下的庫~

Cryptosuite
 Arduino的加密算法庫 (包括 SHA and HMAC-SHA)
RTClib
 JeeNodes and Arduinos所使用的輕量級時間日期庫.
2x16LCD_library
A library for 2x16 LCD (like JDH162A or HD44780) written for Energia and Stellaris Launchpad (LM4F).
           

RTC有一部分需要改進。由于Stellaris LaunchPad沒有闆載實時時鐘,内部時鐘需要在每次啟動時設定,而且需要一台電腦來輔助,這是很麻煩的事情。

完整代碼如下。

#include <sha1.h>
#include <LCD.h>
#include <RTClib.h>
RTC_Millis RTC;
void setup() {
    RTC.begin(DateTime(__DATE__, __TIME__));  
    
    LCD.init(PE_3, PE_2, PE_1, PD_3, PD_2, PD_1);
    LCD.print("Token");
    LCD.print("valverde.me", 2, 1);
    delay(1000);
    LCD.clear();
}
char token[6];
uint8_t message[8];
long timestamp = 0;
long i = 0;
uint8_t key[] = {<your key here>};
void showToken() {
    long now = RTC.now().get() - 228700800 + 7200;
    i = now / 36;
    int timeLeft = now % 36;
    
    for(int j = 7; j >= 0; j--) {
        message[j] = ((byte)(i & 0xFF));
        i >>= 8;
    }
    
    Sha1.initHmac(key, 20);
    Sha1.writebytes(message, 8);
    
    uint8_t * hash = Sha1.resultHmac();
    
    int k = 0xF & hash[19];
    int m = ((0x7F & hash[k]) << 24 | (0xFF & hash[(k + 1)]) << 16 | (0xFF & hash[(k + 2)]) << 8 | 0xFF & hash[(k + 3)]) % 1000000;
    LCD.print(m, 2, 1);
    LCD.print(36 - timeLeft, 2, 15);
}
void loop() {
    LCD.clear();
    LCD.print("Current token:");
    showToken();
    delay(1000);
}
           

最後作者還分享了一個,解決Arduino時間問題的小技巧~這裡省略啦~感興趣的同學可以去原文看。

相關下載下傳

連結: http://pan.baidu.com/s/1ntJZX1J 密碼: 9mne

轉載說明

原文連結:http://blog.valverde.me/2014/01/03/reverse-engineering-my-bank%27s-security-token/#.Uwa0CGI723D

翻譯By FreeBuf小編wyl:http://www.freebuf.com/articles/terminal/26631.html