Android应用安全防护和逆向分析
上QQ阅读APP看书,第一时间看更新

第1章 Android中锁屏密码加密算法分析

为了安全,Android设备在解锁屏幕时会有密码输入,那么这个密码存放在哪里?是否为明文存储?如果是加密存储,那么加密算法是什么?这就是本章要介绍的内容。本章的目的是带领读者踏入移动安全的大门。

1.1 锁屏密码方式

Android中现在支持的锁屏密码主要有两种:一种是手势密码,也就是常见的九宫格密码图;一种是输入密码,分为PIN密码和复杂字符密码,而PIN密码就是数字密码,比较简单。当然现在也有一个高级的指纹密码,这不是本章分析的范围,本章只分析手势密码和输入密码。

1.2 密码算法分析

在设置锁屏密码界面,用工具获取当前的View类,然后一步一步跟入,最终会跟到一个锁屏密码工具类:LockPatternUtils.java。每个版本可能实现逻辑不一样,这里用5.1版本的源码进行分析。

1.2.1 输入密码算法分析

找到源码之后,首先来分析一下输入密码算法:

        public byte[] passwordToHash(String password, int userId) {
            if (password == null) {
                return null;
            }
            String algo = null;
            byte[] hashed = null;
            try {
                    byte[] saltedPassword = (password + getSalt(userId)).getBytes();
                    byte[] sha1 = MessageDigest.getInstance(algo = "SHA-1").
                digest(saltedPassword);
                    byte[] md5 = MessageDigest.getInstance(algo = "MD5").
                digest(saltedPassword);
                    hashed = (toHex(sha1) + toHex(md5)).getBytes();
                } catch (NoSuchAlgorithmException e) {
                    Log.w(TAG, "Failed to encode string because of missing algorithm: "
            + algo);
                }
                return hashed;
            }

可以看到有一个方法passwordToHash,参数为用户输入的密码和当前用户对应的id,一般设备不会有多个用户,所以这里的userId是默认值0。下面就是最为核心的加密算法了:原文密码+设备的salt值,然后分别进行MD5和SHA-1操作,转化成hex值再次拼接,得到的就是最终保存到本地的加密密码内容。而这里最重要的是如何获取设备对应的salt值,这可以一步一步跟踪代码:

        private String getSalt(int userId) {
            long salt = getLong(LOCK_PASSWORD_SALT_KEY, 0, userId);
            if (salt == 0) {
                try {
                    salt = SecureRandom.getInstance("SHA1PRNG").nextLong();
                    setLong(LOCK_PASSWORD_SALT_KEY, salt, userId);
                    Log.v(TAG, "Initialized lock password salt for user: " + userId);
                } catch (NoSuchAlgorithmException e) {
                    throw new IllegalStateException("Couldn't get SecureRandom number", e);
                }
            }
            return Long.toHexString(salt);
        }

查看getSalt方法,首先根据字段key为lockscreen.password_salt从一个地方获取salt值,如果发现这个值为0,就随机生成一个,然后将其保存到那个地方,最后将salt转化成hex值即可。现在需要找到这个地方,继续跟踪代码:

        private long getLong(String secureSettingKey, long defaultValue, int userHandle) {
                try {
                    return getLockSettings().getLong(secureSettingKey, defaultValue,
            userHandle);
            } catch (RemoteException re) {
                    return defaultValue;
                }
        }

猜想应该是保存到一个数据库中了,继续跟踪代码:

        private ILockSettings getLockSettings() {
            if (mLockSettingsService == null) {
            ILockSettings service = ILockSettings.Stub.asInterface(
                        ServiceManager.getService("lock_settings"));
                mLockSettingsService = service;
            }
            return mLockSettingsService;
        }

通过在ServiceManager中获取一个服务来进行操作,在Android中,像这种获取服务的方式最终实现逻辑都是在XXXService类中,这里是LockSettingsService.java类,找到这个类,查看它的getLong方法:

        @Override
            public long getLong(String key, long defaultValue, int userId) throws
                RemoteException {
                checkReadPermission(key, userId);
                String value = mStorage.readKeyValue(key, null, userId);
                return TextUtils.isEmpty(value) ? defaultValue : Long.parseLong(value);
            }

其实到这里就可以看出,非常肯定是数据库中保存的,继续跟踪代码:

        private final LockSettingsStorage mStorage;

            private LockPatternUtils mLockPatternUtils;
            private boolean mFirstCallToVold;

            public LockSettingsService(Context context) {
                mContext = context;
                // Open the database

                mLockPatternUtils = new LockPatternUtils(context);
                mFirstCallToVold = true;

                IntentFilter filter = new IntentFilter();
                filter.addAction(Intent.ACTION_USER_ADDED);
                filter.addAction(Intent.ACTION_USER_STARTING);
                mContext.registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL,
                    filter, null, null);

                mStorage = new LockSettingsStorage(context, new LockSettingsStorage.
                    Callback() {
                    @Override
                    public void initialize(SQLiteDatabase db) {
                        // Get the lockscreen default from a system property, if available
                        boolean lockScreenDisable = SystemProperties.getBoolean(
                                  "ro.lockscreen.disable.default", false);
                        if (lockScreenDisable) {
                            mStorage.writeKeyValue(db,
                                  LockPatternUtils.DISABLE_LOCKSCREEN_KEY, "1", 0);
                        }
                    }
                });
            }

