您好,登錄后才能下訂單哦!
Google Play上的一道逆向題,一共有5關難度,選擇相應的難度,輸入Name和Serial后,點擊submit后,可提示是否通關成功。如圖。
程序總體結構分析
利用ApkIDE對com.me.keygen.activity進行逆向后,發現MainActivity.smali的validateSerial()方法用于判斷是否通關,該方法又調用KeyVerifier.isValid接口進行判斷,如果返回值為1,則通關成功,為0則通關失敗。代碼如下
invoke-interface {v6, v7, v8}, Lcom/me/keygen/verifiers/KeyVerifier;->isValid(Ljava/lang/String;Ljava/lang/String;)Z #調用com.me.keygen.verifiers.KeyVerifier.isValid(String,String)接口 #其中v6為KeyVerifier對象,v7為Name文本框中的String,v8為Serial文本框中的String
而KeyVerifier.isValid()最終會調用ChallengeXVerifier.isValid(),其中X為1-5,代表了用戶選擇的難度,這個難度是在MainActivity.smali中根據用戶的選擇,初始化的currentChallenge參數,進而調用getVerifierForChallenge()方法,構造相應的ChallengeXVerifier類。注意Challenge5除了currentChallenge參數,還會多傳一些參數。
因此針對每一關,關鍵的判斷邏輯都在ChallengeXVerifier.isValid()中。
為了熟悉smali,我們盡量還原原始算法,而不采用暴破和smali注入的方法。
level1:Beginner
直接根據Challenge1Verifier.smali編寫注冊機
public class challenge1 { public static void main(String[] args) { String name = args[0]; int answer = 0; // v0 for(int v3 = 0; v3 < name.length(); v3 = v3+1) { char v1 = name.charAt(v3); int v4 = v1 * v1; answer = answer + v4; answer = answer ^ v1; } System.out.println("The answer is "+answer); } }
level2:Easy
分析Challenge2Verifier.smali
# virtual methods .method public isValid(Ljava/lang/String;Ljava/lang/String;)Z .locals 8 .param p1, "name" # Ljava/lang/String; .param p2, "serial" # Ljava/lang/String; .prologue const/4 v5, 0x0 .line 16 invoke-virtual {p1}, Ljava/lang/String;->length()I move-result v6 # v6: name.length() const/4 v7, 0x4 # v7=4 if-ge v6, v7, :cond_1 #如果v6>=4,則到cond1.否則返回v5,此時值為0,注冊未成功! .line 42 :cond_0 :goto_0 return v5 .line 21 :cond_1 invoke-virtual {p1}, Ljava/lang/String;->toUpperCase()Ljava/lang/String; #name的字符轉成大寫 move-result-object p1 .line 22 const-wide/16 v1, 0x0 #long v1(nameSum)初始為0 .line 23 .local v1, "nameSum":J const/4 v4, 0x0 #v4初始為0 .local v4, "x":I :goto_1 invoke-virtual {p1}, Ljava/lang/String;->length()I move-result v6 #v6=name.length() if-ge v4, v6, :cond_2 # 如果v4大于等于v6,循環結束 .line 25 invoke-virtual {p1, v4}, Ljava/lang/String;->charAt(I)C move-result v6 # v6=name.charAt(v4) int-to-long v6, v6 add-long/2addr v1, v6 #v1=v1+v6 .line 26 const-wide/16 v6, 0x3 #將0x3擴展為64位, v6=0x3 mul-long/2addr v1, v6 # v1 = v1*v6 .line 27 const-wide/16 v6, 0x40 #將0x40擴展為64位, v6=0x40 sub-long/2addr v1, v6 #v1=v1-v6 .line 23 add-int/lit8 v4, v4, 0x1 #v4=v4+1 goto :goto_1 .line 30 :cond_2 #循環結束 invoke-static {v1, v2}, Ljava/lang/Long;->toString(J)Ljava/lang/String; #v1轉為string, 即為serial move-result-object v3 # v3=sumString .line 31 .local v3, "sumString":Ljava/lang/String; const/4 v0, 0x0 .line 32 .local v0, "finalSum":I const/4 v4, 0x0 :goto_2 # 第二個循環體 invoke-virtual {v3}, Ljava/lang/String;->length()I move-result v6 # v6 = sumString.length() if-ge v4, v6, :cond_3 #if v4 >= v6到cond3 .line 34 invoke-virtual {v3, v4}, Ljava/lang/String;->charAt(I)C # move-result v6 #v6=v3.charAt(v4) add-int/lit8 v6, v6, -0x30 #v6=v6-0x30 add-int/2addr v0, v6 #v0=v0+v6 .line 32 add-int/lit8 v4, v4, 0x1 #v4++ goto :goto_2 .line 37 :cond_3 #第二個循環體結束 const/4 v4, 0x0 :goto_3 # 第三個循環體開始 invoke-virtual {p2}, Ljava/lang/String;->length()I move-result v6 # v6 = serial.length() if-ge v4, v6, :cond_4 #判斷v4是否小于serial的長度,是才循環,否則到cond4。 .line 39 invoke-virtual {p2, v4}, Ljava/lang/String;->charAt(I)C move-result v6 # v6 = serial.charAt(v4) add-int/lit8 v6, v6, -0x40 # v6 = v6 - 0x40 sub-int/2addr v0, v6 # v0 = v0+v6 .line 37 add-int/lit8 v4, v4, 0x1 goto :goto_3 .line 42 :cond_4 if-nez v0, :cond_0 #第三個循環體結束,如果v0不為0,則注冊未成功!這個循環似乎是對serial進行一些特殊的判斷,注冊成功必須確保v0為0; const/4 v5, 0x1 goto :goto_0 .end method
上面的代碼首先判斷name的長度是否大于等于4,如果否則直接返回0,注冊不成功。如果是,則將name轉化為大寫后,開始三次循環。
前兩次循環只對name進行操作,最后得到一個finalSum的int變量。算法如下,
public class challenge2 { public static void main(String[] args) { String name = args[0]; long serial = 0; int v5 = 0; int v6 = name.length(); long v1 = 0; if ( v6 >= 4) { name = name.toUpperCase(); for(int v4 = 0; v4 < name.length(); v4 = v4+1) { v6 = name.charAt(v4); v1 = v1 + (long)v6; v1 = v1*0x3; v1 = v1 - (long)0x40; } String sumString = Long.toString(v1); int finalSum = 0; //v0 for (int v4 = 0; v4 < sumString.length(); v4 = v4+1) { v6 = sumString.charAt(v4); v6 = v6 - 0x30; finalSum = finalSum + v6; } System.out.println("The answer is "+finalSum); } else System.exit(0); } }
根據上面的算法,我們運行得到finalSum為23。
e:\heen\practise\com.me.keygen.activity>java challenge2 heen The answer is 23
而第三次循環是將finalSum與serial進行某種運算,最終判斷是否注冊成功。算法如下
for (v4 = 0; v4 < serial.length(); v4 = v4+1) { v6 = serial.charAt(v4); v6 = v6 - 0x40; finalSum = finalSum - v6; } if (finalSum == 0) System.out.println("The answer is "+serial);
只要使finalSum為0,serial就正確,因此只要滿足finalSum-(serial.charAt(i)-0x40)=0的Serial都成立,可以有多個解。在finalSum為23時,讓finalSum減去23個1,就為0。對應23個1,那么serial可為23個0x41(A).也可為21個0x41(A)和1個0x42(B)
level3:Hard
分析Challenge3Verifier.smali。
.line 25 const-string v9, "-" invoke-virtual {p2, v9}, Ljava/lang/String;->split(Ljava/lang/String;)[Ljava/lang/String; #對serial進行分割,根據其中的'-' move-result-object v5 # 結果parts=v5為String[] .line 26 .local v5, "parts":[Ljava/lang/String; array-length v9, v5 #數組長度為v9 const/16 v10, 0x8 if-eq v9, v10, :cond_1 #如果v9為0x8,跳轉到cond_1 .line 92 :cond_0 :goto_0 return v8 # 返回0,注冊不成功 .line 31 :cond_1 const/4 v7, 0x0 .local v7, "x":I :goto_1 # 循環開始 array-length v9, v5 if-ge v7, v9, :cond_2 .line 33 aget-object v9, v5, v7 #將v9 = v5[v7] const-string v10, "[0-9A-F][0-9A-F][0-9A-F][0-9A-F]" invoke-virtual {v9, v10}, Ljava/lang/String;->matches(Ljava/lang/String;)Z move-result v9 #判斷v9是否匹配v10代表的正則表達式,推斷serial應該為XXXX-XXXX-XXXX-...的形式,X為0-9或大寫字母 if-eqz v9, :cond_0 #如果不滿足,注冊不成功 .line 31 add-int/lit8 v7, v7, 0x1 # v7=v7+1 goto :goto_1 # 循環體結束
上述代碼對serial進行判斷和處理,serial的形式應為XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX的形式,其中X為[0-9A-F]中的字符。處理后,每一個XXXX存在名為parts,長度為8的String數組中。
接下來的代碼,都在對名為baos的ByteArrayOutputStream進行操作,得到一個String foo和String lastHalf。其中foo是根據parts的前4個元素作為輸入對baos進行操作得到的,而lastHalf是parts的后4個元素,最后判斷foo的奇數位字符是否與lastHalf的每一位字符相同,相同則注冊成功。整個過程與name無關。
注冊算法如下
import java.nio.charset.Charset; import java.io.ByteArrayOutputStream; import java.security.MessageDigest; import java.io.IOException; import java.security.NoSuchAlgorithmException; public class challenge3 { public static String bytesToHex(byte[] bytes) { char[] hexArray = {0x30,0x31,0x32,0x33,0x34,0x35,0x36,0x37,0x38,0x39,0x41,0x42,0x43,0x44,0x45,0x46}; char[] hexChars = new char[(bytes.length)*2]; int v; for(int j = 0; j < bytes.length;j=j+1){ v = bytes[j] & 0xff; hexChars[2*j] = hexArray[v>>>0x4]; hexChars[2*j+1] = hexArray[v&0xf]; } return new String(hexChars); } public static void main(String[] args) { byte[] secretBytes; String secretKey; String v0 = new String("KeygenChallengeNumber3"); secretBytes = v0.getBytes(Charset.forName("US-ASCII")); String[] parts = {"AAAA","AAAA","AAAA","AAAA"}; //array of length 8, every element is XXXX ByteArrayOutputStream baos = new ByteArrayOutputStream(); baos.write(0x31); int v7; for(v7 = 0;v7 < secretBytes.length; v7=v7+2) { baos.write(secretBytes[v7]); baos.write(v7+1); } for(v7 = 1;v7 < secretBytes.length; v7=v7+2) { baos.write(secretBytes[v7]); baos.write(v7+1); } baos.write(0x30); baos.write(0x30); for(v7 = 0;v7 < 4; v7 = v7+1) { try {//suppose the first 4 parts is "AAAA-AAAA-AAAA-AAAA" byte[] bs = parts[v7].getBytes(Charset.forName("US-ASCII")); baos.write(bs); baos.write(0x2d); } catch(IOException ioe) { } } try { baos.write(secretBytes); } catch(IOException ioe){ } System.out.println("baos is: "+baos.toString()); byte[] result = new byte[0x20]; try { MessageDigest md = MessageDigest.getInstance("MD5"); md.update(baos.toByteArray()); result = md.digest(); } catch(NoSuchAlgorithmException nsae){ } String foo = bytesToHex(result).toUpperCase(); System.out.println(foo+" length:" +foo.length()); /* for(v7 = 0; v7 < foo.length(); v7=v7+2) { if (foo.charAt(v7) != lastHalf.charAt(v7/2)) System.out.println("Register failed!"); } */ } }
上述代碼中,我們假定注冊碼的前半部分為AAAA-AAAA-AAAA-AAAA,運算得到foo,選取foo的奇數位字符進行拼接,得到最后的注冊碼為AAAA-AAAA-AAAA-AAAA-446E-D772-6CD4-052A
e:\heen\practise\com.me.keygen.activity>java challenge3 4C476AE8DF72742463C9D242065324AB length:32
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。