首页 > 代码库 > Android逆向Challenge Write Up(1)

Android逆向Challenge Write Up(1)

Google Play上的一道逆向题,一共有5关难度,选择相应的难度,输入Name和Serial后,点击submit后,可提示是否通关成功。如图。

wKioL1R8CljSDuIRAACZvUZpdSE330.jpg

程序总体结构分析

利用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)



wKiom1R8Gouzr7XwAADZJqme35w965.jpg

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


Android逆向Challenge Write Up(1)