Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Qiling + unicornafl seems like can't find an easy integer overflow #4

Open
dzonerzy opened this issue Jan 31, 2022 · 7 comments
Open
Assignees

Comments

@dzonerzy
Copy link

dzonerzy commented Jan 31, 2022

Basically i created a vulnerable binary and linked it against uclib-ng (arm-eabihf), below the source code:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>


char* c_readfile(char *filename, size_t *filesize)
{
   char *buffer = NULL;
   int read_size;
   FILE *handler = fopen(filename, "rb");
   if (handler)
   {
       fseek(handler, 0, SEEK_END);
       *filesize = ftell(handler);
       rewind(handler);
       buffer = (char*) malloc(sizeof(char) * (*filesize + 1) );
       read_size = fread(buffer, sizeof(char), *filesize, handler);
       // buffer[filesize] = '\0';
       if (*filesize != read_size)
       {
           free(buffer);
           buffer = NULL;
       }
       fclose(handler);
    }
    return buffer;
}

typedef enum {
	STYPE_ANGELO = 0xdeadbeef,
	STYPE_JACK = 0xcafebabe,
	STYPE_DZONERZY = 0xc00fc00f,
	STYPE_INVALID = -1
} stype_t;


typedef char byte;
typedef unsigned char ubyte;

typedef struct {
	stype_t Kind;
	short Length;
	char * Buffer;
} content_t, *pcontent_t;

pcontent_t parse_data(unsigned char * buffer, size_t buf_len) {
	pcontent_t data = (pcontent_t) malloc(sizeof(content_t));
	data->Kind = STYPE_INVALID;
	data->Length = 0;
	data->Buffer = NULL;
	size_t counter = 0;
	while(counter < buf_len) {
		unsigned long long kind = *(unsigned long long *)(buffer + counter);
		switch(kind) {
			case STYPE_ANGELO:
				printf("Got kind ANGELO!\n");
				data->Kind = kind;
				break;
			case STYPE_JACK:
				printf("Got kind JACK!\n");
				data->Kind = kind;
				break;
			case STYPE_DZONERZY:
				printf("Got kind DZONERZY!\n");
                                data->Kind = kind;
				break;
			default:
				printf("Invalid kind 0x%llx\n", kind);
				exit(0);
		}
		counter += sizeof(kind);
		data->Length = *(short *)(buffer + counter);
		counter += sizeof(data->Length);
		printf("Got size: 0x%x\n", data->Length, (short)(data->Length));
		if((short)(data->Length + 1) > 32) { // integer overflow happen here
			printf("invalid length > 32\n");
			exit(0);
		}else{
			data->Buffer = malloc(32);
			memset(data->Buffer, 0, data->Length);
			memcpy(data->Buffer, (buffer + counter), data->Length);
		}
		counter += data->Length;
		printf("counter = %d\n",counter);
	}
	return data;
}


int main(int argc, char ** argv) {
	if(argc == 2) {
		char * inputfile = argv[1];
		printf("Using input file: %s\n", inputfile);
		size_t filesize = 0;
		unsigned char *buffer = c_readfile(inputfile, &filesize);
		if(!buffer) {
			printf("Invalid file specified!\n");
			exit(-1);
		}
		printf("Got file %lu bytes, start parsing\n", filesize);
		pcontent_t ret = parse_data(buffer, filesize);
		printf("Kind: 0x%llx Length: 0x%08x Buffer: %p\n", ret->Kind, ret->Length, ret->Buffer);
	}else{
		printf("Usage: %s <input>\n", argv[0]);
		return -1;
	}

	return 0;
}

Then i created a simple qiling script which make first a snapshot the use the snapshot to fuzz the parsing function

import os
import signal
import sys
import qiling.core
from qiling import *
from qiling.const import QL_VERBOSE
import unicornafl


def snap(ql: qiling.core.Qiling):
    r0 = ql.reg.read("r0")
    size = ql.reg.read("r1")
    mem = ql.mem.read(r0, size)
    # just a check to see if r0 is pointing to the expected buffer
    print(mem)
    ql.save(snapshot="snap.bin")
    exit(0)


