Android逆向学习-攻防世界

发布于 2021-03-22  1595 次阅读


0x00 app1

下载文件,拖入AndroidKiller,查看AndroidManifest.xml文件,找到入口:

查看java源码,找到关键点,输入的内容要和versionName逐位与versionCode异或的结果相等。

在BuildConfig中找到版本信息,就可以写脚本了。

name = "X<cP[?PHNB<P?aj"
code = 15
flag = ''
for i in range(len(name)):
    flag += chr(ord(name[i]) ^ code)

print(flag)

0x01 app2

第二题比起第一题就难了,首先看onClick,对c和d进行了非空判断,结合将该apk在模拟器里运行,不难得知c和d就是我们进行的两个输入。然后将其拼接了起来,传给了SecondActivity这个活动,接下来查看SecondActivity。

  private EditText c;
  private EditText d;
  public void onClick(View paramView)
  {
    switch (paramView.getId())
    {
    default:
      return;
    case 2131165187:
    }
    if ((this.c.getText().length() == 0) || (this.d.getText().length() == 0))
    {
      Toast.makeText(this, "不能为空", 1).show();
      return;
    }
    paramView = this.c.getText().toString();
    String str = this.d.getText().toString();
    Log.e("test", paramView + " test2 = " + str);
    Intent localIntent = new Intent(this, SecondActivity.class);
    localIntent.putExtra("ili", paramView);
    localIntent.putExtra("lil", str);
    startActivity(localIntent);
  }

查看onCreate(),这里有一句 Encryto.doRawData(this, paramBundle + str).equals("VEIzd/V2UPYNdn/bxH3Xig==") ,将我们传入的数据进行了加密,然后和 "VEIzd/V2UPYNdn/bxH3Xig==" 相等,于是我们跟进查看class Encryto。

  protected void onCreate(Bundle paramBundle)
  {
    super.onCreate(paramBundle);
    setContentView(2130903041);
    Object localObject = getIntent();
    paramBundle = ((Intent)localObject).getStringExtra("ili");
    String str = ((Intent)localObject).getStringExtra("lil");
    if (Encryto.doRawData(this, paramBundle + str).equals("VEIzd/V2UPYNdn/bxH3Xig=="))
    {
      ((Intent)localObject).setAction("android.test.action.MoniterInstallService");
      ((Intent)localObject).setClass(this, MoniterInstallService.class);
      ((Intent)localObject).putExtra("company", "tencent");
      ((Intent)localObject).putExtra("name", "hacker");
      ((Intent)localObject).putExtra("age", 18);
      startActivity((Intent)localObject);
      startService((Intent)localObject);
    }
    localObject = getSharedPreferences("test", 0).edit();
    ((SharedPreferences.Editor)localObject).putString("ilil", paramBundle);
    ((SharedPreferences.Editor)localObject).putString("lili", str);
    ((SharedPreferences.Editor)localObject).commit();
  }

在class Encryto中,我们发现他加载了library,于是我们只能去看看so文件了。

  static
  {
    System.loadLibrary("JNIEncrypt");
  }

反编译后,查看函数 doRawData() ,这里把 "thisisatestkey==" 赋值给了v10,然后调用了 AES_128_ECB_PKCS5Padding_Encrypt(v4, (int)v10) ,推测是128位的AES加密,模式是ECB,填充方式是PKCS5Padding,密钥就是v10。

int __cdecl doRawData(int a1, int a2, int a3, int a4)
{
  char *v4; // esi
  int result; // eax
  char *v6; // esi
  size_t v7; // eax
  int v8; // [esp+0h] [ebp-2Ch]
  int (__cdecl *v9)(int, char *, size_t); // [esp+0h] [ebp-2Ch]
  char v10[20]; // [esp+4h] [ebp-28h] BYREF
  unsigned int v11; // [esp+18h] [ebp-14h]

  v11 = __readgsdword(0x14u);
  if ( checkSignature(a1, a2, a3) == 1 )
  {
    strcpy(v10, "thisisatestkey==");
    v4 = (char *)(*(int (__cdecl **)(int, int, _DWORD))(*(_DWORD *)a1 + 676))(a1, a4, 0);
    v8 = AES_128_ECB_PKCS5Padding_Encrypt(v4, (int)v10);
    (*(void (__cdecl **)(int, int, char *))(*(_DWORD *)a1 + 680))(a1, a4, v4);
    result = (*(int (__cdecl **)(int, int))(*(_DWORD *)a1 + 668))(a1, v8);
  }
  else
  {
    v6 = UNSIGNATURE[0];
    v9 = *(int (__cdecl **)(int, char *, size_t))(*(_DWORD *)a1 + 652);
    v7 = strlen(UNSIGNATURE[0]);
    result = v9(a1, v6, v7);
  }
  return result;
}

将之前得到的那串字符串与刚才得到的密钥进行aes解密,得到aimagetencent,但是提交flag不对,,,在wp的帮助下发现密文在一个没有使用过的class里,FileDataActivity.class里找到9YuQ2dk8CSaCe7DTAmaqAA==,解密得到flag。

0x02 easy-apk

在MainActivity的Onclick()中,对我们的输入进行加密后等于5rFf7E2K6rqN7Hpiyush7E6S5fJg6rsi5NBf6NGT5rs= ,跟进查看Base64New().Base64Encode(),是一个变换了码表的base64加密。

  protected void onCreate(Bundle paramBundle)
  {
    super.onCreate(paramBundle);
    setContentView(2130968603);
    ((Button)findViewById(2131427446)).setOnClickListener(new View.OnClickListener()
    {
      public void onClick(View paramView)
      {
        paramView = ((EditText)MainActivity.this.findViewById(2131427445)).getText().toString();
        if (new Base64New().Base64Encode(paramView.getBytes()).equals("5rFf7E2K6rqN7Hpiyush7E6S5fJg6rsi5NBf6NGT5rs="))
        {
          Toast.makeText(MainActivity.this, "验证通过!", 1).show();
          return;
        }
        Toast.makeText(MainActivity.this, "验证失败!", 1).show();
      }
    });
  }

熟悉了base64原理之后就可以写脚本了:

b64_table = ['v', 'w', 'x', 'r', 's', 't', 'u', 'o', 'p', 'q', '3', '4', '5', '6', '7', 'A', 'B', 'C', 'D', 'E', 'F',
             'G', 'H', 'I', 'J', 'y', 'z', '0', '1', '2', 'P', 'Q', 'R', 'S', 'T', 'K', 'L', 'M', 'N', 'O', 'Z', 'a',
             'b', 'c', 'd', 'U', 'V', 'W', 'X', 'Y', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', '8', '9', '+',
             '/']


def encode(text):
    bit_list = ''
    new_bit = []
    base = ''
    for i in text:
        bit_list += '{:0>8}'.format(bin(ord(i)).replace('0b', ''))
    for i in range(0, len(bit_list), 6):
        new_bit.append('{:0<6}'.format(bit_list[i:i + 6]))
    for i in new_bit:
        base += b64_table[int(i, 2)]
    if len(text) % 3 == 1:
        base += '=='
    if len(text) % 3 == 2:
        base += '='
    return base


def decode(text):
    new_bit = []
    bit_list = ''
    flag = []
    for i in text:
        if i == '=':
            break
        new_bit.append(b64_table.index(i))
    for i in new_bit:
        bit_list += '{:0>6}'.format(bin(i).replace('0b', ''))
    for i in range(0, len(bit_list), 8):
        flag.append('{:0>8}'.format(bit_list[i:i + 8]))
    for i in flag:
        print(chr(int(i, 2)), end='')


decode('5rFf7E2K6rqN7Hpiyush7E6S5fJg6rsi5NBf6NGT5rs=')

好久没怎么写代码了,水平又退步了,麻了。

0x03 easyjni

这题onCreate()方法里,获取了我们的输入,然后传给方法a(),判断返回值,返回值为true就成功。

  protected void onCreate(Bundle paramBundle)
  {
    super.onCreate(paramBundle);
    setContentView(2130968603);
    findViewById(2131427446).setOnClickListener(new View.OnClickListener(this)
    {
      public void onClick(View paramView)
      {
        paramView = (EditText)((MainActivity)this.a).findViewById(2131427445);
        if (MainActivity.a(MainActivity.this, paramView.getText().toString()))
        {
          Toast.makeText(this.a, "You are right!", 1).show();
          return;
        }
        Toast.makeText(this.a, "You are wrong! Bye~", 1).show();
      }
    });
  }

然后我们来看看方法a,new了一个对象a,然后将返回值传给方法ncheck(),native()是so文件里的函数,等下看看so文件。a对象的方法a()应该就是换了码表的base64加密。

  private boolean a(String paramString)
  {
    try
    {
      boolean bool = ncheck(new a().a(paramString.getBytes()));
      return bool;
    }
    catch (java.lang.Exception paramString)
    {
    }
    return false;
  }
  private native boolean ncheck(String paramString);

然后看看so文件里的 Java_com_a_easyjni_MainActivity_ncheck() ,首先判断了传入长度是否为32,然后执行以下函数:

    for ( i = 0; i != 16; ++i )
    {
      v7 = &text[i];
      text[i] = input[i + 16];
      v8 = input[i];
      v7[16] = v8;
    }

这里我改了一下一些变量名,input就是传入的内容,也就是我们输入的内容base64编码后的内容,然后不难看出这是将input前16位和后16位调换了顺序,然后存入text中。

然后是如下代码:

    v9 = 0;
    do
    {
      v10 = v9 < 30;
      v13 = text[v9];
      text[v9] = text[v9 + 1];
      text[v9 + 1] = v13;
      v9 += 2;
    }while ( v10 );
    result = memcmp(text, "MbT3sQgX039i3g==AQOoMQFPskB1Bsc7", 0x20u) == 0;

把之前得到的内容每两位调换顺序,然后得到"MbT3sQgX039i3gAQOoMQFPskB1Bsc7",现在我们可以写脚本了,将上一题的脚本改改就能用了。

b64_table = ['i', '5', 'j', 'L', 'W', '7', 'S', '0', 'G', 'X', '6', 'u', 'f', '1', 'c', 'v', '3', 'n', 'y', '4', 'q',
             '8', 'e', 's', '2', 'Q', '+', 'b', 'd', 'k', 'Y', 'g', 'K', 'O', 'I', 'T', '/', 't', 'A', 'x', 'U', 'r',
             'F', 'l', 'V', 'P', 'z', 'h', 'm', 'o', 'w', '9', 'B', 'H', 'C', 'M', 'D', 'p', 'E', 'a', 'J', 'R', 'Z',
             'N']


def encode(text):
    bit_list = ''
    new_bit = []
    base = ''
    for i in text:
        bit_list += '{:0>8}'.format(bin(ord(i)).replace('0b', ''))
    for i in range(0, len(bit_list), 6):
        new_bit.append('{:0<6}'.format(bit_list[i:i + 6]))
    for i in new_bit:
        base += b64_table[int(i, 2)]
    if len(text) % 3 == 1:
        base += '=='
    if len(text) % 3 == 2:
        base += '='
    return base


def decode(text):
    new_bit = []
    bit_list = ''
    flag = []
    for i in text:
        if i == '=':
            break
        new_bit.append(b64_table.index(i))
    for i in new_bit:
        bit_list += '{:0>6}'.format(bin(i).replace('0b', ''))
    for i in range(0, len(bit_list), 8):
        flag.append('{:0>8}'.format(bit_list[i:i + 8]))
    for i in flag:
        print(chr(int(i, 2)), end='')


def neck(text):
    cipher_text = ''
    for i in range(0, 31, 2):
        cipher_text += text[i + 1] + text[i]
    cipher_text = cipher_text[16:] + cipher_text[:16]
    print(cipher_text)
    return cipher_text


decode(neck('MbT3sQgX039i3g==AQOoMQFPskB1Bsc7'))

0x04 easy-so

这题还是先看onclick()方法,调用了cyberpeace对象的Check String()方法,然后Cyberpeace加载了so里的库函数cyberpeace,接下来就去看so文件了。

      public void onClick(View paramView)
      {
        if (cyberpeace.CheckString(((EditText)MainActivity.this.findViewById(2131165233)).getText().toString()) == 1)
        {
          Toast.makeText(MainActivity.this, "验证通过!", 1).show();
          return;
        }
        Toast.makeText(MainActivity.this, "验证失败!", 1).show();
      }

public class cyberpeace
{
  static
  {
    System.loadLibrary("cyberpeace");
  }

  public static native int CheckString(String paramString);
}

拖入ida后,分析了半天我们可以发现逻辑和上一题一样,先将前16位和后16位交换位置,然后每两位互换位置,用上一题的脚本就可以了:

def neck(text):
    cipher_text = ''
    for i in range(0, 31, 2):
        cipher_text += text[i + 1] + text[i]
    cipher_text = cipher_text[16:] + cipher_text[:16]
    return cipher_text


print(neck('f72c5a36569418a20907b55be5bf95ad'))

0x05 app3

这题给了一个ab格式的文件,开始还以为下载错了,百度了一下,是android的备份数据格式。ab文件一般分两种:

未加密,文件前面有24字节文件头,文件头包含none标志,文件头之后就是数据
加密,文件头包含AES-256标志

使用winhex或者其他二进制编辑器打开该文件,发现了none字样,显然是未加密文件。

使用Android backup extractor进行解析:

java -jar abe.jar unpack app3.ab app3.tar

得到压缩文件,进行解压,拿到了apk文件以及数据库文件。

└─com.example.yaphetshan.tencentwelcome
│ Encryto.db
│ _manifest

├─a
│ base.apk

└─db
Demo.db

先尝试用DB Browser for SQLite打开,发现被加密了,那就来看看apk文件。

还是先看MainActivity()的onClick()方法,获取我们的输入后开启了AnotherActivity活动,但是里面没啥东西。

再来看看onCreate()方法,调用了a()方法,实例化对象a,将flag存在数据库中。然后给v0的name和password字段分别赋值为'Stranger'和'123465',接下来实例化了一个a包下的a对象并初始化。然后调用a对象的a()方法,传入两个参数。

        SQLiteDatabase.loadLibs(this);
        this.b = new a(this, "Demo.db", null, 1);
        ContentValues v0 = new ContentValues();
        v0.put("name", "Stranger");
        v0.put("password", Integer.valueOf(0x1E240));
        com.example.yaphetshan.tencentwelcome.a.a v1 = new com.example.yaphetshan.tencentwelcome.a.a();
        String v2 = v1.a(v0.getAsString("name"), v0.getAsString("password"));
        this.a = this.b.getWritableDatabase(v1.a(v2 + v1.b(v2, v0.getAsString("password"))).substring(0, 7));
        this.a.insert("TencentMicrMsg", null, v0);
    }

a()方法将传入的两个参数分别截取前四位然后拼合后返回,也就是说v2值为'Stra1234'。

public class a {
    private String a;

    public a() {
        this.a = "yaphetshan";
    }

    public String a(String arg3) {
        new b();
        return b.b(arg3 + this.a);
    }

    public String a(String arg4, String arg5) {
        return arg4.substring(0, 4) + arg5.substring(0, 4);
    }

    public String b(String arg2, String arg3) {
        new b();
        return b.a(arg2);
    }
}

然后我们跟进查看一下getWritableDatabase()方法是干什么的,这里的关键代码为:

if(this.mName == null) {
    v0_1 = SQLiteDatabase.create(null, arg7);
}

会判断有没有该数据库,没有则用传入的参数作为密码创建一个数据库。那我们只要知道传入参数是多少就可以了。也就是 v1.a(v2 + v1.b(v2, v0.getAsString("password"))).substring(0, 7)

v1也就是a包下的对象a的实例化,b方法传入两个参数,但是只对第一个参数进行操作,将其作为参数传入方法a,也就是说参数就为:b(("Stra1234"+a("Stra1234")+"yaphetshan"))substring(0,7),就可以写脚本了:

package app;

import java.security.MessageDigest;

public class App {
    public static void main(String[] args) {
        String two = ("Stra1234"+a("Stra1234")+"yaphetshan");
        System.out.println(b(two).substring(0,7));
    }

    public static final String a(String arg9) {
        int v0 = 0;
        char[] v2 = new char[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
        try {
            byte[] v1 = arg9.getBytes();
            MessageDigest v3 = MessageDigest.getInstance("MD5");
            v3.update(v1);
            byte[] v3_1 = v3.digest();
            char[] v5 = new char[v3_1.length * 2];
            int v1_1 = 0;
            while (v0 < v3_1.length) {
                int v6 = v3_1[v0];
                int v7 = v1_1 + 1;
                v5[v1_1] = v2[v6 >>> 4 & 15];
                v1_1 = v7 + 1;
                v5[v7] = v2[v6 & 15];
                ++v0;
            }

            return new String(v5);
        } catch (Exception v0_1) {
            return null;
        }
    }

    public static final String b(String arg9) {
        int v0 = 0;
        char[] v2 = new char[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
        try {
            byte[] v1 = arg9.getBytes();
            MessageDigest v3 = MessageDigest.getInstance("SHA-1");
            v3.update(v1);
            byte[] v3_1 = v3.digest();
            char[] v5 = new char[v3_1.length * 2];
            int v1_1 = 0;
            while (v0 < v3_1.length) {
                int v6 = v3_1[v0];
                int v7 = v1_1 + 1;
                v5[v1_1] = v2[v6 >>> 4 & 15];
                v1_1 = v7 + 1;
                v5[v7] = v2[v6 & 15];
                ++v0;
            }

            return new String(v5);
        } catch (Exception v0_1) {
            return null;
        }
    }
}

拿到密码ae56f99,用DB Browser for SQLite打开Encryto.db文件,拿到base64编码后的flag,解码得到flag。

0x06 easyjava

先看看源码,读取输入后调用了一个布尔类型的方法对我们的输入进行检查,必须以flag{ 开头,且以 } 结尾。然后截取了flag大括号里的内容,接下来实例化了两个对象,逐个读取截取的flag字符,然后将这三个值作为参数传递给了一个函数,我们将其重命名为enc。然后将截取的flag字符作为参数先后调用b类的encB方法和a类的encA方法。

    private static char enc(String arg1, b arg2, a arg3) {
        return arg3.encA(arg2.encB(arg1));
    }

    private static Boolean b(String arg8) {
        int v0 = 0;
        if(!arg8.startsWith("flag{")) {  // 必须以flag{开头
            return Boolean.valueOf(false);
        }

        if(!arg8.endsWith("}")) {  // 以}结尾
            return Boolean.valueOf(false);
        }

        String v2 = arg8.substring(5, arg8.length() - 1);  // flag大括号里包裹的内容
        b v4 = new b(((int)2));
        a v5 = new a(((int)3));
        StringBuilder v3 = new StringBuilder();
        int v1 = 0;
        while(v0 < v2.length()) {
            v3.append(MainActivity.enc(v2.charAt(v0) + "", v4, v5));
            if(((int)(((int)(((int)v4.b()) / 25)))) > v1 && ((int)(((int)(((int)v4.b()) / 25)))) >= 1) {
                ++v1;
            }

            ++v0;
        }

        return Boolean.valueOf(v3.toString().equals("wigwrkaugala"));
    }

然后先来看看b类的encB方法,查找截取的flag字符在字符串stringB中出现的位置,然后该值在arrayList中出现的位置,然后调用方法a来对该对象的一些值进行处理。

这里我有个疑问,按照代码所写,应该是先去掉arraylist的首元素,然后将现在的首元素也就是之前的第二个元素加到末尾,但是这样写解密代码并不对,按照首元素移动到末位就对了。

然后将stringB的首字符移动到末尾。

    public Integer encB(String arg5) {
        int v0 = 0;
        Integer v1 = (int)0;
        if(b.stringB.contains(arg5.toLowerCase())) {
            while(v0 < b.arrayList.size() - 1) {
                if(b.arrayList.get(v0) == ((int)b.stringB.indexOf(arg5))) {
                    v1 = (int)v0;
                }

                ++v0;
            }
        }
        else {
            v1 = arg5.contains(" ") ? ((int)-10) : ((int)-1);
        }

        b.a();
        return v1;
    }

    public static void a() {
        b.arrayList.remove(0);
        b.arrayList.add(Integer.valueOf(((Integer)b.arrayList.get(0)).intValue()));
        b.stringB = b.stringB + "" + b.stringB.charAt(0);
        b.stringB = b.stringB.substring(1, 27);
        b.intD = (int)(((int)b.intD) + 1);
    }

然后看看a对象的encA方法,判断参数在arrayList中出现的位置,然后返回该值在stringA对应下标的值,并返回。这里同样也有一个方法对该对象的一些属性进行修改,但是对我们的加解密没有影响就不过多赘述了。

    public char encA(Integer arg5) {
        int v0 = 0;
        Integer v1 = (int)0;
        if(((int)arg5) == -10) {
            a.a();
            return " ".charAt(0);
        }

        while(v0 < a.arrayList.size() - 1) {
            if(a.arrayList.get(v0) == arg5) {
                v1 = (int)v0;
            }

            ++v0;
        }

        a.a();
        return a.stringA.charAt(v1.intValue());
    }

加密过程说完了,这里a和b类实例化的时候,对对象的arraylist进行了初始化,可以不用看他的逻辑,直接写java代码来获得生成的arraylist。

import java.util.ArrayList;

public class App {
    public static ArrayList arrayList;
    static String stringB;
    Integer[] intArrayC;
    static Integer intD;
    static {
        App.arrayList = new ArrayList();
        App.stringB = "abcdefghijklmnopqrstuvwxyz";
        App.intD = (int)0;
    }
    public static void main(String[] args){
        App a = new App(3);
        System.out.println(arrayList);
    }

    public App(int arg8) {  // arg8 = 3
        this.intArrayC = new Integer[]{7, 14, 16, 21, 4, 24, ((int)25), ((int)20), ((int)5), ((int)15), ((int)9), ((int)17), ((int)6), ((int)13), ((int)3), ((int)18), ((int)12), ((int)10), ((int)19), ((int)0), ((int)22), ((int)2), ((int)11), ((int)23), ((int)1), ((int)8)};
        int v0;
        for(v0 = (int)arg8; v0 < this.intArrayC.length; ++v0) {
            arrayList.add(intArrayC[v0]);
        }

        int v0_1;
        for(v0_1 = 0; v0_1 < ((int)arg8); ++v0_1) {
            App.arrayList.add(this.intArrayC[v0_1]);
        }
    }

}
import java.util.ArrayList;

public class AppB {
    public static ArrayList arrayList;
    static String stringB;
    Integer[] intArrayC;
    static Integer intD;

    static {
        AppB.arrayList = new ArrayList();
        AppB.stringB = "abcdefghijklmnopqrstuvwxyz";
        AppB.intD = (int)0;
    }

    public static void main(String[] args) {
        AppB appB = new AppB(2);
        System.out.println(arrayList);
    }


    public AppB(Integer arg9) {
        this.intArrayC = new Integer[]{((int)8), ((int)25), ((int)17), ((int)23), ((int)7), ((int)22), ((int)1), ((int)16), ((int)6), ((int)9), ((int)21), ((int)0), ((int)15), ((int)5), ((int)10), ((int)18), ((int)2), ((int)24), ((int)4), ((int)11), ((int)3), ((int)14), ((int)19), ((int)12), ((int)20), ((int)13)};
        int v0;
        for(v0 = (int)arg9; v0 < this.intArrayC.length; ++v0) {
            AppB.arrayList.add(this.intArrayC[v0]);
        }

        int v0_1;
        for(v0_1 = 0; v0_1 < ((int)arg9); ++v0_1) {
            AppB.arrayList.add(this.intArrayC[v0_1]);
        }
    }
}

解密脚本:

text = "wigwrkaugala"
list_a = [21, 4, 24, 25, 20, 5, 15, 9, 17, 6, 13, 3, 18, 12, 10, 19, 0, 22, 2, 11, 23, 1, 8, 7, 14, 16]
list_b = [17, 23, 7, 22, 1, 16, 6, 9, 21, 0, 15, 5, 10, 18, 2, 24, 4, 11, 3, 14, 19, 12, 20, 13, 8, 25]
stringA = "abcdefghijklmnopqrstuvwxyz"
stringB = "abcdefghijklmnopqrstuvwxyz"
int_a = int_b = 0
flag = ''
for i in text:
    arg5 = list_a[stringA.index(i)]
    flag += stringB[list_b[arg5]]

    int_a += 1
    list_b.append(list_b[0])
    del list_b[0]
    stringB += stringB[0]
    stringB = stringB[1:27]

print(flag)

0x07 RememberOther

读取输入后调用checkSN()方法对输入进行检查,其实要使注册码等于用户名的md5值得奇数位,然后出现一个successed md5,其实可以直接用jeb看到。。。

    private boolean checkSN(String arg12, String arg13) {
        try {
            if(arg12.length() != 0 || arg13.length() != 0) {
                if(arg12 == null || arg12.length() == 0) {
                    return false;
                }

                if(arg13 == null || arg13.length() != 16) {
                    return false;
                }

                MessageDigest digest = MessageDigest.getInstance("MD5");
                digest.reset();
                digest.update(arg12.getBytes());
                String hexstr = MainActivity.toHexString(digest.digest(), "");
                StringBuilder sb = new StringBuilder();
                int i;
                for(i = 0; i < hexstr.length(); i += 2) {
                    sb.append(hexstr.charAt(i));
                }

                boolean v9 = sb.toString().equalsIgnoreCase(arg13);
                if(!v9) {
                    return false;
                }
            }

            return true;
        }
        catch(NoSuchAlgorithmException e) {
            e.printStackTrace();
            return false;
        }
    }

将md5值进行解密,得到YOU_KNOW_,然后由于Word文档里写了 不懂安卓,所以就只是和安卓扯了扯边,,,Have fun~,所以还要在后面加一个ANDROID,,,而且还没有flag{}包裹,极其没有营养。。。

0x08 Ph0en1x-100

这题更多考察的是对so文件的分析,看看onGoClick()方法,读取输入后执行encrypt()方法,然后与getflag()方法返回值同时作为参数执行getSecret()方法,也就是说放我们的输入执行了encrypt()方法后等于getflag()方法的返回值就可以了,这两个方法我们可以在so文件中看到。

    public void onGoClick(View arg5) {
        String sInput = this.etFlag.getText().toString();
        if(this.getSecret(this.getFlag()).equals(this.getSecret(this.encrypt(sInput)))) {
            Toast.makeText(this, "Success", 1).show();
            return;
        }

        Toast.makeText(this, "Failed", 1).show();
    }

将so文件拖入ida后,可以看到都是诸如(*(_DWORD *)a1 + 676)这样的代码,只需要对着a1按下y键 然后输入 JNIEnv* 修改参数类型就可以了。

可以看到 encrypt() 函数在这将a1初始化到s中,然后逐一访问s中的元素,接着每位减一,然后返回,逻辑很简单。

jstring __cdecl Java_com_ph0en1x_android_1crackme_MainActivity_encrypt(JNIEnv *a1, int a2, int a3)
{
  size_t v3; // esi
  const char *s; // edi

  v3 = 0;
  for ( s = (*a1)->GetStringUTFChars(a1, a3, 0); v3 < strlen(s); --s[v3++] )
    ;
  return (*a1)->NewStringUTF(a1, s);
}

getFlag() 函数中,这里v2是v7地址+2,也就是说此时v2对应的地址就是v7的第三个字节处,也就是0x47。

然后在do...while循环中,v3等于v2,然后v2进行自减,也就是说v2会对v7与v6的26个字节进行逆向遍历。

然后让v5的字符逐个与之进行异或并存入他的地址。

jstring __cdecl Java_com_ph0en1x_android_1crackme_MainActivity_getFlag(JNIEnv *a1)
{
  int v1; // esi
  char *v2; // edi
  char v3; // al
  jstring result; // eax
  char v5[14]; // [esp+26h] [ebp-46h] BYREF
  _DWORD v6[9]; // [esp+34h] [ebp-38h] BYREF
  int v7; // [esp+58h] [ebp-14h] BYREF
  unsigned int v8; // [esp+5Ch] [ebp-10h]

  v1 = 38;
  v2 = (char *)&v7 + 2;
  v6[0] = 0x4C42362E;
  v6[1] = 0x3AE0BF5F;
  v8 = __readgsdword(0x14u);
  v6[2] = 0x6320C3A8;
  v6[3] = 0x1CC0B789;
  v6[4] = 0x28C2441D;
  v6[5] = 0xE02ED7F;
  v6[6] = 0x988F665D;
  v6[7] = 0x16D0B7B5;
  v6[8] = 0xFBF8834D;
  v7 = 0x474301;
  strcpy(v5, "Hello Ph0en1x");
  do
  {
    v3 = *v2--;
    v2[1] = (v5[v1 % 13] ^ (v3 + 1 - *v2)) - 1;
    --v1;
  }
  while ( v1 );
  LOBYTE(v6[0]) = (LOBYTE(v6[0]) ^ 0x48) - 1;
  result = (*a1)->NewStringUTF(a1, v6);
  if ( __readgsdword(0x14u) != v8 )
    sub_4B0();
  return result;
}

有了这些就可以写脚本了,先获取这些十六进制数的单个字节,然后逐个进行异或即可。

text = [0x474301, 0xFBF8834D, 0x16D0B7B5, 0x988F665D,
        0xE02ED7F, 0x28C2441D, 0x1CC0B789, 0x6320C3A8,
        0x3AE0BF5F, 0x4C42362E]
flag_list = []
for i in text:
    content = str(hex(i)).replace('0x', '').rjust(8, '0')
    for j in range(0, 8, 2):
        if hex(int('0x' + content[j:j + 2], 16)) != '0x0':
            flag_list.append(int(hex(int('0x' + content[j:j + 2], 16)), 16))

flag_list = list(reversed(flag_list))
v5 = "Hello Ph0en1x"
v1 = len(flag_list) - 1
for i in range(v1, 0, -1):
    v3 = flag_list[i]
    v2 = flag_list[i - 1]
    flag_list[i] = chr(ord(v5[i % 13]) ^ (v3 + 1 - v2) & 255)
flag_list[0] = chr(flag_list[0] ^ 0x48 & 255)
flag = ''.join(flag_list)
print(flag)

0x09 黑客精神

先来看看MainActivity的onCreate()方法,会先判断MyApp.m是否为0,0的话就是未注册,1就是已注册。然后如果m为0,就会调用doRegister()来进行注册,否则调用MyApp.work()。

    @Override  // android.app.Activity
    public void onCreate(Bundle arg6) {
        String str2;
        super.onCreate(arg6);
        this.setContentView(0x7F04001A);  // layout:activity_main
        Log.d("com.gdufs.xman m=", "Xman");
        this.getApplication();
        int m = MyApp.m;
        if(m == 0) {
            str2 = "未注册";
        }
        else {
            str2 = m == 1 ? "已注册" : "已混乱";
        }

        this.setTitle("Xman" + str2);
        this.btn1 = (Button)this.findViewById(0x7F0B0054);  // id:button1
        this.btn1.setOnClickListener(new View.OnClickListener() {
            @Override  // android.view.View$OnClickListener
            public void onClick(View arg5) {
                MainActivity.this.getApplication();
                if(MyApp.m == 0) {
                    MainActivity.this.doRegister();
                    return;
                }

                ((MyApp)MainActivity.this.getApplication()).work();
                Toast.makeText(MainActivity.this.getApplicationContext(), MainActivity.workString, 0).show();
            }
        });
    }

先来看看doRegister(),如果我们点击注册,会调用一个新的Activity,也就是 "com.gdufs.xman.RegActivity" ,然后在RegActivity中,读取了我们的输入,然后调用MyApp的saveSN()方法。如果点击不玩了就会退出。

    public void doRegister() {
        new AlertDialog.Builder(this).setTitle("注册").setMessage("Flag就在前方!").setPositiveButton("注册", new DialogInterface.OnClickListener() {
            @Override  // android.content.DialogInterface$OnClickListener
            public void onClick(DialogInterface arg5, int arg6) {
                Intent intent = new Intent();
                intent.setComponent(new ComponentName("com.gdufs.xman", "com.gdufs.xman.RegActivity"));
                MainActivity.this.startActivity(intent);
                MainActivity.this.finish();
            }
        }).setNegativeButton("不玩了", new DialogInterface.OnClickListener() {
            @Override  // android.content.DialogInterface$OnClickListener
            public void onClick(DialogInterface arg2, int arg3) {
                Process.killProcess(Process.myPid());
            }
        }).show();
    }

接下来来看看MyApp的内容,onCreate()会调用initSN(),然后initSN()、saveSN()、work()都是库函数,那接下来久去看看so文件了。

public class MyApp extends Application {
    public static int m;

    static {
        MyApp.m = 0;
        System.loadLibrary("myjni");
    }

    public native void initSN() {
    }

    @Override  // android.app.Application
    public void onCreate() {
        this.initSN();
        Log.d("com.gdufs.xman m=", String.valueOf(MyApp.m));
        super.onCreate();
    }

    public native void saveSN(String arg1) {
    }

    public native void work() {
    }
}

拖入ida后,没有发现上述提到的几个函数,可以shift+F12,找到这几个函数对应的字符串,双击,然后点击地址,按下x键,就可以找到引用他们的地方,如图,n1、n2、n3就是这几个函数了,按n进行改名就可以了。

在initSN()中,会打开"/sdcard/reg.dat"文件,读取内容,然后判断是否与字符串"EoPAoY62@ElRD"相等,那么我们接下来就要去找这个文件中的字符串在哪进行修改了。

int __fastcall initSN(int a1)
{
  int v2; // r0
  int v3; // r4
  int v4; // r0
  int v5; // r7
  const char *v6; // r5
  int v8; // r0
  int v9; // r1

  v2 = j_fopen("/sdcard/reg.dat", "r+");
  v3 = v2;
  if ( !v2 )
  {
    v4 = a1;
    return setValue(v4, 0);
  }
  j_fseek(v2, 0, 2);
  v5 = j_ftell(v3);
  v6 = (const char *)j_malloc(v5 + 1);
  if ( !v6 )
  {
    j_fclose(v3);
    v4 = a1;
    return setValue(v4, 0);
  }
  j_fseek(v3, 0, 0);
  j_fread(v6, v5, 1, v3);
  v6[v5] = 0;
  if ( !j_strcmp(v6, "EoPAoY62@ElRD") )
  {
    v8 = a1;
    v9 = 1;
  }
  else
  {
    v8 = a1;
    v9 = 0;
  }
  setValue(v8, v9);
  return j_fclose(v3);
}

在saveSN()中,他会先打开"/sdcard/reg.dat"文件,然后对我们的输入进行一次加密,然后存入"/sdcard/reg.dat"文件中。有了加密过程,有了加密后的字符串,我们就可以写脚本了。

int __fastcall saveSN(JNIEnv *a1, int a2, int a3)
{
  int v5; // r7
  const char *v7; // r6
  const char *v8; // r5
  int v9; // r4
  int v10; // r0
  char v11; // r2
  signed int v12; // [sp+8h] [bp-38h]
  char v13[20]; // [sp+10h] [bp-30h] BYREF

  v5 = j_fopen("/sdcard/reg.dat", "w+");
  if ( !v5 )
    return j___android_log_print(3, "com.gdufs.xman", byte_2E5A);
  strcpy(v13, "W3_arE_whO_we_ARE");
  v7 = (*a1)->GetStringUTFChars(a1, a3, 0);
  v8 = v7;
  v12 = j_strlen(v7);
  v9 = 2016;
  while ( 1 )
  {
    v10 = v8 - v7;
    if ( v8 - v7 >= v12 )
      break;
    if ( v10 % 3 == 1 )
    {
      v9 = (v9 + 5) % 16;
      v11 = v13[v9 + 1];
    }
    else if ( v10 % 3 == 2 )
    {
      v9 = (v9 + 7) % 15;
      v11 = v13[v9 + 2];
    }
    else
    {
      v9 = (v9 + 3) % 13;
      v11 = v13[v9 + 3];
    }
    *v8++ ^= v11;
  }
  j_fputs(v7, v5);
  return j_fclose(v5);
}

解密代码:

str1 = "W3_arE_whO_we_ARE"
target = "EoPAoY62@ElRD"
v12 = len(target)
v8 = 0
v9 = 2016
flag = ''
while (1):
    v10 = v8
    if v8 >= v12:
        break
    if v10 % 3 == 1:
        v9 = (v9 + 5) % 16
        v11 = str1[v9 + 1]
    elif v10 % 3 == 2:
        v9 = (v9 + 7) % 15
        v11 = str1[v9 + 2]
    else:
        v9 = (v9 + 3) % 13
        v11 = str1[v9 + 3]
    flag += chr((ord(target[v8]) ^ ord(v11)))
    v8 += 1

print(flag)

将其输入app中,再次打开,点击提示:输入即是flag,格式为xman{......}!

成功!

0x0a easy-dex

(待更新)