APK中如何简单地隐藏/混淆字符串?

16
有时候你需要在应用程序本身中存储密码,比如用于与自己的服务器通信的用户名/密码。在这些情况下,无法遵循存储密码的常规过程——即哈希密码、存储哈希值、与哈希用户输入进行比较——因为你没有任何用户输入来比较哈希值。密码需要由应用程序本身提供。那么如何保护APK中存储的密码呢?类似下面这样的生成密码函数是否足够安全?
String password = "$()&HDI?=!";

简单的混淆:

private String getPassword(){
    String pool = "%&/@$()7?=!656sd8KJ%&HDI!!!G98y/&%=?=*^%&ft4%(";
    return pool.substring(4, 7) + pool.substring(20, 24) + pool.substring(8, 11);
}

我知道ProGuard有一些混淆能力,但我对上述“混淆”技术在编译时的作用以及其他更复杂的技术中有多难被人发现感到好奇。


请注意,攻击者可能会跟随输入密码猜测的操作。历史上,人们更喜欢不存储实际密码进行比较,而是使用计算密集型哈希函数的结果,其想法是即使你知道哈希值(“答案”),也很难找到匹配的密码(“问题”)。但在云计算时代及其邪恶的孪生兄弟僵尸网络的影响下,一个中等长度的密码可能很容易被破解。 - Chris Stratton
请查看我的更新问题。我指的是应用程序本身必须提供密码的情况,而不是用户提供密码的情况。之前我表达得有点不清楚,对此我感到抱歉。 - Magnus
1个回答

20

简而言之,如果您知道如何反编译APK,则可以轻松获取密码,无论代码有多么混淆。不要将密码存储在APK中,这样不安全。

我知道ProGuard具有一些混淆功能,但我想知道上述“混淆”技术在编译时会发生什么,以及其他更复杂的技术是否很难通过查看APK和/或使用其他技术来弄清楚它是什么?

我将向您展示它有多容易。这里是一个我们将要反编译的Android SSCCE

MyActivity.java:

public class MyActivity extends Activity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        TextView text = (TextView) findViewById(R.id.text);
        text.setText(getPassword());
    }

    private String getPassword() {
        String pool = "%&/@$()7?=!656sd8KJ%&HDI!!!G98y/&%=?=*^%&ft4%(";
        return pool.substring(4, 7) + pool.substring(20, 24) + pool.substring(8, 11);
    }
}

main.xml:

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
          android:id="@+id/text"
          android:layout_width="fill_parent"
          android:layout_height="wrap_content"/>

编译并运行后,我们可以在 TextView 上看到 $()&HDI?=! 字符串。

接下来让我们反编译一个 APK:

  1. 解压 myapp.apk 文件,或者在 APK 上右键点击选择 Unzip here。会出现 classes.dex 文件。
  2. 使用 dex2jar 工具将 classes.dex 转换为 JAR 文件。执行命令 dex2jar.sh classes.dex 后,会得到 classes_dex2jar.jar 文件。
  3. 使用一些 Java 反编译器,比如 JD-GUI,打开 classes_dex2jar.jar 文件中的 MyActivity.class,得到以下 Java 代码:

public class MyActivity extends Activity
{
    private String getPassword()
    {
        return "%&/@$()7?=!656sd8KJ%&HDI!!!G98y/&%=?=*^%&ft4%(".substring(4, 7) 
+ "%&/@$()7?=!656sd8KJ%&HDI!!!G98y/&%=?=*^%&ft4%(".substring(20, 24) 
+ "%&/@$()7?=!656sd8KJ%&HDI!!!G98y/&%=?=*^%&ft4%(".substring(8, 11);
    }

    public void onCreate(Bundle paramBundle)
    {
        super.onCreate(paramBundle);
        setContentView(2130903040);
        ((TextView)findViewById(2131034112)).setText(getPassword());
    }
}

ProGuard的作用有限,代码仍然很容易阅读。

基于以上原因,我已经可以回答你的问题了:

下面这个生成密码的函数是否足够安全?

不行。正如你所看到的,它仅仅稍微增加了反混淆代码的难度。我们不应该以这种方式混淆代码,因为:

  1. 它给人一种安全的假象。
  2. 这是对开发者时间的浪费。
  3. 它降低了代码的可读性。

在官方的Android文档中,在安全性和设计部分,他们建议使用以下方法来保护你的Google Play公钥:

不要将您的公钥作为纯字符串嵌入到任何代码中,以使其免受恶意用户和黑客的攻击。相反,从多个片段构造字符串,或使用位运算(例如,与其他字符串进行异或运算)来隐藏实际密钥。密钥本身并不是机密信息,但您不希望使黑客或恶意用户轻松替换公钥为另一个密钥。

好的,让我们试试这个方法:

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    TextView text = (TextView) findViewById(R.id.text);
    text.setText(xor("A@NCyw&IHY", "ehge13ovux"));
}

private String xor(String a, String b) {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < a.length() && i < b.length(); i++) {
        sb.append((char) (a.charAt(i) ^ b.charAt(i)));
    }
    return sb.toString();
}

TextView 上显示 $()&HDI?=!,很好。

反编译版本:

public void onCreate(Bundle paramBundle)
{
    super.onCreate(paramBundle);
    setContentView(2130903040);
    ((TextView)findViewById(2131034112)).setText(xor("A@NCyw&IHY", "ehge13ovux"));
}

private String xor(String paramString1, String paramString2)
{
    StringBuilder localStringBuilder = new StringBuilder();
    for (int i = 0; (i < paramString1.length()) && (i < paramString2.length()); i++) {
        localStringBuilder.append((char)(paramString1.charAt(i) ^ paramString2.charAt(i)));
    }
    return localStringBuilder.toString();
}

和以前非常相似的情况。

即使我们有极其复杂的函数soStrongObfuscationOneGetsBlind(),我们仍然可以运行反编译代码并查看它会产生什么内容。或者逐步进行调试。


15
我喜欢你的答案,但是我缺乏关于该怎么做的建议。“不要在APK中存储密码”并没有什么帮助,因为需要在某个地方存储API密钥。 - arenaq
1
至少可以使用JNI来实现。这可能会使它变得更加困难。 - android developer
JNI仍然将密钥以明文形式保存在.so文件中。请参阅我的文章,其中提到了这一点:https://approov.io/blog/how-to-extract-an-api-key-from-a-mobile-app-with-static-binary-analysis - Exadra37

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接