Description:
Learn how to interact with a process and solve the quiz.
nc ctf.k3rn3l4rmy.com 2200Attachments:
Exploration
This challenge gives us a binary and a template file that demonstrates everything you need to know about the pwntools library to solve a basic pwn challenge.
As with every pwn challenge, we check the obvious things first. We first need to know what type of file we're dealing with, so we run the file command.

It's a 64-bit ELF that's not stripped, meaning that we can read the original function names. Now let's make it executable and run it to find out what we have to do.
$ chmod +x badseed
$ ./badseed
how heavy is an asian elephant on the moon?
idk
wrong bye bye
It looks like a quiz, we already knew that from the challenge description. But now we have to get the answers. With the strings command, we can easily check if there are any hardcoded answers in the binary. We don't find any answers, but we do get other interesting strings!
$ strings badseed
...
how heavy is an asian elephant on the moon?
great 2nd question:
give me the rand() value
wrong bye bye
great 3rd question:
no hint this time... you can do it?!
great heres your shell
...
We now know the questions and the fact that it gives us a shell when we get all three questions right.
As I said earlier, try the obvious first. Run strace and
ltrace to see if there are any interesting system or
library calls. I was not able to get something out of that, so now we
have to disassemble the binary.
Disassembly
I'm going to use Ghidra for this, since it's free and it's able to convert the disassembled code into much more readable C code.
First, import the binary into Ghidra and press all the analyze buttons.
In the Symbol Tree on the left, click on the main function, because this is the starting point of the program.

On the right side, you can see that Ghidra tried to reconstruct the C code for us (pseudo code). This is much easier to read and you don't need to look at any assembly code to solve this challenge.

Here you can see that the main function loads 3 functions to load the
questions, and at the end we have a gz() function that will
give us a shell.
Click on the question_one function to see its code.

At first, this looks really cryptic, but if you look closely, there's
an if-statement that compares local_18 to
local_24 and one of them is the input.
We see that local_18 is last set to
(int)(local_1c / local_20), which means
4000 / 6.035077 if we replace those variables with their
values. If we put this into a calculator, we get
662.791874901. Notice the (int) before the
division means that we have to convert that number to an integer, so
it's simply 662. It outputs wrong if that number is not
equal to our input, so 662 must be the correct answer!
$ ./badseed
how heavy is an asian elephant on the moon?
662
great 2nd question:
give me the rand() value
We get another question. This one asks for the rand()
value that the binary generated. Again, we read the pseudo code for this
question.

This one seems to be harder, but if you stare at it for a while, you
notice the srand function (line 15).
This function sets the starting point for pseudo random number
generation (PRNG). This is interesting, because the first argument is
supposed to be the seed. You don't want the seed to be a constant value,
otherwise it's going to produce the same numbers every time you run the
program. And as you see, the argument is local_18 which is
set to a time() function on line 13.
If you don't know what time() does, it simply gets the current unix time. This number is set as the seed. Yes, it does change everytime you run it, but that doesn't mean you should use it in a program like this.
The problem with time is that we also have easy access to that function, so we can also set that time as our starting point (seed) as soon as we get the second question and it should be the same seed as in the binary!
Writing a script
We need to send the first answer to the program, then somehow generate a number with the correct seed.
from pwn import *
context.log_level = 'debug'
p = process('./badseed')
p.recvuntil(b"?") # recv until our input
answer1 = b"662"
p.sendline(answer1) # send input
# here comes our pseudo random number generation partIn the template we got from the description, we were told that you can use the time function from libc using pwntools, but why not write your own C program that does exactly what the binary does?
Head back to the pseudo code of the second question, and just straight up copy it into a guess.c file.
/*guess.c*/
// add headers
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main() {
long in_FS_OFFSET;
int local_24;
int local_20;
int local_1c;
time_t local_18;
long local_10;
//local_10 = *(long *)(in_FS_OFFSET + 0x28); we don't need this
local_18 = time((time_t *)0x0);
// __isoc99_scanf(&DAT_00402034,&local_24); we don't need this either
srand((unsigned int)local_18); // change "uint" to "unsigned int"
local_20 = rand();
local_1c = rand();
printf("%d", local_1c); // this should be the answer
return 0;
}
Compile the program with gcc:
$ gcc guess.c -o guess
When running it, we get a random number that was seeded with the unix time!
We only have to run that once we reach the second question. To achieve this, I'll use the subprocess library.
Back to your python script. The following lines of code will run the guess binary and get the number it produced:
from subprocess import *
guess = check_output(["./guess"])
print("sending", int(guess))
p.sendline(guess)
p.recvline()If we run it ...
$ python3 script.py [+] Starting local process './badseed' argv=[b'./badseed'] : pid 247 [DEBUG] Received 0x2c bytes: b'how heavy is an asian elephant on the moon?\n' [DEBUG] Sent 0x4 bytes: b'662\n' sending 1794842070 [DEBUG] Sent 0xb bytes: b'1794842070\n' [+] Receiving all data: Done (104B) [DEBUG] Received 0x67 bytes: b'\n' b'great 2nd question:\n' b'give me the rand() value\n' b'great 3rd question:\n' b'no hint this time... you can do it?!\n'
Our shell is getting closer!
We don't get a prompt for the last question, so let's look in Ghidra:

