本文主要对安卓平台上的匿名社交媒体应用Yik Yak进行逆向分析,在分析的过程中发现该APP用到了代码混淆、字符串混淆、防签名篡改校验等技术,最后通过整个分析过程总结了APP分析的流程和方法。

0×01 Yik Yak介绍

每隔一段时间,我就会遇到一个实现了一些加固技术的APP,从而使逆向更加有趣。最近,当我尝试为Yik Yak代理API 请求时,就遇到了这种情况。其中,Yik Yak是一款流行的专用于移动平台的社交媒体应用,它允许半径为5英里(通常是大学校园)之内的半匿名用户之间进行交流。

在执行self-MITM攻击时打开应用程序,能够有效地杀掉所有API通信,通常来说是SSL pinning的一个指标。

0×02 混淆措施

在反编译该APK文件且检查Java源代码后,很明显地发现该APP的开发人员还使用了一种混淆工具来优化/保护他们的构建。这里有一个示例方法:

public int hashCode() {
        if (e == null) {
            i1 = 0;
        } else {
            i1 = e.hashCode();
        }
        l2 = f;
        i3 = g;
        if (h) {
            c1 = '\u04CF';
        } else {
            c1 = '\u04D5';
        }
        j3 = fs.a(i);
        if (j == null) {
            j1 = 0;
        } else {
            j1 = j.hashCode();
        }
        k3 = Arrays.hashCode(k);
        l3 = Arrays.hashCode(l);
        i4 = Arrays.hashCode(m);
        if (n == null) {
            k1 = 0;
        } else {
            k1 = n.hashCode();
        }
        if (o == null) {
            l1 = 0;
        } else {
            l1 = o.hashCode();
        }
        j4 = (int) (p ^ p >>> 32);
        if (q != null) {
            i2 = q.hashCode();
        }
        return ((((((l1 + (k1 + ((((j1 + ((c1 + (((i1 + ((j2 + 527) * 31 + k2) * 31) * 31 + l2) * 31 + i3) * 31) * 31 + j3) * 31) * 31 + k3) * 31 + l3) * 31 + i4) * 31) * 31) * 31 + j4) * 31 + i2) * 31 + Arrays.hashCode(r)) * 31 + s) * 31 + fs.a(t)) * 31 + b();
    }

注意,变量、类和方法名已经从原始的友好的形式进行了重命名。随着我查看更多的代码,发现它也似乎还进行了字符串常量混淆操作,这种特性存在于DexGuard等第三方工具中。通常来说,虽然代码混淆是一个很好的习惯,但它只能通过使代码变得难以阅读,从而能够放缓攻击者的逆向进度。根据我的经验,混淆也能使Java反编译结果变得不可靠,所以我大多时候会在smali层面进行分析。

0×03 搜索字符串

接下来,我开始对smali源文件执行grep操作,以搜索与常见SSL pinning实现相关的字符串。很快,我找到了自认为是pinning校验的代码:

.method public checkServerTrusted([Ljava/security/cert/X509Certificate;Ljava/lang/String;)V
    .locals 3
 
    .prologue
    const/4 v2, 0x0
 
    .line 153
    iget-object v0, p0, LCG;->e:Ljava/util/Set;
 
    aget-object v1, p1, v2
 
    invoke-interface {v0, v1}, Ljava/util/Set;->contains(Ljava/lang/Object;)Z
 
    move-result v0
 
    if-eqz v0, :cond_0
 
    .line 163
    :goto_0
    return-void
 
    .line 160
    :cond_0
    invoke-direct {p0, p1, p2}, LCG;->a([Ljava/security/cert/X509Certificate;Ljava/lang/String;)V
 
    .line 161
    invoke-direct {p0, p1}, LCG;->a([Ljava/security/cert/X509Certificate;)V
 
    .line 162
    iget-object v0, p0, LCG;->e:Ljava/util/Set;
 
    aget-object v1, p1, v2
 
    invoke-interface {v0, v1}, Ljava/util/Set;->add(Ljava/lang/Object;)Z
 
    goto :goto_0
.end method

通过编辑上面的方法直接返回void,我绕过了此方法。但在构建、签名,并安装了这个新的APK后,我得到了与上面相同的“网络连接(Internet Connection)”错误。在以不同的编辑/构建方式尝试多次,且每次都得到相同的结构之后,我开始怀疑Yik Yak使用了一些篡改检测逻辑(一种包签名验证),以此来防止自己被他人逆向分析。于是,我安装了一个未修改但经过重签名的APK,仍然出现相同的错误,这一结果使我确认了我怀疑的内容。

0x04 搜索决策点

为了绕过篡改检测,我改变了焦点并开始搜索它的决策点。在安卓中,开发人员可以通过以下方式使用PackageManager类访问包签名:

PackageManager pm = context.getPackageManager();
String packageName = context.getPackageName();
 
Signature[] sigs = pm.getPackageInfo(packageName, PackageManager.GET_SIGNATURES).signatures;

因为签名是作为android.content.pm.Signature的实例返回的,所以我搜索了代码并发现下面的方法:

public static Signature[] a(Context context)
    {
        if (context != null) goto _L2; else goto _L1
_L1:
        PackageManager packagemanager;
        return null;
_L2:
        if ((packagemanager = context.getPackageManager()) == null) goto _L1; else goto _L3
_L3:
        try
        {
            context = packagemanager.getPackageInfo(context.getPackageName(), 64);
        }
        // Misplaced declaration of an exception variable
        catch (Context context)
        {
            context.printStackTrace();
            return null;
        }
        if (context == null) goto _L1; else goto _L4
_L4:
        context = ((PackageInfo) (context)).signatures;
        return context;
    }

很显然,这个方法用作一个包装器来获取当前构建的签名。注意,在第12行,值64正确匹配了PackageInfo.GET_SIGNATURES的常量值。搜索这个类的使用后,得到了下面的几个结果:

0×05 提取签名

我没有试图解决多决策点,而是决定通过修改上述方法返回官方Yik Yak构建的签名,以此来欺骗包签名。为了做到这一点,我需要知道应用程序正在寻找的签名。在上述结果中,我简要地搜索了一下那些代码,但是并没有发现硬编码签名(可能是由于混淆)。我没有进一步搜索(或调试)代码,而是跑了一个脚本,该脚本会从一个给定的APK中提取签名:

0×06 绕过签名校验

在得到期待的签名后,我修改了上述smali方法,让它直接返回该签名:

.method public static a(Landroid/content/Context;)[Landroid/content/pm/Signature;
    .locals 4
 
    .prologue
    const/4 v0, 0x0
    
    const-string v0, "3082019d30820106a003020102020452ab687f300d06092a864886f70d010105050030123110300e0603550403130777696c6c69616d3020170d3133313231333230303531395a180f32313133313131393230303531395a30123110300e0603550403130777696c6c69616d30819f300d06092a864886f70d010101050003818d00308189028181008fdfd8a1c6319b8d45445dc9c28a89600062dd00ad14c5ee3fac8d4812d5dfa3a5c6e534f242d5e91d6acb1807d618d44731973c4f69c328b6b755962810ed2cf8ff19fa5c6de40a34be5e92c6686e772fa864784e74144465272c260f877395df37b897e8147bbcdce15b8f11ee125c82bf9d2de9beb92056edea6f301d15f70203010001300d06092a864886f70d0101050500038181000a2f44f1a8d78b4d1965f0e60f9ef10826827ae131e6c4a3f976fc85f36f94578a698f904fd0a37a690f3dd338c16c3e408d77670543bb5b022d7c1bc86a0574e3e593092f1e06de141f04f6a68d78dbc5aa36f0a82062ecb03c1e7285a55b5ccfea58c193572d8d7542ca7a31748aabc7edff7990048a11ae5ef090074c9b25"
 
    .line 11
    .local v0, "fake":Ljava/lang/String;
    const/4 v2, 0x1
 
    new-array v1, v2, [Landroid/content/pm/Signature;
 
    const/4 v2, 0x0
 
    new-instance v3, Landroid/content/pm/Signature;
 
    invoke-direct {v3, v0}, Landroid/content/pm/Signature;-><init>(Ljava/lang/String;)V
 
    aput-object v3, v1, v2
 
    .line 13
    .local v1, "sig":[Landroid/content/pm/Signature;
    return-object v1  
.end method

这是Java中相同的代码:

public static Signature[] a(Context context)
{
    String fake = "3082019d30820106a003020102020452ab687f300d06092a864886f70d010105050030123110300e0603550403130777696c6c69616d3020170d3133313231333230303531395a180f32313133313131393230303531395a30123110300e0603550403130777696c6c69616d30819f300d06092a864886f70d010101050003818d00308189028181008fdfd8a1c6319b8d45445dc9c28a89600062dd00ad14c5ee3fac8d4812d5dfa3a5c6e534f242d5e91d6acb1807d618d44731973c4f69c328b6b755962810ed2cf8ff19fa5c6de40a34be5e92c6686e772fa864784e74144465272c260f877395df37b897e8147bbcdce15b8f11ee125c82bf9d2de9beb92056edea6f301d15f70203010001300d06092a864886f70d0101050500038181000a2f44f1a8d78b4d1965f0e60f9ef10826827ae131e6c4a3f976fc85f36f94578a698f904fd0a37a690f3dd338c16c3e408d77670543bb5b022d7c1bc86a0574e3e593092f1e06de141f04f6a68d78dbc5aa36f0a82062ecb03c1e7285a55b5ccfea58c193572d8d7542ca7a31748aabc7edff7990048a11ae5ef090074c9b25";
 
    Signature[] sig = new Signature[]{new Signature(fake)};
 
    return sig;
}

现在,我已经绕过了包签名检查,于是安装新的构建以进行测试。不幸的是,出现了同样的错误。因为这可能是因为我错过了一些额外的pinning代码,所以我再一次搜索了整个源码,最终发现这个方法:

public void a(String s, List list)
    {
        List list1;
        boolean flag;
        flag = false;
        list1 = (List)b.get(s);
        if (list1 != null) goto _L2; else goto _L1
_L1:
        return;
_L2:
        int l = list.size();
        int i = 0;
label0:
        do
        {
label1:
            {
                if (i >= l)
                {
                    break label1;
                }
                if (list1.contains(a((X509Certificate)list.get(i))))
                {
                    break label0;
                }
                i++;
            }
        } while (true);
        if (true) goto _L1; else goto _L3
_L3:
        StringBuilder stringbuilder = (new StringBuilder()).append("Certificate pinning failure!").append("\n  Peer certificate chain:");
        int i1 = list.size();
        for (int j = 0; j < i1; j++)
        {
            X509Certificate x509certificate = (X509Certificate)list.get(j);
            stringbuilder.append("\n    ").append(a(((Certificate) (x509certificate)))).append(": ").append(x509certificate.getSubjectDN().getName());
        }
 
        stringbuilder.append("\n  Pinned certificates for ").append(s).append(":");
        i1 = list1.size();
        for (int k = ((flag) ? 1 : 0); k < i1; k++)
        {
            s = (Dx)list1.get(k);
            stringbuilder.append("\n    sha1/").append(s.b());
        }
 
        throw new SSLPeerUnverifiedException(stringbuilder.toString());
    }

通过“return-void”绕过这个方法使得pinning实现被禁用,从而我能够成功地代理APP的API请求。

GET https://notify.yikyakapi.net/api/getAllForUser/***REMOVED*** HTTP/1.1
Host: notify.yikyakapi.net
Connection: Keep-Alive
Accept-Encoding: gzip
Cookie: __cfduid=***REMOVED***
User-Agent: Dalvik/2.1.0 (Linux; U; Android 5.1.1; Nexus 6 Build/LMY48M) 3.0

0×07 总结

在信息安全的世界中,充满了公司糟糕地处理软件安全的例子,但是可以明显地看到,像Yik Yak这样的公司还是一直在努力提高软件的安全性的,至少在安卓APP方面是这样。

*参考来源:randywestergrenJackFree编译,转载请注明来自FreeBuf黑客与极客(FreeBuf.com)

源链接

Hacking more

...