Soluções de Desafios Criptográficos para CTF: Do Clássico ao RSA Moderno

Cifra de César

Dado o texto cifrado vxshyk{g9g9g099-hg33-4f60-90gk-ikff1f36953j}, aplica-se uma rotação de caracteres. Aplicando deslocamento reverso, obtém-se a flag.

# Decodificação de César
def caesar_decodificar(texto, deslocamento):
    resultado = []
    for caractere in texto:
        if caractere.isalpha():
            base = ord('a') if caractere.islower() else ord('A')
            resultado.append(chr((ord(caractere) - base - deslocamento) % 26 + base))
        else:
            resultado.append(caractere)
    return ''.join(resultado)

mascara = "vxshyk{g9g9g099-hg33-4f60-90gk-ikff1f36953j}"
print(caesar_decodificar(mascara, 6))
# qsnctf{b9b9b099-cb33-4a60-90bf-dfaa1a36953e}

Base64 e Base32

Strings codificadas em Base64 podem ser reconhecidas pelo preenchimento e alfabeto. Para o exemplo cXNuY3RmezY4NjkwOGJjLTFiZjItNGMxOS04YTAxLWIyYzc3NjAwOTkyOH0=, a decodificação direta retorna a flag.

Para cadeias hexadecimais seguidas de Base32 e Base64, o pipeline é: hexadecimal → Base32 → Base64.

Código Morse

O sinal .----/-----/.-/----./...-- segue o padrão Morse internacional. Cada sequência separada por barra representa um caractere.

Transformações e Esteganografia

Bear / 熊曰

Textos que começam com "熊曰" indicam codificação específica de uma ferramenta chinesa. O processo consiste em colar o conteúdo no decodificador correspondente.

Decodificação por Alfabeto de Sobrenomes

Um texto extenso composto por sobrenomes chineses codifica, na verdade, uma sequência hexadecimal. Cada sobrenome mapeia para um valor numérico que, concatenado, forma bytes hexadecimais. Convertendo para ASCII, revela-se o conteúdo original.

Desafio ASCII e César Combinados

Quando um desafio entrega uma sequência de números decimais, o primeiro passo é converter para caracteres ASCII. Se o resultado parecer um UUID com máscara, testa-se deslocamentos de César. Uma vez identificado o padrão qsnctf{...}, resta fazer força bruta sobre os últimos caracteres e verificar o hash MD5.

import hashlib
import string

alfabeto = string.digits + string.ascii_letters

def forcar_uuid(prefixo, alvo):
    for a in alfabeto:
        for b in alfabeto:
            for c in alfabeto:
                candidato = prefixo + a + b + c
                digest = hashlib.md5(candidato.encode()).hexdigest()
                if digest[:36] == alvo:
                    return candidato
    return None

prefixo_uuid = "0ef1ea8c-2e72-4b9f-87e1-08d53a80c"
hash_alvo = "8a2a057c9bb3a3463e7583663341e9e5"
print(forcar_uuid(prefixo_uuid, hash_alvo))
# 0ef1ea8c-2e72-4b9f-87e1-08d53a80c743

RSA — Níveis Iniciais

Baby RSA com Soma e Produto

Conhecendo s = p + q e t = (p+1)(q+1), calcula-se n = t - s - 1. Com e e d, aplica-se a exponenciação modular para obter o plaintext.

from Crypto.Util.number import long_to_bytes

def resolver_baby_rsa(s, t, e, d, cifra):
    modulo = t - s - 1
    return long_to_bytes(pow(cifra, d, modulo))

s = 0x1232fecb92adead91613e7d9ae5e36fe6bb765317d6ed38ad890b4073539a6231a6620584cea5730b5af83a3e80cf30141282c97be4400e33307573af6b25e2ea
t = 0x5248becef1d925d45705a7302700d6a0ffe5877fddf9451a9c1181c4d82365806085fd86fbaab08b6fc66a967b2566d743c626547203b34ea3fdb1bc06dd3bb765fd8b919e3bd2cb15bc175c9498f9d9a0e216c2dde64d81255fa4c05a1ee619fc1fc505285a239e7bc655ec6605d9693078b800ee80931a7a0c84f33c851740
e = 0xe6b1bee47bd63f615c7d0a43c529d219
d = 0x2dde7fbaed477f6d62838d55b0d0964868cf6efb2c282a5f13e6008ce7317a24cb57aec49ef0d738919f47cdcd9677cd52ac2293ec5938aa198f962678b5cd0da344453f521a69b2ac03647cdd8339f4e38cec452d54e60698833d67f9315c02ddaa4c79ebaa902c605d7bda32ce970541b2d9a17d62b52df813b2fb0c5ab1a5
c = 0x50ae00623211ba6089ddfae21e204ab616f6c9d294e913550af3d66e85d0c0693ed53ed55c46d8cca1d7c2ad44839030df26b70f22a8567171a759b76fe5f07b3c5a6ec89117ed0a36c0950956b9cde880c575737f779143f921d745ac3bb0e379c05d9a3cc6bf0bea8aa91e4d5e752c7eb46b2e023edbc07d24a7c460a34a9a