果然发现是保存到一个数据库中,继续查看LockSettingsStorage.java类:

        class DatabaseHelper extends SQLiteOpenHelper {
            private static final String TAG = "LockSettingsDB";
            private static final String DATABASE_NAME = "locksettings.db";

            private static final int DATABASE_VERSION = 2;

            private final Callback mCallback;
                  public DatabaseHelper(Context context, Callback callback) {
                      super(context, DATABASE_NAME, null, DATABASE_VERSION);
                      setWriteAheadLoggingEnabled(true);
                      mCallback = callback;
                  }

                  private void createTable(SQLiteDatabase db) {
                      db.execSQL("CREATE TABLE " + TABLE + " (" +
                              "_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
                              COLUMN_KEY + " TEXT, " +
                              COLUMN_USERID + " INTEGER, " +
                              COLUMN_VALUE + " TEXT" +
                              "); ");
                  }

                  @Override
                  public void onCreate(SQLiteDatabase db) {
                      createTable(db);
                      mCallback.initialize(db);
                  }

                  @Override
                  public void onUpgrade(SQLiteDatabase db, int oldVersion, int
                      currentVersion) {
                      int upgradeVersion = oldVersion;
                      if (upgradeVersion == 1) {
                          // Previously migrated lock screen widget settings. Now defunct.
                          upgradeVersion = 2;
                      }

                      if (upgradeVersion ! = DATABASE_VERSION) {
                          Log.w(TAG, "Failed to upgrade database! ");
                      }
                  }
              }

看到了,数据库名字叫作locksettings.db,那么它保存在哪里呢?继续看这个类:

        private static final String SYSTEM_DIRECTORY = "/system/";
        private static final String LOCK_PATTERN_FILE = "gesture.key";
        private static final String LOCK_PASSWORD_FILE = "password.key";

可以看到有两个key文件,它们就是用来保存加密之后的手势密码和输入密码的,将信息保存到本地,下次开机解锁需要读取这个文件内容进行密码比对。看到一个目录是system,所以数据库和这两个key文件很可能保存到目录/data/system/下了,为了再证实一下,直接用find命令去根目录下搜索这个数据库文件也是可以的。最终确定是该目录:

这里可能会提示找不到find命令,这时需要安装busybox工具,才能使用这个命令。找到这个数据库文件就好办了,直接取出来,然后用SQLite工具进行查看即可,当然也可以直接在手机中查看。为了方便还是取出来看,如图1-1所示。

图1-1 数据库信息

这里看到了表格字段,并且获取到这个值了,那么下面就要用这个值来验证上面的分析是否正确。首先给设备设置一个简单的输入密码,这里直接输入简单的“1234”,然后会在/data/system目录下生成一个密码加密key文件/data/system/password.key,将该文件导出来,如图1-2所示。

图1-2 加密文件

下面就用简单的Java代码手动实现这个算法,看看分析是否正确。加密算法不用自己写,直接从上面的源码中拷贝出来就可以了。代码如下:

        /**
        * 输入密码加密算法
        * @param password
        * @return
        */
        public static byte[] passwordToHash(String password) {
            if (password == null) {
                return null;
            }
            byte[] hashed = null;
            try {
                byte[] saltedPassword = (password + SALT).getBytes();
                byte[] sha1 = MessageDigest.getInstance("SHA-1").digest(saltedPassword);
                byte[] md5 = MessageDigest.getInstance("MD5").digest(saltedPassword);
                hashed = (toHex(sha1) + toHex(md5)).getBytes();
            } catch (Exception e) {
            }
            return hashed;
        }

        private static String toHex(byte[] ary) {
            final String hex = "0123456789ABCDEF";
            String ret = "";
            for (int i = 0; i < ary.length; i++) {
                ret += hex.charAt((ary[i] >> 4) & 0xf);
                ret += hex.charAt(ary[i] & 0xf);
            }
            return ret;
        }

这里的salt值是我们从数据库中得到的,不过要记得进行hex转化:

        /**
        * 设备的salt值,可以利用反射获取,也可以去/data/system/locksettings.db数据库中查看
        * 需要注意的是数据库中保存的是long类型,这里需要进行hex转化
        */
        private final static String SALT = Long.toHexString(-9167742676506383495l);

然后用“1234”原文密码去生成加密之后的信息:

        System.out.println("passwordinfo:"+new String(passwordToHash("1234")));

运行结果如图1-3所示。

图1-3 运行结果

