namhyung / uftrace

Function graph tracer for C/C++/Rust/Python
https://uftrace.github.io/slide/
GNU General Public License v2.0
3.01k stars 441 forks source link

Add detecting vulnerable function in uftrace command-live option #240

Open vngkv123 opened 6 years ago

vngkv123 commented 6 years ago

I used to gdb debugger with "peda" plugin, which is very useful to reverse engineering and debugging.

That plugin show disassemble result in gdb with colorful strings. Below code block is regarded as vulnerable function in GDB-peda plugin.

483 # vulnerable C functions, source: rats/flawfinder
484 VULN_FUNCTIONS = [
485     "exec", "system", "gets", "popen", "getenv", "strcpy", "strncpy", "strcat", "strncat",
486     "memcpy", "bcopy", "printf", "sprintf", "snprintf", "scanf",  "getchar", "getc", "read",
487     "recv", "tmp", "temp"
488 ]

So, i add some code in cmd-replay.c because command-live function call command-replay. Below code block is my added code in cmd-replay.c

  43 /* Vulnerable symbol lists
  44  * usually those functions cause format string bug, bof, command-injection, etc) */
  45
  46 #define sym_num 24
  47 char *vuln_sym_list[sym_num] = {"exec", "abort", "system", "gets", "fgets", "popen", "getenv", "strcpy", "strncpy", "strcat", "strncat",
  48         "memcpy", "bcopy", "printf", "sprintf", "snprintf", "__isoc99_scanf",  "getchar", "getc", "read",
  49             "recv", "tmp", "temp", "scanf"};
  50
  51 static int find_vuln_sym(char *__symname){
  52     for(int idx = 0; idx < sym_num; idx++){
  53         if(!strncmp(vuln_sym_list[idx], __symname, strlen(__symname)))
  54             return 1;
  55     }
  56     return 0;
  57 }

and add that function to this line. ( 754 ~ 755 )

 749             pr_out(" %*s", depth * 2, "");
 750             if (tr.flags & TRIGGER_FL_COLOR) {
 751                 pr_color(tr.color, "%s", symname);
 752                 pr_out("%s%s\n", args, retval);
 753             }
 754             if( find_vuln_sym(symname) )
 755                 pr_red("%s%s%s\n", symname, args, retval);
 756             else
 757                 pr_green("%s%s%s\n", symname, args, retval);

As you know this is just DEMO version, not perfect code. My standards for filtering vulnerable function are here.

I want to develop this in uftrace for finding vulnerable functoin.

namhyung commented 6 years ago

Well, I like the idea but I think it'd be nice to have more flexibility. For example, it could color functions from specific modules, or list of known functions (like your case) or functions given by user. Also it'd be better for user to be able to choose which color will be used.

honggyukim commented 6 years ago

In this case, I think it'd be nice to write a python script in scripts directory for your purpose. You use uftrace-options to list up all the functions with preferred colors.

Please see the example in script command man page. It shows as follows:

$ cat arg.py
#
# uftrace-option: -A a@arg1 -R b@retval
#
def uftrace_entry(ctx):
    if "args" in ctx:
    print(ctx["name"] + " has args")
def uftrace_exit(ctx):
    if "retval" in ctx:
    print(ctx["name"] + " has retval")

$ uftrace record -S arg.py abc
a has args
b has retval
$ uftrace script -S arg.py
a has args
b has retval

For your example, I think the below python script can do for your purpose.

$ cat vulnerable-funcs-checker.py
# uftrace-option: -T exec|system|gets|popen|getenv|strcpy|strncpy|strcat|strncat|memcpy|bcopy|printf|sprintf|snprintf|scanf|getchar|getc|read|recv|tmp|temp@color=red

UFTRACE_FUNC = [ "exec", "system", "gets", "popen", "getenv", "strcpy", "strncpy", "strcat", "strncat", "memcpy", "bcopy", "printf", "sprintf", "snprintf", "scanf", "getchar", "getc", "read", "recv", "tmp", "temp" ]

