记录一次移动终端安全的作业,学习apk的逆向技术
环境准备
apktool.jar:用来解包、重打包
Android Studio:调试apk
jadx-gui:反编译apk为java源码,静态
解包
1
| apktool.bat b crackme.apk
|
开启debug模式
找到 AndroidManifest.xml
文件中 application
这一行属性,并添加 android:debuggable="true"
(如果已经有的话就不用了)
重打包
1
| apktool b /crackme -o crackme_debug.apk
|
生成密钥
用as自带的工具生成一个key
签名
给刚刚生成的crackme_debug.apk签名
1
| jarsigner -verbose -keystore C:\Users\14169\key.jks -signedjar C:\Users\14169\Desktop\crackme_debug.apk C:\Users\14169\Desktop\crackme_debug.apk key0
|
可以验证一下签名
1
| jarsigner -verify -verbose -certs crackme_debug.apk
|
调试
使用Android Studio 的 smalidea 插件。
打开 Android Studio 的 Plugins 界面(File -> Settings -> Plugins),点开右上角小齿轮,选择 Install Plugin from Disk ,完成插件安装,之后就可以在smali上下断点进行调试了
crackme.apk
正向逻辑分析
根据jadx反编译的代码分析,点击Activate后会进入_activate_click()
中,
首先判断输入是否为空,再判断是否为数字,再判断长度是否为11位,之后传入_encrypt(_inputbox, _made_key());
中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| public static String _activate_click() throws Exception { String _inputbox = _inputbox("Input Your Code:", "Registration", "", ""); if (_inputbox.equals("")) { _hide_kk(); return ""; } else if (Common.Not(Common.IsNumber(_inputbox))) { _hide_kk(); return ""; } else if (_inputbox.length() != 11) { _hide_kk(); return ""; } else { main mainVar = mostCurrent; if (!_mac_address.equals("")) { _encrypt(_inputbox, _made_key()); } _hide_kk(); mostCurrent._activity.CloseMenu(); Common.Msgbox("Please Restart Application to complete Activation process!", "Thank you", mostCurrent.activityBA); mostCurrent._activity.Finish(); return ""; } }
|
_encrypt函数的加密逻辑使用的是DES/ECB/NoPadding,加密的明文是 用户输入+ “zAWS!”
而生成密钥的函数_made_key()
则是使用了substring(imei,0,2) + _mac_address + substring(imei,_imei.length() - 2,_imei.length())
加密的结果用_write2file
写入了文件key.txt中
回到_activate_click()
函数,加密完成后用mostCurrent._activity.Finish();
停止进程
而apk每次启动时都会进入_activity_create
,里面调用了_check_code()
先用_decrypt
去解密key.txt中的内容,然后做出判断
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
| public static boolean _check_code() throws Exception { String _readfile = _readfile(); if (_readfile.equals("")) { return false; } String _decrypt = _decrypt(_readfile); String _calcaulate_codes = _calcaulate_codes(_fix_imei(true)); String _calcaulate_codes2 = _calcaulate_codes(_fix_imei(false)); String _get_correct_code = _get_correct_code(_decrypt); StringBuilderWrapper stringBuilderWrapper = new StringBuilderWrapper(); stringBuilderWrapper.Initialize(); stringBuilderWrapper.Append(BA.NumberToString(_key_from_imei_number)).ToString(); if (_get_correct_code.substring(0, 1).equalsIgnoreCase(String.valueOf(stringBuilderWrapper)) && _get_correct_code.substring(1, 6).equalsIgnoreCase(_calcaulate_codes) && _get_correct_code.substring(_get_correct_code.length() - 5, _get_correct_code.length()).equalsIgnoreCase(_calcaulate_codes2)) { main mainVar = mostCurrent; _yes_reg = "About"; return true; } return false; }
public static String _get_correct_code(String str) throws Exception { return str.substring(5, 6) + str.substring(str.length() - 5, str.length()) + str.substring(0, 5); }
|
_calcaulate_codes
和_calcaulate_codes2
_key_from_imei_number
的值
所以我们的_get_correct_code
应该为
推算出输入应该为
patch
题目要求新的apk对任意字符成功注册,
我们首先让_activate_click()
中对前面几步判断全跳过,这里可以修改第一步判断是否为空的逻辑,非空直接跳到cond_2,越过中间的两步判断
由于输入长度没有限制,所以在加解密的时候des填充方式需要修改一下,这里已经改成了pkcs5
再修改_check_code()
的跳转逻辑,只要非空直接跳转到cond_3,直接注册成功
之后继续重打包,签名,输入任意字符即可激活成功
crackme2.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
| public void onClick(View v) { switch (v.getId()) { case R.id.btn_validate : EditText et = (EditText) findViewById(R.id.edt_code); String serial = et.getText().toString(); if (validateSerial(serial) == 0) { Toast.makeText(this, "Invalid code.\nTry again.", 1).show(); return; } Toast.makeText(this, "Code is valid!", 1).show(); et.setVisibility(4); Button btn = (Button) findViewById(R.id.btn_validate); btn.setVisibility(4); TextView tv = (TextView) findViewById(R.id.tv_reg_text); tv.setText("Code Accepted :D"); return; default: return; } }
private String generateIDHash() throws Exception { String deviceID = getMobileID(); MessageDigest m = MessageDigest.getInstance("MD5"); m.update(deviceID.getBytes(), 0, deviceID.length()); byte[] digest = m.digest(); byte[] transform = new byte[digest.length]; int digestPos = 0; int transformPos = 0; while (digestPos < digest.length) { int nextPos = digestPos >= digest.length + (-1) ? 0 : digestPos + 1; transform[transformPos] = (byte) (digest[digestPos] ^ digest[nextPos]); digestPos += 2; transformPos++; } String hash = new BigInteger(1, transform).toString(16); return hash.substring(0, 15); }
|
编写注册机
写一个函数generateSerial()用于生成注册码
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
| package org.example;
import java.math.BigInteger; import java.security.MessageDigest;
public class RegistrationKeyGenerator {
public static void main(String[] args) { String serial = generateSerial(); System.out.println("Generated Serial: " + serial); }
private static String generateSerial() { try { String deviceID = "358240051111110";
MessageDigest m = MessageDigest.getInstance("MD5"); m.update(deviceID.getBytes(), 0, deviceID.length()); byte[] digest = m.digest(); byte[] transform = new byte[digest.length]; int digestPos = 0; int transformPos = 0; while (digestPos < digest.length) { int nextPos = digestPos >= digest.length - 1 ? 0 : digestPos + 1; transform[transformPos] = (byte) (digest[digestPos] ^ digest[nextPos]); digestPos += 2; transformPos++; } String hash = new BigInteger(1, transform).toString(16); hash = hash.substring(0, 15);
return hash; } catch (Exception e) { e.printStackTrace(); } return null; } }
|
新建一个as的项目,创建一个按钮
监听点击事件,调用generateSerial();
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main);
View generateButton = findViewById(R.id.btn_generate); generateButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (hasReadPhoneStatePermission()) { generateSerial(); } else { requestReadPhoneStatePermission(); } } }); }
|
完整代码
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 68 69 70 71 72 73 74
| public class MainActivity extends Activity {
private static final int PERMISSION_REQUEST_READ_PHONE_STATE = 1;
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main);
View generateButton = findViewById(R.id.btn_generate); generateButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (hasReadPhoneStatePermission()) { generateSerial(); } else { requestReadPhoneStatePermission(); } } }); }
private boolean hasReadPhoneStatePermission() { if (Build.VERSION.SDK_INT >= 23) { return ContextCompat.checkSelfPermission(this, Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED; } return true; }
@SuppressLint("HardwareIds") private void generateSerial() { if (hasReadPhoneStatePermission()) { try { TelephonyManager telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE); if (telephonyManager != null) { if (ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED) { String deviceID = telephonyManager.getDeviceId(); String serial = generateIDHash(deviceID); Toast.makeText(this, "Generated Serial: " + serial, Toast.LENGTH_SHORT).show(); } else { Toast.makeText(this, "Permission denied. Unable to generate serial.", Toast.LENGTH_SHORT).show(); } } } catch (SecurityException e) { e.printStackTrace(); Toast.makeText(this, "SecurityException: " + e.getMessage(), Toast.LENGTH_SHORT).show(); } catch (Exception e) { e.printStackTrace(); Toast.makeText(this, "Exception: " + e.getMessage(), Toast.LENGTH_SHORT).show(); } } }
private void requestReadPhoneStatePermission() { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_PHONE_STATE}, PERMISSION_REQUEST_READ_PHONE_STATE); }
private String generateIDHash(String deviceID) throws Exception { MessageDigest m = MessageDigest.getInstance("MD5"); m.update(deviceID.getBytes(), 0, deviceID.length()); byte[] digest = m.digest(); byte[] transform = new byte[digest.length]; int digestPos = 0; int transformPos = 0; while (digestPos < digest.length) { int nextPos = digestPos >= digest.length - 1 ? 0 : digestPos + 1; transform[transformPos] = (byte) (digest[digestPos] ^ digest[nextPos]); digestPos += 2; transformPos++; } String hash = new BigInteger(1, transform).toString(16); return hash.substring(0, 15); } }
|
AndroidManifest.xml 文件中添加权限
1
| <uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
最后构建成apk即可
请求权限后同意即可,会生成正确的注册码
Reference
https://blog.csdn.net/Breeze_CAT/article/details/104588568/