- So your strategy when you're fuzzing,
if you don't know a whole lot
about the target that you're fuzzing,
don't wait until you have a complete suite of tests
with the fuzzer.
Always fuzz.
That's my recommendation to you,
is that fuzzing takes a long time.
And so you really don't want to be waiting
until the very end when you have a collection of tests.
Just start fuzzing.
And it could just be a simple mutational fuzzer.
In some cases you're doing an evolutionary fuzzer
you can set up something really quickly.
So fuzzing could take really a long time.
If you want it to be complete, it can take weeks.
And so if you can, try to fuzz in parallel
If you know a target,
if you know the version of software that's being run,
run as many instances of that as you can.
You can even run them in a cloud
like in Amazon Web Services or Azure.
Just have multiple instances of it,
and you're fuzzing at the same time.
Maybe starting from a different test case.
And that way you finish things a lot faster,
and you can get better results.
So when you're looking for fuzzing,
you're looking for code coverage.
In other words, your one exercise as much
of the program's parser and code that's based on the parser.
You know, information that comes from the parser
you want to execute as much of that as possible.
So you can get as much coverage.
Some of the basic fuzzers that you're doing,
where they're like mutational fuzzers,
they really only find results for bugs
that are what are called shallow.
In other words, you're not going very far
into the parser itself.
You're doing maybe just a little bit of things
on the outside.
And really to get any further than that,
you either need to do the generational fuzzer,
where you have information
about how the protocol is supposed to work.
Or the evolutionary fuzzers,
which actually monitor the program
so that you can get more deep into the program.
And so that's what you need to have a complex
and sometimes, even a custom written fuzzer.
So, in other words, you're writing everything
either from scratch or you're using some kind
of fuzzing framework.
And I'll go through some of those a little later on.
And there are certain kinds of vulnerabilities and bugs
that you will not find with fuzzing.
This is an example, there are some vulnerabilities
that will not crash a program, you know,
because they're just basically a logic error.
Or they allow you some additional access to do things
that you shouldn't be doing, like injecting a command.
That's a very common vulnerability,
especially on network equipment,
is a command injection vulnerability.
And what that means is that normally you would
issue a command on the command line interface
and it would run some program on, in the operating system.
And then you may be able to tack on something.
Like if you do a semicolon
you can maybe tack on like a shell command to that
and be able to do things like, you know,
executing a Netcat and calling home to you,
and then getting that, a shell.
Another thing you can do is maybe directory traversal.
So you can access part of the file system
that you shouldn't be able to access.
These are things that fuzzing will not uncover,
because they do not cause the program to crash,
and there's really no way of knowing, you know,
what it is that it was looking at.
So in these cases, you may be able to do some kind
of instrumentation and analysis after the fact
to find out what the device is responding with.
But in other cases, you just won't find them.
So here's how you choose what to fuzz.
When I approach a target
and I want to know what I should work on first,
first I want to know what user inputs are used
by the program.
What actual user inputs can I supply,
and which of those are from an untrusted source?
In other words, are they coming
from a session that is already authenticated?
You know, if I'm already logged in with SSH
it may not be as important
if I had actually been able to send this
as a packet over the internet, for instance.
Okay, so if you have those kinds of inputs
that are untrusted, you wanted,
well, you would want to prioritize those above all others.
What features are mature versus immature?
If it's a new feature,
it may have been implemented just recently
and the code may not have been tested as well.
And so if you know a part of the feature,
or you know a feature of the code that is newer,
you may fuzz that first because the code
has not been as well vetted.
And then there are other things that you need to worry
about when you're choosing what to fuzz.
Are there input constraints that,
which the parser checks first?
In other words, does the input have to be null-terminated?
In other words, are they doing something
like we did earlier with a string copy?
Are they using a stir copy?
So the input must be null-terminated,
otherwise it just continues to copy.
You have certain packets and payloads that you need
that have a checksum in them.
And that checksum has to be correct
before it'll ever look any further into the packet.
You need to know that
that checksum has to be fixed up first.
Otherwise, the parser will just see that that's no good
and just throw the packet away.
And the same thing,
if you're doing a secure connection using TLS,
you need to have, maybe you have to have that set up first.
You have to actually have a connection set up first.
The input may have length fields.
It has count fields
and those have to match some kind of payload.
Those are all fix ups that you would have to do
when you're choosing what to fuzz.
And so here is a drawing of program input.
Some of these may be fairly obvious to you,
like maybe a socket that you're listening
on the network for.
And then, there are others
that may not be very obvious to you.
You have environment,
which are values that are stored with the program
when the program executes.
Those can be like the path variable that allows you
to look in the file system for certain programs.
You may be able to modify that environment
before you invoke a program.
You have file systems and files
that are accessible from the application.
Maybe when you're running the application
it has files that it uses
on the file system for doing sockets.
That's called a UNIX domain socket.
You have memory.
So memory could be mapped into an application
from somewhere else.
It could be shared memory based on a file.
So if you write into that file
and that memory is executable,
then you might be able to run things that are directly
in that shared memory.
You have POSIX message queues,
and then you have libraries that are mapped in.
And those libraries can either be libraries
that are on disk, like the libc.
There's also libraries called
the virtual dynamic shared object,
which are libraries that the kernel itself provides
to each process.
If you ever heard of a, an attack recently
called Dirty Cow,
one of the ways to be able to exploit a process
was to overwrite the virtual dynamic shared object.
So that when something got called in there from the kernel,
you're actually calling some attack or code
and you've actually taken the process over.
So this is the fuzzing process.
We're gonna start from the top and work our way down.
So what you'll do first is you'll set
up the test environment so that it's in the state
that you want.
You'll pick a random seed.
Typically, when you're doing some kind of fuzzing
you'll have a random number generator
that will determine what inputs you actually fuzz
and then the values that you might use.
So you want to have a random seed
that is available that you know,
so that when you're fuzzing
and then you cause a crash later on,
you want to be able to start from that random seed.
So if you're not recording that anywhere,
if you fuzz again, you use a different seed,
you may have different test cases.
So then you also want to choose the input.
You wanna choose what value you want to fuzz.
And then, the fuzzer will create a test case.
In other words, it creates an anomalous message
based on that field.
And then, you run the target
with that test case as the input.
And then, you instrument the target
and you check for a crash.
And if you have a crash,
then you would record the test case inputs
and then the crash data.
And then, you would move on and go back
to choose the input again.
And if you don't crash, then I guess it was okay.
And there wasn't anything that happened.
So you may kill the target and then restart it again,
just to get you back into that original state.