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