kelinci理解

kelinci

简介

kelinci是一个能够在不修改AFL代码的情况下对Java进行模糊测试的工具。

架构&&原理

852cdbf3bc36c8fac6bf577f15e6471d.png

kelinic主要分为两个大模块,分别为Fuzzer端和 Java端。

Fuzzer端

Fuzzer端主要由AFL和一个C文件interface.c组成。

对于AFL来说,AFL只是以为在fuzz interface这个程序,而不是java程序。

AFL在运行过程中会将输入文件传递给interface,然后interface会将文件传递给java端,java端会将运行结果反馈给fuzz端,从而使AFL进一步fuzz。

Java端

Java端的Instrumentor主要有两部分,一个TCP服务器,和真正的要测试的程序。

待测试程序会先使用Java的ASM库进行插桩。

TCP服务器负责和Fuzzer端的interface通信。并运行被插桩的目标程序。

细节

Mem.java

kelinci受制于语言,没有像AFL一样直接开辟共享内存区域,而是自己定义了一个Mem静态工具类来当共享内存。

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
public class Mem {

public static final int SIZE = 65536;
public static byte mem[] = new byte[SIZE];
public static int prev_location = 0;

/**
* Clears the current measurements.
*/
public static void clear() {
for (int i = 0; i < SIZE; i++)
mem[i] = 0;
}

/**
* Prints to stdout any cell that contains a non-zero value.
*/
public static void print() {
for (int i = 0; i < SIZE; i++) {
if (mem[i] != 0) {
System.out.println(i + " -> " + mem[i]);
}
}
}
}
Kelinci.java

kelinci类中的main函数首先会解析命令行参数,然后加载目标程序的main函数,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ClassLoader classloader = Thread.currentThread().getContextClassLoader();
try {
Class<?> target = classloader.loadClass(mainClass);
targetMain = target.getMethod("main", String[].class);
} catch (ClassNotFoundException e) {
System.err.println("Main class not found: " + mainClass);
return;
} catch (NoSuchMethodException e) {
System.err.println("No main method found in class: " + mainClass);
return;
} catch (SecurityException e) {
System.err.println("Main method in class not accessible: " + mainClass);
return;
}

然后会拉起一个服务端线程和fuzz线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

Thread server = new Thread(new Runnable() {
@Override
public void run() {
runServer();
}
});
server.start();

Thread fuzzerRuns = new Thread(new Runnable() {
@Override
public void run() {
doFuzzerRuns();
}
});
fuzzerRuns.start();

服务器线程会先创建一个socket对象,并监听端口的请求。监听到请求后会将请求放入队列中,供fuzz线程使用,若队列已满则返回队列已满的信号。

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
try (ServerSocket ss = new ServerSocket(port)) {
while (true) {
Socket s = ss.accept();
boolean status = false;
if (requestQueue.size() < maxQueue) {
status = requestQueue.offer(new FuzzRequest(s));
}
if (!status) {
OutputStream os = s.getOutputStream();
os.write(STATUS_QUEUE_FULL);
os.flush();
s.shutdownOutput();
s.shutdownInput();
s.close();
}
}
} catch (BindException be) {
System.err.println("Unable to bind to port " + port);
System.exit(1);
} catch (Exception e) {
System.err.println("Exception in request server");
e.printStackTrace();
System.exit(1);
}
}

fuzz线程会不断的从请求队列中拉取请求,拉取新的请求后会定义输入输出流,然后将共享内存清0

