h_one 发表于 2014-6-7 16:29

Keygen Challenge

本帖最后由 zxcfvasd 于 2014-6-7 16:29 编辑

【标题】: Keygen Challenge 攻略
【作者】: h_one

也有段时间没有发帖子了,前两天找到Claud大侠推荐的一个android cm,A Keygen Challenge for Android初学android的菜菜也来练练手,今天将自己分析的过程与大家分享下。

这个游戏共有5关:


一.程序流程分析
使用APK改之理反编译 通过AndroidManfest.xml找到入口Activity的smali进行分析。找到oncreate创建下拉列表,共5个选项,并为每个选项绑定一个值,依次是1~5 分别表示难易程度
#levels
    const v6, 0x1090008

    invoke-static {p0, v5, v6}, Landroid/widget/ArrayAdapter;->createFromResource(Landroid/content/Context;II)Landroid/widget/ArrayAdapter;
#ArrayAdapter adapter = ArrayAdapter.createFromResource(R.id.levels, v6);
    move-result-object v0

    .line 64
    .local v0, "adapter":Landroid/widget/ArrayAdapter;, "Landroid/widget/ArrayAdapter<Ljava/lang/CharSequence;>;"
    const v5, 0x1090009

    invoke-virtual {v0, v5}, Landroid/widget/ArrayAdapter;->setDropDownViewResource(I)V

    .line 65
    iget-object v5, p0, Lcom/me/keygen/activity/MainActivity;->levelSelect:Landroid/widget/Spinner;
#adapter.setDropDownViewResource(v5);
    invoke-virtual {v5, p0}, Landroid/widget/Spinner;->setOnItemSelectedListener(Landroid/widget/AdapterView$OnItemSelectedListener;)V

    .line 67
    iget-object v5, p0, Lcom/me/keygen/activity/MainActivity;->levelSelect:Landroid/widget/Spinner;
#创建下拉列表框, 并且位每一个选项绑定一个值
    invoke-virtual {v5, v0}, Landroid/widget/Spinner;->setAdapter(Landroid/widget/SpinnerAdapter;)V

    .line 69
    iget-object v5, p0, Lcom/me/keygen/activity/MainActivity;->submit:Landroid/widget/Button;

    new-instance v6, Lcom/me/keygen/activity/MainActivity$1;

    invoke-direct {v6, p0}, Lcom/me/keygen/activity/MainActivity$1;-><init>(Lcom/me/keygen/activity/MainActivity;)V

    invoke-virtual {v5, v6}, Landroid/widget/Button;->setOnClickListener(Landroid/view/View$OnClickListener;)V
#创建 注册按钮监听事件
    .line 77
进入按钮事件后调用validateSerial() 函数,获取用户名和序列号,然后根据选择的currentChallenge字段来进入不同难度,默认值是1,最后调用接口函数isValid对用户名序列号验证
.line 102
    iget-object v0, p0, Lcom/me/keygen/activity/MainActivity;->name:Landroid/widget/EditText;

    invoke-virtual {v0}, Landroid/widget/EditText;->getText()Landroid/text/Editable;

    move-result-object v0

    invoke-virtual {v0}, Ljava/lang/Object;->toString()Ljava/lang/String;

    move-result-object v7
#String name = this.name.getText().toString();
    .line 103
    .local v7, "name":Ljava/lang/String;
    iget-object v0, p0, Lcom/me/keygen/activity/MainActivity;->serial:Landroid/widget/EditText;

    invoke-virtual {v0}, Landroid/widget/EditText;->getText()Landroid/text/Editable;

    move-result-object v0

    invoke-virtual {v0}, Ljava/lang/Object;->toString()Ljava/lang/String;

    move-result-object v8
#String serial = this.serial.getText().toString();
    .line 104
    .local v8, "serial":Ljava/lang/String;
    iget v0, p0, Lcom/me/keygen/activity/MainActivity;->currentChallenge:I
#currentChallenge 字段 是根据下拉列表选择的按钮,也就是难易程序的选择
    invoke-direct {p0, v0}, Lcom/me/keygen/activity/MainActivity;->getVerifierForChallenge(I)Lcom/me/keygen/verifiers/KeyVerifier;

    move-result-object v6