This piece of code does a bunch of operations and uses the same random seed method! That means that we know what those random values are going to be at runtime. We now need to generate the number again and do the same operations as the binary does.
Like in the second question, nothing stops us from copying this code and running it on runtime using subprocess. Same thing, but different code!
/* guess2.c */
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main() {
/*copied the interesting part from Ghidra*/
long in_FS_OFFSET;
int local_2c;
unsigned int local_28; // change "uint" to "unsigned int"
int local_24;
int local_20;
int local_1c;
time_t local_18;
long local_10;
//local_10 = *(long*)(in_FS_OFFSET + 0x28); not important
local_18 = time((time_t*)0x0) + 3;
srand((unsigned int)local_18); // change "uint" to "unsigned int"
local_28 = rand();
srand(local_28);
local_24 = rand();
local_20 = (int)local_28 / local_24;
local_1c = local_20 % 1000;
printf("%d", local_1c);
return 0;
}Compile this with:
$ gcc guess2.c -o guess2
Add this last piece of code to your python script:
guess2 = check_output(["./guess2"])
print("sending", int(guess2))
p.sendline(guess2)
p.interactive() # got a shell!$ python3 script.py [+] Starting local process './badseed' argv=[b'./badseed'] : pid 300 [DEBUG] Received 0x2c bytes: b'how heavy is an asian elephant on the moon?\n' [DEBUG] Sent 0x4 bytes: b'662\n' sending 1342572816 [DEBUG] Sent 0xb bytes: b'1342572816\n' sending 0 [DEBUG] Sent 0x2 bytes: b'0\n' [*] Switching to interactive mode [DEBUG] Received 0x7e bytes: b'\n' b'great 2nd question:\n' b'give me the rand() value\n' b'great 3rd question:\n' b'no hint this time... you can do it?!\n' b'great heres your shell\n' great 2nd question: give me the rand() value great 3rd question: no hint this time... you can do it?! great heres your shell
We win!
but it's a local shell :/
Making it work remotely
If we want a remote shell we can do a slight modification to our script:
#p = process('./badseed')
p = remote("ctf.k3rn3l4rmy.com", 2200)This should now connect to the remote server.
Unfortunately the service is not up at the moment, but if we were to connect to it, the last guess would not be correct because the times were a bit out of sync. That means I had to modify the seed until it worked.
Since Unix time is just a number, we can add and subtract to it to change the seed and hopefully get it synced with the server. It turned out I had to add 3 to the seed to make it work, so in my guess2.c I had:
//local_18 = time((time_t*)0x0);
local_18 = time((time_t*)0x0) + 3;I ran it again and got a shell!
$ cat flag.txt
flag{i_0_w1th_pwn70ols_i5_3a5y}
Final python script:
from pwn import *
context.log_level = 'debug'
p = remote("ctf.k3rn3l4rmy.com", 2200)
p.recvuntil(b"?") # recv until our input
answer1 = b"662"
p.sendline(answer1) # send input
# here goes our random number generator
from subprocess import *
guess = check_output(["./guess"])
print("sending", int(guess))
p.sendline(guess)
p.recvline()
guess2 = check_output(["./guess2"])
print("sending", int(guess2))
p.sendline(guess2)
p.interactive() # got a shell!guess.c
// guess.c
//
// add headers
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main() {
long in_FS_OFFSET;
int local_24;
int local_20;
int local_1c;
time_t local_18;
long local_10;
local_18 = time((time_t*)0x0);
srand((unsigned int)local_18);
local_20 = rand();
local_1c = rand();
printf("%d", local_1c);
return 0;
}guess2.c
/* guess2.c */
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main() {
long in_FS_OFFSET;
int local_2c;
unsigned int local_28;
int local_24;
int local_20;
int local_1c;
time_t local_18;
long local_10;
local_18 = time((time_t*)0x0) + 3;
srand((unsigned int)local_18);
local_28 = rand();
srand(local_28);
local_24 = rand();
local_20 = (int)local_28 / local_24;
local_1c = local_20 % 1000;
printf("%d", local_1c);
return 0;
}