1
2
3
4
5
6
7
8
9
10
while (true) {
try {
FuzzRequest request = requestQueue.poll();
if (request != null) {
InputStream is = request.clientSocket.getInputStream();
OutputStream os = request.clientSocket.getOutputStream();

Mem.clear();
byte result = STATUS_CRASH;
ApplicationCall appCall = null;

以本地模式来举例,首先读取path的长度,然后读取path,根据path定义appCall,期间异常全部按照连接异常来返回。

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
if (mode == LOCAL_MODE) {						
// read the length of the path (integer)
int pathlen = is.read() | is.read() << 8 | is.read() << 16 | is.read() << 24;
if (pathlen < 0) {
result = STATUS_COMM_ERROR;
} else {

// read the path
byte input[] = new byte[pathlen];
int read = 0;
while (read < pathlen) {
if (is.available() > 0) {
input[read++] = (byte) is.read();
} else {
result = STATUS_COMM_ERROR;
break;
}
}
}
String path = new String(input);
appCall = new ApplicationCall(path);
}

/* DEFAULT MODE */
}

处理好输入文件后,执行appCall,并根据执行的状态返回不同的结果,将status和共享内存通过TCP连接,写回Fuzzer端。

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
if (result != STATUS_COMM_ERROR && appCall != null) {
// run app with input
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Long> future = executor.submit(appCall);
try {
future.get(timeout, TimeUnit.MILLISECONDS);
result = STATUS_SUCCESS;
} catch (TimeoutException te) {
future.cancel(true);
result = STATUS_TIMEOUT;
} catch (Throwable e) {
future.cancel(true);
if (e.getCause() instanceof RuntimeException) {
} else if (e.getCause() instanceof Error) {
}
e.printStackTrace();
}
executor.shutdownNow();
}
// send back status
os.write(result);
// send back "shared memory" over TCP
os.write(Mem.mem, 0, Mem.mem.length);
// close connection
os.flush();
request.clientSocket.shutdownOutput();
request.clientSocket.shutdownInput();
request.clientSocket.setSoLinger(true, 100000);
request.clientSocket.close();

appCall中,会调用runApplication函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private static class ApplicationCall implements Callable<Long> {
byte input[];
String path;

ApplicationCall(byte input[]) {
this.input = input;
}

ApplicationCall(String path) {
this.path = path;
}

@Override
public Long call() throws Exception {
if (path != null)
return runApplication(path);
return runApplication(input);
}
}

runApplicatio函数会执行目标程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private static long runApplication(String filename) {
long runtime = -1L;

String[] args = Arrays.copyOf(targetArgs, targetArgs.length);
for (int i = 0; i < args.length; i++) {
if (args[i].equals("@@")) {
args[i] = filename;
}
}
long pre = System.nanoTime();
try {
targetMain.invoke(null, (Object) args);
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
e.printStackTrace();
throw new RuntimeException("Error invoking target main method");
}
runtime = System.nanoTime() - pre;
return runtime;
}
MethodTransformer.java

插桩的实现主要在MethodTransformer.java里面,InstrumentLocation里面,仿照AFL的方式进行插桩。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void instrumentLocation() {
Integer id = getNewLocationId();
mv.visitFieldInsn(GETSTATIC, "edu/cmu/sv/kelinci/Mem", "mem", "[B");
mv.visitLdcInsn(id);
mv.visitFieldInsn(GETSTATIC, "edu/cmu/sv/kelinci/Mem", "prev_location", "I");
mv.visitInsn(IXOR);
mv.visitInsn(DUP2);
mv.visitInsn(BALOAD);
mv.visitInsn(ICONST_1);
mv.visitInsn(IADD);
mv.visitInsn(I2B);
mv.visitInsn(BASTORE);
mv.visitIntInsn(SIPUSH, (id >> 1));
mv.visitFieldInsn(PUTSTATIC, "edu/cmu/sv/kelinci/Mem", "prev_location", "I");
}

访问类方法,分支时,均会调用插桩函数。

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
@Override
public void visitCode() {
mv.visitCode();

/**
* Add instrumentation at start of method.
*/
instrumentLocation();
}

@Override
public void visitJumpInsn(int opcode, Label label) {
mv.visitJumpInsn(opcode, label);

/**
* Add instrumentation after the jump.
* Instrumentation for the if-branch is handled by visitLabel().
*/
instrumentLocation();
}

@Override
public void visitLabel(Label label) {
mv.visitLabel(label);

/**
* Since there is a label, we most probably (surely?) jump to this location. Instrument.
*/
instrumentLocation();
}
Instrumentor.java

在Instrumentor.java中,创建ClassTransformer对象,对输入的class进行插桩。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
for (String cls : inputClasses) {
System.out.println("Instrumenting class: " + cls);
InputStream bytecode = classloader.getResourceAsStream(cls);

ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
ClassTransformer ct = new ClassTransformer(cw);
ClassReader cr;
try {
cr = new ClassReader(bytecode);
} catch (IOException | NullPointerException e) {
System.err.println("Error loading class: " + cls);
e.printStackTrace();
return;
}
interface.c

interface中

首先会创建属于AFL的共享内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
char* shmname = getenv(SHM_ENV_VAR);
int status = 0;
uint8_t kelinci_status = STATUS_SUCCESS;
if (shmname) {

/* Running in AFL */
in_afl = 1;

/* Set up shared memory region */
LOG("SHM_ID: %s\n", shmname);
key_t key = atoi(shmname);

if ((trace_bits = shmat(key, 0, 0)) == (uint8_t*) -1) {
DIE("Failed to access shared memory 2\n");
}
LOGIFVERBOSE("Pointer: %p\n", trace_bits);
LOG("Shared memory attached. Value at loc 3 = %d\n", trace_bits[3]);

然后会开始fork server

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
while(1) {
if(read(198, &status, 4) != 4) {
DIE("Read failed\n");
}

int child_pid = fork();
if (child_pid < 0) {
DIE("Fork failed\n");
} else if (child_pid == 0) {
LOGIFVERBOSE("Child process, continue after pork server loop\n");
break;
}

LOGIFVERBOSE("Child PID: %d\n", child_pid);
write(199, &child_pid, 4);

LOGIFVERBOSE("Status %d \n", status);

if(waitpid(child_pid, &status, 0) <= 0) {
DIE("Fork crash");
}

LOGIFVERBOSE("Status %d \n", status);
write(199, &status, 4);
}

子进程会不断尝试连接java端端服务器,将文件长度和文件的路径发送到服务器。发送后会等待java端返回的状态和共享内存。并将传递过来的共享内存写入真正的共享内存之中。

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
 do {
// if this is not the first try, sleep for 0.1 seconds first
if(try > 0)
usleep(100000);

setup_tcp_connection(server, port);

/* Send mode */
write(tcp_socket, &mode, 1);

/* LOCAL MODE */
if (mode == LOCAL_MODE) {

// get absolute path
char path[10000];
realpath(filename, path);

// send path length
int pathlen = strlen(path);
if (write(tcp_socket, &pathlen, 4) != 4) {
DIE("Error sending path length");
}
LOG("Sent path length: %d\n", pathlen);

// send path
if (write(tcp_socket, path, pathlen) != pathlen) {
DIE("Error sending path");
}
LOG("Sent path: %s\n", path);

nread = read(tcp_socket, &kelinci_status, 1);
if (nread != 1) {
LOG("Failure reading exit status over socket.\n");
kelinci_status = STATUS_COMM_ERROR;
goto cont;
}
LOG("Return kelinci_status = %d\n", status);

/* Read "shared memory" over TCP */
uint8_t *shared_mem = malloc(SHM_SIZE);
for (int offset = 0; offset < SHM_SIZE; offset += SOCKET_READ_CHUNK) {
nread = read(tcp_socket, shared_mem+offset, SOCKET_READ_CHUNK);
if (nread != SOCKET_READ_CHUNK) {
LOG("Error reading from socket\n");
kelinci_status = STATUS_COMM_ERROR;
...
/* If successful, copy over to actual shared memory */
for (int i = 0; i < SHM_SIZE; i++) {
if (shared_mem[i] != 0) {
LOG("%d -> %d\n", i, shared_mem[i]);
trace_bits[i] += shared_mem[i];
}
}
3.21补充

实际上手发现了不一样的地方,一般进行AFL模糊测试时,会用afl-gcc对目标程序进行编译插桩,而kelinci并没有。

而是用afl-fuzz直接运行,因为在interface.c中,作者利用了afl本身的管道通信和forkserver的机制。

具体的细节如下面两张图所示

cd3a9f96112118f9c9ebebe88ae5bac2.png

dc96f0165c42a7fc1631b82e25e18391.png

而在interface.c中,作者没有对interface进行插桩,但根据代码可以理解为作者手动进行了插桩,利用了AFL的forkserver机制。

首先开启forkserver机制,通过向FORKSRV_FD+1即199这个文件描述符,向状态管道中写入4个字节的值,用来告知afl fuzz,fork server成功启动,等待下一步指示。

1
2
3
4
5
LOG("Starting fork server...\n");
if (write(199, &status, 4) != 4) {
LOG("Write failed\n");
goto resume;
}

进入__afl_fork_wait_loop循环,从FORKSRV即198中读取字节到_afl_temp,直到读取到4个字节,这代表afl fuzz命令我们新建进程执行一次测试。

1
2
3
if(read(198, &status, 4) != 4) {
DIE("Read failed\n");
}

fork出子进程,原来的父进程充当fork server来和fuzz进行通信,而子进程则继续执行target。父进程即fork server将子进程的pid写入到状态管道,告知fuzz。

1
2
LOGIFVERBOSE("Child PID: %d\n", child_pid);
write(199, &child_pid, 4);

然后父进程即fork server等待子进程结束,并保存其执行结果到_afl_temp中,然后将子进程的执行结果,从_afl_temp写入到状态管道,告知fuzz。

1
2
3
4
5
6
if(waitpid(child_pid, &status, 0) <= 0) {
DIE("Fork crash");
}

LOGIFVERBOSE("Status %d \n", status);
write(199, &status, 4);

父进程不断轮询__afl_fork_wait_loop循环,不断从控制管道读取,直到fuzz端命令fork server进行新一轮测试。

运行

作者在项目里给了运行实例。

首先要编译目标程序

1
javac src/*.java -d bin

然后对目标程序进行插桩,这里由于java反射安全等原因,只能用jdk8,才能成功运行并进行插桩。

1
java -cp ../../instrumentor/build/libs/kelinci.jar edu.cmu.sv.kelinci.instrumentor.Instrumentor -i bin -o bin-instrumented

然后可以开始启动kelinci的server端

1
java -cp bin-instrumented edu.cmu.sv.kelinci.Kelinci SimpleBuggy @@

再启动AFL端即可对目标java程序进行fuzz

1
afl-fuzz -i in_dir -o out_dir ../../fuzzerside/interface @@

kelinci理解
http://jty-123.github.io/2024/03/17/kelinci/
作者
Jty
发布于
2024年3月17日
许可协议