Rank: 27 Team: ICEDTEA I participated in this cute CTF with ICEDTEA as my last CTF before chinese new year, and I almost AK all Crypto Challenges … (Except BabyRNG) After all, I really love these cute challenges and they can also be a good piece of lecture to my junior teammates in ICEDTEA!
This is a RNG with an initial state “Mvm” with every 3*3 matrix input (splitted from the flag with 3 chars a set). It’s trivial that the state is always predictable in the 9 operations rng.get_random_num()(exponential), and the output value is actually a linear sum with the input matrix and old state*new state as paired parameters. With 9 output values, the problem would become an easy linear equation (because there’re also 9 unknown variables). exp.sage
from Crypto.Util.number import long_to_bytes samples=[6192533, 82371, 86024, 4218430, 12259879, 16442850, 6736271, 7418630, 15483781] p=next_prime(2**24) F=GF(p) gen=F(2) state = vector(F, map(ord, "Mvm")) params=[] defgen_params(): for i inrange(9): old_state=[_ for _ in state] cur=[] for j inrange(len(state)): state[j]=gen**state[j] for j inrange(3): for k inrange(3): cur.append(state[j]*old_state[k]) params.append(cur)
gen_params() M=Matrix(F, params) flag='' for i in M.solve_right(samples): flag+=long_to_bytes(int(i)).decode()
print(flag)
sourceless-crypto
A black box crypto challenge… First tested with ‘aaaaaa…’ and found out the key value is in some order(like increasing but not), also the max input size is 197 chars (256-len(flag)), so the key may be a permutation of ASCII chars. Later tested with ‘abcdef…’ and obtained something like this:
1 2 3 4 5 6 7
Welcome to sourceless-crypto, enjoy the pain 1 -> Show Flag 2 -> Encrypt Plaintext 3 -> exit Operation: 2 Enter plaintext: abcdefghijklmnopqrstuvwxyz Encrypted plaintext: b'KOOKK77;;??;;77++//++77;;?'
Cool, some repeated pairs of charaters appeared many times, maybe about bitwise operation…XOR?! (because of the LSB, and I guessed the key is in some kind of order) To prove my concept, directly connected to the server two times with diffrent inputs, the xor value between the ciphertext and plaintext are all the same, tada~ Last, just observed that the key permutation is a change of order from 0123 to 0132 (same for other sets of numbers). exp.py
1 2 3 4 5 6 7 8
from pwn import xor dataset='1032547698badcfe' key=b'' for i in dataset: for j in dataset: key+=bytes.fromhex(i+j)
print(xor(enc, key))
This challenge should be renamed to GUESSY-crypto Btw, I love this sense of humor!
curved-mvm
A signer based on fastecdsa. Player have two operation chances and the operations are:
Obtain a signed message.
Sign a specify message back, check the value and get the flag if correct.
import os from json import dumps from secrets import randbits from Crypto.Util.number import bytes_to_long from hashlib import sha1
FLAG = os.getenv("FLAG", "MVM{f4ke_fl4g}")
# a wonderful curve p = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF a = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC b = 0x5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B Gx = 0x6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296 Gy = 0x4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5
F = GF(p) EC = EllipticCurve(F, [a, b]) n = EC.order()
SECRET_KEY = bytes_to_long(os.urandom(69420)) % n G = EC([Gx, Gy]) assert G in EC
defsign_msg(msg: str): z = bytes_to_long(sha1(msg.encode()).digest()) % n k = (randbits(K_SIZE) + 2) % n R = k * G r = ZZ(R.x()) % n s = (k.inverse_mod(n) * (z + r * SECRET_KEY)) % n return {"r": hex(r), "s": hex(s)}
defsign_bogus(): return sign_msg(SAMPLE_MSG)
defverify_signature(r, s, msg): z = bytes_to_long(sha1(msg.encode()).digest()) % n
if r < 1or r >= n or s < 1or s >= n: return {"error": "funny user uwu"}
w = s.inverse_mod(n)
u1 = (z * w) % n u2 = (r * w) % n
P = u1 * G + u2 * Q
should_r = ZZ(P.x()) % n
if should_r == r: return {"flag": FLAG} else: # user funny return {"error": "invalid signature"}
defmvm(): r = prompt_integer("r") s = prompt_integer("s") try: return verify_signature(r, s, REQUIRED_MSG) except: return {"error": "funny user"}
try: recv = input(prompt) except Exception: print("user too funny, complaints will be ignored\n")
if recv notin operations: print("funny operation\n") return prompt_operation()
return operations[recv]
defprompt_integer(name: str): prompt = f"{name}: " try: recv = input(prompt) except: print("user too funny, complaints will be sent to /dev/null\n") return prompt_integer(name)
try: number = int(recv, 16) except: print("user supplied number too funny, complaints will be ignored\n") return prompt_integer(name)
if number <= 1: print("user supplied number funny.\n") return prompt_integer(name)
return ZZ(number)
funny_credits = FUNNY_CREDITS_FOR_FREE_TRIAL
if __name__ == "__main__": print(f"Welcome to {CHALL_NAME!r}, enjoy the pain!\n")
if funny_credits == 0: print("ran out of funny credits, bye") exit()
print()
Take a close look at function sign_msg
1 2 3 4 5 6 7
defsign_msg(msg: str): z = bytes_to_long(sha1(msg.encode()).digest()) % n k = (randbits(K_SIZE) + 2) % n R = k * G r = ZZ(R.x()) % n s = (k.inverse_mod(n) * (z + r * SECRET_KEY)) % n return {"r": hex(r), "s": hex(s)}
We would get the value of R, since the value of k is too small, it’s feasible to bruteforce/discrete_log on R and obtain the value of k. After that, the value of k, z, r, n are all known so the value of SECRET_KEY would be leaked, just simply sign a message back and get flag! exp.sage
import os from json import dumps from secrets import randbits from Crypto.Util.number import * from hashlib import sha1
p = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF a = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC b = 0x5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B Gx = 0x6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296 Gy = 0x4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5
K_SIZE = 18 SECRET_KEY = 0x7ea F = GF(p) EC = EllipticCurve(F, [a, b]) n = EC.order()
deffind_secret(r, s): z = bytes_to_long(sha1("hardcoded cuz reasons".encode()).digest()) % n R = x2point(r) randkey = discrete_log(x2point(r), G, G.order(), operation='+') secret_key = (randkey*s-z)*inverse(r, n)%n return secret_key
defsign_msg(msg: str): z = bytes_to_long(sha1(msg.encode()).digest()) % n k = (randbits(K_SIZE) + 2) % n R = k * G r = ZZ(R[0]) % n s = (k.inverse_mod(n) * (z + r * SECRET_KEY)) % n return {"r": hex(r), "s": hex(s)}
r = int(input("r >> "), 16) s = int(input("s >> "), 16) SECRET_KEY=find_secret(r, s) print(sign_msg("mvm mvm mvm"))
With connection by hand.
fastcrypto
An RSA CRT service which it’s multiplication is based on NTT Algorithm. chall.py
from Crypto.Util.number import getPrime, bytes_to_long as btl, inverse from secrets import randbits from nttmul import conv from SECRET import FLAG import signal
TIMEOUT = 30
B = 7639# chosen by fair dice roll
deffromPoly(arr): num = 0 for idx inreversed(range(len(arr))): num *= B num += arr[idx] return num
deftoPoly(num): arr = [] while num != 0: num, tmp = divmod(num, B) arr.append(tmp)
defmodpow(base: int, ex: int, mod: int) -> int: ret = 1 while ex: if ex & 1 == 1: ret = multiply(ret, base) ret %= mod base = multiply(base, base) base %= mod ex >>= 1 return ret
if __name__ == '__main__': p = getPrime(512) q = getPrime(512) N = p * q e = 0x10001 FLAG = btl(FLAG) signal.alarm(TIMEOUT) print('### FASTCRYPTO ###') print(f'{N = }') whileTrue: print('1. Get flag\n2. Encrypt data\n3. Exit') choice = input('choice: ') assert choice == '1'or choice == '2'or choice == '3' if choice == '1': data = randbits(1024) enc = encrypt(p, q, e, data) print(f'{enc = }') decryptedData = int(input('pt: ')) assert data == decryptedData
enc = encrypt(p, q, e, FLAG) print(f'{enc = }') elif choice == '2': data = randbits(1024) print(f'{data = }') enc = encrypt(p, q, e, data) print(f'{enc = }') else: exit()
Is suspecious of chosing a B value 7639, an uncommon prime which is quite too small and would have some bias while encountering higher coefficients. So it’s probably feasible to do a RSA Fault Attack if the output value is wrong! The exploit tells everything ⬇️. exp.py
I would call it a base for every permutation group (P * C._state)**k, and there’re only 1185 bases. After some trials, the order of those bases are quite small, so it’s DLP can be solved with discrete_log function …
Next is the signer:
1 2 3 4 5 6 7 8 9 10 11 12
defsign(msg, d): k = n r_hash = n
while gcd(k, n) != 1or gcd(r_hash, n) != 1or (k % 1260) in forbidden: k = randint(2, n - 1) r = spicy_rubik_pow(k) r_hash = hash_to_int(str(r)) s = pow(k, -1, n) * (hash_to_int(msg) + r_hash*d) % n
return r, s
After solving the value of k from discrete_log of r (brute force all bases and find the right one), the rest values like hash_to_int(msg), r_hash are trivial so that the value of d can be recovered (which is the secret key).
Finally is the encryption with those keys:
1 2 3 4 5
for i inrange(0, len(flag), 4): secret = vector([flag[i + j] for j inrange(4)]) ds = vector(privkeys[i:i+4])
out.write(str(secret * ds) + "\n")
It’s a linear operation with the size of keys are much more larger than ASCII chars, which can simply calculated with LLL after obtained the keys. Btw, 4 chars are actually able to brute-force
defhash_to_int(m): returnint(sha256(m.encode()).hexdigest(), 16) % n
defgen_all_states(): cur_id=0 for i inrange(1260): if (i notin forbidden): int2id[i]=cur_id cur_id+=1 C = RubiksCube().move(rubik_moves * i) all_states.append(P * C._state)
defparse_data(): for i inrange(32): enc_keys.append({'gp':PermutationGroupElement(data[i*2]), 'sign':int(data[i*2+1])}) for i inrange(4): enc_flags.append(int(data[64+i]))
defcrack_key(id, gp, sign): for i in trange(len(all_states)): try: k=discrete_log(gp, all_states[i]) print(f"[*] mvm_{id} trying {k}") r_hash=hash_to_int(str(gp)) cur_key=((sign*k-hash_to_int(f"mvm_{id}"))*pow(r_hash, -1, n))%n keys.append(cur_key) print(f"[*] mvm_{id} secret cracked!") except Exception as e: print('', end='')
defget_flag(used_keys, out_int): global flags M=[[0]*6for _ inrange(5)] for i inrange(4): M[i][0]=used_keys[i] M[i][i+1]=-1 M[4][0]=out_int M[4][5]=2**1024 M=Matrix(ZZ, M) A=M.LLL() for v in A: if v[0]==0and v[-1]==2**1024: flags+=v[1:-1]
gen_all_states() print('[*] Generated all States') parse_data() print('[*] Data Parsed!') for i inrange(len(enc_keys)): crack_key(i, enc_keys[i]['gp'], enc_keys[i]['sign'])
for i inrange(4): get_flag(keys[i*4:i*4+4], enc_flags[i])
print(bytes(flags))
Web
submission
A web service that user are allowed to uploads .txt files, and they would be moved to /upload folder and:
1
exec("chmod *");
The flag is also inside /upload folder. Actually, it’s dangerous to use a wildcard parameter like * directly inside a command cuz it would probably leads to a wildcard injection vulnerability.
For example, if there’s a file named --help inside a folder and doing find *, a manual would pop out cause the --help file name were seen as a parameter passwd to find command
Next, there’s a --reference=<file name> option in chmod command which would change all files into the same permision as <file name>.
Exploit Step:
upload a file named .txt (be shown as a hidden file in linux file system, won’t be listed in *)
upload a file named --reference=.txt (change all files permission same as .txt, which would be readable)
read flag~
Misc
Playing with the cert…
mvm
Change all M into 0 and V into 1, binary decode and got a piece of brainfuck code, solve them with online decoder.
trophy-plus
With the idea from challenge “mvm”, I found that there’re some tiny MVM aroung the cert, probably the same concept? And the answer is … yep! But tearing my eyes, actually …
trophy-plus64
Another Human-based OCR There’s a REAL Certification on the stamp at the corner of the cert, after base decoded the first few lines a flag like string appeared … (Thanks a lot to my teammate wang for this). After that, I just guessed the correct flag! {mu5t_b3_feo_typ1ng_th1r_by_h4nd_1375105304248361} => {mu5t_b3_fun_typ1ng_th15_by_h4nd_1375105304248361}