Software Integrity Blog

 

Abuse cases: How to think like a hacker

Writing abuse cases is an exercise in “thinking like the enemy.” It’s a great way to help secure your software and systems and stay ahead of attacks.

Use cases have become common practice in agile software development to help developers deliver code that meets intended feature requests. Product managers draft use cases to ensure the code they write meets their business objectives. Developers build the application to those specs. In a perfect world, it prevents unnecessary and redundant work.

Unfortunately, the security world hasn’t evolved in the same ways. There are no protections in place to help developers anticipate what a malicious user might do with a feature. But there should be.

Use cases create a path of least resistance for users to get what they want. But you also need to put the most resistance between bad guys and the data/access they want. And you want to do this while minimizing the impact on legitimate users.

Get the 2019 Gartner Magic Quadrant for AST

What you need, then, are the opposite of use cases: abuse cases. You need to think like an attacker at the unit, system, and infrastructure levels. To prepare your codebases for use cases that compromise sensitive information. To prevent embarrassing business failures.

Sure, the bulk of this burden falls to your security and product management teams. It’s impossible to define thorough abuse cases entirely within development. But developers can do a few things now to get the process started. Let’s break these down.

Abuse cases at the unit test view

Developers write unit tests for use cases. For each function or method, they write a test or two to ensure the function does what it’s supposed to.

To switch to “abuse case” thinking, ask yourself: How can I interrogate the system for useful information by feeding it unexpected input: the wrong type or size? What happens if I feed it null input? How could I harm the system by calling a function repeatedly?

Include at least one abuse case for every positive test case you have. If the security benefits of coding defensively like this are not immediately apparent, look at it this way: Thinking small will help you think large. If you practice thinking about how to break your functions, then you’ll have an easier time with the following sections.

Unit tests are great for solidifying individual components of your application. But security errors often happen at the intersection of two components. For that, we’ll need something a little more integrated.

Abuse cases at the system view

The system view is how most of the bad guys will access an application—interfacing with it as a user or as a scripted farce of a user.

System-level abuses cases are a bit trickier, but with enough thought, you can avoid a number of scary scenarios and build protections into your automated testing suite. Selenium has become the default for testing web applications. Using Selenium, you can test for a number of major flaws, including SQL injections, cross-site scripting attacks, and session hijacking.

Next, look at your authentication:

  • Can you brute force the username or password field in any way?
  • Is there a time delta between a request with an invalid username and a request with an invalid password?
  • Are your database IDs guessable? Are they sequential?

Your system is your code, but your system is not ONLY your code. Remember human factors too:

  • What type of attacks would two-factor authentication prevent?
  • Can a poorly trained support technician give harmful access to a user’s account?
  • Can an unexpected user action wreak havoc on your system?

The “at scale” or stress-test level

Another method of attacking your application happens only at scale: the dreaded DDoS attack. Hackers are finding increasingly creative ways to bring your system down by maxing out resources—commonly CPU, bandwidth, or RAM (whichever breaks first). When multiple attackers work together, it’s called a distributed denial-of-service or DDoS attack.

A 2015 DDoS attack leveraged the sizable daily traffic to China’s Baidu search engine. Hackers inserted malicious code into the search results page, causing anybody using the search engine to unwittingly participate in a DDoS against GitHub.

You also might remember an early episode of “Mr. Robot,” where they took this one step further. After fsociety’s DDoS attack caused the servers to reboot, they installed a rootkit that gave them control over the Evil Corp servers.

Organizations try to combat high load with auto-scaling, which leads to a secondary vector of attack—bankrupting a company with infrastructure costs. Oh, and people can also attack hard drive space too.

These types of exploits can only happen at scale. The abuse cases are obvious:

  • Can you, through the user-level interface, max out CPU, bandwidth, or RAM?
  • Can you fill up the system’s hard drive?
  • Is an edge caching layer missing? If not, does it have built-in DDoS protection, if any?

There are plenty of load-testing tools that you can use, from the simple siege tool to the more complex WebLOAD, and more. Remember, with abuse cases you are trying to break your app in as many ways you can think of.

The secret weapon: fuzzing

It’s not a pragmatic use of your time to attempt to document all the ways your system could be hacked and all the methods people might use. Luckily, just like Alan Turing didn’t do all his cryptography by hand, you can get a computer to do this for you.

Enter fuzzing.

Developed at the University of Wisconsin, fuzzing is a technique where random input, pseudorandom input, or user behavior is applied to a system. This technique is incredibly useful when it comes to helping you detect the unexpected.

Let’s take a look at this example. Here’s a painfully simple PHP function:

function doubleNumber($x) {
    return $x * 2;
}

A simple, deterministic test might run it through a hundred or so variations and just ensure that it’s working.

public function testPoorly() {
    for($i = -100; $i < 100; $i++) {
        $this->assertEquals(doubleNumber($i), $i * 2);
    }
}

This is obviously ridiculous, but it becomes more interesting when we add fuzzing to it:

public function testMultiplicationProperties() {
    for($i = 0; $i < 50000; $i++) {
        $positiveRand = mt_rand(1, 1000000);
        $isDoubleBigger = $positiveRand < doubleNumber($positiveRand);
        $this->assertEquals($isDoubleBigger, true);
    }
    for($i = 0; $i < 50000; $i++) {
        $negativeRand = mt_rand(-1000000, -1);
        $isDoubleSmaller = doubleNumber($negativeRand) < $negativeRand;
        $this->assertEquals($isDoubleSmaller, true);
    }
}

So here we have a much wilder and more random test of our simple function that tests random numbers between 1 and 1 million, 50,000 times in a row. While it’s doubtful that you’ll run into any flaws trying to double a number in PHP, this becomes infinitely more complex the instant you start writing “real functions” that do “real things.”

Note that another emergent property of fuzzing is that you end up testing properties of your code instead the direct results. In the above example, the code tests:

  • For positive input, is the output BIGGER than the input?
  • For negative input, is the output SMALLER than the input?

Now our very simple example has become fairly interesting. Fuzzing has that effect on testing. You’ll start to notice some unexpected results soon, even at the unit test level. You can even apply fuzzing at the system level with Selenium.

How to create abuse cases to secure your system

Hacks can happen at every layer, so be prepared. Writing misuse or abuse cases is an exercise in “thinking like the enemy.” It’s a great way to train yourself to have a security-first mindset. If you’re actively thinking of ways that your system may be compromised, then you’re that much further ahead in the great arms race of software security.

 

More by this author