#KeyVerifier kv = getVerifierForChallenge(this.currentChallenge);
    .line 105
    .local v6, "kv":Lcom/me/keygen/verifiers/KeyVerifier;
    if-nez v6, :cond_0

    .line 107
    const-string v0, "No Verifier Present For This Level"

    invoke-static {p0, v0, v10}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;

    move-result-object v0

    invoke-virtual {v0}, Landroid/widget/Toast;->show()V

    .line 129
    :goto_0
    return-void

    .line 110
    :cond_0
    invoke-interface {v6, v7, v8}, Lcom/me/keygen/verifiers/KeyVerifier;->isValid(Ljava/lang/String;Ljava/lang/String;)Z
#kv.isValid()   调用KeyVerifier接口的isValid对用户名序列号验证
    move-result v9


二.闯关
0x1
第一关很简单,直接看源码,写出注册机搞定


抠出代码写注册机. 我用的C
int main(int argc, char* argv[])
{
    char username = {0};
    printf("input username:");
    scanf("%s", username);

    int i = 0, m = 0;
    int len = strlen(username);
    for (int j = 0; j < len; j++)
    {
      m = username;
      i = m ^ i + m * m;
    }
    printf("%d\n", i);
    getchar();
}


0x2
第二关也比较简单,需要源码与smlia结合看




上图就是算法,首先用户名大于4位,将输入用户名转化成大写,依次取每一位变换求和得到 l , 将求和值转化成字符串str2。这里反编译出现了不可见的字符,对比查看smali,一个是0x30,一个是0x40,相差0x10。j是str2每一位加0x30求和值,因此输入的序列号如果与str2长度相同的话只需要每一位比str都大0x10,这样就可以找到序列号。
根据以上分析写出注册机:
int main(int argc, char* argv[])
{
    char username = {0};
    printf("input username:");
    scanf("%s", username);

   
/************************************************************************/
/*                     challeng1                                        */
/************************************************************************/
/*int i = 0, m = 0;
    int len = strlen(username);
    for (int j = 0; j < len; j++)
    {
      m = username;
      i = m ^ i + m * m;
    }
    printf("%d\n", i);
    getchar();*/



/************************************************************************/
/*                  challeng2                                       */
/************************************************************************/
    int name_len = 0,i = 0;
    name_len = strlen(username);
    char str1 = {0};
    long nameSum = 0;
    for (i = 0; i < name_len; i++)
    {
      if (username >= 'a' && username <= 'z')
   {
            username -= 32;
      }
    }
    for (i = 0; i < name_len; i++)
    {
      nameSum = 3 * (nameSum + username) - 64;
    }

itoa(nameSum, str1, 10);
    char *finalsum = new char;
    for (i = 0; i < strlen(str1); i++)
    {
      finalsum = str1 + 16;
    }
    printf("%s\n", finalsum);
    return 0;
}


0x3
这个比前两个稍微难了些,但也只是java层面的。编译为源码后只能看到一部分java代码了,关键的isVaild函数做了处理,只有老老实实的看smali了.


检查Serial字符串格式
string[] parts = serial.split("-");
if(parts.length != 0x8)
    return;

说明序列号格式是 XXXXX-XXXX-XXXXX-XXXXXX-XXXX-XXXX-XXXX-XXXX形式的
.line 25
    const-string v9, "-"

