Tool Learning 2 - Migrate ChakraCore to Fuzzilli

Add Extra Target to Fuzzilli

In addition to the existing targets in Fuzzilli's github repo, we can add extra targets, and make fuzzilli works on more JavaScript Engine.

Migrate Fuzzilli to ChakraCore Engine

ChakraCore is an open source Javascript engine with a C API. Previously Microsoft Edge use this engine (However, they no longer use this until March 2021). So, in this post, I am going to migrate Fuzzilli to this engine, and try whether fuzzilli can easily migrate to other targets or not.

Clone the code

Make sure you have installed clang/clang++ and llvm-dev respectively. The reason of installing clang/clang++ is for using its SanitizerCoverage (document) to get edge coverage on every execution.

1
2
3
$ cd fuzzilli/Targets
$ git clone https://github.com/chakra-core/ChakraCore.git
$ cd ChakraCore

Code Modification

Modify Makefile and Implement Code Coverage

First, change the Makefile. Add set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -lrt") to bin/ch/CMakeLists.txt. Search key work shared library below in vim can easily jump to this location.

For avoiding the issues happen on Linking, I add the code in coverage.c (under fuzzilli/Targets) to the library in bin/ch/ch.cpp. I also added some head file at the beginning.

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
// SanitzerCoverage-based coverage collection code for libcoverage.
// Copy+paste this code into the JavaScript shell binary.
//
// BEGIN FUZZING CODE
//
/* Required includes for coverage tracking. */
#include <fcntl.h>
#include <errno.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/mman.h>
#include <cerrno>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <string>

#define REPRL_CRFD 100
#define REPRL_CWFD 101
#define REPRL_DRFD 102
#define REPRL_DWFD 103

#define SHM_SIZE 0x100000
#define MAX_EDGES ((SHM_SIZE - 4) * 8)

//#define CHECK(cond) if (!(cond)) { fprintf(stderr, "\"" #cond "\" failed\n"); _exit(-1); }