def start_afl(ql: qiling.core.Qiling, input_file):
    def place_input_callback(uc: unicornafl.Uc, fuzzed: bytes, persistent_round: int, data):
        # r0 is a pointer to fuzzed input
        # r1 is the buffer length
        allocated_mem = ql.mem.map_anywhere(len(fuzzed))
        # write the fuzzed input to the allocated memory
        ql.mem.write(allocated_mem, fuzzed)
        # overwrite r0 pointer with the new buffer location
        ql.reg.write("r0", allocated_mem)
        # update buffer length
        ql.reg.write("r1", len(fuzzed))
        return True
    try:
        if not ql.uc.afl_fuzz(input_file=input_file,
                              place_input_callback=place_input_callback,
                              exits=[ql.os.exit_point]):
            print("Ran once without AFL attached.")
            exit(0)
    except unicornafl.UcAflError as ex:
        if ex != unicornafl.UC_AFL_RET_CALLED_TWICE:
            raise


def my_syscall_write(ql: Qiling, write_fd, write_buf, write_count, *args, **kw):
    data = ql.mem.read(write_buf, write_count)
    # disable printing
    # print(data.decode(), end="")
    return write_count


def mem_read_write(ql: Qiling, size, addr, value, unk):
    if not ql.mem.is_mapped(addr, size):
        os.abort()


def abort(ql, int_code):
    os.kill(os.getpid(), signal.SIGSEGV)


def emulate(path, rootfs, input_file):
    ql = Qiling(path, rootfs, verbose=QL_VERBOSE.DEBUG)
    # This is done only the first time to save snapshot
    # ql.hook_address(snap, address=0x00010b30)
    # ql.run()

    ql.restore(snapshot="./snap.bin")  # restore the snapshot right before calling the parse function
    ql.hook_address(start_afl, 0x00010b30, user_data=input_file)  # hook right before the parsing function and place
    # fuzzed input in memory
    ql.set_syscall(0x4, my_syscall_write)  # disable stdout/stderr printing
    ql.hook_mem_write(mem_read_write)  # hook memory to check if we are writing on unallocated memory

    try:
        ql.emu_start(begin=0x00010b30, end=0x0010b34)
    except:
        abort(ql, 0)


if __name__ == "__main__":
    unicornafl.monkeypatch()
    if len(sys.argv) == 2:
        emulate(["./test", ""], "./rootfs", sys.argv[1])
    else:
        print(f"Usage {sys.argv[0]} <input>")

Anyway seems like after 5 completed cycles it still can't find the vulnerable path, while forcing it into place_input_callback just works fine and make afl register the crash.

I run afl with

AFL_DEBUG=1 afl-fuzz -D -U -i input/ -o output/ -- python3 main.py @@

test-arm.tar.gz