print(resolver_baby_rsa(s, t, e, d, c))

RSA Básico com Fatoração Direta

Para pequenos primos, a fatoração é direta. Com p e , computa-se φ(n) = (p-1)(q-1), inverte-se e e descriptografa-se.

import gmpy2
from Crypto.Util.number import long_to_bytes

p = 262248800182277040650192055439906580479
q = 262854994239322828547925595487519915551
e = 65533
c = 27565231154623519221597938803435789010285480123476977081867877272451638645710

phi = (p - 1) * (q - 1)
n = p * q
d = gmpy2.invert(e, phi)
m = pow(c, d, n)
print(long_to_bytes(m))

Desafios RSA Avançados

RSA com Fatoração por Pequenos Fatores

Quando n é produto de vários primos pequenos, utiliza-se a função totiente de Euler para obter φ(n) sem fatoração tradicional.

import gmpy2
import libnum

n = 157077292656328898849823499976497003976795705913326943955927601882559735301000546878663484930436631929909115065166613744548816622146802007640124796249330573411377703969505934904150600987843325674764620305047603408490558134670867673308099650843329640744997672015466571290660161290811275435569339606335117906571999000341133024698424364682800683662193063661214736762852739324479859236963365531207752799197178993887860855078852702337761399225640575281412171035871278933493943575572155382899938265639764715616686123949482372238288859715465115400317136714757882965887595246507450491169518000205087415380208167764110920711042584766805992237919576823121108078407699912757901788925718859790257450499775129521327827653298451904392241906547672843110356658889638496906522290674659574024024440113632175010053065452660076447040937842478007881589334096496073556056726805396937630799201696246079227214272205462258357482722478243481697053301054600954126539848778226175296162997813416634702496577009409960503948474494741296663849482119365434792563324547643352816519125305335959420429699475765642610737903235960423173
c = 44457399775772165283580795763046604956432217865936749114390645714446263790235445725770165521476841968764175721036280702731933849090719866149354613431301887740671003826556620460836983488011711209908075106260857650574672356032244606425941095128801765463716482316101398637519304864271794460829068714740938719022156283319142938782439784724450045931039355442034325311037568791297455084676548879770834712506552233840348850684727096270392080049993135041218143811167688449496243036317450681348089315258831745988434134987055263393540923865029931594717328162951158311497514418799360413513590684301435386737514918075848373373755748782672860711406169316940293554209702288482064854840802876490202123903888235028119047988176327629542924415737212649237787748145773301112682790682933658516724691338727523894513267588035437093188599375494920656327919129240066252636130803666175859640361767805549884909317548802917210333235914904622641997249853362378711924024129399688535136879208010081166848163897114124726692078532337827810846421365846926064892472698603597461932481745017020417072013702099809833423003201003030492
e = 65537

phi = euler_phi(n)
d = gmpy2.invert(e, phi)
m = pow(c, d, n)
print(libnum.n2s(int(m)))

RSA com Derivadas e Relação Especial

Dado z = 1/(arctan(p))' - 1/(arth(q))', sabe-se que (arctan x)' = 1/(1+x²) e (arth x)' = 1/(1-x²). Logo, z = p² + q². Combinando com n = pq, temos p + q = √(z + 2n) e φ(n) = n - (p+q) + 1.

import gmpy2
from Crypto.Util.number import long_to_bytes

c = 7922547866857761459807491502654216283012776177789511549350672958101810281348402284098310147796549430689253803510994877420135537268549410652654479620858691324110367182025648788407041599943091386227543182157746202947099572389676084392706406084307657000104665696654409155006313203957292885743791715198781974205578654792123191584957665293208390453748369182333152809882312453359706147808198922916762773721726681588977103877454119043744889164529383188077499194932909643918696646876907327364751380953182517883134591810800848971719184808713694342985458103006676013451912221080252735948993692674899399826084848622145815461035
z = 32115748677623209667471622872185275070257924766015020072805267359839059393284316595882933372289732127274076434587519333300142473010344694803885168557548801202495933226215437763329280242113556524498457559562872900811602056944423967403777623306961880757613246328729616643032628964072931272085866928045973799374711846825157781056965164178505232524245809179235607571567174228822561697888645968559343608375331988097157145264357626738141646556353500994924115875748198318036296898604097000938272195903056733565880150540275369239637793975923329598716003350308259321436752579291000355560431542229699759955141152914708362494482
n = 15310745161336895413406690009324766200789179248896951942047235448901612351128459309145825547569298479821101249094161867207686537607047447968708758990950136380924747359052570549594098569970632854351825950729752563502284849263730127586382522703959893392329333760927637353052250274195821469023401443841395096410231843592101426591882573405934188675124326997277775238287928403743324297705151732524641213516306585297722190780088180705070359469719869343939106529204798285957516860774384001892777525916167743272419958572055332232056095979448155082465977781482598371994798871917514767508394730447974770329967681767625495394441
e = 65537

