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
亿速云「云服务器」,即开即用、新一代英特尔至强铂金CPU、三副本存储NVMe SSD云盘,价格低至29元/月。点击查看>>
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。