美文网首页
浅析Seafile网盘apk的端对端加密方式

浅析Seafile网盘apk的端对端加密方式

作者: 看雪学院 | 来源:发表于2018-10-26 18:21 被阅读49次


    写在开篇

    使用了架设在自己服务器上的Seafile一段时间,本菜对于其宣传的“端对端加密,数据在客户端解密”这个特性产生了好奇。很想了解一下网络传输、加密的过程是如何运作的。不多说,我们马上配置好Android上的抓包环境,用fiddler看看网络传输的内容吧。

    环境配置

    系统: Windows 7、nexus 5

    程序: Seafile 2.2 for Android

    程序下载地址

    https://www.dreamcracker.online/d/93572bfed8124e798eab/files/p=/Crack_Me/com.seafile.seadroid2_83.apk

    要求: 分析逻辑

    使用工具:fiddler、apktool、AndroidKiller、Android Studio、 smalidea插件

    抓包分析

    确认端对端加密设置启用后,打开设置好的加密资料库secret,输入密码:

    此时捕获到了这样几个包(关键位置已打马赛克):

    这里post密码,服务器认可。

    返回了资料库的一些信息,比较值得关注的有加密类型、magic、random_key。

    接下来请求根目录内容,Response以json形式展现了资料库根目录内容。

    此时,我们请求一个文件,看看传输来的数据如何。

    获得服务器生成的下载的链接,然后得到一段密文。这里文件的大小是120字节,而传输过来的文件大小是128字节。

    再随机拍张照片上传,此时抓到的文件数据和本地的很不相同:

    综上所述,我们可以做出如下猜测。

    服务器对于加密资料库提供了端对端加密的方式,即文件在本地解密,即使有人攻击了传输链条中任意一环,也无法直接获得明文消息。

    加密资料库以“端加密方式”开放时,会生成随机消息用于加密。

    加密方式是通用的公开算法,且有可能是cbc模式。

    提出假设

    我们使用最新的apktool做为反编译工具,发现提示反编译成功。那么,直接设置我们的apktool的路径作为Android Killer的默认反编译器。我们直接全局搜索“加密”:

    <string name="enc_on">数据在本地解密</string>

    <string name="enc_title">加密资料库解密方式</string>

    <string name="encrypt">加密</string>

    接着全局搜索enc_title,发现出现很多xml文件,再仔细看看,其实更多是重复的多语种的string资源文件。经过筛选,发现\res\xml\settings.xml的这一行很可疑:

    全局搜索“client_encrypt_switch_key”,发现SettingsManager.class有两个函数较为可疑,我们将其smali文件转成Java代码查看:

    public void setupEncrypt(boolean paramBoolean)

    {

    this.settingsSharedPref.edit().putBoolean("client_encrypt_switch_key", paramBoolean).commit();

    }

    public boolean isEncryptEnabled()

    {

    return this.settingsSharedPref.getBoolean("client_encrypt_switch_key", false);

    }

    isEncryptEnabled明显是判断“端对端”加密方式是否打开的函数,想必要调查用户选择的加密方式的手法也就是调用它了,全局搜索“isEncryptEnabled”试试。

    搜索结果有两个是\data路径下的SeafRepoEncrypt、SeafRepo两个类文件。但项目内引用SeafRepoEncrypt类的次数较少。我们来看看SeafRepoEncrypt的定义:

    package com.seafile.seadroid2.data;

    import android.text.TextUtils;

    import com.seafile.seadroid2.SettingsManager;

    import org.json.JSONException;

    import org.json.JSONObject;

    public class SeafRepoEncrypt

    {

    public String encKey;

    public int encVersion;

    public boolean encrypted;

    public String id;

    public boolean isGroupRepo;

    public boolean isPersonalRepo;

    public boolean isSharedRepo;

    public String magic;

    public long mtime;

    public String name;

    public String owner;

    public String permission;

    public String root;

    public long size;

    static SeafRepoEncrypt fromJson(JSONObject paramJSONObject)

    throws JSONException

    {

    SeafRepoEncrypt localSeafRepoEncrypt = new SeafRepoEncrypt();

    localSeafRepoEncrypt.magic = paramJSONObject.optString("magic");

    localSeafRepoEncrypt.permission = paramJSONObject.getString("permission");

    localSeafRepoEncrypt.encrypted = paramJSONObject.getBoolean("encrypted");

    localSeafRepoEncrypt.encVersion = paramJSONObject.optInt("enc_version");

    localSeafRepoEncrypt.mtime = paramJSONObject.getLong("mtime");

    localSeafRepoEncrypt.owner = paramJSONObject.getString("owner");

    localSeafRepoEncrypt.id = paramJSONObject.getString("id");

    localSeafRepoEncrypt.size = paramJSONObject.getLong("size");

    localSeafRepoEncrypt.name = paramJSONObject.getString("name");

    localSeafRepoEncrypt.root = paramJSONObject.getString("root");

    localSeafRepoEncrypt.encKey = paramJSONObject.optString("random_key");

    localSeafRepoEncrypt.isGroupRepo = paramJSONObject.getString("type").equals("grepo");

    localSeafRepoEncrypt.isPersonalRepo = paramJSONObject.getString("type").equals("repo");

    localSeafRepoEncrypt.isSharedRepo = paramJSONObject.getString("type").equals("srepo");

    return localSeafRepoEncrypt;

    }

    public boolean canLocalDecrypt()

    {

    return (this.encrypted) && (this.encVersion == 2) && (!TextUtils.isEmpty(this.magic)) && (SettingsManager.instance().isEncryptEnabled());

    }

    }

    搜索其调用,还是有两个较为可疑的引用,seadroid2\ui\dialog下SetPasswordTask、PasswordDialog类。

    其中,PasswordDialog类的onTaskSuccess函数使用用户输入的密码、random_key生成了password:

    public void onTaskSuccess()

    {

    Object localObject = this.dataManager.getCachedRepoEncryptByID(this.repoID);

    String str = this.passwordText.getText().toString().trim();

    if ((localObject != null) && (((SeafRepoEncrypt)localObject).canLocalDecrypt()))

    {

    if (TextUtils.isEmpty(((SeafRepoEncrypt)localObject).magic)) {

    return;

    }

    try

    {

    localObject = Crypto.generateKey(str, ((SeafRepoEncrypt)localObject).encKey, ((SeafRepoEncrypt)localObject).encVersion);

    this.dataManager.setRepoPasswordSet(this.repoID, (String)((Pair)localObject).first, (String)((Pair)localObject).second);

    }

    catch (UnsupportedEncodingException|NoSuchAlgorithmException localUnsupportedEncodingException)

    {

    ThrowableExtension.printStackTrace(localUnsupportedEncodingException);

    }

    }

    else

    {

    this.dataManager.setRepoPasswordSet(this.repoID, str);

    }

    super.onTaskSuccess();

    }

    然后调用了this.dataManager.setRepoPasswordSet:

    public void setRepoPasswordSet(String paramString1, String paramString2)

    {

    passwords.put(paramString1, new PasswordInfo(paramString2, Utils.now()));

    }

    public void setRepoPasswordSet(String paramString1, String paramString2, String paramString3)

    {

    if ((!TextUtils.isEmpty(paramString1)) && (!TextUtils.isEmpty(paramString2)) && (!TextUtils.isEmpty(paramString3))) {

    dbHelper.saveEncKey(paramString2, paramString3, paramString1);

    }

    }

    saveEncKey函数向数据库插入一条记录:

    public void saveEncKey(String paramString1, String paramString2, String paramString3)

    {

    Object localObject = getEnckey(paramString3);

    if ((localObject != null) && (!TextUtils.isEmpty((CharSequence)((Pair)localObject).first)))

    {

    if ((((String)((Pair)localObject).first).equals(paramString1)) && (((String)((Pair)localObject).second).equals(paramString2))) {

    return;

    }

    delEnckey(paramString3);

    }

    localObject = new ContentValues();

    ((ContentValues)localObject).put("enc_key", paramString1);

    ((ContentValues)localObject).put("enc_iv", paramString2);

    ((ContentValues)localObject).put("repo_id", paramString3);

    this.database.insert("EncKey", null, (ContentValues)localObject);

    }

    再来看看生成password的Crypto代码:

    public static Pair generateKey(String paramString1, String paramString2, int paramInt)

    throws UnsupportedEncodingException, NoSuchAlgorithmException

    {

    byte[] arrayOfByte = deriveKey(paramString1, paramInt);

    paramString1 = new SecretKeySpec(arrayOfByte, "AES");

    arrayOfByte = deriveIv(arrayOfByte);

    paramString1 = deriveKey(seafileDecrypt(fromHex(paramString2), paramString1, arrayOfByte), paramInt);

    return new Pair(paramString1, toHex(deriveIv(fromHex(paramString1))));

    }

    其中,seafileDecrypt的定义:

    private static byte[] seafileDecrypt(byte[] paramArrayOfByte1, SecretKey paramSecretKey, byte[] paramArrayOfByte2)

    {

    try

    {

    Cipher localCipher = Cipher.getInstance("AES/CBC/PKCS7Padding");

    localCipher.init(2, paramSecretKey, new IvParameterSpec(paramArrayOfByte2));

    paramArrayOfByte1 = localCipher.doFinal(paramArrayOfByte1);

    return paramArrayOfByte1;

    }

    catch (InvalidAlgorithmParameterException paramArrayOfByte1)

    {

    paramSecretKey = TAG;

    paramArrayOfByte2 = new StringBuilder();

    paramArrayOfByte2.append("InvalidAlgorithmParameterException ");

    paramArrayOfByte2.append(paramArrayOfByte1.getMessage());

    Log.e(paramSecretKey, paramArrayOfByte2.toString());

    ThrowableExtension.printStackTrace(paramArrayOfByte1);

    return null;

    }

    catch (IllegalBlockSizeException paramArrayOfByte1)

    {

    paramSecretKey = TAG;

    paramArrayOfByte2 = new StringBuilder();

    paramArrayOfByte2.append("IllegalBlockSizeException ");

    paramArrayOfByte2.append(paramArrayOfByte1.getMessage());

    Log.e(paramSecretKey, paramArrayOfByte2.toString());

    ThrowableExtension.printStackTrace(paramArrayOfByte1);

    return null;

    }

    catch (BadPaddingException paramArrayOfByte1)

    {

    ThrowableExtension.printStackTrace(paramArrayOfByte1);

    paramSecretKey = TAG;

    paramArrayOfByte2 = new StringBuilder();

    paramArrayOfByte2.append("seafileDecrypt BadPaddingException ");

    paramArrayOfByte2.append(paramArrayOfByte1.getMessage());

    Log.e(paramSecretKey, paramArrayOfByte2.toString());

    return null;

    }

    catch (NoSuchPaddingException paramArrayOfByte1)

    {

    ThrowableExtension.printStackTrace(paramArrayOfByte1);

    paramSecretKey = TAG;

    paramArrayOfByte2 = new StringBuilder();

    paramArrayOfByte2.append("NoSuchPaddingException ");

    paramArrayOfByte2.append(paramArrayOfByte1.getMessage());

    Log.e(paramSecretKey, paramArrayOfByte2.toString());

    return null;

    }

    catch (InvalidKeyException paramArrayOfByte1)

    {

    ThrowableExtension.printStackTrace(paramArrayOfByte1);

    paramSecretKey = TAG;

    paramArrayOfByte2 = new StringBuilder();

    paramArrayOfByte2.append("InvalidKeyException ");

    paramArrayOfByte2.append(paramArrayOfByte1.getMessage());

    Log.e(paramSecretKey, paramArrayOfByte2.toString());

    return null;

    }

    catch (NoSuchAlgorithmException paramArrayOfByte1)

    {

    ThrowableExtension.printStackTrace(paramArrayOfByte1);

    paramSecretKey = TAG;

    paramArrayOfByte2 = new StringBuilder();

    paramArrayOfByte2.append("NoSuchAlgorithmException ");

    paramArrayOfByte2.append(paramArrayOfByte1.getMessage());

    Log.e(paramSecretKey, paramArrayOfByte2.toString());

    }

    return null;

    }

    好了~

    至此,我们可以提出假设:

    在输入密码进入加密资料库时,会利用random_key、用户输入的密码生成一个最终password,这个password将作为aes_cbc加盐算法的key被存储。而在下载/上传文件时,必然调用加解密函数,存储的password也必然被查询。

    搜集证据

    我们在Android Studio中安装smalidea插件、载入反编译的smali项目。连接Nexus 5后,对于Crypto中的重点函数布下断点,开始动态调试。我们输入资料库密码后确认,服务器发来消息:

    我们再看看此时断点的情况:

    用password、enc_type、random_key构成参数,开始构造最终key:

    构造key、iv。继续跟:

    发现加盐了:

    最终生成一组密码对。

    当我们在资料库中下载一个文件时,传回的文件流如图所示:

    而程序此时断在了解密函数:

    解密函数的密钥对与上面生成的相匹配。

    等到程序返回,就会将解出的明文写入文件。

    再来观察一下细节,加解密相关的函数很简单朴素:

    Cipher localCipher = Cipher.getInstance("AES/CBC/PKCS7Padding");

    localCipher.init(1, paramSecretKey, new IvParameterSpec(paramArrayOfByte2));

    paramArrayOfByte1 = localCipher.doFinal(paramArrayOfByte1, 0, paramInt);

    return paramArrayOfByte1;

    private static byte[] seafileDecrypt(byte[] paramArrayOfByte1, SecretKey paramSecretKey, byte[] paramArrayOfByte2)

    {

    try

    {

    Cipher localCipher = Cipher.getInstance("AES/CBC/PKCS7Padding");

    localCipher.init(2, paramSecretKey, new IvParameterSpec(paramArrayOfByte2));

    paramArrayOfByte1 = localCipher.doFinal(paramArrayOfByte1);

    return paramArrayOfByte1;

    }

    而构造key函数却很值得玩味:

    //password、random_key、enc_type

    public static Pair generateKey(String paramString1, String paramString2, int paramInt)

    throws UnsupportedEncodingException, NoSuchAlgorithmException

    {

    //根据口令生成Key,将其保存为SecretKeySpec类型

    byte[] arrayOfByte = deriveKey(paramString1, paramInt);

    paramString1 = new SecretKeySpec(arrayOfByte, "AES");

    //再根据生成的Key生成iv

    arrayOfByte = deriveIv(arrayOfByte);

    paramString1 = deriveKey(seafileDecrypt(fromHex(paramString2), paramString1, arrayOfByte), paramInt);

    return new Pair(paramString1, toHex(deriveIv(fromHex(paramString1))));

    }

    以口令生成key_iv对,解密random_key,用其生成此次通信的key_iv。

    而构造key、iv的函数都是利用PKCS5S2ParametersGenerator 这个类将传入的数据+盐生成一个值:

    private static byte[] deriveKey(String paramString, int paramInt)

    throws UnsupportedEncodingException, NoSuchAlgorithmException

    {

    PKCS5S2ParametersGenerator localPKCS5S2ParametersGenerator = new PKCS5S2ParametersGenerator(new SHA256Digest()); //

    localPKCS5S2ParametersGenerator.init(PBEParametersGenerator.PKCS5PasswordToUTF8Bytes(paramString.toCharArray()), salt, ITERATION_COUNT);

    if (paramInt == 2) {}

    for (paramInt = KEY_LENGTH;; paramInt = KEY_LENGTH_SHORT) {

    break;

    }

    return ((KeyParameter)localPKCS5S2ParametersGenerator.generateDerivedMacParameters(paramInt * 8)).getKey();

    }

    PKCS5S2ParametersGenerator定义:

    Generator for PBE derived keys and ivs as defined by PKCS 5 V2.0 Scheme 2


    盐值:

    private static byte[] salt = { -38, -112, 69, -61, 6, -57, -52, 38 };


    技术总结

    我们通过抓包分析、静态分析提出了关于seafile端对端加密方式的假设,最终以动态分析的方式确认了我们的想法。根据官网的介绍:

       Seafile 支持端到端的加密技术来保护你的数据安全。 Seafile 也包含了以下的安全特性:

       两步验证

       服务器端数据加密

       所有的数据传输使用 HTTPS/TLS 协议

       远程删除

       文件病毒扫描

       通过外链上传的文件提供即时病毒扫描

    可以推论,一次加密资料库传输是这样发展的:

    client                                                                server

    input password

    send password  >>>>>>>>>>>>>>>>>>>>>    verifyRepoPassword

    ____________________________________    encrypt random_key with RepoPassword(key_iv)

    _______________<<<<<<<<<<<<<<<<<<<<

    decrypt it with RepoPassword(key_iv)

    get ture key_iv

    接着用 ture key_iv 加解密文件,客户端服务器间传输文件都是加密后的密文。

    试想,排除控制了使用者终端这种极端情况,如果攻击者可以侦听https传输的内容,但没有获取最开始传输的口令,那么就无法解析传输的内容。

    后记

    第一次写安卓破文,难免有疏漏。如果各位大侠有问题或者建议可以在下方留言。

    参考文章

    https://blog.csdn.net/ASSYIRAN/article/details/82994042

    https://www.52pojie.cn/thread-658865-1-1.html

    原文作者:顾言庭

    原文链接:https://bbs.pediy.com/thread-247351.htm

    转载请注明:转自看雪学院

    更多阅读:

    1、[原创]《0day安全...(第二版)》第3章第4节开发通用的shellcode在win10系统下测试的问题

    2、[原创]一个拼凑起来的CVE-2018-8373的EXP

    3、[原创]Xposed________监听微信登录帐号和密码

    4、[原创]对照Java源码学习smali语法,根据smali代码反推java代码

    5、[翻译]Java类型混淆,沙箱逃逸

    相关文章

      网友评论

          本文标题:浅析Seafile网盘apk的端对端加密方式

          本文链接:https://www.haomeiwen.com/subject/oxeltqtx.html