invoke-virtual {p2, v9}, Ljava/lang/String;->split(Ljava/lang/String;)[Ljava/lang/String;

    move-result-object v5
#String[] parts = serial.split("-");
    .line 26
    .local v5, "parts":[Ljava/lang/String;
    array-length v9, v5

    const/16 v10, 0x8

    if-eq v9, v10, :cond_1
#if(parts.length != 0x8); return ;
    .line 92
    :cond_0
    :goto_0
    return v8

    .line 31
    :cond_1

这是一个for循环 ,8段字符串正则匹配,由const-string v10, "" 可以知道每段4个字符且每个字符为0~9 A~F
serial:xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx(0~9||A~F)
:cond_1
    const/4 v7, 0x0
#int x = 0;
    .local v7, "x":I
    :goto_1
    array-length v9, v5
#v9 = parts.length;
    if-ge v7, v9, :cond_2
#if(v7 >= 0x8)
    .line 33
    aget-object v9, v5, v7

    const-string v10, ""

    invoke-virtual {v9, v10}, Ljava/lang/String;->matches(Ljava/lang/String;)Z
#每段字符串正则匹配   
    move-result v9

    if-eqz v9, :cond_0

    .line 31
    add-int/lit8 v7, v7, 0x1

    goto :goto_1

接下来是两个for循环,将secretBytes 数据写入输出流

ByteArrayOutputStream baos = new ByteArrayOutputStream();
baos.write(0x31);
for(int i = 0; i < secretBytes.length; i+=2)
{
   baos.write(secretBytes);
   baos.write(i+1);
}
for(int i = 1; i < secretBytes.length; i += 2)
{
   baos.write(secretByte);
   baos.write(i+1);
}


.line 39
    :cond_2
    new-instance v0, Ljava/io/ByteArrayOutputStream;
#ByteArrayOutputStream baos = new ByteArrayOutputStream();
    invoke-direct {v0}, Ljava/io/ByteArrayOutputStream;-><init>()V

    .line 41
    .local v0, "baos":Ljava/io/ByteArrayOutputStream;
    const/16 v9, 0x31

    invoke-virtual {v0, v9}, Ljava/io/ByteArrayOutputStream;->write(I)V
#baos.write(0x31);
    .line 43
    const/4 v7, 0x0
#int i = 0;
    :goto_2
    iget-object v9, p0, Lcom/me/keygen/verifiers/challenge/Challenge3Verifier;->secretBytes:[B

    array-length v9, v9

    if-ge v7, v9, :cond_3
# i < secretBytes.length;
    .line 45
    iget-object v9, p0, Lcom/me/keygen/verifiers/challenge/Challenge3Verifier;->secretBytes:[B

    aget-byte v9, v9, v7

    invoke-virtual {v0, v9}, Ljava/io/ByteArrayOutputStream;->write(I)V
#baos.write(secretBytes);
    .line 46
    add-int/lit8 v9, v7, 0x1

    invoke-virtual {v0, v9}, Ljava/io/ByteArrayOutputStream;->write(I)V
#baos.write(i+1);
    .line 43
    add-int/lit8 v7, v7, 0x2
#i+=2;
    goto :goto_2

    .line 49
    :cond_3
    const/4 v7, 0x1
##int v7 = 1;
    :goto_3
    iget-object v9, p0, Lcom/me/keygen/verifiers/challenge/Challenge3Verifier;->secretBytes:[B

    array-length v9, v9

    if-ge v7, v9, :cond_4
# v7 > secretBytes.length;
    .line 51
    iget-object v9, p0, Lcom/me/keygen/verifiers/challenge/Challenge3Verifier;->secretBytes:[B

    aget-byte v9, v9, v7

    invoke-virtual {v0, v9}, Ljava/io/ByteArrayOutputStream;->write(I)V
#baos.write(secretByte);
    .line 52
    add-int/lit8 v9, v7, 0x1

    invoke-virtual {v0, v9}, Ljava/io/ByteArrayOutputStream;->write(I)V
#baos.write(i+1);
    .line 49
    add-int/lit8 v7, v7, 0x2

    goto :goto_3接下来一个for循环是将序列号的前4段部分 经处理依次写入标准输出流

baos.write(0x30);
baos.write(0x30);
for(int = 0; i < parts.length/2; i++)
{
    baos.write(parts.Charset.forName("US_ASCII"));
baos.wirte("-");
}
baos.write(secretBytes);

.line 55
    :cond_4
    invoke-virtual {v0, v11}, Ljava/io/ByteArrayOutputStream;->write(I)V
#baos.write(0x30);
    .line 56
    invoke-virtual {v0, v11}, Ljava/io/ByteArrayOutputStream;->write(I)V
#baos.write(0x30);
    .line 58
    const/4 v7, 0x0
#i = 0;
    :goto_4
    array-length v9, v5 #v5存放是注册码数组(parts)
#0x8 = parts.length
    div-int/lit8 v9, v9, 0x2
#v9 = 4; 获取序列号的前四段
    if-ge v7, v9, :cond_5
#i < 4
    .line 62
    :try_start_0
    aget-object v9, v5, v7
#parts
    const-string v10, "US_ASCII"

    invoke-static {v10}, Ljava/nio/charset/Charset;->forName(Ljava/lang/String;)Ljava/nio/charset/Charset;

    move-result-object v10

    invoke-virtual {v9, v10}, Ljava/lang/String;->getBytes(Ljava/nio/charset/Charset;)[B

    move-result-object v9

    invoke-virtual {v0, v9}, Ljava/io/ByteArrayOutputStream;->write([B)V
#baos.write(parts.Charset.forName("US_ASCII"));
    .line 63
    const/16 v9, 0x2d
#紧接着写入“-”
    invoke-virtual {v0, v9}, Ljava/io/ByteArrayOutputStream;->write(I)V
    :try_end_0
    .catch Ljava/lang/Exception; {:try_start_0 .. :try_end_0} :catch_0

    .line 58
    add-int/lit8 v7, v7, 0x1
#i++;
    goto :goto_4

    .line 64
    :catch_0
    move-exception v1

    .local v1, "e":Ljava/lang/Exception;
    goto :goto_0

    .line 68
    .end local v1    # "e":Ljava/lang/Exception;
    :cond_5
    :try_start_1
    iget-object v9, p0, Lcom/me/keygen/verifiers/challenge/Challenge3Verifier;->secretBytes:[B
#baos.write(secretBytes);
    invoke-virtual {v0, v9}, Ljava/io/ByteArrayOutputStream;->write([B)V
    :try_end_1
    .catch Ljava/lang/Exception; {:try_start_1 .. :try_end_1} :catch_1

接下来将前面准备的标准输出流数据经行MD5摘要,最后经过MD5处理后得到的32位字符串会保存在v2寄存器上,此处我们可以smali注入查看

byte[] result = new byte;
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(baos.toByteArray());
result = md.digest();
String sFinalKey = bytesToHex(md.digest()).toUpperCase();

.line 72
    const/16 v9, 0x20

    new-array v6, v9, [B
#byte[] result = new byte;
    .line 75
    .local v6, "result":[B
    :try_start_2
    const-string v9, "MD5"

    invoke-static {v9}, Ljava/security/MessageDigest;->getInstance(Ljava/lang/String;)Ljava/security/MessageDigest;

    move-result-object v4
#MessageDigest md = MessageDigest.getInstance("MD5");
    .line 76
    .local v4, "md":Ljava/security/MessageDigest;
    invoke-virtual {v0}, Ljava/io/ByteArrayOutputStream;->toByteArray()[B

    move-result-object v9

    invoke-virtual {v4, v9}, Ljava/security/MessageDigest;->update([B)V
#md.update(baos.toByteArray());
    .line 77
    invoke-virtual {v4}, Ljava/security/MessageDigest;->digest()[B
    :try_end_2
    .catch Ljava/lang/Exception; {:try_start_2 .. :try_end_2} :catch_2

    move-result-object v6
#result = md.digest();
    .line 80
    invoke-static {v6}, Lcom/me/keygen/verifiers/challenge/Challenge3Verifier;->bytesToHex([B)Ljava/lang/String;

    move-result-object v9

    invoke-virtual {v9}, Ljava/lang/String;->toUpperCase()Ljava/lang/String;
#String sFinalKey = bytesToHex(md.digest()).toUpperCase();
    move-result-object v2   #此处的v2就是最终MD5处理后的字符串,在这里我们可以进行Smila注入,查看v2值


将前面得到的MD5字符串,取偶数位,依次和输入序列号的后16位比较。因此我们可以在前面经行Smali注入,取出偶数位则是正确序列号.line 81
    .local v2, "foo":Ljava/lang/String;
    invoke-virtual {p2}, Ljava/lang/String;->length()I

    move-result v9

div-int/lit8 v9, v9, 0x2
#v9 = serial.length/2
    invoke-virtual {p2, v9}, Ljava/lang/String;->substring(I)Ljava/lang/String;

    move-result-object v3
#lastHalf = serial.substring(serial.length/2); 取输入序列号后半部分
    .line 82
    .local v3, "lastHalf":Ljava/lang/String;
    const-string v9, "-"

    const-string v10, ""

    invoke-virtual {v3, v9, v10}, Ljava/lang/String;->replaceAll(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
#lastHalf.replaceAll("-", ""); 去除后半部分序列号中的“-”
    move-result-object v3

    .line 84
    const/4 v7, 0x0
#int i = 0;
    :goto_5
    invoke-virtual {v2}, Ljava/lang/String;->length()I

    move-result v9

    if-ge v7, v9, :cond_6
#i < sFinalString.length();
    .line 86
    invoke-virtual {v2, v7}, Ljava/lang/String;->charAt(I)C

    move-result v9
#v9 = sFinalString.charAt(i);

    div-int/lit8 v10, v7, 0x2
#v10 = i/2;
    invoke-virtual {v3, v10}, Ljava/lang/String;->charAt(I)C
#v10 = lastHalf.charAt(v10);
    move-result v10

    if-ne v9, v10, :cond_0
    #关键比较,将serial前半部分处理后的数据,与后半部分比较
    #sFinalString是经过MD5处理后32的字符串, lastHalf是序列号后半部分且去除“-”的字符串
    #sFinalString cmp lastHalf
    .line 84
    add-int/lit8 v7, v7, 0x2
#i += 2;
    goto :goto_5

    .line 69
    .end local v2    # "foo":Ljava/lang/String;
    .end local v3    # "lastHalf":Ljava/lang/String;
    .end local v4    # "md":Ljava/security/MessageDigest;
    .end local v6    # "result":[B
    :catch_1
    move-exception v1

    .restart local v1    # "e":Ljava/lang/Exception;
    goto/16 :goto_0

    .line 78
    .end local v1    # "e":Ljava/lang/Exception;
    .restart local v6    # "result":[B
    :catch_2
    move-exception v1

    .restart local v1    # "e":Ljava/lang/Exception;
    goto/16 :goto_0
算法到此结束可以发现整个过程与用户名毫无关系。
smlia 注入
java代码:
log.v("H_ONE3", v2);

smlia代码:
const-string v12, "H_ONE3"
invoke-static{v12, v2}, Landroid/util/Log;->v(Ljava/lang/String;Ljava/lang/String;)I

输入序列号1111-1111-1111-1111-2222-2222-2222-2222,然后logcat 查看.

序列号为1111-1111-1111-1111-890D-0171-0C54-E3A1 通关!


0x4
这个也是java层代码,且没有任何混淆和反调试,可以直接得到源码



发现又是一大坨算法. 这些都是浮云. 注意上面圈出的
localBigInteger1是经过一些列的算法处理paramString1后得到的结果,localBigInteger2是序列号字符串转化成BigInteger类型数据
boolean bool = localBigInteger1.xor(localBigInteger2).equals(new BigInteger("0"));一句可以说明localBigInteger1就是正确的序列号。啥也不说了,直接注入干掉。

对应的smali代码以及注入
.line 62
    .local v5, "serialInt":Ljava/math/BigInteger;
    sget-object v7, Ljava/lang/System;->out:Ljava/io/PrintStream;
#BigInteger serialInt = new BigInteger(serial);
    invoke-virtual {v2}, Ljava/math/BigInteger;->toString()Ljava/lang/String;
#bigOne.toString();
    move-result-object v8 #key

   
#此处smlia注入
    const-string v10, "H_ONE4"
    invoke-static{v10, v8},Landroid/util/Log;->v(Ljava/lang/String;Ljava/lang/String;)I
   
    invoke-virtual {v7, v8}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
#PrintStream.println(bigOne.toString);
    .line 63
    invoke-virtual {v2, v5}, Ljava/math/BigInteger;->xor(Ljava/math/BigInteger;)Ljava/math/BigInteger;
#bigOne.xor(serialInt)
    move-result-object v7
#v7 = bigOne.xor(serialInt)
    new-instance v8, Ljava/math/BigInteger;
   
    const-string v9, "0"

    invoke-direct {v8, v9}, Ljava/math/BigInteger;-><init>(Ljava/lang/String;)V
#BigInteger v8 = new BigInteger("0"); v8也就是数字0
    invoke-virtual {v7, v8}, Ljava/math/BigInteger;->equals(Ljava/lang/Object;)Z
    :try_end_0
    .catch Ljava/lang/Exception; {:try_start_0 .. :try_end_0} :catch_0
#BigOne xor serialInt等于0说明BigOne 等于 serialInt而serialInt就是输入的字符串
#所以此处可smila注入
    move-result v7

    .line 64
    .end local v5    # "serialInt":Ljava/math/BigInteger;
    :goto_2
    return v7
eg:
用户名:h_one
序列号: 94449870367585271808



0x5
第5关同样只有是java层代码,同样可以编译出源码,但是编译出的源码有时流程会出问题,说以关键地放最好还是对比smali看
比如:


上面圈出地方,编译的源码对比smali看可以发现出错了

第五关也就从这里分析起走。设备ID作为Challeng5Verifier参数传入,并由id变量保存,接下来分析接口函数isValid分析

好吧又可以直接注入上面Long.toString(l); 则是对应的正确序列号
对应smlia以及注入
   .line 52
    .end local v8    # "y":I
    :cond_2
    iget-object v9, p0, Lcom/me/keygen/verifiers/challenge/Challenge5Verifier;->matrix:[[I

    invoke-direct {p0, v9}, Lcom/me/keygen/verifiers/challenge/Challenge5Verifier;->normalizeMatrix([[I)V

    .line 53
    iget-object v9, p0, Lcom/me/keygen/verifiers/challenge/Challenge5Verifier;->matrix:[[I

    invoke-direct {p0, v9}, Lcom/me/keygen/verifiers/challenge/Challenge5Verifier;->mst([[I)J

    move-result-wide v5
#long val = mst(this.matrix);   这个就是经过用户名处理后得到的正常序列号

    .line 54
    .local v5, "val":J
    sget-object v9, Ljava/lang/System;->out:Ljava/io/PrintStream;

    invoke-virtual {v9, v5, v6}, Ljava/io/PrintStream;->println(J)V

    .line 55
    invoke-direct {p0, p1, p2}, Lcom/me/keygen/verifiers/challenge/Challenge5Verifier;->makeKey(Ljava/lang/String;Ljava/lang/String;)[B

    move-result-object v3
#byte[] input = makeKey(name, serial);
    .line 56
    .local v3, "input":[B
    invoke-static {v5, v6}, Ljava/lang/Long;->toString(J)Ljava/lang/String;

    move-result-object v9
# 这里经行注入
    const-string v11, "H_ONE5"
    invoke-static{v11, v9}, Landroid/util/Log;->v(Ljava/lang/String;Ljava/lang/String;)I
   
    invoke-direct {p0, p1, v9}, Lcom/me/keygen/verifiers/challenge/Challenge5Verifier;->makeKey(Ljava/lang/String;Ljava/lang/String;)[B

    move-result-object v2
eg:
用户名:h_one


三.总结:个人觉得andoird程序若只是在java层面编写的,对于一个crack来说相当好搞。核心代码以及反调试最好用c/c++编写,NDK编译.so链接库。



h_one 发表于 2014-6-13 16:52

zc123 发表于 2014-6-11 19:29
请问2楼的附件是未修改的吗

对,没有修改过的.

zaq4736 发表于 2014-6-13 12:57

教程很详细。值得学习!

小野 发表于 2014-6-12 17:58

顶。,膜拜大大

刘宏伟大人丶 发表于 2014-6-12 17:17

给跪了 - - 膜拜大神

h_one 发表于 2014-6-7 16:33

附件上传

羅少 发表于 2014-6-7 17:01

@Hmily   必须加精!!!

ii丶BigBreast 发表于 2014-6-7 17:01

板凳,围观大牛

Hmily 发表于 2014-6-7 17:39

羅少 发表于 2014-6-7 17:01
@Hmily   必须加精!!!

这个@小试锋芒 版主吧。

闹够了没有 发表于 2014-6-7 18:00

膜拜大大

h_one 发表于 2014-6-7 22:39

羅少 发表于 2014-6-7 17:01
@Hmily   必须加精!!!

哈哈哈,,,{:1_927:}

h_one 发表于 2014-6-7 22:41

闹够了没有 发表于 2014-6-7 18:00
膜拜大大

这个cm游戏挺不错的.

D13 发表于 2014-6-9 20:34

围观大大。。

JoyChou 发表于 2014-6-9 22:58

哈哈。应该给个pediy链接
页: [1] 2 3 4 5 6
查看完整版本: Keygen Challenge