2

I had a network security class in my university. And there is a challenge for finding a secret number. Here is the code

#include <stdlib.h>
#include <time.h>
#include <stdio.h>

void init() {
    setbuf(stdin, NULL);
    setbuf(stdout, NULL);
}

int main() {
    init();
    srand(time(0));
    int secret  = 0;
    puts("Your secret: ");
    scanf("%d", &secret);
    if(secret == rand()) {
        system("/bin/sh");
    } else {
        puts("failed");
    }
}

I actually could not understand my professor's explanation. Anyone can explain the meaning of this code, and how can i find the secret number?

Rohan Bari
  • 7,482
  • 3
  • 14
  • 34
  • 1
    There is nothing secret about `srand`/`rand`. You can write an analogues program that just tells you the result of `rand`. If you run the two programs quickly enough in succession (so that `time(0)` is the same value in each) you know the "secret" easily. – user17732522 Nov 29 '22 at 05:37
  • Even without running quickly enough, one could modify the output of `time(0)` to the previous or next seconds. – Sebastian Nov 29 '22 at 06:33
  • ty guys, i now know the theory of srand and rand, but I try many ways to run two programs, but it does not work T T. Can you explain more how to do this? – Peter Harry Nov 29 '22 at 06:42
  • Create a program, which only outputs time. Then try to guess it's output from a second program doing the same. If that works, you can switch to srand. – Sebastian Nov 29 '22 at 07:06
  • 2
    This is a C-programming question. Tag altered. – Rohan Bari Nov 29 '22 at 07:16

2 Answers2

3

Technically you shouldn't be able to find the "secret" number - that's the whole point of the exercise: the "secret number" is generated using a strong PRNG seeded with a more-or-less random set of bits coming from the system clock.

Theoretically, if you have complete knowledge about the system - i.e. you both know the specific PRNG implementation used and the exact value used to seed it (the result of the time() call) - you should be able to guess the number simply by seeding another instance of the same PRNG and getting the next random int from it.

In this specific case, as the attacker you have complete control of the execution of the code and you should be able to predict the value returned from time() with very good accuracy (it is only a second resolution), so if you can build a program on the same system, that takes the predicted time value, seeds it to the same srand() implementation and returns the first random int - you can guess the "secret number".

So maybe the point of the exercise is to show that PRNG security trivially depends on knowing to start when no one is looking - otherwise you aren't secure, and possibly to not use time() to seed srand() and instead rely on something actually robust, like /dev/urandom .

Guss
  • 30,470
  • 17
  • 104
  • 128
  • 1
    Calling `rand()` a "strong prng` is a little misleading. On some platforms it might have a good quality implementation but on others it's awful. Either way it shouldn't be used for cryptographic tasks and a secure library like openssl should be used instead (which is presumably the point of this exercise) – Alan Birtles Nov 29 '22 at 06:41
  • See https://stackoverflow.com/questions/52869166/why-is-the-use-of-rand-considered-bad – Alan Birtles Nov 29 '22 at 06:45
  • @AlanBirtles, not disagreeing here, but OpenSSL obviously uses a PRNG, and while it has some desired features (specifically for multi-threaded cryptographic applications - which this OP isn't) I doubt you can say that for a simple use case such as this it is definitely better than - for example - LLVM's C library's `rand()` - assuming a good seed is provided (hence my `/dev/urandom` comment) – Guss Nov 29 '22 at 06:55
  • @Guss LLVM's libc implementation for `rand` has `RAND_MAX == 32767` (see https://github.com/llvm/llvm-project/blob/main/libc/src/stdlib/rand.cpp#L17). In that case probably seeding and algorithm don't even matter since repeatedly trying in a loop will be successful in reasonable time. It also is an LCG, for which it is possible to predict the rest of the sequence from a few sequential values (though granted there is only one value in OP's code). – user17732522 Nov 29 '22 at 07:12
  • Ok, I chose that implementation without checking, under the assumption that a reputable project such as LLVM will have a good implementation. I stand corrected - this is horrible, especially the "we copied the example from the standard" comment. While I still contend that you don't need OpenSSL for "good enough" random numbers, I'll remember not to rely on "famous project must have good code" logic in the future. – Guss Nov 29 '22 at 07:18
  • @Guss Yes, that is a particular terrible implementation, but the problem is that even widely used implementations may not be good enough. [Musl uses a LCG](https://git.musl-libc.org/cgit/musl/tree/src/prng/rand.c) as I think many smaller C libs do. And I think glibc also only uses a LFSR that can still be broken without significant computational effort from a small sequence of consecutive values, although the not as trivially as for a LCG. – user17732522 Nov 29 '22 at 08:12
  • Perhaps the C/C++ implementations do not want to change the algorithm to be deterministic between versions? – Sebastian Nov 29 '22 at 09:32
  • @user17732522 `RAND_MAX == 32767` is irrelevant to the number of bits in the PRNG engine. Also the size of `unsigned` as used in `srand(unsigned s)` is irrelevant to the number of bits in the PRNG engine. The quality of randomness of `rand()` is not limited by the seed and `RAND_MAX`. The _repeatability_ is limited to `unsigned` width. – chux - Reinstate Monica Nov 29 '22 at 10:18
  • @chux-ReinstateMonica Sure, but just trying random choices has a 1/RAND_MAX chance to be correct because the program in the question just uses one value from `rand()`. So a few ten thousand random trials will typically be enough to pass the test in the question. – user17732522 Nov 29 '22 at 10:57
1

Pseudo random number generators rely on a seed to generate their random numbers: if you use the same seed, you'll get the same sequence of "random" numbers.

See here:

Consider this code:

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    srand(0);

    printf("random number 1: %d\n", rand());
    printf("random number 2: %d\n", rand());
    printf("random number 3: %d\n", rand());
    printf("random number 4: %d\n", rand());

    return 0;
}

Running the program (a.out) multiple times always generates the same numbers:

marco@Marcos-MacBook-Pro-16 Desktop % ./a.out
random number 1: 520932930
random number 2: 28925691
random number 3: 822784415
random number 4: 890459872
marco@Marcos-MacBook-Pro-16 Desktop % ./a.out
random number 1: 520932930
random number 2: 28925691
random number 3: 822784415
random number 4: 890459872
marco@Marcos-MacBook-Pro-16 Desktop % ./a.out
random number 1: 520932930
random number 2: 28925691
random number 3: 822784415
random number 4: 890459872
marco@Marcos-MacBook-Pro-16 Desktop % ./a.out
random number 1: 520932930
random number 2: 28925691
random number 3: 822784415
random number 4: 890459872

Of course the exact numbers will differ on the exact implementation used to generate the numbers. So the sequence of numbers probably will be different on your system. Nevertheless, using the same seed will always result in the same sequence.

Real systems use a combination of "real" (based on physical randomness) random numbers + a pseudo random number generator simply because its way more efficient to generate random numbers that way.

These are usually cryptographically secure pseudorandom number generators because the numbers are used for cryptographic operations (cryptography heavily relies on randomness to work).

The basic idea is as long as the initial seed is "secret" (unknown) you can't work it back and determine the pre-defined sequence of numbers generated.

It is possible (and has been done) to work back the initial seed simply by looking at the numbers generated by a pseudorandom number generator.

Now on how to solve the exercise given by your professor:

The easiest way would be to "freeze" time to have a fixed seed value for the random numbers (as shown in my code example above).

Since there isn't an easy way to do that, you can print the current seed by running another program to just output the first random number generated (since that's the "secret"):

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main(void) {
    srand(time(0));

    printf("the secret number is %d\n", rand());

    return 0;
}

You can then use that number to "unlock" the program given by your professor.

However you have to do that within a second or less, since time() returns a new value every second.

The more reliable way would be to have your program input the "random" number as soon as you generated it.

Here's an example code of how you could do that:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>

// where to put the "random" number on disk
const char *tmp_file = "/tmp/input";
// where the executable of your professor is
const char *executable = "/path/to/your/professors/executable";

void writeRandomNumberToDisk(const char *path, int number) {
    char buf[128];

    // convert int to string
    memset(buf, 0, sizeof(buf));
    snprintf(buf, sizeof(buf), "%d\n", number);

    FILE *fp = fopen(path, "w+");
    fwrite(buf, strlen(buf), 1, fp);
    fclose(fp);
}

int main(void) {
    srand(time(0));

    int secret = rand();

    printf("the secret number is %d\n", secret);

    writeRandomNumberToDisk(tmp_file, secret);

    char buf[512];

    memset(buf, 0, sizeof(buf));
    snprintf(buf, sizeof(buf), "/bin/sh -c 'cat %s | %s'", tmp_file, executable);

    printf("Now executing %s\n", buf);
    system(buf);

    return 0;
}

Essentially, it writes the first "random" number to disk, then invokes the shell that will feed the "random" number into the program.

You can also bypass the file system entirely by using something like:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>

// where the executable of your professor is
const char *executable = "/path/to/your/professors/executable";

int main(void) {
    srand(time(0));

    int secret = rand();

    printf("the secret number is %d\n", secret);

    char buf[512];

    memset(buf, 0, sizeof(buf));
    snprintf(buf, sizeof(buf), "/bin/sh -c 'printf \"%d\\n\" | %s'", secret, executable);

    printf("Now executing %s\n", buf);
    system(buf);

    return 0;
}
Marco
  • 7,007
  • 2
  • 19
  • 49
  • ty! and ty all of u guys! i now completely understand the mechanism of srand/rand. And also for this (easy...) question and already solved it. This helps a lot to me – Peter Harry Nov 29 '22 at 09:32