A secret flag is safely hidden away within client-side scripts. We
were told JavaScript is an insecure medium for secret storage, so we
decided to use C++ instead.
[0x0000019a]> iz [Strings] nth paddr vaddr len size section type string ――――――――――――――――――――――――――――――――――――――――――――――――――――――― 92 0x000098d9 0x000098d9 24 25 data ascii OrpheanBeholderScryDoubt 96 0x00009979 0x00009979 64 65 data ascii ./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 97 0x000099ba 0x000099ba 60 61 data ascii $2b$12$uAfq9EI1EoIC316VgA3azeOyogkKzG4zz2kF8M.l.D4h4nT4WsidK 98 0x000099f7 0x000099f7 60 61 data ascii $2b$12$NmhDm/LZzjanlv6xuHCsVe8JJNlvEb3uYUEQ03abPIlCuTE6qtrT. # ... 40 hashes total ...
Obviously the flag is stored as a series of bcrypt hashes, which are
all prefixed with $2b$12$.
Cracking the Flag
If we test single characters, we can see that the flag start with 2,
but the next character is unknown.
Since the hashes are cumulative, each one validates the entire flag
prefix up to that point. We can recover the flag by brute-forcing each
character in sequence and appending it to the previously discovered
string.
for i, h inenumerate(hashes): print(f"[*] Cracking index {i:2d}... ", end="", flush=True) for c in charset: if bcrypt.checkpw((flag + c).encode(), h): flag += c print(f"Found: {c} | Current: {flag}") break else: print("Failed to crack.") break
We stored the flag within this binary, but made a few errors when
trying to print it. Can you make use of these errors to recover the
flag?
Overview
1 2
❯ file flag_errata.exe flag_errata.exe: PE32+ executable for MS Windows 5.02 (console), x86-64 (stripped to external PDB), 9 sections
A PE64 binary (GCC 7.3 / MinGW-w64) with a monstrous 53KB
main function. It prompts for a 40-character password,
validates each character through error-code accumulation, then returns
-247 on any mismatch.
Each character block calls \(N\)
consecutive functions from this cycle, accumulates the error codes, then
checks sum % 126 against the input.
Algebraic Solution
The key insight: since the error codes cycle deterministically, we
can bypass the Windows environment entirely and solve this as pure
modular arithmetic.
I use Arch BTW.
Let:
\(S = \sum_{k=0}^{12} e_k\), the
sum of one full cycle (all 13 error codes)
\(P(r) = \sum_{k=0}^{r-1} e_k\),
the partial sum of the first \(r\)
codes
\[\text{password}[i] \equiv q_i \cdot S +
P(r_i) \pmod{126}\]
The flag format 247CTF{...} gives us 8 known characters
for free. Three of them — blocks 2, 3, 6 — share the same remainder
\(r = 9\): same position in the
error-code cycle, different lap counts. Shared \(P(9)\) cancels on subtraction.
Same process with the remaining known characters fills all partial
sums:
\(P(r)\)
Value
Source
\(P(3)\)
10
'2' (ASCII 50)
\(P(4)\)
16
\(e_3 = 6\)
\(P(5)\)
34
\(e_4 = 18\)
\(P(6)\)
58
'4' (ASCII 52)
\(P(7)\)
90
'T' (ASCII 84)
\(P(9)\)
45
'7' (ASCII 55)
\(P(10)\)
42
'F' (ASCII 70)
\(P(12)\)
99
\(e_{10}, e_{11}\)
With \(S\) and all \(P(r)\) determined, every character computes
directly:
Block
Calls
\(q\)
\(r\)
Calculation
Char
0
16
1
3
\((40+10) \bmod 126\)
2
7
191
14
9
\((560+45) \bmod 126\)
e
9
109
8
5
\((320+34) \bmod 126\)
f
15
285
21
12
\((840+99) \bmod 126\)
9
20
298
22
12
\((880+99) \bmod 126\)
a
39
35
2
9
\((80+45) \bmod 126\)
}
All 40 characters land in the valid charset — 247CTF{
prefix, [0-9a-f] hex body, } suffix —
confirming the solution without ever running the binary.
Character-by-Character
Breakdown
Block
Calls
q
r
Formula
Char
0
16
1
3
(1*40+10)%126=50
2
1
45
3
6
(3*40+58)%126=52
4
2
217
16
9
(16*40+45)%126=55
7
3
139
10
9
(10*40+45)%126=67
C
4
46
3
7
(3*40+90)%126=84
T
5
101
7
10
(7*40+42)%126=70
F
6
321
24
9
(24*40+45)%126=123
{
7
191
14
9
(14*40+45)%126=101
e
8
280
21
7
(21*40+90)%126=48
0
9
109
8
5
(8*40+34)%126=102
f
10
19
1
6
(1*40+58)%126=98
b
11
256
19
9
(19*40+45)%126=49
1
12
109
8
5
(8*40+34)%126=102
f
13
241
18
7
(18*40+90)%126=54
6
14
215
16
7
(16*40+90)%126=100
d
15
285
21
12
(21*40+99)%126=57
9
16
17
1
4
(1*40+16)%126=56
8
17
215
16
7
(16*40+90)%126=100
d
18
217
16
9
(16*40+45)%126=55
7
19
215
16
7
(16*40+90)%126=100
d
20
298
22
12
(22*40+99)%126=97
a
21
285
21
12
(21*40+99)%126=57
9
22
280
21
7
(21*40+90)%126=48
0
23
215
16
7
(16*40+90)%126=100
d
24
217
16
9
(16*40+45)%126=55
7
25
12
0
12
(0*40+99)%126=99
c
26
38
2
12
(2*40+99)%126=53
5
27
12
0
12
(0*40+99)%126=99
c
28
191
14
9
(14*40+45)%126=101
e
29
109
8
5
(8*40+34)%126=102
f
30
16
1
3
(1*40+10)%126=50
2
31
217
16
9
(16*40+45)%126=55
7
32
12
0
12
(0*40+99)%126=99
c
33
16
1
3
(1*40+10)%126=50
2
34
109
8
5
(8*40+34)%126=102
f
35
45
3
6
(3*40+58)%126=52
4
36
217
16
9
(16*40+45)%126=55
7
37
285
21
12
(21*40+99)%126=57
9
38
17
1
4
(1*40+16)%126=56
8
39
35
2
9
(2*40+45)%126=125
}
Anti-Analysis
TLS Callbacks: Two callbacks
(0x401C50, 0x401C20) execute before
main — a common anti-debug vector.
53KB of bloat: Up to 321 API calls per character,
making manual static analysis impractical without scripting.
Environment sensitivity: Functions like
CreateProcessA and SetConsoleDisplayMode
return different error codes under a debugger, making dynamic analysis a
trap. The algebraic approach sidesteps this entirely by reasoning at the
mathematical level, independent of the Windows runtime.
# Initialize the state at entry initial_state = project.factory.entry_state( add_options = { angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY, angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS } )
simulation = project.factory.simgr(initial_state)
# Explore searching for the print_flag function and avoiding no_flag # print_flag: 0x08048596 # no_flag: 0x8048609 simulation.explore(find=0x08048596, avoid=0x8048609)
if simulation.found: solution_state = simulation.found[0] print(f"Found solution: {solution_state.posix.dumps(sys.stdin.fileno()).decode()}") else: raise Exception('Could not find the solution')
if __name__ == "__main__": solve('./angr-y_binary')
Execution
Running the script gives us the password:
1 2
❯ python solve.py wgIdWOS6Df9sCzAfiK
Connecting to the server with the found password:
1 2 3
❯ nc 0c4c28058a1f7a2f.247ctf.com 50230 Enter a valid password: wgIdWOS6Df9sCzAfiK
Your task is to find the first two primes above 1,000,000 whose digit
sum is also prime.
你的任务是找到大于1,000,000的前两个质数,其各位数字之和也是质数。
Challenge
Find the first two primes greater than 1,000,000 where the sum of
their digits is also prime. Concatenate them as the answer.
Solution
1 2 3 4 5 6 7 8 9 10 11 12
defis_prime(n): return n > 1andall(n % i != 0for i inrange(2, int(n**0.5) + 1))
p = 1000000 found = [] whilelen(found) < 2: p += 1 if is_prime(p) and is_prime(sum(int(d) for d instr(p))): found.append(p)
print(f'{found[0]}{found[1]}') # 10000331000037
The first two primes are 1000033 and 1000037. Their digit sums are
1+0+0+0+0+3+3 = 7 (prime) and 1+0+0+0+0+3+7 = 11 (prime). Concatenated:
10000331000037.
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: 1. The program requires a
16-character unlock code. 2. Each character is validated via
arithmetic/logical operations (XOR, ADD, SUB). 3. Validated characters
serve as XOR keys to decrypt the subsequent ciphertext region. 4. 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{********************************}
247CTF{fb88b9fe80e969e73a27541f62d6f89c}
+++
SYSTEM STATUS: ACTIVEENCRYPTED SECTOR 7PRTS_TERMINAL_V2.0PROTOCOL: 0x2AENCRYPTED DATA STREAMSYSTEM: ONLINE