可以发现内容和上面的password.key内容完全一致,也就验证了上面的分析完全正确。到这里就分析完了输入密码的加密算法,总结一点就是:MD5(输入明文密码+设备的salt).Hex+SHA1(输入明文密码+设备的salt).Hex就是最终的加密内容。而这里最重要的是如何获取设备的salt值,可以用反射机制进行获取,新建一个简单的Android项目:

        try{
            Class<? > clazz1 = Class.forName("com.android.internal.widget.
                LockPatternUtils");
            Object lockUtils = clazz1.getConstructor(Context.class).newInstance(this);
            Class<? > lockUtilsClazz = lockUtils.getClass();
            Method getSaltM = lockUtilsClazz.getDeclaredMethod("getSalt", int.class);
            getSaltM.setAccessible(true);
            Object saltObj = getSaltM.invoke(lockUtils, 0);
            Log.i("jw", "salt:"+saltObj);
        }catch(Exception e){
            Log.i("jw", "err:"+Log.getStackTraceString(e));
        }

这样就不用去查看数据库获取salt值了,方便快捷,打印的日志信息如下:

这是数据库中的long类型值转化成hex之后的值。

1.2.2 手势密码算法分析

下面来分析手势密码,代码依然在LockPatternUtils.java中:

        public static byte[] patternToHash(List<LockPatternView.Cell> pattern) {
            if (pattern == null) {
                return null;
            }

            final int patternSize = pattern.size();
            byte[] res = new byte[patternSize];
            for (int i = 0; i < patternSize; i++) {
                LockPatternView.Cell cell = pattern.get(i);
                res[i] = (byte) (cell.getRow() * 3 + cell.getColumn());
            }
            try {
                MessageDigest md = MessageDigest.getInstance("SHA-1");
                byte[] hash = md.digest(res);
                return hash;
            } catch (NoSuchAlgorithmException nsa) {
                return res;
            }
        }

这个算法比较简单,就是九宫格图案转化成字节数组,然后用SHA1加密即可。关于九宫格不再多说了,从0开始顺时针计数到8,类似图1-4所示。

图1-4 九宫格

看一下代码,有行和列之分。比如L形状的手势密码应该是00 03 06 07 08,这样组成五个字节。这里为了验证手势密码是否正确,设置一个简单的手势密码,如图1-5所示。

图1-5 简单的手势密码

然后在/data/system目录下生成一个密码文件/data/system/gesture.key,取出来用二进制工具查看,不然可能看到的是乱码,st r.这里用的是010Editor工具查看,如图1-6所示。

图1-6 手势密码加密内容

为了最大化地还原算法,依然把源码拷贝出来,然后定义一个手势九宫格类,构造出这个手势的点数据:

        /**
        * 手势密码加密算法
        * @param pattern
        * @return
        */
        public static byte[] patternToHash(List<LockPatternView.Cell> pattern) {
            if (pattern == null) {
                return null;
            }

            int patternSize = pattern.size();
            byte[] res = new byte[patternSize];
            for (int i = 0; i < patternSize; i++) {
                LockPatternView.Cell cell = pattern.get(i);
                res[i] = (byte) (cell.row * 3 + cell.column);
            }
            try {
                MessageDigest md = MessageDigest.getInstance("SHA-1");
                byte[] hash = md.digest(res);
                return hash;
            } catch (Exception nsa) {
            }
            return null;
        }

这是源码的加密算法,下面再构造出手势点数据:

        //手势密码模拟
        List<LockPatternView.Cell> pattern = new ArrayList<LockPatternView.Cell>();
        LockPatternView.Cell cell1 = new LockPatternView.Cell(0, 0);
        pattern.add(cell1);
        LockPatternView.Cell cell2 = new LockPatternView.Cell(0, 3);
        pattern.add(cell2);
        LockPatternView.Cell cell3 = new LockPatternView.Cell(0, 6);
        pattern.add(cell3);
        LockPatternView.Cell cell4 = new LockPatternView.Cell(0, 7);
        pattern.add(cell4);
        LockPatternView.Cell cell5 = new LockPatternView.Cell(0, 8);
        pattern.add(cell5);
        System.out.println("pattern:"+toHex(patternToHash(pattern)));

手势点数据应该是00 01 02 05 08,打印看结果,如图1-7所示。

图1-7 运行结果

从运行结果发现,一模一样,这样就完美地分析完了手势密码加密算法。

这里再总结一下两种方式锁屏密码算法。

第一种:输入密码算法

对输入的明文密码+设备的salt值进行MD5和SHA1操作,之后转化成hex值进行拼接即可,最终加密信息保存到本地目录/data/system/password.key。

第二种:手势密码算法

将九宫格手势密码中的点数据转化成对应的字节数组,然后直接进行SHA1加密即可。最终加密信息保存到本地目录/data/system/gesture.key。

1.3 本章小结

读完本章是不是迫不及待地想动手尝试一下?在操作之前一定要记住,先得到设备的salt值,然后要注意源码版本。