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
passwordto be the equivalent ofcorrect_hashdoesn't work, aspasswordgets hashed usingMD5()(setting the two to be equal is impossible anyway, asscanfwill insert precisely one unique ASCIINULcharacter into the 30 spaces available to it, meaning the two arrays can never be equivalent.NULcharacters additionally (to my knowledge) cannot be inserted in the middle of ascanfstring).Overwriting the maximum number of bytes with
scanf(which will always append aNULcharacter) means the last 3 bytes ofcorrect_hashwill always be0x00 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 ofstrlen(password)(which will give a value of 29 instead of something convenient like 16 thanks to only finishing the length count upon hitting aNULcharacter) in the call toMD5(). This means that instead of hashing the 16-character password to produce the expected output, the call toMD5()will hash 16 characters frompasswordfollowed by 13 characters fromcorrect_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.
- 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
Some notes (mentioned in the explanations above):
scanfis limited to a 29 character string, but will append an ASCIINULcharacter, allowing 30 characters total (16 from thepasswordarray, 14 from thecorrect_hasharray) to be overwritten.ASCII
NULcharacters cannot be input viascanfso thestrlen(password)in the call toMD5()(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).
Just to merge the comments into an answer here:
The crux of the matter is, that
scanfwill 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" | programor 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\x43\x00\x81\xae\xf3\xdf\xa2\x45\xb1\x57\x19\xb3\xa9\xb8\x7d\x00\x91\xeb" | ./vulnerablewhere 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\xebending. The last 3 bytes there weren't copied anyway, but I left them in to show the string and hash.