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编写注册机

publicclasschallenge1{publicstaticvoidmain(String[]args){Stringname=args[0];intanswer=0;//v0for(intv3=0;v3<name.length();v3=v3+1){charv1=name.charAt(v3);intv4=v1*v1;answer=answer+v4;answer=answer^v1;}System.out.println("Theansweris"+answer);}}

level2:Easy

分析Challenge2Verifier.smali

#virtualmethods.methodpublicisValid(Ljava/lang/String;Ljava/lang/String;)Z.locals8.paramp1,"name"#Ljava/lang/String;.paramp2,"serial"#Ljava/lang/String;.prologueconst/4v5,0x0.line16invoke-virtual{p1},Ljava/lang/String;->length()Imove-resultv6#v6:name.length()const/4v7,0x4#v7=4if-gev6,v7,:cond_1#如果v6>=4,则到cond1.否则返回v5,此时值为0,注册未成功!.line42:cond_0:goto_0returnv5.line21:cond_1invoke-virtual{p1},Ljava/lang/String;->toUpperCase()Ljava/lang/String;#name的字符转成大写move-result-objectp1.line22const-wide/16v1,0x0#longv1(nameSum)初始为0.line23.localv1,"nameSum":Jconst/4v4,0x0#v4初始为0.localv4,"x":I:goto_1invoke-virtual{p1},Ljava/lang/String;->length()Imove-resultv6#v6=name.length()if-gev4,v6,:cond_2#如果v4大于等于v6,循环结束.line25invoke-virtual{p1,v4},Ljava/lang/String;->charAt(I)Cmove-resultv6#v6=name.charAt(v4)int-to-longv6,v6add-long/2addrv1,v6#v1=v1+v6.line26const-wide/16v6,0x3#将0x3扩展为64位,v6=0x3mul-long/2addrv1,v6#v1=v1*v6.line27const-wide/16v6,0x40#将0x40扩展为64位,v6=0x40sub-long/2addrv1,v6#v1=v1-v6.line23add-int/lit8v4,v4,0x1#v4=v4+1goto:goto_1.line30:cond_2#循环结束invoke-static{v1,v2},Ljava/lang/Long;->toString(J)Ljava/lang/String;#v1转为string,即为serialmove-result-objectv3#v3=sumString.line31.localv3,"sumString":Ljava/lang/String;const/4v0,0x0.line32.localv0,"finalSum":Iconst/4v4,0x0:goto_2#第二个循环体invoke-virtual{v3},Ljava/lang/String;->length()Imove-resultv6#v6=sumString.length()if-gev4,v6,:cond_3#ifv4>=v6到cond3.line34invoke-virtual{v3,v4},Ljava/lang/String;->charAt(I)C#move-resultv6#v6=v3.charAt(v4)add-int/lit8v6,v6,-0x30#v6=v6-0x30add-int/2addrv0,v6#v0=v0+v6.line32add-int/lit8v4,v4,0x1#v4++goto:goto_2.line37:cond_3#第二个循环体结束const/4v4,0x0:goto_3#第三个循环体开始invoke-virtual{p2},Ljava/lang/String;->length()Imove-resultv6#v6=serial.length()if-gev4,v6,:cond_4#判断v4是否小于serial的长度,是才循环,否则到cond4。.line39invoke-virtual{p2,v4},Ljava/lang/String;->charAt(I)Cmove-resultv6#v6=serial.charAt(v4)add-int/lit8v6,v6,-0x40#v6=v6-0x40sub-int/2addrv0,v6#v0=v0+v6.line37add-int/lit8v4,v4,0x1goto:goto_3.line42:cond_4if-nezv0,:cond_0#第三个循环体结束,如果v0不为0,则注册未成功!这个循环似乎是对serial进行一些特殊的判断,注册成功必须确保v0为0;const/4v5,0x1goto:goto_0.endmethod

上面的代码首先判断name的长度是否大于等于4,如果否则直接返回0,注册不成功。如果是,则将name转化为大写后,开始三次循环。

前两次循环只对name进行操作,最后得到一个finalSum的int变量。算法如下,

publicclasschallenge2{publicstaticvoidmain(String[]args){Stringname=args[0];longserial=0;intv5=0;intv6=name.length();longv1=0;if(v6>=4){name=name.toUpperCase();for(intv4=0;v4<name.length();v4=v4+1){v6=name.charAt(v4);v1=v1+(long)v6;v1=v1*0x3;v1=v1-(long)0x40;}StringsumString=Long.toString(v1);intfinalSum=0;//v0for(intv4=0;v4<sumString.length();v4=v4+1){v6=sumString.charAt(v4);v6=v6-0x30;finalSum=finalSum+v6;}System.out.println("Theansweris"+finalSum);}elseSystem.exit(0);}}

根据上面的算法,我们运行得到finalSum为23。

e:\heen\practise\com.me.keygen.activity>javachallenge2heenTheansweris23

而第三次循环是将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("Theansweris"+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。

.line25const-stringv9,"-"invoke-virtual{p2,v9},Ljava/lang/String;->split(Ljava/lang/String;)[Ljava/lang/String;#对serial进行分割,根据其中的'-'move-result-objectv5#结果parts=v5为String[].line26.localv5,"parts":[Ljava/lang/String;array-lengthv9,v5#数组长度为v9const/16v10,0x8if-eqv9,v10,:cond_1#如果v9为0x8,跳转到cond_1.line92:cond_0:goto_0returnv8#返回0,注册不成功.line31:cond_1const/4v7,0x0.localv7,"x":I:goto_1#循环开始array-lengthv9,v5if-gev7,v9,:cond_2.line33aget-objectv9,v5,v7#将v9=v5[v7]const-stringv10,"[0-9A-F][0-9A-F][0-9A-F][0-9A-F]"invoke-virtual{v9,v10},Ljava/lang/String;->matches(Ljava/lang/String;)Zmove-resultv9#判断v9是否匹配v10代表的正则表达式,推断serial应该为XXXX-XXXX-XXXX-...的形式,X为0-9或大写字母if-eqzv9,:cond_0#如果不满足,注册不成功.line31add-int/lit8v7,v7,0x1#v7=v7+1goto: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无关。


注册算法如下

importjava.nio.charset.Charset;importjava.io.ByteArrayOutputStream;importjava.security.MessageDigest;importjava.io.IOException;importjava.security.NoSuchAlgorithmException;publicclasschallenge3{publicstaticStringbytesToHex(byte[]bytes){char[]hexArray={0x30,0x31,0x32,0x33,0x34,0x35,0x36,0x37,0x38,0x39,0x41,0x42,0x43,0x44,0x45,0x46};char[]hexChars=newchar[(bytes.length)*2];intv;for(intj=0;j<bytes.length;j=j+1){v=bytes[j]&0xff;hexChars[2*j]=hexArray[v>>>0x4];hexChars[2*j+1]=hexArray[v&0xf];}returnnewString(hexChars);}publicstaticvoidmain(String[]args){byte[]secretBytes;StringsecretKey;Stringv0=newString("KeygenChallengeNumber3");secretBytes=v0.getBytes(Charset.forName("US-ASCII"));String[]parts={"AAAA","AAAA","AAAA","AAAA"};//arrayoflength8,everyelementisXXXXByteArrayOutputStreambaos=newByteArrayOutputStream();baos.write(0x31);intv7;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{//supposethefirst4partsis"AAAA-AAAA-AAAA-AAAA"byte[]bs=parts[v7].getBytes(Charset.forName("US-ASCII"));baos.write(bs);baos.write(0x2d);}catch(IOExceptionioe){}}try{baos.write(secretBytes);}catch(IOExceptionioe){}System.out.println("baosis:"+baos.toString());byte[]result=newbyte[0x20];try{MessageDigestmd=MessageDigest.getInstance("MD5");md.update(baos.toByteArray());result=md.digest();}catch(NoSuchAlgorithmExceptionnsae){}Stringfoo=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("Registerfailed!");}*/}}

上述代码中,我们假定注册码的前半部分为AAAA-AAAA-AAAA-AAAA,运算得到foo,选取foo的奇数位字符进行拼接,得到最后的注册码为AAAA-AAAA-AAAA-AAAA-446E-D772-6CD4-052A

e:\heen\practise\com.me.keygen.activity>javachallenge34C476AE8DF72742463C9D242065324ABlength:32