#define CHECK(cond) \
if (!(cond)) { \
fprintf(stderr, "\"" #cond "\" failed\n"); \
_exit(-1); \
}

struct shmem_data {
uint32_t num_edges;
unsigned char edges[];
};

struct shmem_data* __shmem;

uint32_t *__edges_start, *__edges_stop;
void __sanitizer_cov_reset_edgeguards() {
uint64_t N = 0;
for (uint32_t *x = __edges_start; x < __edges_stop && N < MAX_EDGES; x++)
*x = ++N;
}

extern "C" void __sanitizer_cov_trace_pc_guard_init(uint32_t *start, uint32_t *stop) {
// Avoid duplicate initialization
if (start == stop || *start)
return;

if (__edges_start != NULL || __edges_stop != NULL) {
fprintf(stderr, "Coverage instrumentation is only supported for a single module\n");
_exit(-1);
}

__edges_start = start;
__edges_stop = stop;

// Map the shared memory region
const char* shm_key = getenv("SHM_ID");
if (!shm_key) {
puts("[COV] no shared memory bitmap available, skipping");
__shmem = (struct shmem_data*) malloc(SHM_SIZE);
} else {
int fd = shm_open(shm_key, O_RDWR, S_IREAD | S_IWRITE);
if (fd <= -1) {
fprintf(stderr, "Failed to open shared memory region: %s\n", strerror(errno));
_exit(-1);
}

__shmem = (struct shmem_data*) mmap(0, SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (__shmem == MAP_FAILED) {
fprintf(stderr, "Failed to mmap shared memory region\n");
_exit(-1);
}
}

__sanitizer_cov_reset_edgeguards();

__shmem->num_edges = stop - start;
printf("[COV] edge counters initialized. Shared memory: %s with %u edges\n", shm_key, __shmem->num_edges);
}

extern "C" void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
// There's a small race condition here: if this function executes in two threads for the same
// edge at the same time, the first thread might disable the edge (by setting the guard to zero)
// before the second thread fetches the guard value (and thus the index). However, our
// instrumentation ignores the first edge (see libcoverage.c) and so the race is unproblematic.
uint32_t index = *guard;
// If this function is called before coverage instrumentation is properly initialized we want to return early.
if (!index) return;
__shmem->edges[index / 8] |= 1 << (index % 8);
*guard = 0;
}

//
// END FUZZING CODE
//

Add REPRL

After this, I follow the instruction on here to add code of REPRL (Read-Eval-Print-Reset-Loop) in the ch.cpp. REPRL is for reducing the overhead occurred when starting a new instance of JSE, and this loop needs to be triggered by a unique command line option (make sure to include as processArgument in the profile).

The code implementation I referred to some patches under Targets folder. This also can be search by grep -rn "HELO" * or grep -rn "char helo" * (under fuzzilli/Targets) and find some code implementation examples, but ensure that you have clone all JSE source code under that folder.

After doing this, my implementation is as below (file located ChakraCore/bin/ch/ch.cpp) :

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
/* Run the fuzzilli run-eval-print-repeat loop, and exit at the end. */
if (options.m_reprl) {
/* REPRL: let parent know we are ready */
char helo[] = "HELO";
CHECK(write(REPRL_CWFD, helo, 4) == 4);//write "HELO" on REPRL_CWFD
CHECK(read(REPRL_CRFD, helo, 4) == 4); //read 4 bytes on REPRL_CRFD

if (memcmp(helo, "HELO", 4) != 0) {
fprintf(stderr, "[REPRL] Invalid response from parent\n");
exit(EXIT_FAILURE); //break if 4 read bytes do not equal "HELO"
}

do {//while true
// Keep indention for easier diffing
unsigned action;
size_t script_size;
char *buffer;
CHECK(read(REPRL_CRFD, &action, 4) == 4); //read 4 bytes on REPRL_CRFD
if (action != 'cexe') {
fprintf(stderr, "[REPRL] Unknown action: %u\n", action);
exit(EXIT_FAILURE);//break if 4 read bytes do not equal "cexe"
} else{
//read 8 bytes on REPRL_CRFD, store as unsigned 64 bit integer size
CHECK(read(REPRL_CRFD, &script_size, 8) == 8);
}

buffer = (char *) malloc((sizeof(char) * (script_size + 1))); //allocate size+1 bytes

char *ptr = buffer; //ptr is buffer
size_t remaining = script_size;
while (remaining > 0) {
ssize_t rv = read(REPRL_DRFD, ptr, remaining);//read size bytes from REPRL_DRFD into allocated buffer
if (rv <= 0) {
fprintf(stderr, "Failed to load script\n");
_exit(-1);
}
remaining -= rv;
ptr += rv;
}

buffer[script_size] = '\0';
if (0 == (result = eval_buf(ctx, buffer, script_size, "reprl", 0))) {
js_std_loop(ctx); //Execute buffer as javascript code
} else {
fprintf(stderr, "Failed to eval_buf reprl\n");
}

js_free(ctx, buffer); //Store return value from JS execution

// In REPRL mode, stdout and stderr may be regular files, so we need to fflush them here.
fflush(stdout); //Flush stdout
fflush(stderr); //Flush stderr


int status = (result & 0xff) << 8; //Mask return value with 0xff and shift it left by 8
CHECK(write(REPRL_CWFD, &status, 4) == 4); //write that value over REPRL_CWFD
//Reset the Javascript engine
__sanitizer_cov_reset_edgeguards();//Call __sanitizer_cov_reset_edgeguards to reset coverage
} while (true);
}

Add Builtin Function

Next step is to add a builtin function that crashes directly to help Fuzzilli test if it can catch the crash of the targeted engine. The targeted file is lib/Runtime/Library/GlobalObject.cpp. This part I will not put the code here. I made this as a patch and named ch.patch, using the following command to patch the directory.

1
2
$ cd Targets/ChakraCore
$ patch -p1 < ch.patch

Create Fuzzilli Profile

Create ChakraCoreProfile.swift, this is a file guide Fuzzilli how to fuzz the targeted engine. Put this profile file in FuzzilliCli/Profiles.

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
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import Fuzzilli
let ChakraCoreProfile = Profile(
processArguments: ["-ForceJITCFGCheck", "-MaxJitThreadCount:1", "-collectgarbage"],
// processEnv: [:],
processEnv: ["UBSAN_OPTIONS":"handle_segv=0"],
codePrefix: """
function main() {
""",
codeSuffix: """
CollectGarbage();
}
main();
""",
ecmaVersion: ECMAScriptVersion.es6,
crashTests: ["fuzzilli('FUZZILLI_CRASH', 0)", "fuzzilli('FUZZILLI_CRASH', 1)"],
additionalCodeGenerators:
WeightedList<CodeGenerator>([]),
additionalProgramTemplates:
WeightedList<ProgramTemplate>([]),
disabledCodeGenerators: [],
additionalBuiltins: ["CollectGarbage" : .function([] => .undefined)]
)

Add this Profile into Profile.swift (locate in Fuzzilli/Profiles/Profile.swift). My implementation is as below.

Make sure to rebuild fuzzilli after doing this:

1
2
$ cd fuzzilli
$ swift build -c release -Xlinker='-lrt'

Build successfully!

Run the Fuzzilli

Build it at first. The building instruction is at here. The executable file will be generate in out/Debug/bin/ch/ch. Install the dependency of building ChakraCore at first.

1
2
3
4
$ sudo apt-get update
$ sudo apt-get dist-upgrade
$ sudo apt-get install -y git build-essential cmake clang libicu-dev
$ sudo apt-get install -y libunwind8-dev #For version 1.X

If build with cmake:

1
2
3
4
5
6
7
$ cd fuzzilli/Targets/ChakraCore
$ export CFLAGS="-fsanitize-coverage=trace-pc-guard -O3 -fsanitize=address"
$ export CXXFLAGS="-fsanitize-coverage=trace-pc-guard -O3 -fsanitize=address"
$ ./build.sh --static -d -j --cc=/path/to/clang --cxx=/path/to/clang++
# Can specify --debug or --test-build to build.sh to select Debug or Test build flavors respectively. Default is Release build.
# Can specify --static to build ChakraCore as a static library.
# Also can run as ./build.sh --static -d --cxx=/usr/bin/clang++-10 --cc=/usr/bin/clang-10 -j

If build with ninja:

1
2
3
$ sudo apt-get install -y ninja-build
$ ninja --version #Ensure ninjs has been installed correctly
$ ./build.sh -n #Specify the -n flag to build.sh to build with ninja

Let's try fuzzing!

1
2
3
$ cd fuzzilli/Targets/ChakraCore
$ mkdir fuzz_ch_out
$ swift run FuzzilliCli --jobs=2 --profile=chakracore --storagePath=fuzz_ch_out/ out/Debug/ch #or this "out/Debug/bin/ch/ch"

However, I met some problems here...

I try to rebuild fuzzilli and ChakraCore, it met error during compilation again. If want to record the error, use the following command:

1
$ script -f output.txt

After collecting all compilation information, using Ctrl + d to stop. Then, I fixed all the bugs and build successfully.