┌─ process timing ────────────────────────────────────┬─ overall results ────┐
│        run time : 0 days, 0 hrs, 21 min, 3 sec      │  cycles done : 25    │ <- 25 cycles completed :(
│   last new find : 0 days, 0 hrs, 20 min, 53 sec     │ corpus count : 16    │
│last saved crash : none seen yet                     │saved crashes : 0     │
│ last saved hang : none seen yet                     │  saved hangs : 0     │
├─ cycle progress ─────────────────────┬─ map coverage┴──────────────────────┤
│  now processing : 4.311 (25.0%)      │    map density : 0.63% / 0.68%      │
│  runs timed out : 0 (0.00%)          │ count coverage : 1.22 bits/tuple    │
├─ stage progress ─────────────────────┼─ findings in depth ─────────────────┤
│  now trying : havoc                  │ favored items : 8 (50.00%)          │
│ stage execs : 58/145 (40.00%)        │  new edges on : 8 (50.00%)          │
│ total execs : 133k                   │ total crashes : 0 (0 saved)         │
│  exec speed : 102.7/sec              │  total tmouts : 0 (0 saved)         │
├─ fuzzing strategy yields ────────────┴─────────────┬─ item geometry ───────┤
│   bit flips : 1/704, 0/689, 0/659                  │    levels : 2         │
│  byte flips : 0/88, 0/73, 0/43                     │   pending : 1         │
│ arithmetics : 0/4927, 0/977, 0/19                  │  pend fav : 0         │
│  known ints : 8/398, 6/1849, 0/1888                │ own finds : 15        │
│  dictionary : 0/0, 0/0, 0/0                        │  imported : 0         │
│havoc/splice : 0/73.8k, 0/47.3k                     │ stability : 100.00%   │
│py/custom/rq : unused, unused, unused, unused       ├───────────────────────┘
│    trim/eff : 92.26%/27, 0.00%                     │          [cpu000:100%]
└────────────────────────────────────────────────────┘
@domenukk
Copy link
Member

domenukk commented Feb 9, 2022

The fuzzer won't be able to brute-force 32 bit values, usually.
You can try to hand the values as token/dictionary to AFL and it should work.
Alternatively, switch to QEMU mode and use complog or wait for upstream unicorn to fix CMP hooks, cc @wtdcode

@wtdcode
Copy link
Collaborator

wtdcode commented Feb 14, 2022

Could you have a retry now?

@dzonerzy
Copy link
Author

Sure I'll let you know once tested.

@dzonerzy
Copy link
Author

dzonerzy commented Feb 16, 2022

I tried after updating I'm having a different issue now, here's the code:

import os
import sys
from qiling import Qiling
from qiling.const import QL_VERBOSE
from qiling.extensions.afl import ql_afl_fuzz


def start_afl(ql: Qiling, user_data):
    def place_input_callback(_ql: Qiling, fuzzed: bytes, persistent_round: int):
        size = len(fuzzed)
        mem = _ql.reg.read("r0")  # here r0 should point to buffer, instead I get 0, seems like uc context is lost
        _ql.reg.write("r1", size)
        _ql.mem.write(mem, fuzzed)
        return True
    try:
        ql_afl_fuzz(ql, input_file=user_data, place_input_callback=place_input_callback, exits=[ql.os.exit_point])
    except:
        os.abort()


def emulate(binary, rootfs, fuzzed_binary):
    ql = Qiling(binary, rootfs, verbose=QL_VERBOSE.DEBUG)
    ql.restore(snapshot="./snap.bin")
    ql.hook_address(start_afl, 0x00010b30, user_data=fuzzed_binary)
    ql.emu_start(begin=0x00010b30, end=0x0010b34)


if __name__ == "__main__":
    emulate(["./test", "./pier"], "./rootfs", sys.argv[1])

Inside the place_input_callback callback the Qiling context seems wrong, in fact r0 register inside the start_afl callback point to the buffer while inside the place_input_callback is zero. That wasn't happening with the previous version. cc @wtdcode

@dzonerzy
Copy link
Author

Any updates on this?

@futhewo
Copy link

futhewo commented Nov 11, 2024

I have a bug I think is related.
I have a very dumb target (see below) I compiled in ARM and try to fuzz with afl-unicorn.

int parse(char* p_buf, size_t d_len) {
    if (d_len < 8) return -1;
    uint64_t in = *((uint64_t*)p_buf);
    if (in == 0x4041424344454647) {
        // SEGFAULT;
        int* a = 0;
        *a = 0;
        return 100;
    }
    return 0;
}

Unfortunately, no matter how long I fuzz this I only have two edges (<8 and >=8 length).
I patched the cmplog hooks to check if they were executed at all and nope. They are not.

So, yeah it confirms what @domenukk said: seems like CMP hooks don't work on ARM in unicorn.
Any idea / reference I can rely on to fix this?
Perhaps it is fixed in current mainstream unicorn?

@wtdcode
Copy link
Collaborator

wtdcode commented Nov 11, 2024 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants