今天找到肖大前段时间推荐的一个链接,关于Android的一个apk,详见:
A Keygen Challenge for Android http://www.kanxue.com/bbs/showthread.php?t=185648
目前就玩到了第三关,特发此贴记录一下过程,有兴趣的一起玩起~
●
[*]Beginner
[*]Easy
[*]Hard
首先先对界面做一个展示吧,该软件分为5个等级,每个等级适用了不同的算法,当然难度随着等级提高也会提高.前两个算法还是挺简单的,各大牛可以忽略~
1.Beginner
第一关较为简单,采用VTS直接逆向并查看java源码可以找到对应的算法:
string1,string2分别表示输入的用户名还有序列号
根据这段源码,我们很快就确定了算法1的计算流程。首先根据输入的用户名,取用户名的每一位进行一些操作,之后 int k = Integer.parseInt(paramString2),意味着序列号是数字,最后判断是否通过验证就是判断k与i(用户名运算的值)是否相等。
那么注册机也就很简单了,直接复制出来,输入用户名把i输出就是序列号了
import java.io.IOException;
import java.util.Scanner;
//xbalien 78388
public class Challenge1Verifier {
public static void main(String[] args) throws IOException{
Scanner in = new Scanner(System.in);
String userName = in.next();
key(userName);
}
public static void key(String paramString1){
int i = 0;
for (int j = 0; j < paramString1.length(); j++){
int m = paramString1.charAt(j);
i = m ^ i + m * m;
}
System.out.println(i);
}
}
2.Easy
第二关需要结合smali语法,才能解决,光看源码会有一些小细节不好处理~
算法很容易看懂,首先用户名要大于4位,将用户名转为大写字母后,取其中每一位进行运算,生成一个中间变量str2,之后对str2中的每一个字符进行运算,算出一个结果j。
而对于输入的序列,也参与了运算并重j中不断的减去其计算值,最后结果当j变为0的时候判别为输入正确。
当然,由于反编译结果,出现了一个不可见的字符,因此需要通过smali确定两者分别相加的到底是什么值。一个为0x30一个为0x40,两者相差16,因此输入的序列如果与str2长度相同的话只需要每一位比str2都大0x10,就可以找到正确序列,因此算法也就很简单了:
import java.io.IOException;
import java.util.Scanner;
//xbalien AIECIE
public class Challenge2Verifier {
public static void main(String[] args) throws IOException{
Scanner in = new Scanner(System.in);
String userName = in.next();
key(userName);
}
public static void key(String paramString1)
{
if (paramString1.length() >= 4){
String str1 = paramString1.toUpperCase();
long l = 0L;
for (int i = 0; i < str1.length(); i++)
l = 3L * (l + str1.charAt(i)) - 64L;
String str2 = Long.toString(l);
System.out.println(str2);
byte keyByte[] = new byte[str2.length()];
for (int k = 0; k < str2.length(); k++){
keyByte[k] = (byte) (str2.charAt(k) + 16);
}
System.out.println(new String(keyByte));
}
}
}
3.Hard
这一题开始相对就有一些复杂了,因为编译为源码后只能看到一部分,因此其他部分就是分析smali了。
能看到就是public static String bytesToHex(byte[] paramArrayOfByte)函数,以及一些内置的关键值
byte[] secretBytes = this.secretKey.getBytes(Charset.forName("US-ASCII"));
String secretKey = "KeygenChallengeNumber3";
那么关键就是分析public boolean isValid(String paramString1, String paramString2)了,暂时没有找到更好的工具查看跳转之类的,于是使用了IDA作为静态分析工具,因此载入后进入isValid函数,进行分析:
CODE:00002880 Method 68 (0x44):
CODE:00002880 public boolean
CODE:00002880 com.me.keygen.verifiers.challenge.Challenge3Verifier.isValid(
CODE:00002880 java.lang.String p0,
CODE:00002880 java.lang.String p1)
CODE:00002880 this = v12
CODE:00002880 p0 = v13
CODE:00002880 p1 = v14
CODE:00002880 const/16 v11, 0x30
CODE:00002884 const/4 v8, 0
CODE:00002886 const-string v9, asc_3BC2 # "-"
CODE:0000288A invoke-virtual {p1, v9}, <ref String.split(ref) imp. @ _def_String_split@LL> # ;输入的序列号需要有-作为分隔符
CODE:00002890 move-result-object v5 # ;分割后的每一段存放在v5
CODE:00002892 array-length v9, v5
CODE:00002894 const/16 v10, 8
CODE:00002898 if-eq v9, v10, loc_289E # ;前提必须有8段,7个-的输入序列才能通过
CODE:0000289C
CODE:0000289C locret: # CODE XREF: Challenge3Verifier_isValid@ZLL+36j
CODE:0000289C # Challenge3Verifier_isValid@ZLL+F0j ...
CODE:0000289C return v8
CODE:0000289E # ---------------------------------------------------------------------------
CODE:0000289E
CODE:0000289E loc_289E: # CODE XREF: Challenge3Verifier_isValid@ZLL+18j
CODE:0000289E const/4 v7, 0
CODE:000028A0
CODE:000028A0 loc_28A0: # CODE XREF: Challenge3Verifier_isValid@ZLL+3Ej
CODE:000028A0 array-length v9, v5
CODE:000028A2 if-ge v7, v9, loc_28C0
CODE:000028A6 aget-object v9, v5, v7
CODE:000028AA const-string v10, a09aF09aF09aF09 # "[0-9A-F][0-9A-F][0-9A-F][0-9A-F]"
CODE:000028AE invoke-virtual {v9, v10}, <boolean String.matches(ref) imp. @ _def_String_matches@ZL> # ;匹配正则表达式(每一段为4个,且要满足)
CODE:000028B4 move-result v9
CODE:000028B6 if-eqz v9, locret
CODE:000028BA add-int/lit8 v7, v7, 1
CODE:000028BE goto loc_28A0
CODE:000028C0 # ---------------------------------------------------------------------------
CODE:000028C0
CODE:000028C0 loc_28C0: # CODE XREF: Challenge3Verifier_isValid@ZLL+22j
CODE:000028C0 new-instance v0, <t: ByteArrayOutputStream>
CODE:000028C4 invoke-direct {v0}, <void ByteArrayOutputStream.<init>() imp. @ _def_ByteArrayOutputStream__init_@V> # ;新建字节数组输出流
CODE:000028CA const/16 v9, 0x31
CODE:000028CE invoke-virtual {v0, v9}, <void ByteArrayOutputStream.write(int) imp. @ _def_ByteArrayOutputStream_write@VI> # ;49写入字节数组流中
CODE:000028D4 const/4 v7, 0
CODE:000028D6
CODE:000028D6 loc_28D6: # CODE XREF: Challenge3Verifier_isValid@ZLL+7Cj
CODE:000028D6 iget-object v9, this, Challenge3Verifier_secretBytes # ;取出关键运算值,secretBytes,进行适当的算法,写入字节数组流
CODE:000028DA array-length v9, v9
CODE:000028DC if-ge v7, v9, loc_28FE
CODE:000028E0 iget-object v9, this, Challenge3Verifier_secretBytes
CODE:000028E4 aget-byte v9, v9, v7
CODE:000028E8 invoke-virtual {v0, v9}, <void ByteArrayOutputStream.write(int) imp. @ _def_ByteArrayOutputStream_write@VI>
CODE:000028EE add-int/lit8 v9, v7, 1
CODE:000028F2 invoke-virtual {v0, v9}, <void ByteArrayOutputStream.write(int) imp. @ _def_ByteArrayOutputStream_write@VI>
CODE:000028F8 add-int/lit8 v7, v7, 2
CODE:000028FC goto loc_28D6 # ;取出关键运算值,secretBytes,进行适当的算法,写入字节数组流
CODE:000028FE # ---------------------------------------------------------------------------
CODE:000028FE
CODE:000028FE loc_28FE: # CODE XREF: Challenge3Verifier_isValid@ZLL+5Cj
CODE:000028FE const/4 v7, 1
CODE:00002900
CODE:00002900 loc_2900: # CODE XREF: Challenge3Verifier_isValid@ZLL+A6j
CODE:00002900 iget-object v9, this, Challenge3Verifier_secretBytes # ;再次取出关键运算值,secretBytes,进行适当的算法,写入字节数组流
CODE:00002904 array-length v9, v9
CODE:00002906 if-ge v7, v9, loc_2928
CODE:0000290A iget-object v9, this, Challenge3Verifier_secretBytes
CODE:0000290E aget-byte v9, v9, v7
CODE:00002912 invoke-virtual {v0, v9}, <void ByteArrayOutputStream.write(int) imp. @ _def_ByteArrayOutputStream_write@VI>
CODE:00002918 add-int/lit8 v9, v7, 1
CODE:0000291C invoke-virtual {v0, v9}, <void ByteArrayOutputStream.write(int) imp. @ _def_ByteArrayOutputStream_write@VI>
CODE:00002922 add-int/lit8 v7, v7, 2
CODE:00002926 goto loc_2900 # ;再次取出关键运算值,secretBytes,进行适当的算法,写入字节数组流
CODE:00002928 # ---------------------------------------------------------------------------
CODE:00002928
CODE:00002928 loc_2928: # CODE XREF: Challenge3Verifier_isValid@ZLL+86j
CODE:00002928 invoke-virtual {v0, v11}, <void ByteArrayOutputStream.write(int) imp. @ _def_ByteArrayOutputStream_write@VI>
CODE:0000292E invoke-virtual {v0, v11}, <void ByteArrayOutputStream.write(int) imp. @ _def_ByteArrayOutputStream_write@VI> # ;写入两个48
CODE:00002934 const/4 v7, 0
CODE:00002936
前面这一段算法很好理解,首先,用户输入的序列号需要有-,一共需要有八段,也就是七个-存在;之后对于每一段要匹配正则表达式"[0-9A-F][0-9A-F][0-9A-F][0-9A-F]"。也就是说每段需要4位数,且满足0-9A-F;之后取出之前的关键值进行处理之后写入字节数组流里。
那么关键在于字节数组流里面的处理是怎么处理的,为了方便查看,接下来使用动态调试查看,我采用apktool+eclipse,进行动态调试,可以根据寄存器查看结果:
根据这里,我在结合静态分析的内容,大致可以看出,前一部分字节数组流存放的分布为:
49 se[0] 1 se[2] 3 …… se[20] 21 se[1] 2 se[3] 4 …… se[21] 22之后再加入了48 48
接下来又一个字节数组流的处理:
CODE:00002936 loc_2936: # CODE XREF: Challenge3Verifier_isValid@ZLL+ECj
CODE:00002936 array-length v9, v5 # ;取出输入序列中的前4段xxxx-xxxx-xxxx-xxxx-写入字节数组流
CODE:00002938 div-int/lit8 v9, v9, 2
CODE:0000293C if-ge v7, v9, loc_2972
CODE:00002940 # try 0x2940-0x2968:
CODE:00002940 aget-object v9, v5, v7
CODE:00002944 const-string v10, aUs_ascii # "US_ASCII"
CODE:00002948 invoke-static {v10}, <ref Charset.forName(ref) imp. @ _def_Charset_forName@LL>
CODE:0000294E move-result-object v10
CODE:00002950 invoke-virtual {v9, v10}, <ref String.getBytes(ref) imp. @ _def_String_getBytes@LL>
CODE:00002956 move-result-object v9
CODE:00002958 invoke-virtual {v0, v9}, <void ByteArrayOutputStream.write(ref) imp. @ _def_ByteArrayOutputStream_write@VL>
CODE:0000295E const/16 v9, 0x2D
CODE:00002962 invoke-virtual {v0, v9}, <void ByteArrayOutputStream.write(int) imp. @ _def_ByteArrayOutputStream_write@VI>
CODE:00002968 add-int/lit8 v7, v7, 1
CODE:0000296C goto loc_2936 # ;取出输入序列中的前4段xxxx-xxxx-xxxx-xxxx-写入字节数组流
CODE:0000296E # ---------------------------------------------------------------------------
CODE:0000296E # catch Exception:
CODE:0000296E move-exception v1
CODE:00002970 goto locret
CODE:00002972 # ---------------------------------------------------------------------------
CODE:00002972 # try 0x2972-0x297C:
CODE:00002972
CODE:00002972 loc_2972: # CODE XREF: Challenge3Verifier_isValid@ZLL+BCj
CODE:00002972 iget-object v9, this, Challenge3Verifier_secretBytes
CODE:00002976 invoke-virtual {v0, v9}, <void ByteArrayOutputStream.write(ref) imp. @ _def_ByteArrayOutputStream_write@VL>
存入 xxxx-xxxx-xxxx-xxxx- 对应的字节流 和 se全部字节流
在接下去:
CODE:00002972 loc_2972: # CODE XREF: Challenge3Verifier_isValid@ZLL+BCj
CODE:00002972 iget-object v9, this, Challenge3Verifier_secretBytes
CODE:00002976 invoke-virtual {v0, v9}, <void ByteArrayOutputStream.write(ref) imp. @ _def_ByteArrayOutputStream_write@VL> # ;将secretBytes也一同写入字节数组流
CODE:0000297C const/16 v9, 0x20
CODE:00002980 new-array v6, v9, <t: byte[]>
CODE:00002984 # try 0x2984-0x29A4:
CODE:00002984 const-string v9, aMd5 # ;对之前写入字节数组流的bytes进行MD5计算
CODE:00002988 invoke-static {v9}, <ref MessageDigest.getInstance(ref) imp. @ _def_MessageDigest_getInstance@LL>
CODE:0000298E move-result-object v4
CODE:00002990 invoke-virtual {v0}, <ref ByteArrayOutputStream.toByteArray() imp. @ _def_ByteArrayOutputStream_toByteArray@L>
CODE:00002996 move-result-object v9
CODE:00002998 invoke-virtual {v4, v9}, <void MessageDigest.update(ref) imp. @ _def_MessageDigest_update@VL>
CODE:0000299E invoke-virtual {v4}, <ref MessageDigest.digest() imp. @ _def_MessageDigest_digest@L>
CODE:000029A4 move-result-object v6
CODE:000029A6 invoke-static {v6}, <ref Challenge3Verifier.bytesToHex(ref) Challenge3Verifier_bytesToHex@LL> # ;对于算好的MD5字节调用自定义的bytesToHex
CODE:000029AC move-result-object v9
CODE:000029AE invoke-virtual {v9}, <ref String.toUpperCase() imp. @ _def_String_toUpperCase@L> # ;转换为大写
CODE:000029B4 move-result-object v2
CODE:000029B6 invoke-virtual {p1}, <int String.length() imp. @ _def_String_length@I>
CODE:000029BC move-result v9
CODE:000029BE div-int/lit8 v9, v9, 2
CODE:000029C2 invoke-virtual {p1, v9}, <ref String.substring(int) imp. @ _def_String_substring@LI>
CODE:000029C8 move-result-object v3
CODE:000029CA const-string v9, asc_3BC2 # "-"
CODE:000029CE const-string v10, empty_str
CODE:000029D2 invoke-virtual {v3, v9, v10}, <ref String.replaceAll(ref, ref) imp. @ _def_String_replaceAll@LLL> # ;选取输入的序列后面4段,同时把-去掉,排成一个16字节
CODE:000029D8 move-result-object v3
CODE:000029DA const/4 v7, 0
到这一步,我们知道了该算法根据输入序列号的前4段计算了一个值,那么后面就是判断部分了,根据输入序列的后4段,判断是否正确
CODE:000029DC loc_29DC: # CODE XREF: Challenge3Verifier_isValid@ZLL+184j
CODE:000029DC invoke-virtual {v2}, <int String.length() imp. @ _def_String_length@I>
CODE:000029E2 move-result v9
CODE:000029E4 if-ge v7, v9, loc_2A12 # ;进行比较,若都相等,V8为1也就是正确
CODE:000029E8 invoke-virtual {v2, v7}, <char String.charAt(int) imp. @ _def_String_charAt@CI> # ;取经过计算后的,处于偶数位置的字符
CODE:000029EE move-result v9
CODE:000029F0 div-int/lit8 v10, v7, 2
CODE:000029F4 invoke-virtual {v3, v10}, <char String.charAt(int) imp. @ _def_String_charAt@CI> # ;去输入序列后4段去掉-的字符的每一个字符
CODE:000029FA move-result v10
CODE:000029FC if-ne v9, v10, locret
CODE:00002A00 add-int/lit8 v7, v7, 2
CODE:00002A04 goto loc_29DC
CODE:00002A06 # ---------------------------------------------------------------------------
CODE:00002A06 # catch Exception:
CODE:00002A06 move-exception v1
CODE:00002A08 goto/16 locret
CODE:00002A0C # ---------------------------------------------------------------------------
CODE:00002A0C # catch Exception:
CODE:00002A0C move-exception v1
CODE:00002A0E goto/16 locret
CODE:00002A12 # ---------------------------------------------------------------------------
CODE:00002A12
CODE:00002A12 loc_2A12: # CODE XREF: Challenge3Verifier_isValid@ZLL+164j
CODE:00002A12 const/4 v8, 1 # ;进行比较,若都相等,V8为1也就是正确
CODE:00002A14 goto/16 locret
CODE:00002A14 Method End
到这一部分,我们基本已经了解这整个算法的流程。其中发现用户名是没有参与运算的。
那么开始写注册算法吧(我就假定了前4段字符为0123-4567-89AB-CDEF-,开始计算):
public class Challenge3Verifier {
static String preKey = "0123-4567-89AB-CDEF-";
static byte[] preKeyBytes = preKey.getBytes(Charset.forName("US-ASCII"));
static String secretKey = "KeygenChallengeNumber3";
static byte[] secretBytes = secretKey.getBytes(Charset.forName("US-ASCII"));
public static String bytesToHex(byte[] paramArrayOfByte){
char[] arrayOfChar1 = { 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 65, 66, 67, 68, 69, 70 };
char[] arrayOfChar2 = new char[2 * paramArrayOfByte.length];
for (int i = 0; i < paramArrayOfByte.length; i++){
int j = 0xFF & paramArrayOfByte[i];
arrayOfChar2[(i * 2)] = arrayOfChar1[(j >>> 4)];
arrayOfChar2[(1 + i * 2)] = arrayOfChar1[(j & 0xF)];
}
return new String(arrayOfChar2);
}
public static void key() throws IOException, NoSuchAlgorithmException {
byte[] mdkey;
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byteArrayOutputStream.write(49);
for (int i = 0; i < secretBytes.length; i += 2) {
byteArrayOutputStream.write(secretBytes[i]);
byteArrayOutputStream.write(i+1);
}
for (int i = 1; i < secretBytes.length; i += 2) {
byteArrayOutputStream.write(secretBytes[i]);
byteArrayOutputStream.write(i+1);
}
byteArrayOutputStream.write(48);
byteArrayOutputStream.write(48);
byteArrayOutputStream.write(preKeyBytes);
byteArrayOutputStream.write(secretBytes);
MessageDigest md5 = MessageDigest.getInstance("MD5");
md5.update(byteArrayOutputStream.toByteArray());
mdkey = md5.digest();
String preFinalKey = bytesToHex(mdkey);
preFinalKey.toUpperCase();
byte keyByte[] = new byte[16];
int j = 0;
for (int i = 0 ; i < preFinalKey.length(); i += 2) {
keyByte[j] = (byte) preFinalKey.charAt(i);
j ++;
}
System.out.println(preFinalKey);
String key = new String(keyByte);
String finalKey = preKey;
for (int i = 0; i <= key.length() - 4; i += 4) {
finalKey += key.substring(i, i+4);
if (i != key.length() - 4) {
finalKey += '-';
}
}
System.out.println(finalKey);
}
public static void main(String[] args) {
try {
key();
} catch (NoSuchAlgorithmException | IOException e) {
e.printStackTrace();
}
}
}
最后算出来key为0123-4567-89AB-CDEF-327D-8367-2F13-AAD9,成功通过。
ps:由于还是处于java层面,因此注册机也比通过看汇编写来的容易了,第一次尝试玩着玩意,如有不对请指出~
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
上传的附件: