静态分析java层 2014 ASIS Cyber Security Contest Finals Numdroid 逆向分析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 protected void onCreate (Bundle savedInstanceState) { super.onCreate (savedInstanceState); setContentView (R.layout.activity_main); ArrayTools.each_with_index (buttons, new EachIndexAction <Integer>() { @Override public void action (final int i, Integer element) { ((Button) MainActivity.this .findViewById (element.intValue ())).setOnClickListener (new View.OnClickListener () { @Override public void onClick (View arg0) { MainActivity.this .clicked (i); } }); } }); this .mScreen = (EditText) findViewById (R.id.editText1); this .mScreen.setEnabled (false ); this .mOk = (ImageButton) findViewById (R.id.ok); this .mDel = (ImageButton) findViewById (R.id.del); this .mOk.setOnClickListener (new View.OnClickListener () { @Override public void onClick (View arg0) { MainActivity.this .ok_clicked (); } }); this .mDel.setOnClickListener (new View.OnClickListener () { @Override public void onClick (View arg0) { MainActivity.this .del_clicked (); } }); }
首先遍历按钮数组,并进行初始化和设置点击事件 MainActivity.this.clicked(i);
然后对OK和del按钮进行初始化并设置点击事件分别为 MainActivity.this.ok_clicked();
和 MainActivity.this.del_clicked();
1 2 3 4 5 public void clicked (int i) { DebugTools.log ("number: " + i); this .mScreen.setText (this .mScreen.getText ().append ((CharSequence) Integer.toString (i))); DebugTools.log ("current Pass: " + ((Object) this .mScreen.getText ())); }
点击按钮时候会先log记录下来,然后再将数字转为String显示到屏幕上,然后再log记录下当前的passwd
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 protected void ok_clicked () { DebugTools.log ("clicked password: " + ((Object) this .mScreen.getText ())); boolean result = Verify.isOk (this , this .mScreen.getText ().toString ()); DebugTools.log ("password is Ok? : " + result); if (result) { Intent i = new Intent (this , (Class<?>) LipSum.class ); Bundle b = new Bundle (); b.putString ("flag" , this .mScreen.getText ().toString ().substring (0 , 7 )); i.putExtras (b); startActivity (i); return ; } Toast.makeText (this , R.string.wrong, 1 ).show (); this .mScreen.setText ("" ); }
点击OK按钮,会先log记录当前的passwd,然后通过Verify.isOk对其进行check,如果密码正确会进入到分支,然后会把flag放到屏幕上,显然这就是胜利条件了
第一个idea,对isOk进行hook,测试之后发现,他的flag是md5的密码,显然偷鸡失败(/(ㄒoㄒ)/~~
那么就要研究isOk函数的逻辑:
1 2 3 4 5 6 7 8 9 public static boolean isOk (Context c, String _password) { String password = _password; if (_password.length () > 7 ) { password = _password.substring (0 , 7 ); } String r = OneWayFunction (password); DebugTools.log ("digest: " + password + " => " + r); return r.equals ("be790d865f2cea9645b3f79c0342df7e" ); }
password只取前7个数字进OneWayFunction函数,之后将其返回的值与一个hash进行比对
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 private static String OneWayFunction (final String password) { String[] hashes = {"MD2" , "MD5" , "SHA-1" , "SHA-256" , "SHA-384" , "SHA-512" }; List<byte[]> bytes = ArrayTools.map (ArrayTools.select (ArrayTools.map (hashes, new MapAction <String, byte[]>() { @Override public byte[] action (String element) { try { MessageDigest digest = MessageDigest.getInstance (element); digest.update (password.getBytes ()); return digest.digest (); } catch (NoSuchAlgorithmException e) { return null; } } }), new SelectAction <byte[]>() { @Override public boolean action (byte[] element) { return element != null; } }), new MapAction <byte[], byte[]>() { @Override public byte[] action (byte[] element) { byte[] b = new byte[8 ]; for (int i = 0 ; i < b.length / 2 ; i++) { b[i] = element[i]; } for (int i2 = 0 ; i2 < b.length / 2 ; i2++) { b[(b.length / 2 ) + i2] = element[(element.length - i2) - 2 ]; } return b; } }); byte[] b2 = new byte[bytes.size () * 8 ]; for (int i = 0 ; i < b2.length; i++) { b2[i] = bytes.get (i % bytes.size ())[i / bytes.size ()]; } try { MessageDigest digest = MessageDigest.getInstance ("MD5" ); digest.update (b2); byte[] messageDigest = digest.digest (); StringBuilder hexString = new StringBuilder (); for (byte aMessageDigest : messageDigest) { String h = Integer.toHexString (aMessageDigest & 255 ); while (h.length () < 2 ) { h = "0" + h; } hexString.append (h); } return hexString.toString (); } catch (NoSuchAlgorithmException e) { return "" ; } }
发现hash基本都是单向的,发现如果只是爆破的话只有10^7的大小,感觉可以尝试
方案1: 网上师傅的方案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 .line 26 :cond_0 const/4 v0, 0x0 .local v0, "i":I :goto_0 const v2, 0x98967f if-lt v0, v2, :cond_1 .line 38 return-void .line 28 :cond_1 invoke-static {v0}, Ljava/lang/Integer;->toString(I)Ljava/lang/String; move-result-object v2 invoke-static {v2}, Lcom/example/forloop/MainActivity;->isOk(Ljava/lang/String;)Z move-result v1 .line 29 .local v1, "ok":Z const v2, 0x186a0 rem-int v2, v0, v2 if-nez v2, :cond_2 .line 31 invoke-static {v0}, Ljava/lang/Integer;->toString(I)Ljava/lang/String; move-result-object v2 invoke-virtual {p0, v2}, Lcom/example/forloop/MainActivity;->log(Ljava/lang/String;)V .line 33 :cond_2 if-eqz v1, :cond_3 .line 35 invoke-static {v0}, Ljava/lang/Integer;->toString(I)Ljava/lang/String; move-result-object v2 invoke-virtual {p0, v2}, Lcom/example/forloop/MainActivity;->log(Ljava/lang/String;)V .line 26 :cond_3 add-int/lit8 v0, v0, 0x1 goto :goto_0
1 2 3 4 5 :cond_0 const/4 v0, 0x0 .local v0, "i":I :goto_0
这里首先初始化v0=0
,然后初始化循环器i(存储在v0中)
1 2 const v2, 0x98967f # 加载最大值 9,999,999 (0x98967F) if-lt v0, v2, :cond_1 # 如果 i >= 9,999,999,跳转到 :cond_1(退出循环)
if v0 less than v2
(即v0<v2
)就会跳转到cond_1
1 2 3 4 invoke-static {v0}, Ljava/lang/Integer;->toString(I)Ljava/lang/String; move-result-object v2 # 将 i 转为字符串 invoke-static {v2}, Lcom/example/forloop/MainActivity;->isOk(Ljava/lang/String;)Z move-result v1 # 调用 isOk() 并存储结果到 v1(ok)
v0转换为字符串,然后将结果存储到v2,之后调用isOk,参数是v2,然后将返回值放入v1
1 2 3 4 5 6 7 8 const v2, 0x186a0 # 加载 100,000 (0x186A0) rem-int v2, v0, v2 # 计算 i % 100,000 if-nez v2, :cond_2 # 如果余数为0(每100,000次),执行打印 # 打印当前进度 invoke-static {v0}, Ljava/lang/Integer;->toString(I)Ljava/lang/String; move-result-object v2 invoke-virtual {p0, v2}, Lcom/example/forloop/MainActivity;->log(Ljava/lang/String;)V
if v2 no equal zero
,就是v2!=0
就会跳转到cond_2
标签处执行,否则继续执行下一条指令
1 2 3 4 5 6 7 :cond_2 if-eqz v1, :cond_3 # 如果 isOk() 返回 true,执行成功逻辑 # 打印成功的数字 invoke-static {v0}, Ljava/lang/Integer;->toString(I)Ljava/lang/String; move-result-object v2 invoke-virtual {p0, v2}, Lcom/example/forloop/MainActivity;->log(Ljava/lang/String;)V
如果v1==0
就会打印成功的数字,调用的是log进行打印
1 2 3 :cond_3 add-int/lit8 v0, v0, 0x1 # i++ goto :goto_0 # 跳回循环开头
如果没满足,就执行i++
然后跳转回去
但是为了将其粘贴到apk中就要对其进行修改
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 const/4 v0, 0x0 .local v0, "i":I :goto_3 const v2, 0x98967f if-lt v0, v2, :cond_3 return-void .line 28 :cond_3 invoke-static {v0}, Ljava/lang/Integer;->toString(I)Ljava/lang/String; move-result-object v2 invoke-static {p0, v2}, Lio/asis/ctf2014/numdriod/Verify;->isOk(Landroid/content/Context;Ljava/lang/String;)Z move-result v1 .line 29 .local v1, "ok":Z const v4, 0x186a0 rem-int v4, v0, v4 if-nez v4, :cond_6 .line 31 invoke-static {v0}, Ljava/lang/Integer;->toString(I)Ljava/lang/String; move-result-object v2 invoke-static {v2}, Lio/asis/ctf2014/numdriod/tools/DebugTools;->log(Ljava/lang/String;)V :cond_6 if-eqz v1, :cond_2 goto :goto_4 .line 31 invoke-static {v0}, Ljava/lang/Integer;->toString(I)Ljava/lang/String; move-result-object v2 invoke-static {v2}, Lio/asis/ctf2014/numdriod/tools/DebugTools;->log(Ljava/lang/String;)V .line 26 :cond_2 add-int/lit8 v0, v0, 0x1 goto :goto_3
方案2: 这个方案是直接将Verify中加个main函数,然后修改一些报错,直接运行即可,参照https://gist.github.com/volpino/2edea8503822aefb4c9e
得到答案是
1 2 ................> java .\sample\Verify.java FOUND!! 3130110
2014 Sharif University Quals CTF Commercial Application 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 private void checkLicenceKey (final Context context) { if (this .app.getDataHelper ().getConfig ().hasLicence ()) { showAlertDialog (context, OK_LICENCE_MSG); return ; } LayoutInflater li = LayoutInflater.from (context); View promptsView = li.inflate (R.layout.propmt, (ViewGroup) null); AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder (context); alertDialogBuilder.setView (promptsView); final EditText userInput = (EditText) promptsView.findViewById (R.id.editTextDialogUserInput); alertDialogBuilder.setCancelable (false ).setPositiveButton ("Continue" , new DialogInterface.OnClickListener () { @Override public void onClick (DialogInterface dialog, int id) { String userEnteredValue = userInput.getText ().toString (); String storedKey = MainActivity.this .app.getDataHelper ().getConfig ().getSecurityKey (); String iv = MainActivity.this .app.getDataHelper ().getConfig ().getSecurityIv (); boolean licenceKeyIsValid = KeyVerifier.isValidLicenceKey (userEnteredValue, storedKey, iv); if (licenceKeyIsValid) { MainActivity.this .app.getDataHelper ().updateLicence (2014 ); MainActivity.isRegisterd = true ; MainActivity.this .showAlertDialog (context, MainActivity.OK_LICENCE_MSG); return ; } MainActivity.this .showAlertDialog (context, MainActivity.NOK_LICENCE_MSG); } }).setNegativeButton ("Cancel" , new DialogInterface.OnClickListener () { @Override public void onClick (DialogInterface dialog, int id) { dialog.cancel (); } }); AlertDialog inputLicenceDialog = alertDialogBuilder.create (); inputLicenceDialog.show (); }
关键函数,处理key过程如下
按下continue之后,usrinput会被转为string进入
1 2 3 4 String userEnteredValue = userInput.getText().toString();String storedKey = MainActivity.this .app.getDataHelper().getConfig().getSecurityKey();String iv = MainActivity.this .app.getDataHelper().getConfig().getSecurityIv();boolean licenceKeyIsValid = KeyVerifier.isValidLicenceKey(userEnteredValue, storedKey, iv);
会从配置中得到storedKey和iv,之后进入是否有效的判断
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding" ;public static final String VALID_LICENCE = "29a002d9340fc4bd54492f327269f3e051619b889dc8da723e135ce486965d84" ;public static boolean isValidLicenceKey (String userInput, String secretKey, String iv) { String encryptUserInputData = encrypt (userInput, secretKey, iv); return encryptUserInputData.equals (VALID_LICENCE); } public static String encrypt (String userInput, String secretKey, String iv) { try { SecretKeySpec secretKeySpec = new SecretKeySpec (hexStringToBytes (secretKey), "AES" ); Cipher cipher = Cipher.getInstance (CIPHER_ALGORITHM); IvParameterSpec ivSpec = new IvParameterSpec (iv.getBytes ()); cipher.init (1 , secretKeySpec, ivSpec); byte[] encryptedBytes = cipher.doFinal (userInput.getBytes ()); String encryptedText = bytesToHexString (encryptedBytes); return encryptedText; } catch (Exception e) { e.printStackTrace (); return "" ; } } public static String bytesToHexString (byte[] bytes) { StringBuilder sb = new StringBuilder (); for (byte b : bytes) { sb.append (String.format("%02x" , Integer.valueOf (b & 255 ))); } return sb.toString (); } public static byte[] hexStringToBytes (String s) { int len = s.length (); byte[] data = new byte[len / 2 ]; for (int i = 0 ; i < len; i += 2 ) { data[i / 2 ] = (byte) ((Character.digit (s.charAt (i), 16 ) << 4 ) + Character.digit (s.charAt (i + 1 ), 16 )); } return data; }
主要是通过encrypt函数进行加密,然后和invalid_license进行比较
在getconfig中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public AppConfig getConfig () { AppConfig agency = new AppConfig (); Cursor cursor = this .myDataBase.rawQuery(SELECT_QUERY, null ); if (cursor.moveToFirst()) { agency.setId(cursor.getInt(0 )); agency.setName(cursor.getString(1 )); agency.setInstallDate(cursor.getString(2 )); agency.setValidLicence(cursor.getInt(3 ) > 0 ); agency.setSecurityIv(cursor.getString(4 )); agency.setSecurityKey(cursor.getString(5 )); agency.setDesc(cursor.getString(7 )); } return agency; }
可以发现在这里设置了iv和Key
1 2 3 4 5 6 7 private final Context myContext;private SQLiteDatabase myDataBase;private static String DB_PATH = "/data/data/edu.sharif.ctf/databases/" ;private static String DB_NAME = "db.db" ;private static String TABLE_NAME = "config" ;public static final String UPDATE_QUERY = "UPDATE " + TABLE_NAME + " SET d=?" ;public static final String SELECT_QUERY = "SELECT * FROM " + TABLE_NAME + " WHERE a=1" ;
是从数据库中得到的,且有给出路径,那么直接
1 adb pull /data/data/edu.sharif.ctf/databases/db.db
获得数据库,然后就能得到iv和Key了
1 2 iv=a5efdbd57b84ca36 key=37eaae0141f1a3adf8a1dee655853714
因为AES CBC是对称加密的,所以加解密的Key都是一样的
写个解密AES/CBC/PKCS5Padding的java类即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 import javax.crypto.Cipher;import javax.crypto.spec.IvParameterSpec;import javax.crypto.spec.SecretKeySpec;public class Main { public static final String iv="a5efdbd57b84ca36" ; public static final String Key="37eaae0141f1a3adf8a1dee655853714" ; public static final String en_text="29a002d9340fc4bd54492f327269f3e051619b889dc8da723e135ce486965d84" ; public static void main (String[] args) { String data=decrypt (en_text,Key,iv); System.out.println (data); } public static String decrypt (String paramString1, String paramString2, String paramString3) { try { SecretKeySpec localSecretKeySpec = new SecretKeySpec (hexStringToBytes (paramString2), "AES" ); Cipher localCipher = Cipher.getInstance ("AES/CBC/PKCS5Padding" ); localCipher.init (Cipher.DECRYPT_MODE, localSecretKeySpec, new IvParameterSpec (paramString3.getBytes ())); byte[] bytes = localCipher.doFinal (hexStringToBytes (paramString1)); String flag = "" ; for (byte b : bytes) { flag += (char ) b; } return flag; } catch (Exception localException) { localException.printStackTrace (); } return "" ; } public static String bytesToHexString (byte[] bytes) { StringBuilder sb = new StringBuilder (); for (byte b : bytes) { sb.append (String.format("%02x" , Integer.valueOf (b & 255 ))); } return sb.toString (); } public static byte[] hexStringToBytes (String s) { int len = s.length (); byte[] data = new byte[len / 2 ]; for (int i = 0 ; i < len; i += 2 ) { data[i / 2 ] = (byte) ((Character.digit (s.charAt (i), 16 ) << 4 ) + Character.digit (s.charAt (i + 1 ), 16 )); } return data; } }
成功得到fl-ag-IS-se-ri-al-NU-MB-ER
2015-0CTF-vezel 1 2 3 4 5 6 7 8 9 10 11 public void confirm (View v) { String s = getPackageName(); String first = String.valueOf(getSig(s)); String next = getCrc(); String flag = "0CTF{" + first + next + "}" ; if (flag.equals(this .et.getText().toString())) { Toast.makeText(this , "Yes!" , 0 ).show(); } else { Toast.makeText(this , "0ops!" , 0 ).show(); } }
主要代码如上,发现是通过getSig和getCrc来拼接获得flag的
那么考虑直接hook getSig和getCrc获得对应的值
1 2 3 4 5 6 7 8 9 10 11 12 13 Java.perform(function() { var MainActivity = Java.use('com.ctf.vezel.MainActivity' ); MainActivity.getSig.implementation = function(packageName) { var result = this .getSig(packageName); console.log("Signature hashCode: " + result); return result; }; MainActivity.getCrc.implementation = function() { var result = this .getCrc(); console.log("Crs: " + result); return result; }; });
1 2 Signature hashCode: -183971537 Crs: 1189242199
得到flag为0CTF{-1839715371189242199}
2017 XMAN HelloSmali2 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 package com.example.hellosmali.hellosmali;public class Digest { public static boolean check (String input) { if (input != null && input.length() != 0 ) { char [] charinput = input.toCharArray(); StringBuilder v2 = new StringBuilder (); for (char c : charinput) { String intinput = Integer.toBinaryString(c); while (intinput.length() < 8 ) { intinput = "0" + intinput; } v2.append(intinput); } while (v2.length() % 6 != 0 ) { v2.append("0" ); } String v1 = String.valueOf(v2); char [] v4 = new char [v1.length() / 6 ]; for (int i = 0 ; i < v4.length; i++) { int v6 = Integer.parseInt(v1.substring(0 , 6 ), 2 ); v1 = v1.substring(6 ); v4[i] = "+/abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" .charAt(v6); } StringBuilder v3 = new StringBuilder (String.valueOf(v4)); if (input.length() % 3 != 1 ) { if (input.length() % 3 == 2 ) { v3.append("!" ); } } else { v3.append("!?" ); } String key = String.valueOf(v3); if (key.equals("xsZDluYYreJDyrpDpucZCo!?" )) { return true ; } return false ; } return false ; } }
逆向函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public class re1 { public static void main (String[] args) { String key="xsZDluYYreJDyrpDpucZCo!?" ; String charset="+/abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" ; key=key.substring(0 ,key.length()-2 ); System.out.println(key); StringBuilder binary_string=new StringBuilder (); for (int i=0 ;i<key.length();i++){ char c=key.charAt(i); int index=charset.indexOf(c); String binary=String.format("%6s" ,Integer.toBinaryString(index)).replace(' ' ,'0' ); binary_string.append(binary); } StringBuilder result=new StringBuilder (); String binary=binary_string.toString(); for (int i=0 ;i<binary.length();i+=8 ){ if (i+8 >binary.length()){ break ; } String byteStr=binary.substring(i,i+8 ); int charCode=Integer.parseInt(byteStr,2 ); result.append((char )charCode); } System.out.println(result); } }
1 2 xsZDluYYreJDyrpDpucZCo eM_5m4Li_i4_Ea5y
得到结果
静态分析原生层 2015 - 海峡两岸 - 一个 APK,逆向试试吧 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public native boolean testFlag (String str) ;static { System.loadLibrary ("mobicrackNDK" ); } @Override protected void onCreate (Bundle savedInstanceState) { super.onCreate (savedInstanceState); setContentView (R.layout.activity_crack_me); this .inputButton = (Button) findViewById (R.id.input_button); this .pwdEditText = (EditText) findViewById (R.id.pwd); this .inputButton.setOnClickListener (new View.OnClickListener () { @Override public void onClick (View v) { CrackMe.this .input = CrackMe.this .pwdEditText.getText ().toString (); if (CrackMe.this .input != null) { if (CrackMe.this .testFlag (CrackMe.this .input)) { Toast.makeText (CrackMe.this , CrackMe.this .input, 1 ).show (); } else { Toast.makeText (CrackMe.this , "Wrong flag" , 1 ).show (); } } } }); }
不难看出,testFlag就是核心的函数,加载的mobicrackNDK
但是so文件dump下来直接搜索符号是没有的,因此考虑寻找JNI_Onload
函数(通过 JNI_OnLoad
动态注册)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 jint JNI_OnLoad (JavaVM *vm, void *reserved) { int v3; char *v4; int v5; FILE *stream; int v8; v8 = 0 ; printf ("JNI_OnLoad" ); if ( (*vm)->GetEnv (vm, (void **)&v8, 65540 ) ) goto LABEL_6; v3 = v8; v4 = classPathName[0 ]; stream = (FILE *)((char *)&_sF + 168 ); fprintf ((FILE *)((char *)&_sF + 168 ), "RegisterNatives start for '%s'" , classPathName[0 ]); v5 = (*(int (__fastcall **)(int , char *))(*(_DWORD *)v3 + 24 ))(v3, v4); if ( !v5 ) { fprintf (stream, "Native registration unable to find class '%s'" , v4); LABEL_6: fputs ("GetEnv failed" , (FILE *)((char *)&_sF + 168 )); return -1 ; } if ( (*(int (__fastcall **)(int , int , char **, int ))(*(_DWORD *)v3 + 860 ))(v3, v5, off_400C, 2 ) < 0 ) { fprintf (stream, "RegisterNatives failed for '%s'" , v4); goto LABEL_6; } return 65540 ; }
1 if ( (*(int (__fastcall **)(int , int , char **, int ))(*(_DWORD *)v3 + 860 ))(v3, v5, off_400C, 2 ) < 0 )
这就是注册的函数
1 2 3 4 5 6 7 8 9 .data:0000400 C off_400C DCD aTestflag ; DATA XREF: JNI_OnLoad+60 ↑o .data:0000400 C ; JNI_OnLoad+68 ↑o ... .data:0000400 C ; "testFlag" .data:00004010 DCD aLjavaLangStrin_0 ; "(Ljava/lang/String;)Z" .data:00004014 DCD abcdefghijklmn+1 .data:00004018 DCD aHello ; "hello" .data:0000401 C DCD aLjavaLangStrin_1 ; "()Ljava/lang/String;" .data:00004020 DCD native_hello+1 .data:00004020 ; .data ends
可以看到testFlag就是abcdefghijklmn函数,第二个参数括弧里的是参数,括弧外的是返回值的类型,可以看到testFlag函数的参数类型是String,参数返回类型是bool型
1 bool __fastcall abcdefghijklmn (int a1, int a2, int a3)
a1
:JNIEnv*
指针(通过 jniEnv
全局变量缓存)
a2
:未使用
a3
:输入的 Java 字符串对象(jstring
)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 bool __fastcall abcdefghijklmn (JNIEnv *a1, int a2, void *a3) { int v5; size_t i; jmethodID v7; struct _jfieldID *key; jobject v9; const char *v10; jclass v12; const char *user_input; char s2[12 ]; char v15[140 ]; if ( !jniEnv ) jniEnv = a1; memset (&v15[12 ], 0 , 0x80 u); user_input = (*jniEnv)->GetStringUTFChars (jniEnv, a3, 0 ); v5 = 0 ; if ( strlen (user_input) == 0x10 ) { for ( i = 0 ; i != 8 ; ++i ) s2[i] = user_input[i] - i; v5 = 0 ; s2[8 ] = 0 ; if ( !strcmp (seed[0 ], s2) ) { v12 = (*jniEnv)->FindClass (jniEnv, "com/example/mobicrackndk/Calc" ); if ( !v12 ) { _android_log_print(4 , "log" , "class,failed" ); goto LABEL_11; } v7 = (*jniEnv)->GetStaticMethodID (jniEnv, v12, "calcKey" , "()V" ); if ( !v7 ) { _android_log_print(4 , "log" , "method,failed" ); LABEL_11: exit (1 ); } _JNIEnv::CallStaticVoidMethod (jniEnv, v12, v7); key = (*a1)->GetStaticFieldID (a1, v12, "key" , "Ljava/lang/String;" ); if ( !key ) _android_log_print(4 , "log" , "fid,failed" ); v9 = (*a1)->GetStaticObjectField (a1, v12, key); v10 = (*jniEnv)->GetStringUTFChars (jniEnv, v9, 0 ); while ( i < strlen (v10) + 8 ) { s2[i + 4 ] = user_input[i] - i; ++i; } v15[8 ] = 0 ; return strcmp (v10, v15) == 0 ; } } return v5; }
逆向得到,我们的输入的前八个字节减去索引要与seed[0]相等,后八个字节减去索引要与key相同
1 2 3 4 5 6 7 8 9 10 11 package com.example.mobicrackndk; public class Calc { public static String key; public static void calcKey () { StringBuffer sb = new StringBuffer ("c7^WVHZ," ); key = sb.reverse ().toString (); } }
key=",ZHVW^7c"
所以写个脚本即可
1 2 3 4 5 6 7 8 9 10 11 12 13 public class My { public static void main (String[] args) { String key = "QflMn`fH,ZHVW^7c" ; char [] chars = key.toCharArray (); StringBuilder result = new StringBuilder (); for (int i = 0 ; i < chars.length; i++) { result.append ((char )(chars[i] + i)); } System.out.println (result.toString ()); } }
但是wp说这是错误的
发现是在_init_my
中存在对seed的修改
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 size_t _init_my(){ size_t i; size_t result; for ( i = 0 ; ; ++i ) { result = strlen (seed[0 ]); if ( i >= result ) break ; t[i] = seed[0 ][i] - 3 ; } seed[0 ] = t; byte_4038 = 0 ; return result; }
都减了3,对脚本进行修改
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class My { public static void main (String[] args) { String key = "QflMn`fH,ZHVW^7c" ; char [] chars = key.toCharArray (); StringBuilder result = new StringBuilder (); for (int i = 0 ; i < chars.length; i++) { if (i<8 ){ chars[i]-=3 ; } result.append ((char )(chars[i] + i)); } System.out.println (result.toString ()); } }
成功
静态分析综合题目 2017 ISCC Crackone 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 protected void onCreate (Bundle savedInstanceState) { super.onCreate (savedInstanceState); setContentView (R.layout.activity_main); this .editFlag = (EditText) findViewById (R.id.textView3); this .button = (Button) findViewById (R.id.button); this .button.setOnClickListener (new View.OnClickListener () { @Override public void onClick (View v) { String flag = Digest.encode (MainActivity.this .editFlag.getText ().toString ()).trim (); Log.i ("ISCC" , flag); int result = MainActivity.this .checkFlag (flag.getBytes (), flag.length ()); if (result == 1 ) { Toast.makeText (MainActivity.this , "Flag验证成功!" , 0 ).show (); } else { Toast.makeText (MainActivity.this , "Flag验证失败!" , 0 ).show (); } } }); }
checkFlag为核心函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 _BOOL4 __cdecl native_checkFlag (JNIEnv *a1, int a2, void *usrinput, int size) { char *usrinput_arr; int idx; char *end; char tmp; jbyte *src; src = (jbyte *)malloc (size); (*a1)->GetByteArrayRegion (a1, usrinput, 0 , size, src); usrinput_arr = (char *)malloc (size + 1 ); memset (usrinput_arr, 0 , size + 1 ); memcpy (usrinput_arr, src, size); if ( size / 2 > 0 ) { idx = 0 ; end = &usrinput_arr[size]; do { tmp = usrinput_arr[idx] - 5 ; usrinput_arr[idx++] = *(end - 1 ); *--end = tmp; } while ( idx != size / 2 ); } usrinput_arr[size] = 0 ; free (usrinput_arr); free (src); return strcmp (usrinput_arr, "=0HWYl1SE5UQWFfN?I+PEo.UcshU" ) == 0 ; }
逆向得到脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class My { public static void main (String[] args) { StringBuffer key = new StringBuffer ("=0HWYl1SE5UQWFfN?I+PEo.UcshU" ); String re_key=key.reverse().toString(); char [] chars = re_key.toCharArray(); int idx=re_key.length(); StringBuffer result=new StringBuffer (); for (int i=0 ; i<idx;i++) { if (i<idx/2 ) { result.append((char )(chars[i]+5 )); continue ; } result.append(chars[i]); } System.out.println(result); } }
1 ZmxhZ3tJU0NDSkFWQU5ES1lYWH0=
但是在进入checkFlag之前,还对其进行了编码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 package org.isclab.iscc; public class Digest { private static final String TAG = "Util/Digest" ; private static String str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" ; public static String encode (String srcStr) { if (srcStr != null && srcStr.length () != 0 ) { char [] srcStrCh = srcStr.toCharArray (); StringBuilder asciiBinStrB = new StringBuilder (); for (char c : srcStrCh) { String asciiBin = Integer.toBinaryString (c); while (asciiBin.length () < 8 ) { asciiBin = "0" + asciiBin; } asciiBinStrB.append (asciiBin); } while (asciiBinStrB.length () % 6 != 0 ) { asciiBinStrB.append ("0" ); } String asciiBinStr = String.valueOf (asciiBinStrB); char [] codeCh = new char [asciiBinStr.length () / 6 ]; for (int i = 0 ; i < codeCh.length; i++) { int index = Integer.parseInt (asciiBinStr.substring (0 , 6 ), 2 ); asciiBinStr = asciiBinStr.substring (6 ); codeCh[i] = str.charAt (index); } StringBuilder code = new StringBuilder (String.valueOf (codeCh)); if (srcStr.length () % 3 == 1 ) { code.append ("==" ); } else if (srcStr.length () % 3 == 2 ) { code.append ("=" ); } for (int i2 = 76 ; i2 < code.length (); i2 += 76 ) { code.insert (i2, "\r\n" ); } code.append ("\r\n" ); return String.valueOf (code); } return srcStr; } }
简单分析,可以发现是标准base64,就不用自己写脚本了,cyberchef得到
成功
2018 强网杯 picture lock 在android中,onActivityResult
是一个android.app.Activity
类预先定义好的回调函数,只有当内部方法调用startActivityForResult(Intent intent, int requestCode)
来启动另一个Activity的时候才会调用这个回调函数,被启动的Activity必须在结束前调用setResult来设置返回结果和finish来关闭自己,关闭完之后原Activity就会调用onActivityResult
方法,并将之前启动的Activity的resultCode和intent以及一开始调用新Activity传入的requestCode一起传入onActivityResult
MediaStore内容提供器中,_data
代表绝对路径
逆向分析:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 private String j () { try { Signature[] signatureArr = getPackageManager().getPackageInfo("com.a.sample.picturelock" , 64 ).signatures; MessageDigest messageDigest = MessageDigest.getInstance("MD5" ); for (Signature signature : signatureArr) { messageDigest.update(signature.toByteArray()); } byte [] digest = messageDigest.digest(); StringBuilder sb = new StringBuilder (); for (byte b : digest) { int i = b & 255 ; if (i < 16 ) { sb.append("0" ); } sb.append(Integer.toHexString(i)); } return sb.toString(); } catch (PackageManager.NameNotFoundException | NoSuchAlgorithmException e) { return "" ; } } @Override protected void onActivityResult (int i, int i2, Intent intent) { super .onActivityResult(i, i2, intent); switch (i) { case 1 : if (intent != null ) { String[] strArr = {"_data" }; Cursor query = getContentResolver().query(intent.getData(), strArr, null , null , null ); query.moveToFirst(); String file_path = query.getString(query.getColumnIndex(strArr[0 ])); query.close(); enc(file_path, getFilesDir().getAbsolutePath() + file_path.substring(file_path.lastIndexOf("/" )) + ".lock" , j()); i(); Toast.makeText(this , String.format("%s encrypting" , file_path), 1 ).show(); break ; } break ; } } @Override protected void onCreate (Bundle bundle) { super .onCreate(bundle); setContentView(R.layout.activity_main); findViewById(R.id.encrypt).setOnClickListener(new View .OnClickListener() { @Override public void onClick (View view) { if (a.a(this , "android.permission.WRITE_EXTERNAL_STORAGE" ) != 0 ) { android.support.v4.a.a.a((Activity) this , new String []{"android.permission.WRITE_EXTERNAL_STORAGE" }, 1 ); return ; } Intent intent = new Intent ("android.intent.action.PICK" , (Uri) null ); intent.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/*" ); MainActivity.this .startActivityForResult(intent, 1 ); } }); findViewById(R.id.refresh).setOnClickListener(new View .OnClickListener() { @Override public void onClick (View view) { MainActivity.this .i(); } }); i(); }
看native层的enc函数 参数(文件的真实路径,目标路径,文件签名的md5),第三个参数可以hook看一眼即可得到:f8c49056e4ccf9a11e090eaf471f418d
不hook也可以直接得到
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 PS D:\android_re\CTF\wiki\picture> apksigner verify --verbose --print-certs picturelock.apk Verifies Verified using v1 scheme (JAR signing) : true Verified using v2 scheme (APK Signature Scheme v2): true Verified using v3 scheme (APK Signature Scheme v3): false Verified using v3.1 scheme (APK Signature Scheme v3.1 ): false Verified using v4 scheme (APK Signature Scheme v4): false Verified for SourceStamp: false Number of signers: 1 Signer #1 certificate DN: CN= a, OU=b, O=c, L=d, ST=e, C=ffSigner #1 certificate SHA-256 digest: ba12c13fd60e0def17ae3aee4e6a816782d0367ff02e37ccad5d6e86870c8e38 Signer #1 certificate SHA-1 digest: 48e7045 ee60d9d8a257c5275e3650609a5cca13e Signer #1 certificate MD5 digest: f8c49056e4ccf9a11e090eaf471f418d Signer #1 key algorithm: RSA Signer #1 key size (bits): 2048 Signer #1 public key SHA-256 digest: 4 de55872bc804bf5fccc61c6e42377b24cd6eefc183a800c5807f759f7f9796d Signer #1 public key SHA-1 digest: dba8196f6f49a1d1291e4c03102bc7225b6f1f25 Signer #1 public key MD5 digest: d246e0ae52179dfe9daca889ee3133ef
看到Signer #1 certificate MD5 digest: f8c49056e4ccf9a11e090eaf471f418d
就能得到
so程序的enc程序在调试的时候,老是出现错误,因此就在此留下一个小坑吧,等我变强之后再回来搞定😎
android逆向之动态调试 java层调试 工具:android studio、apktools、smalidea、已经root的手机
首先apktools反编译apk为output
1 apktool d app.apk -o output
然后
1 2 3 4 5 adb shell #adb进入命令行模式 su #切换至超级用户 magisk resetprop ro.debuggable 1 stop start #一定要通过该方式重启
设置手机的ro.debuggable为1,这样任何程序都可以在我们的手机上进行调试(但是缺点就是每次重启都会失效,因此在启动之后都要设置一次
找到对应的调试进程
1 u0_a137 9261 7805 1120068 56804 SyS_epoll_wait 0 S com.a.sample.picturelock
PID为9261
因此设置
1 2 PS C:\Users\Lenovo> adb forward tcp:8700 jdwp:9261 8700
这个8700是android studio中设置的
来到android studio中
设置如上的部分,output是apktool反编译apk为smali的文件,其中output中的smali文件夹要将其Mark为Sources Root权限
之后直接到output中进行调试即可
原生层调试 1 adb shell am start -D -n com.a.sample.picturelock/.MainActivity
然后在adb shell中push进去ida/dbgsrv下对应的server,运行,自动监听 23946 port
然后
1 adb forward tcp:23946 tcp:23946
将PC端的端口转发到手机上对应的服务端口
之后ida
Debug->attach->localhost:23946
选中对应的服务即成功attack上
然后android studio也attach上java层即可