def uftrace_entry(ctx):
    # this is called only when the listed functions are entered
    pass

def uftrace_exit(ctx):
    # this is called only when the listed functions are exited
    pass

You can simply use adding -S vulnerable-funcs-checker.py. the below shows an example:

$ uftrace -S vulnerable-funcs-checker.py --nest-libcall /bin/ps
flavono123 commented 6 years ago

@honggyukim I just read the man uftrace-script, and it seemed a little faults with python indents.

Run with the arg.py in man page occurs an error:

$ uftrace record -S scripts/arg.py tests/abc
  File "/home/flavono123/git/PR/uftrace/scripts/arg.py", line 6
    print(ctx["name"] + " has args")
        ^
IndentationError: expected an indented block
WARN: scripts/arg.py cannot be imported!

You hope to be followed right?:

$ cat scripts/arg.py
#
# uftrace-option: -A a@arg1 -R b@retval
#
def uftrace_entry(ctx):
    if "args" in ctx:
        print(ctx["name"] + " has args")
def uftrace_exit(ctx):
    if "retval" in ctx:
        print(ctx["name"] + " has retval")

$ uftrace record -S scripts/arg.py tests/abc
a has args
b has retval
$ uftrace script -S scripts/arg.py 
a has args
b has retval

Tested on my system, and use abc compiled from tests/s-abc.c with -pg -g as you see.

ParkHanbum commented 6 years ago

@vngkv123 as you know, uftrace can parse argument automatically or handcrafted. you can use this for catch the vulnerable situation.

for example, when you want notice about possibility of format string attack then first compare arguments of printf function that check the format string parameter match the number of arguments, and then let user know about it is vulnerable if not match between format string parameter with number of arguments.

honggyukim commented 6 years ago

Hi @flavono123,

I just read the man uftrace-script, and it seemed a little faults with python indents. Run with the arg.py in man page occurs an error:

You're right. The indentation has to be fixed in the man page. Please send a PR to fix it!

flavono123 commented 6 years ago

@honggyukim 🙌 I'm happy to PR that, but how can I compile(?) the md to .1 file? the doc/uftrace-script.md looks good:

    def uftrace_entry(ctx):
        if "args" in ctx:
            print(ctx["name"] + " has args")
    def uftrace_exit(ctx):
        if "retval" in ctx:
            print(ctx["name"] + " has retval")

But in the doc/uftrace-script.1, it seems NOT adjusted, hard-coding is illegal(read-only file):

def\ uftrace_entry(ctx):
\ \ \ \ if\ "args"\ in\ ctx:
\ \ \ \ print(ctx["name"]\ +\ "\ has\ args")
def\ uftrace_exit(ctx):
\ \ \ \ if\ "retval"\ in\ ctx:
\ \ \ \ print(ctx["name"]\ +\ "\ has\ retval")

How can I fix a .1 file? Any references ?

namhyung commented 6 years ago

@flavono123 You cannot change the man page, we only do it for .md files. If it's ok in .md but failed in .1, that's not an issue.

namhyung commented 6 years ago

@ParkHanbum Checking number of argument seems not possible as it changed whenever called (in different callsites). But we can check the format string itself to have vulnerable specifier ("%n") or the format string is constant or not (by checking its address) from a security perspective.

honggyukim commented 6 years ago

@flavono123 As @namhyung said, you have to modify doc/uftrace-script.md file. doc/uftrace-script.1 is generated with make install so no need to modify it. And I found that print() statement begins with a tab instead of white spaces. You can change the tab to 8 white spaces.

flavono123 commented 6 years ago

@namhyung @honggyukim Oh, I got it. The problem was about tab and white-space! I'll PR for this 😃

namhyung commented 6 years ago

@vngkv123 It seems your list of vulnerable functions are too generic. For example, memcpy and read are used frequently in many program and blindly saying that they are vulnerable doesn't make sense IMO. You'd be better writing a script to check the buffer and size for that. Thoughts?

vngkv123 commented 6 years ago

So, i've post some question about how to get register state to make script which do as sanitizer. If i can get memory state and register state whenever i want, it is possible to detect some uninitialized stack or some kind of UAF bug and type-confusion, etc)... As you comment, i know that vulnerable function lists are so generic. But, main intention is just to watch those function lists carefully anyway.

namhyung commented 6 years ago

I suggest you start with a single specific case first - as I said the format string check or buffer overflow check can be done within the current limitation of uftrace.

honggyukim commented 6 years ago

For example, you can write a python script whether the src buffer in memcpy overlaps with dest buffer.

void *memcpy(void *dest, const void *src, size_t n);

You can check if dest + n goes beyond src address. I think this can be one of the single specific cases.

vngkv123 commented 6 years ago

I've try to make some usable script... but if can't get register state and VA_ARG, it's hard to realize it...

#
# vuln.py
#
# uftrace-option: --nest-libcall -T memcpy@filter,arg3 -T printf@filter,arg1/s -T __isoc99_scanf@filter,arg1/s
#
#   void *memcpy(void *dest, const void *src, size_t n);
#
# Only "memcpy" calls this script and other functions never.
'''
script_context = {
    int       tid;
    int       depth;
    long      timestamp;
    long      duration;    # exit only
    long      address;
    string    name;
    list      args;        # entry only (if available)
    value     retval;      # exit  only (if available)
};
'''
import re
UFTRACE_FUNC = [ "memcpy", "printf", "strcpy", "read", "gets", "fgets", "__isoc99_scanf" ]
# FUNC_DICT -> check arg number
FUNC_DICT = { "memcpy" : 1, "printf" : 1, "strcpy" : 1, "__isoc_scanf" : 1, "read" : 2, "gets" : 1, "fgets" : 1  }
fmt_cnt = 0
def red_print(msg):
    print "\x1b[1;31m" + msg + "\x1b[1;m"
def uftrace_begin():
    pass
def uftrace_entry(ctx):
    argv = ctx["args"]
    func = ctx["name"]
    for tfunc in UFTRACE_FUNC:
        if func == tfunc:
            check_sanity(func, argv)
def uftrace_exit(ctx):
    pass
def uftrace_end():
    global fmt_cnt
    print "[+} Format string number : ", fmt_cnt
    pass
################################################################

def check_sanity(symname, argv):
    global fmt_cnt
    if "__isoc99_scanf" == symname:
        if '%s' in argv[0]:
            # very simple case
            red_print("[+] Buffer Overflow Detect in " + symname)
    if "printf" == symname:
        fmt = re.findall(r"%[diouxXeEfFgGaAcsCSPnmztjLlh]", argv[0])
        for i in fmt:
            fmt_cnt += 1

###################################################################

test code

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

char data[256];

int main(int argc, char *argv[]){
    printf("Hello, World\n");
    char buf[256] = {0, };
    scanf("%s", buf);
    memcpy(data, buf, 256);
    printf("String : %s\n", data);
    return 0;
}

result

root@ubuntu:/home/asiagaming/uftrace# ./uftrace -S ./scripts/vuln.py ./test
Hello, World
[+] Buffer Overflow Detect in __isoc99_scanf
String : abcd
[+} Format string number :  1
# DURATION    TID     FUNCTION
 487.005 ms [65194] | __isoc99_scanf("%s");
 450.928 us [65194] | printf("String : %s\n");

Are no way to solve those problems ? T_T

namhyung commented 6 years ago

What is the base pointer? Anyway it currenlt doesn't have a way to pass such info to a script.

For VA_ARGS, you can instead request enough argument even if they are invalid - for example, you can specify 10 arguments for printf() and only use some of them according to the format string.