仍然是因为某些原因,需要学学浏览器pwn
环境
depot_tools建议直接去gitlab里下,github上这个我用魔法都没下下来
下完之后执行
echo 'export PATH=$PATH:"/root/depot_tools"' >> ~/.bashrc
路径换成自己的就ok了
然后是ninja
git clone https://github.com/ninja-build/ninja.git
cd ninja && ./configure.py --bootstrap && cd ..
echo 'export PATH=$PATH:"/root/ninja"' >> ~/.bashr
没什么好说的,路径记得换成自己的
然后执行
fetch v8
cd v8
拉个v8源码下来,gclient sync似乎自动执行了,我亲测是这样的反正
然后你可以选择先编译个最新版的v8出来试试(编译这玩意是有点子慢的)
tools/dev/v8gen.py x64.debug
ninja -C out.gn/x64.debug
tools/dev/v8gen.py x64.release
ninja -C ./out.gn/x64.release
也可以选择直接编译题目的版本,一般题目会给commitid,比如我一会要学的第一道题starctf 的 oob题目,commitid是6dc88c191f5ecc5389dc26efa3ca0907faef3598,那就先这样再这样再这样。
git reset --hard 6dc88c191f5ecc5389dc26efa3ca0907faef3598
git apply < oob.diff
# 编译debug版本
tools/dev/v8gen.py x64.debug
ninja -C out.gn/x64.debug d8
# 编译release版本
tools/dev/v8gen.py x64.release
ninja -C out.gn/x64.release d8
环境差不多就先这些,后面遇到了再补充
starctf oob
这个题我真的是服了,切到对应版本以后,里面的代码居然有的是python2语法有的是python3语法,换了半天也没编译出来,还好手里有编译好的release版本,属实无语住了。
然后咱们看diff
diff --git a/src/bootstrapper.cc b/src/bootstrapper.cc
index b027d36..ef1002f 100644
--- a/src/bootstrapper.cc
+++ b/src/bootstrapper.cc
@@ -1668,6 +1668,8 @@ void Genesis::InitializeGlobal(Handle<JSGlobalObject> global_object,
Builtins::kArrayPrototypeCopyWithin, 2, false);
SimpleInstallFunction(isolate_, proto, "fill",
Builtins::kArrayPrototypeFill, 1, false);
+ SimpleInstallFunction(isolate_, proto, "oob",
+ Builtins::kArrayOob,2,false);
SimpleInstallFunction(isolate_, proto, "find",
Builtins::kArrayPrototypeFind, 1, false);
SimpleInstallFunction(isolate_, proto, "findIndex",
diff --git a/src/builtins/builtins-array.cc b/src/builtins/builtins-array.cc
index 8df340e..9b828ab 100644
--- a/src/builtins/builtins-array.cc
+++ b/src/builtins/builtins-array.cc
@@ -361,6 +361,27 @@ V8_WARN_UNUSED_RESULT Object GenericArrayPush(Isolate* isolate,
return *final_length;
}
} // namespace
+BUILTIN(ArrayOob){
+ uint32_t len = args.length();
+ if(len > 2) return ReadOnlyRoots(isolate).undefined_value();
+ Handle<JSReceiver> receiver;
+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+ isolate, receiver, Object::ToObject(isolate, args.receiver()));
+ Handle<JSArray> array = Handle<JSArray>::cast(receiver);
+ FixedDoubleArray elements = FixedDoubleArray::cast(array->elements());
+ uint32_t length = static_cast<uint32_t>(array->length()->Number());
+ if(len == 1){
+ //read
+ return *(isolate->factory()->NewNumber(elements.get_scalar(length)));
+ }else{
+ //write
+ Handle<Object> value;
+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+ isolate, value, Object::ToNumber(isolate, args.at<Object>(1)));
+ elements.set(length,value->Number());
+ return ReadOnlyRoots(isolate).undefined_value();
+ }
+}
BUILTIN(ArrayPush) {
HandleScope scope(isolate);
diff --git a/src/builtins/builtins-definitions.h b/src/builtins/builtins-definitions.h
index 0447230..f113a81 100644
--- a/src/builtins/builtins-definitions.h
+++ b/src/builtins/builtins-definitions.h
@@ -368,6 +368,7 @@ namespace internal {
TFJ(ArrayPrototypeFlat, SharedFunctionInfo::kDontAdaptArgumentsSentinel) \
/* https://tc39.github.io/proposal-flatMap/#sec-Array.prototype.flatMap */ \
TFJ(ArrayPrototypeFlatMap, SharedFunctionInfo::kDontAdaptArgumentsSentinel) \
+ CPP(ArrayOob) \
\
/* ArrayBuffer */ \
/* ES #sec-arraybuffer-constructor */ \
diff --git a/src/compiler/typer.cc b/src/compiler/typer.cc
index ed1e4a5..c199e3a 100644
--- a/src/compiler/typer.cc
+++ b/src/compiler/typer.cc
@@ -1680,6 +1680,8 @@ Type Typer::Visitor::JSCallTyper(Type fun, Typer* t) {
return Type::Receiver();
case Builtins::kArrayUnshift:
return t->cache_->kPositiveSafeInteger;
+ case Builtins::kArrayOob:
+ return Type::Receiver();
// ArrayBuffer functions.
case Builtins::kArrayBufferIsView:
csdn的markdown不支持diff语法,真丑啊。
+ SimpleInstallFunction(isolate_, proto, "oob",
+ Builtins::kArrayOob,2,false);
这两行告诉我们作者给array添加了一个函数叫oob
下面是oob的具体实现
+BUILTIN(ArrayOob){
+ uint32_t len = args.length();
+ if(len > 2) return ReadOnlyRoots(isolate).undefined_value();
+ Handle<JSReceiver> receiver;
+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+ isolate, receiver, Object::ToObject(isolate, args.receiver()));
+ Handle<JSArray> array = Handle<JSArray>::cast(receiver);
+ FixedDoubleArray elements = FixedDoubleArray::cast(array->elements());
+ uint32_t length = static_cast<uint32_t>(array->length()->Number());
+ if(len == 1){
+ //read
+ return *(isolate->factory()->NewNumber(elements.get_scalar(length)));
+ }else{
+ //write
+ Handle<Object> value;
+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+ isolate, value, Object::ToNumber(isolate, args.at<Object>(1)));
+ elements.set(length,value->Number());
+ return ReadOnlyRoots(isolate).undefined_value();
+ }
+}
获取参数的数量,然后根据参数个数进行不同的操作
如果参数数量大于2则直接抛出undefined
如果参数数量小于等于2,则先把array转成doublearray
然后判断如果无额外参数(第一个是this),则是read功能,返回array[length]
如果传入了一个参数,则是write功能,将value写入到doublearray[length]中
diff文件到这里就解释完了
接下来熟悉熟悉v8对象的内存布局
var a = [1.1, 2.2, 3.3, 4];
%DebugPrint(a);
%SystemBreak();
var b = [1, 2, 3];
%DebugPrint(b);
%SystemBreak();
var c = [a, b]
%DebugPrint(c);
%SystemBreak();
边写边记录,%DebugPrint可以打印对象的详细内存信息,%SystemBreak()可以下断点,用gdb调一下这段代码
gdb ./d8
set args --allow-natives-syntax ./test.js
调试的时候记得加上–allow-natives-syntax,这样才能用上面说的那两个调试函数。
这样就断下来了
由于这个题我没有debug版的d8可以用,所以debug_print的结果看不到了,也不能说看不到,不过信息非常少:
十分穷酸,只给我把a这个大小为4的JSArray的地址打印出来了
但是我依然可以使用一些命令来查看:
pwndbg> job 0x2bb952fcde81
0x2bb952fcde81: [JSArray]
- map: 0x025e82cc2ed9 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x1e15429d1111 <JSArray[0]>
- elements: 0x2bb952fcde51 <FixedDoubleArray[4]> [PACKED_DOUBLE_ELEMENTS]
- length: 4
- properties: 0x291530640c71 <FixedArray[0]> {
#length: 0x145325ac01a9 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x2bb952fcde51 <FixedDoubleArray[4]> {
0: 1.1
1: 2.2
2: 3.3
3: 4
}
比如使用job命令查看对象,可以看到有几个数据类型:map,prototype,elements等
这里解释一下为什么这么多以1结尾的地址,因为v8会用把指针用最低比特置1的方式进行标记。
这些属性的含义如下:
map:定义了如何访问对象
prototype:对象的原型(如果有)
elements:对象的地址
length:长度
properties:属性,存有map和length
从数据中也能看出来,当我们声明了一个对象的时候,真正的数据是存放在elements所指向的地址中的。还要注意一件事情,那就是elements的地址和对象本身的地址的关系:
对象本身的地址为0x2bb952fcde81
其对应的elements的地址为0x2bb952fcde51
elements的结构为:
pwndbg> job 0x2bb952fcde51
0x2bb952fcde51: [FixedDoubleArray]
- map: 0x2915306414f9 <Map>
- length: 4
0: 1.1
1: 2.2
2: 3.3
3: 4
由一个map地址和具体的数据组成,所以说当我们申请一个对象的时候,v8先申请了一个elements用于存放数据,然后紧接着又申请了一块内存用于存放对象本身的结构信息。
elements先说到这里,接下来说一说map的结构,以数组本身的map为例:
pwndbg> job 0x025e82cc2ed9
0x25e82cc2ed9: [Map]
- type: JS_ARRAY_TYPE
- instance size: 32
- inobject properties: 0
- elements kind: PACKED_DOUBLE_ELEMENTS
- unused property fields: 0
- enum length: invalid
- back pointer: 0x025e82cc2e89 <Map(HOLEY_SMI_ELEMENTS)>
- prototype_validity cell: 0x145325ac0609 <Cell value= 1>
- instance descriptors #1: 0x1e15429d1f49 <DescriptorArray[1]>
- layout descriptor: (nil)
- transitions #1: 0x1e15429d1eb9 <TransitionArray[4]>Transition array #1:
0x291530644ba1 <Symbol: (elements_transition_symbol)>: (transition to HOLEY_DOUBLE_ELEMENTS) -> 0x025e82cc2f29 <Map(HOLEY_DOUBLE_ELEMENTS)>
- prototype: 0x1e15429d1111 <JSArray[0]>
- constructor: 0x1e15429d0ec1 <JSFunction Array (sfi = 0x145325ac6791)>
- dependent code: 0x2915306402c1 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 0
一个map里包含了一系列信息如:
对象的动态类型,即String,Uint8Array,HeapNumber等。
对象的大小(以字节为单位)
对象的属性及其存储位置
数组元素的类型,例如,unboxed的双精度数或带标记的指针
对象的原型(如果有)
map决定了如何访问一个对象,也标识了一个对象的类型。
再来看看properties的结构:
pwndbg> job 0x291530640c71
0x291530640c71: [FixedArray]
- map: 0x291530640801 <Map>
- length: 0
它的结构就相对来说简单很多,里面只有一个map和一个length,后面有用到再细说,先混个脸熟。
大致熟悉了一下v8里的一些比较重要的数据类型以及对象的结构,接下来回想一下diff,既然是作者自己给array添加的函数,那必然意味着存在漏洞,write的功能是把一个用户自定义的value写入到doublearray[length]的位置,但是众所周知数组下标是0~length-1,所以这里其实相当于发生了一个越界写,同样的read那里也存在着一个越界读。
刚才我们提到,当申请一个array的时候,程序是先申请elements,然后再申请对象结构,而对象结构是以一个map开头的,当我们修改数据的时候,实际上是在修改elements,如果我们越界修改了elements,刚好就会覆盖掉这个对象本身的map地址,所以相当于我们有了读取所申请对象的map地址以及任意修改此map地址的权限。这就是本题的漏洞所在。
写个代码测试一下是否真的能修改map的值
先来点辅助函数尝尝,因为无论是越界读还是越界写都是浮点数形式,所以需要辅助函数来帮助我们在整数和浮点数之间转换,方法就是开一块空间让整数和浮点数共用,然后转一下就行。
var buf =new ArrayBuffer(16);
var float64 = new Float64Array(buf);
var bigUint64 = new BigUint64Array(buf);
//
function f2i(f)
{
float64[0] = f;
return bigUint64[0];
}
//
function i2f(i)
{
bigUint64[0] = i;
return float64[0];
}
//
function hex(i)
{
return i.toString(16).padStart(16, "0");
}
var a=[1,2,3,4]
var map_addr=f2i(a.oob())
console.log("[*] oob return data: "+hex(map_addr));
a.oob(i2f(0x6161616161616161n));
%DebugPrint(a);
%SystemBreak();
然后就翻车了,如图所示
可以看到之前我们用浮点数组尝试的时候,elements下面紧挨着的就是对象结构,也正是这点能够让我们越界一个下标读写就能读写到本对象的map信息,但是这里情况有变?假定v8对于对象的内存分配方式不变,那么elements是不是有点太大了,如果是这种情况那么我们越界一个下标读写就读写不到本对象的map了。参考的几篇文章似乎也没有在这里提到这个问题,可能因为他们没有像我一样傻到用全是整数的数组进行测试吧。
经过查询发现,原来都是整数的数组和包含了浮点数的数组是两种类型的数据,从map信息就能够看出来
const array = [1, 2, 3];
// elements 类型: PACKED_SMI_ELEMENTS
array.push(4.56);
// elements 类型: PACKED_DOUBLE_ELEMENTS
array.push('x');
// elements 类型: PACKED_ELEMENTS
v8都有哪些数组类型可以看这个图
注意这里的速度由上至下是越来越慢的,并且降级是不可逆的
不管怎么样这个整数array的大小总归是不太对劲的,于是用tele查看一下内存