Adam Shostack here. One of the really exciting things about being in the Microsoft Security Engineering Center is all of the amazing collaborators we have around the company. People are always working to make security engineering easier and more effective. When we talk about security testing, we often focus on what it can’t do. “You can’t test security in,” and “test will never find everything.” But much like there’s code that’s easy to get wrong, there’s code that’s hard to test. Writing code to be testable has a long history, and one we don’t often talk about in security. Today’s post is from Hassan Sultan, who’s responsible for one of our internal fuzzing tools. We hope it inspires you to think about the question “How can I make the security of my code more easily tested?”
And here’s Hassan:
Security testing is an integral part of the software development lifecycle. At Microsoft, the biggest part of the security testing done is usually implemented through a technique called fuzz testing: sending unexpected input to the product and checking whether it behaves in an acceptable way (i.e. it doesn’t crash, hang, leak memory…). We also use other techniques such as static source code analysis but today we’re going to focus on fuzz testing and how you can best make use of it.
Almost every software company and every software project has to perform within constraints, they can be financial, the project has to be completed within a set budget, or time-driven, the project has to ship within a specific timeframe. The corollary is that the product must be of the highest quality possible within those constraints. How then can you perform efficient, quick and cheap security testing?
One approach we have started using at Microsoft is to change our engineering and test engineering practices to make fuzz testing easier, it’s a little bit of additional upfront work but with great savings in terms of time and resources quickly appearing over the life of the project.
There are two popular approaches to fuzz testing, considering data exchanges between a producer (the software sending data) and the consumer (the target software processing the data):
- Generation fuzzing : You create unexpected input from scratch and send it to the target, the fuzzer is effectively the producer
- Man-in-the-middle(MITM) fuzzing: You intercept the data as it flows from an existing producer to the consumer and modify it on the fly before it reaches the consumer
A couple of things are obvious when comparing these two approaches:
- Generation fuzzing requires quite a bit of work to create entirely the data exchange as the generated data must be conforming enough to be accepted by the target, but you have complete control and freedom to generate anything you want, hence you control the fuzzing quality end-to-end
- MITM fuzzing requires little upfront effort but depends on existing producers to generate data that can be modified, hence the quality of the fuzz testing depends on the producer and the data it generates
The approach I’m going to talk about is based on MITM fuzzing; the goal is to develop functionality tests in a way that makes them easily reusable as producers for MITM fuzz testing, as well as to provide a bit of functionality in the actual product to make fuzz testing more efficient. This approach makes security testing much cheaper to implement, is quite efficient and allows improving the fuzzing over time without having to rewrite numerous security tests.
MITM fuzzing using functionality tests has the following drawbacks:
- Tests tend to not behave correctly when they receive an unexpected response from the target following a fuzzed exchange as they never were meant to deal with such thing, they can hang or crash
- Tests rely on a long timeout to receive a response, and thus block sometimes for many seconds when data is fuzzed and the consumer drops the request without responding
- Tests bail out at the first failure they encounter and do not execute the following test cases, the fuzz testing can thus not be applied to those exchanges
- Test setup code is mixed with test code and causes the fuzzer to corrupt the data exchange required for setup, thus preventing the actual test from running,
- Modifying data on the fly efficiently is cumbersome to impossible when the following is present in the data exchanged:
- Encrypted data
- Compressed data
- Checksums & signatures
The approach here is thus to fix all these problems at the source, we have listed the steps required along with each step’s priority, obviously the more you do, the better, but if in a crunch, start from the top of the list and go down as far as you can.
Test engineering changes for functionality tests
- P1: Harden functionality tests to handle unexpected responses from the consumer gracefully
- P1: Have configurable timeouts that can be shortened when the tests are used for fuzzing
- P2: Isolate setup code in tests so that the setup part can be signaled to the fuzzer which will then pause fuzzing until setup completes
- P2: Tests containing multiple test cases should allow running an arbitrary test case individually without the need to run them all in a sequence
Engineering changes in the product
- P1: Provide a test hook in the product to disable checksum validation in components
- P1: Provide a test hook to turn compression and decompression routines into no-op : what comes in comes out unmodified, the communication will occur correctly using uncompressed data and the fuzzer can thus modify the content easily and efficiently
- P2: Provide a test hook to disable signature validation
- P2: Provide a test hook to turn encryption and decryption routines into no-op
(A test hook is a configuration option that modifies the product’s behavior when set, it can be removed before the product ships if needed)
These modifications to the way tests and products are engineered are minor and cheap to implement when planned early on and will produce tremendous benefits by:
- Enabling security testing to be applied early on in products, which will allow to find and remedy issues early in the product cycle
- Significantly reducing the cost of security testing as this can now be performed by reusing functionality tests, you can improve security testing simply by improving your functionality tests
- Allowing all security tests to improve automatically as the fuzzing engine used gets smarter, since the security tests are decoupled from the fuzzer itself, you can thus stage your investment in security testing gradually by adding more intelligence into your fuzzer
- Lessening the number of vulnerabilities and thus lessening servicing costs
Ultimately, using both Generation fuzzing and MITM fuzzing would be ideal, as generation fuzzing provides a few benefits that won’t be attained by MITM fuzzing(the ability to create very specific scenarios for example), but when dealing with time and resource constraints, the MITM fuzzing approach allows for efficient fuzzing that can be improved over time at a minimal cost.