5

As part of a security CS course, my class has been given the task of exploiting a vulnerability to beat a password check using a stack/buffer overflow. The code with the vulnerability is as follows:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <openssl/md5.h>

int main(int argc, char **argv) {
    char correct_hash[16] = {
        0xd0, 0xf9, 0x19, 0x94, 0x4a, 0xf3, 0x10, 0x92,
        0x32, 0x98, 0x11, 0x8c, 0x33, 0x27, 0x91, 0xeb
    };
    char password[16];

    printf("Insert your password: ");
    scanf("%29s", password);

    MD5(password, strlen(password), password);

    if (memcmp(password, correct_hash, 16) == 0) {
        printf("Correct Password!\n");
    } else {
        printf("Wrong Password, sorry!\n");
    }
    return 0;
}

I understand the classic "stack-smashing" principle (I think), and there is a clear overflow vulnerability here, where the first 14 bytes of the correct_hash array can be overwritten, by inputting a password longer than 15 characters when prompted. However, I don't understand how to leverage this to allow the memcmp check to pass, completing the challenge. Some of the things I have discovered/attempted:

  • Setting password to be the equivalent of correct_hash doesn't work, as password gets hashed using MD5() (setting the two to be equal is impossible anyway, as scanf will insert precisely one unique ASCII NUL character into the 30 spaces available to it, meaning the two arrays can never be equivalent. NUL characters additionally (to my knowledge) cannot be inserted in the middle of a scanf string).

  • Overwriting the maximum number of bytes with scanf (which will always append a NUL character) means the last 3 bytes of correct_hash will always be 0x00 0x91 0xeb. Attempting to randomly generate a 16-character password that then hashes to something with these last 3 bytes/characters (reasonably computationally easy, given the use of MD5) doesn't work, however, due to the use of strlen(password) (which will give a value of 29 instead of something convenient like 16 thanks to only finishing the length count upon hitting a NUL character) in the call to MD5(). This means that instead of hashing the 16-character password to produce the expected output, the call to MD5() will hash 16 characters from password followed by 13 characters from correct_hash, producing a different final hashed value.

    • To get around this problem, I believe one would have to find a 29-character string (call it S) where the first 16 characters of S hash to a string R comprised of the last 13 characters of S, followed by 0x00 0x91 0xeb. I'm not sure how viable finding this through random MD5 hash computation is, but it don't fancy my chances.

Some notes (mentioned in the explanations above):

  • scanf is limited to a 29 character string, but will append an ASCII NUL character, allowing 30 characters total (16 from the password array, 14 from the correct_hash array) to be overwritten.

  • ASCII NUL characters cannot be input via scanf so the strlen(password) in the call to MD5() (if the maximum password length is used) will be 29.

So the question here is, how else could I go about doing this? Am I missing something extremely obvious? Is random generation a viable solution, or even the only solution?

The solution has to use a buffer overflow (otherwise I imagine I could do something like preload a memcmp that always returns 0), and has to be executable as a shell script (if that's of any relevance).

chqrlie
  • 131,814
  • 10
  • 121
  • 189
Murray
  • 315
  • 5
  • 21
  • 3
    The crux of the matter is, that scanf will happily accept a zero-byte as part of a string and will _not_ treat it as whitespace (thus, will not stop reading further bytes into the string). With this information, your task should be easy. – Ctx Feb 26 '16 at 18:41
  • 1
    `char password[16]; ... scanf("%29s", password);` is not exploiting a vulnerablity. It's deliberately creating one. – Weather Vane Feb 26 '16 at 18:42
  • 1
    @Ctx: that's true but entering `NUL` bytes from the terminal is not easy. – chqrlie Feb 26 '16 at 18:46
  • 1
    @chqrlie Seriously? Redirect a file or use `echo -ne "\x00" | program` or the like – Ctx Feb 26 '16 at 18:48
  • @Ctx Interesting - I couldn't get it do so, but I was providing input via bash, using some backtick-quoted perl code. In this instance it seemed to completely ignore any null characters I had inserted. – Murray Feb 26 '16 at 18:49
  • @WeatherVane The purpose of this exercise was to learn about these types of vulnerabilities (presumably without it being too easy, as a straight `scanf` with no limitations would have been), to better prevent them or spot them in the wild. I'll admit, it looks a little strange. A potential premise here: modified code, where the password array was shortened from 30 to 16 characters? – Murray Feb 26 '16 at 18:53
  • @B.Martin it is a pity you cannot use a `*` followed by a size argument as you can in `printf()`. In `scanf()` it has a different meaning. I suggest more robust code would use `fgets` with a `sizeof` argument that would follow any change to `password[]` length. – Weather Vane Feb 26 '16 at 18:57
  • @B.Martin Redirecting an input file is probably the preferred way here. Did it work now? – Ctx Feb 26 '16 at 19:10
  • 3
    @Ctx Yes, that seems to have done it! Thanks a lot. The final command I used was: `echo -ne "\x49\x5a\x4e\x52\x48\x49\x41\x56\x5a\x43\x54\x52\x51\x4c\x43\x00\x81\xae\xf3\xdf\xa2\x45\xb1\x57\x19\xb3\xa9\xb8\x7d\x00\x91\xeb" | ./vulnerable` where the first set of 16 bytes (up to and including the first `\x00`) were a randomly generated string, which, when hashed produced the second set of 16 bytes, with the required `\x00\x91\xeb` ending. The last 3 bytes there weren't copied anyway, but I left them in to show the string and hash. – Murray Feb 26 '16 at 19:43

1 Answers1

6

Just to merge the comments into an answer here:

The crux of the matter is, that scanf will happily accept a zero-byte as part of a string and will not treat it as whitespace (thus, will not stop reading further bytes into the string).

Redirect a file or use echo -ne "\x00" | program or the like. Redirecting an input file is probably the preferred way here.

The final command I used was: echo -ne "\x49\x5a\x4e\x52\x48\x49\x41\x56\x5a\x43\x54\x52\x51\x4c\x4‌​3\x00\x81\xae\xf3\xd‌​f\xa2\x45\xb1\x57\x1‌​9\xb3\xa9\xb8\x7d\x0‌​0\x91\xeb" | ./vulnerable where the first set of 16 bytes (up to and including the first \x00) were a randomly generated string, which, when hashed produced the second set of 16 bytes, with the required \x00\x91\xeb ending. The last 3 bytes there weren't copied anyway, but I left them in to show the string and hash.

Murray
  • 315
  • 5
  • 21