soma_pq = gmpy2.iroot(z + 2 * n, 2)[0]
phi = n - soma_pq + 1
d = gmpy2.invert(e, phi)
print(long_to_bytes(pow(c, d, n)))

Cifras Clássicas e Modernas

Cifra de Hill

Para uma cifra de Hill 2×2, descobre-se a matriz chave a partir de pares de letras conhecidos (como o prefixo utflag) e aplica-se a inversa modular para decifrar.

Cifra de Teclado

Sequências como ooo yyy ii w uuu ee iii ee uuu ooo r yyy yyy e representam teclas pressionadas repetidamente em teclado telefônico. O número de repetições indica a letra dentro da tecla.

def decodificar_teclado(texto):
    mapeamento = [" ", " ", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"]
    resultado = ""
    for parte in texto.split():
        tecla = len(parte)
        indice = len(parte) - 1
        resultado += mapeamento[tecla][indice]
    return resultado

print(decodificar_teclado("ooo yyy ii w uuu ee iii ee uuu ooo r yyy yyy e"))
# youareverygood

Bacon e Códigos Especiais

Na cifra de Bacon, . e - (ou A/B) representam 5 bits que mapeiam para letras. Em alguns desafios, é necessário trocar o papel dos símbolos e aplicar análise de frequência.

AES com Chave e IV Derivos

Quando a chave é gerada por repetição de 2 bytes e o IV é derivado de XOR entre chave e um valor conhecido, pode-se recuperar ambos a partir do output.

from Crypto.Util.number import long_to_bytes, bytes_to_long
from Crypto.Cipher import AES

valor_x = 91144196586662942563895769614300232343026691029427747065707381728622849079757
cifra = b'\x8c-\xcd\xde\xa7\xe9\x7f.b\x8aKs\xf1\xba\xc75\xc4d\x13\x07\xac\xa4&\xd6\x91\xfe\xf3\x14\x10|\xf8p'

dados = long_to_bytes(valor_x)
chave = dados[:16] * 2
iv_numerico = bytes_to_long(chave[16:]) ^ bytes_to_long(dados[16:])
iv = long_to_bytes(iv_numerico)

aes = AES.new(chave, AES.MODE_CBC, iv)
print(aes.decrypt(cifra))

Hashes, Base Variantes e Outras Técnicas

Ataque a Hashes por Dicionário

Quando cada caractere de uma flag é hasheado individualmente com dupla aplicação de MD5, itera-se sobre caracteres imprimíveis até encontrar correspondências.

from hashlib import md5
import string

hashes = ['999a215b1f8372bb0f1c84c467a1506b', '02b0b94ee1fa195ae7959560893f7e3c']  # exemplo
caracteres = string.printable

def decodificar_hash(lista_hash):
    flag = ""
    for h in lista_hash:
        for c in caracteres:
            d1 = md5(c.encode()).hexdigest()
            if md5(d1.encode()).hexdigest() == h:
                flag += c
                break
    return flag

Base64 com Alfabeto Personalizado

Para alfabetos Base64 modificados, cria-se um mapeamento entre o alfabeto padrão e o personalizado, depois decodifica-se normalmente.

import base64

def decodificar_base64_customizado(cifra, alfabeto_personalizado):
    alfabeto_padrao = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="
    tabela = str.maketrans(alfabeto_personalizado, alfabeto_padrao)
    return base64.b64decode(cifra.translate(tabela))

exemplo = "FlZNfnF6Qol6e9w17WwQQoGYBQCgIkGTa9w3IQKw"
alfabeto = "".join([chr(65+i) for i in range(26)])
# decodificar_base64_customizado(exemplo, alfabeto)

Desafios de Fatoração e Chaves Públicas

Para arquivos PEM/DER com chave pública RSA, após fatorar n, utiliza-se rsatool para gerar a chave privada e o OpenSSL para decifrar.

# Gerar chave privada
# python rsatool.py -f DER -o chave.der -p <primo_p> -q <primo_q>

# Decodificar base64 da cifra
# openssl enc -base64 -d -in entrada.enc -out saida.enc

# Decifrar
# openssl pkeyutl -decrypt -in saida.enc -inkey chave.der -out flag.txt
</primo_q></primo_p>

Conceitos Adicionais

  • Cifra de Vigenère com chave conhecida: quando a chave é qsnctf ou derivada do contexto, aplica-se a decodificação polialfabética direta.
  • ROT13 e codificações múltiplas: muitos desafios empliham ROT13, Base85, Morse e gzip. A ordem de aplicação é determinada pelo contexto.
  • Rabbit e outras cifras: textos começando com U2FsdGVkX1 indicam cifras derivadas de OpenSSL, frequentemente Rabbit ou AES com senha.
  • Esquemas de limiar: desafios que dividem segredos entre múltiplos guardiões usam Shamir's Secret Sharing; com 3 de 5 partes, recupera-se o polinômio.
  • Mersenne Twister: quando números pseudoaleatórios são gerados com random do Python, a biblioteca randcrack pode prever valores futuros a partir de 624 bits consecutivos.

Tags: CTF criptografia rsa AES cifra-césar

Publicado em 7-5 22:42