The 55 aa signature at the end confirms it is a standard boot sector.
Boot Sector Fundamentals
When a computer powers on and completes the Power-On Self-Test (POST), the BIOS reads the first sector (512 bytes) of the disk into physical memory between 0x7C00 and 0x7DFF. It then sets the CPU’s Instruction Pointer (IP) to 0x7C00 to begin execution.
Therefore, the base address of this program in memory is 0x7C00. When performing static analysis or debugging, any hardcoded physical addresses must have 0x7C00 subtracted to find their corresponding file offset.
Static Analysis and Memory Mapping
Analyzing the assembly code reveals two critical memory locations:
1. Flag Ciphertext Area (0x7DAA)
Calculate the file offset: $$0x7DAA - 0x7C00 = 0x01AA$$ Looking at the hex dump at 0x01AA:
The ciphertext indeed begins here, with the first 7 bytes representing the 247CTF{ prefix.
2. Input Buffer (0x7DEC)
Calculate the file offset: $$0x7DEC - 0x7C00 = 0x01EC$$ The region of null bytes before the 55 aa signature is used directly as a buffer for keyboard input.
Core Logic Deconstruction
Focusing on sub_1016A in IDA (the primary decryption logic):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
seg000:016B mov bx, 7DECh ; BX points to the input buffer seg000:016E mov si, 7DAAh ; SI points to the start of the ciphertext seg000:0171 add si, 7 ; Skip "247CTF{"
; Validate the first input character and decrypt seg000:0174 mov al, 4Bh ; 'K' seg000:0176 xor al, 0Ch ; AL = 0x4B ^ 0x0C = 0x47 ('G') seg000:0178 cmp [bx], al ; Check if input[0] == 'G' seg000:017A jnz loc_1027C ; Jump to failure if not equal
; Decrypt ciphertext using the validated char in AL seg000:017E xor [si], al ; XOR decryption seg000:0180 inc si seg000:0181 xor [si], al ; Each input char decrypts TWO bytes seg000:0183 inc bx seg000:0184 inc si
Logic Summary:
The program requires a 16-character unlock code.
Each character is validated via arithmetic/logical operations (XOR, ADD, SUB).
Validated characters serve as XOR keys to decrypt the subsequent ciphertext region.
Each key character decrypts 2 bytes of ciphertext.
Solution Script
We can simulate the assembly operations to recover the unlock code and then use it to XOR the ciphertext bytes.
defisPrime(n): ifint(n).bit_length() < 444: returnFalse basis = [ 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, ] if n <= 1: returnFalse if n == 2or n == 3: returnTrue if n % 2 == 0: returnFalse r, s = 0, n - 1 while s % 2 == 0: r += 1 s //= 2 for b in basis: x = pow(b, s, n) if x == 1or x == n - 1: continue for _ inrange(r - 1): x = pow(x, 2, n) if x == n - 1: break else: returnFalse returnTrue
defget_flag(): try: p = int(input("give me your P:")) q = int(input("give me your Q:")) r = int(input("give me your R:")) n = int(input("give me your N:"))
if isPrime(p) and isPrime(q) and isPrime(r) and isPrime(n) and p * q * r == n: if n.bit_length() < 1024: print("It's 2021 now,young man.") return m = int.from_bytes(flag, "big") print("here is your flag %d" % pow(m, 0x10001, n)) else: print("mission impossible. CIA needed") return except: print("wrong") return
if __name__ == "__main__": get_flag() exit(1)
Core idea
The service asks for P, Q, R, N such that:
isPrime(P), isPrime(Q), isPrime(R), and isPrime(N) are all true.
N = P * Q * R.
N has at least 1024 bits.
N is obviously composite, so the key is to make N a strong pseudoprime for all hardcoded Miller-Rabin bases.
Construction outline
Use an Arnault-style construction with:
P = a*k + 1
Q = b*k + 1
R = c*k + 1
Choose a, b, c as distinct primes with a, b, c ≡ 1 (mod 8) (for base-2 behavior), e.g. 89, 113, 137.
To make (P-1), (Q-1), (R-1) divide (N-1), solve the congruence system:
1 2 3
k ≡ -(b+c)(bc)^(-1) (mod a) k ≡ -(a+c)(ac)^(-1) (mod b) k ≡ -(a+b)(ab)^(-1) (mod c)
For odd MR bases B in {3,5,...,79}, force k ≡ 0 (mod B). Also force k ≡ 2 (mod 4) so P, Q, R ≡ 3 (mod 4).
Then scan k until P, Q, R are all truly prime (with a strong primality test like sympy.isprime).
if __name__ == "__main__": P = 45427420268475430659332737993000283397102585042957378767593137448789955507087370207886940708232722175257488588695287994058256318553211 Q = 57677511127390153533759543743921708133399911346676222480202522828238932273043514983047464045284242761843777646320983632905426561758571 R = 69927601986304876408186349494843132869697237650395066192811908207687909038999659758207987382335763348430066703946679271752596804963931 C = 21903103496980081373563573693903562346746553643549818726007975948477827819459765088909748053913796144132553228584337940471254732244734196780412249598279393335004210910953390996721397236654824645841526330517109905833017093822718222876587329963794042086917871402474385749212962074053842003782042533173326441388721313429460738550791493248565473365397568505962364729491646535933145902419195195919991941091
We created a service which can read and print the flag for you. To use the application, you first need to enter a valid product key. Can you reverse the algorithm and generate a valid key?
Reversing
The binary reads a product key, validates it, and prints the flag only if validation succeeds.
int __fastcall main(int argc, char **argv, char **envp) { int is_valid; char input_key[136];
setbuf(stdout, 0); memset(input_key, 0, 129); puts("Enter a valid product key to gain access to the flag:"); fgets(input_key, 128, stdin); input_key[strcspn(input_key, "\n")] = 0;
from z3 import * from z3 import BitVec, BitVecVal, If, Solver, ZeroExt, sat
s = Solver()
# 1. 定义 32 个 8-bit 的符号变量(代表字符) flag = [BitVec(f"flag[{i}]", 8) for i inrange(32)]
# 2. 施加字符集约束 ('@' to 'Z') for i inrange(32): # Integer Promotion converts c to a 32-bit integer = 8 bits(char) + 24 bits s.add(flag[i] >= 65, flag[i] <= 90)
# 3. 核心转换逻辑 (transform_char) deftransform(c): # z3 的 If 语法:If(条件, 真值, 假值) return If(c <= 77, ZeroExt(24, c) + 181, ZeroExt(24, c) + 177)
# 4. 累加校验和计算 # 使用 32-bit 位向量防止整数溢出,就像 C 语言里的 int 一样 # Integer Promotion converts c to a 32-bit integer = 8 bits(char) + 24 bits checksum = BitVecVal(247, 32) for i inrange(1, 32): checksum += transform(flag[i]) - i + 247
if s.check() == sat: model = s.model() # 提取计算出的字符并拼接 flag = "".join(chr(model[flag[i]].as_long()) for i inrange(32)) print(f"[+] Valid Product Key: {flag}")
Solver output:
1
BUYRSCLZHPATAQZSLJMJMKOOBFRAOVUX
Remote verification
1
nc 892dc593a381aaea.247ctf.com 50478
1 2 3 4
Enter a valid product key to gain access to the flag: BUYRSCLZHPATAQZSLJMJMKOOBFRAOVUX Valid product key! 247CTF{********************************}
An important USB drive containing sensitive information has been encrypted by some new ransomware variant. Can you reverse the ransomware encryption function and recover the files?
1. Initial Analysis
We are provided with a BitLocker-encrypted USB image (encrypted_usb.dd) and a large list of potential recovery keys (recovery_keys_dump.txt).
The goal is to find the correct recovery key, mount the image, and then deal with the “ransomware” that has encrypted the files inside.
2. BitLocker Decryption
Attempting John the Ripper
First, I tried using bitlocker2john to extract the hash and then cracked it with the provided recovery keys.
1 2 3 4
❯ bitlocker2john -i encrypted_usb.dd > hash ❯ john --wordlist=./recovery_keys_dump.txt --fork=6 hash ... 0 password hashes cracked, 4 left
John didn’t seem to find the key directly (possibly due to format mismatch or configuration). Instead of debugging the hash format, I moved to a more direct approach: brute-forcing the mount command using dislocker.
Brute-forcing with Dislocker
I wrote a simple bash script to iterate through the recovery_keys_dump.txt and attempt to mount the image.
❯ sudo mount /mnt/bitlocker_img/dislocker-file /mnt/unlocked_usb
3. Ransomware Analysis
Inside the mounted drive, we find several encrypted files and the ransomware binary itself:
1 2 3 4 5 6 7 8 9
❯ ls -lh /mnt/unlocked_usb/ total 3.2M -rwxrwxrwx 1 root root 474K Oct 8 2022 crypto_passphrase.png.xxx.crypt -rwxrwxrwx 1 root root 15K Oct 8 2022 cryptor -rwxrwxrwx 1 root root 9.2K Oct 8 2022 do_not_open.png.xxx.crypt -rwxrwxrwx 1 root root 133K Oct 8 2022 meeting_minutes.png.xxx.crypt -rwxrwxrwx 1 root root 889K Oct 8 2022 passwords.png.xxx.crypt -rwxrwxrwx 1 root root 386 Oct 8 2022 ransom.txt -rwxrwxrwx 1 root root 1.7M Oct 8 2022 salary_screenshot.png.xxx.crypt
The ransom.txt claims to use a “secure XOR encryption algorithm”.
1 2
Your files have been encrypted using a secure xor encryption algorithm and are completely unrecoverable! To decrypt your files, you need your secret encryption key.
4. Recovery (Known Plaintext Attack)
Since the files are PNGs, we can perform a Known Plaintext Attack. We know that PNG files always start with the same 8-byte magic header: 89 50 4E 47 0D 0A 1A 0A.
By XORing the first 8 bytes of an encrypted file with this known PNG header, we can recover the XOR key.
Using CyberChef:
Input the first few bytes of do_not_open.png.xxx.crypt.
XOR with the PNG magic bytes 89 50 4E 47 0D 0A 1A 0A.
The result reveals the key repeats as 66 63 6f 79 (ASCII: fcoy).
Applying the XOR key fcoy to the entire file do_not_open.png.xxx.crypt recovers the original image containing the flag.
247CTF{494f7cceb2baf33a0879543fe673blae}
5. Deep Dive: Reversing the cryptor Binary
I was curious about how the binary actually worked, so I threw it into IDA Pro.
Main Logic
The program expects a 4-character key as a command-line argument. It then iterates through the current directory, looking for files with the .xxx extension.
We have a honey pot running on one of our internal networks. We received an alert today that the machine was compromised, but we can’t figure out what the attacker did. Can you find the flag hidden in the attacker’s payload?
我们在内部网络中运行了一个蜜罐。今天我们收到警报,显示该机器已被入侵,但我们无法确定攻击者做了什么。你能找到隐藏在攻击者有效载荷中的 flag 吗?
Network Traffic
The provided logs show SMB (Server Message Block) traffic on port 445 (microsoft_ds). The sequence of SMBNegotiate_Request and SMB2_Negotiate_Protocol_Request suggests an attempt to exploit an SMB vulnerability.
1 2 3 4
0001 Ether / IP / TCP 192.168.10.168:microsoft_ds > 10.0.5.15:42799 SA / Padding 0003 Ether / IP / TCP 10.0.5.15:42799 > 192.168.10.168:microsoft_ds PA / NBTSession / SMB_Header / SMBNegotiate_Request ... 0203 Ether / IP / TCP 10.0.5.15:43947 > 192.168.10.168:microsoft_ds PA / NBTSession / SMB2_Header / SMB2_Negotiate_Protocol_Request / Raw
Payload Extraction
Following the TCP stream and exporting the raw data with Wireshark, we get the following hex dump:
flag = [BitVec(f"flag[{i}]", 32) for i inrange(40)]
for i inrange(40): s.add(flag[i] >= 32, flag[i] <= 126)
for line in raw_js.strip().split("&&"): line = line.strip() ifnot line: continue
# Convert: A ^ (B == N) -> (A ^ B) == N if"^"in line: line = re.sub(r"(.*)\^\s*\((.*)\s*==\s*(-?\d+)\)", r"(\1 ^ (\2)) == \3", line)
s.add(eval(line))
if s.check() == sat: m = s.model() result = "".join([chr(m[flag[i]].as_long()) for i inrange(40)]) print(f"\n[+] Result: {result}") else: print("\n[-] Unsatisfiable.")
Notes
^ in JavaScript is bitwise XOR (not exponent), so constraints must be modeled as XOR equations.
32-bit BitVec variables are used to match JavaScript bitwise operation behavior.