├── CHEATSHEET.md ├── README.md ├── lock-code ├── puzzle.png └── solve.py ├── server ├── code.c ├── manual.py ├── server └── solve.py ├── sonda ├── README.md ├── SOLUTION_1.md ├── SOLUTION_2.md ├── key-formatter.py ├── solver.py ├── sonda └── sonda.c └── sweet ├── solve.py └── sweet.c /CHEATSHEET.md: -------------------------------------------------------------------------------- 1 | # Cheatsheet 2 | 3 | ### BitVec 4 | 5 | - A vector of bits 6 | - Has no initial value 7 | - A `BitVec` of `8 bits` = `1 byte` 8 | 9 | ### BitVecVal 10 | 11 | - A `BitVec` with initial value 12 | 13 | ### ZeroExt / SignExt 14 | 15 | - A way to convert between different Z3 data types 16 | - `ZeroExt` extends with `trailing` 0s 17 | - `SignExt` extends with `leading` 0s 18 | - `ZeroExt(24, foo)` -> extends an `8-bit` byte `foo` to a `32-bit` int 19 | 20 | ### Solver 21 | 22 | - The theorem solver 23 | - Constrains are added using `Solver.add(...)` 24 | 25 | ### Arithmetic logic 26 | 27 | - When performing operations on Z3 data types, Z3 will use the `expression` rather than computing the values: 28 | 29 | ```python 30 | foo = BitVecVal(0, 8) 31 | bar = [BitVec(f"bar_{i}", 8) for i in range(0, 3)] 32 | for i in range(0, 3): 33 | foo += bar[i] 34 | 35 | # result actual value of foo: 36 | # (foo + bar[0] + bar[1] + bar[2]) 37 | ``` 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Z3 Python CTF 2 | 3 | Solving various CTF challenges using Z3 in Python. 4 | 5 | ## Intro 6 | 7 | - ### [Z3 Basics](https://ericpony.github.io/z3py-tutorial/guide-examples.htm) 8 | - ### [Z3 Advanced](https://ericpony.github.io/z3py-tutorial/advanced-examples.htm) 9 | - ### [Cheatsheet](./CHEATSHEET.md) 10 | 11 | ## Examples 12 | 13 | - ### [Sonda](sonda) 14 | - ### [Lock code](lock-code) 15 | - ### [Custom Crypto](https://github.com/ViRb3/pwnEd-ctf/blob/master/customcrypto) 16 | - ### [MathGenMe](https://github.com/ViRb3/pwnEd-ctf/blob/master/mathgenme) 17 | - ### [server](server) 18 | - ### [sweet](sweet) 19 | -------------------------------------------------------------------------------- /lock-code/puzzle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViRb3/z3-python-ctf/0aca1874e5f7d642384201aa40e16530c15fe350/lock-code/puzzle.png -------------------------------------------------------------------------------- /lock-code/solve.py: -------------------------------------------------------------------------------- 1 | from z3 import * 2 | 3 | s = Solver() 4 | code = IntVector("x", 3) 5 | 6 | 7 | def GoodValueBadPlace(nums, count): 8 | exps = [] 9 | for i in range(len(nums)): 10 | for j in range(len(code)): 11 | if i == j: 12 | continue 13 | else: 14 | exps.append(If(nums[i] == code[j], 1, 0)) 15 | return Sum(exps) == count 16 | 17 | 18 | # one number is correct and well placed 19 | s.add(Or(code[0] == 2, code[1] == 9, code[2] == 1)) 20 | # one number is correct but wrong placed 21 | s.add(GoodValueBadPlace([2, 4, 5], 1)) 22 | # two numbers are correct but wrong placed 23 | s.add(GoodValueBadPlace([4, 6, 3], 2)) 24 | # nothing is correct 25 | s.add(And(code[0] != 5, code[1] != 7, code[2] != 9)) 26 | # one number is correct but wrong placed 27 | s.add(GoodValueBadPlace([5, 6, 9], 1)) 28 | 29 | while s.check(): 30 | m = s.model() 31 | print(m[code[0]], m[code[1]], m[code[2]]) 32 | s.add(Or([m[code[i]] != code[i] for i in range(3)])) 33 | -------------------------------------------------------------------------------- /server/code.c: -------------------------------------------------------------------------------- 1 | // byte_4040C0 = 0D 02 0B 13 1B 09 0A 00 10 06 07 1A 05 12 04 19 11 0E 17 16 0F 1C 1D 18 08 15 01 03 1F 0C 1E 14 2 | 3 | // reorders 4 | dest = a1; 5 | for (i = 0; i < 32; ++i) 6 | src[i] = dest[byte_4040C0[i]]; 7 | src[32] = 0; 8 | strcpy(dest, src); 9 | return v5; 10 | 11 | 05 -> 90 12 | // byte_4040E0 = A8 5F 43 DF 90 15 A2 F5 77 48 49 6C 67 20 0E CD B6 C8 4A E7 89 2F A1 A6 E8 B7 E1 C6 58 A9 D4 5A 4D 9E 34 05 53 C2 76 D3 C5 B3 BF C9 AF 98 25 68 D9 2D E6 65 D7 59 D6 0A 31 8F 99 AA 7C C0 35 B5 ED 4B EB D5 8E 6B 9D 37 2E 62 0F 07 9B 87 B8 BD DE 69 C7 CF 66 46 60 04 D0 A7 F8 70 7E FA 9A 03 08 C4 F6 8B 79 33 23 DD DA C1 13 CE 16 EE 93 63 12 6F 83 0D 71 64 4C 51 00 BA EF 95 6E 22 E5 94 30 FB 14 41 7A 1C 2A 56 B9 38 42 F0 44 F3 F2 9F 52 4E D8 CB 24 32 BE 0C A3 09 85 01 1D A5 28 45 F4 47 CC AE C3 AB A0 92 72 57 AC 3E E3 B4 74 1B 81 4F DC 2B 50 02 27 B2 6D F1 54 FE 80 5E 3B 36 E2 FF 11 EA FD 1A 97 86 26 73 B1 D2 3A 1E 5D 39 7F 1F A4 91 5C 55 EC E4 29 8C F7 7D 18 82 BC 2C 75 40 BB 17 8D F9 D1 E9 0B 7B 10 CA 6A FC 19 3C 8A B0 AD 21 96 5B 06 61 3D 3F 88 78 DB 84 9C E0 13 | 14 | dest = a1; 15 | for (i = 0; i < 32; ++i) 16 | src[i] = byte_4040E0[dest[i]]; 17 | src[32] = 0; 18 | strcpy(dest, src); 19 | return v5; 20 | 21 | // byte_4040B0 = 42 33 21 68 00 00 00 00 00 00 00 00 00 00 00 00 22 | 23 | for (i = 0; i < 32; ++i) 24 | *(_BYTE *)(a1 + i) ^= byte_4040B0[i % 4]; 25 | return v3; 26 | 27 | // byte_404090 = 50 21 50 EB 86 B0 44 65 4F 3E 44 0D 41 EA A2 EB 13 E4 B2 0C 4F FD F6 9E C9 30 45 0D 54 30 D7 11 28 | 29 | for (i = 0; i < 32; ++i) 30 | { 31 | if (*(char *)(a1 + i) != (unsigned __int8)byte_404090[i]) 32 | return 0; 33 | } -------------------------------------------------------------------------------- /server/manual.py: -------------------------------------------------------------------------------- 1 | arrs = [ 2 | "0D 02 0B 13 1B 09 0A 00 10 06 07 1A 05 12 04 19 11 0E 17 16 0F 1C 1D 18 08 15 01 03 1F 0C 1E 14", 3 | "A8 5F 43 DF 90 15 A2 F5 77 48 49 6C 67 20 0E CD B6 C8 4A E7 89 2F A1 A6 E8 B7 E1 C6 58 A9 D4 5A 4D 9E 34 05 53 C2 76 D3 C5 B3 BF C9 AF 98 25 68 D9 2D E6 65 D7 59 D6 0A 31 8F 99 AA 7C C0 35 B5 ED 4B EB D5 8E 6B 9D 37 2E 62 0F 07 9B 87 B8 BD DE 69 C7 CF 66 46 60 04 D0 A7 F8 70 7E FA 9A 03 08 C4 F6 8B 79 33 23 DD DA C1 13 CE 16 EE 93 63 12 6F 83 0D 71 64 4C 51 00 BA EF 95 6E 22 E5 94 30 FB 14 41 7A 1C 2A 56 B9 38 42 F0 44 F3 F2 9F 52 4E D8 CB 24 32 BE 0C A3 09 85 01 1D A5 28 45 F4 47 CC AE C3 AB A0 92 72 57 AC 3E E3 B4 74 1B 81 4F DC 2B 50 02 27 B2 6D F1 54 FE 80 5E 3B 36 E2 FF 11 EA FD 1A 97 86 26 73 B1 D2 3A 1E 5D 39 7F 1F A4 91 5C 55 EC E4 29 8C F7 7D 18 82 BC 2C 75 40 BB 17 8D F9 D1 E9 0B 7B 10 CA 6A FC 19 3C 8A B0 AD 21 96 5B 06 61 3D 3F 88 78 DB 84 9C E0", 4 | "42 33 21 68 00 00 00 00 00 00 00 00 00 00 00 00", 5 | "50 21 50 EB 86 B0 44 65 4F 3E 44 0D 41 EA A2 EB 13 E4 B2 0C 4F FD F6 9E C9 30 45 0D 54 30 D7 11", 6 | ] 7 | arrs = [[int(x) for x in bytearray.fromhex(b)] for b in arrs] 8 | src = [0] * 32 9 | 10 | for i in range(32): 11 | src[i] = arrs[3][i] ^ arrs[2][i % 4] 12 | 13 | arrs_1_reverse = {} 14 | for i in range(256): 15 | arrs_1_reverse.update({arrs[1][i]: i}) 16 | 17 | src = [arrs_1_reverse[v] for v in src] 18 | 19 | final = [0] * 32 20 | for i in arrs[0]: 21 | final[arrs[0][i]] = src[i] 22 | 23 | print("".join([chr(f) for f in final])) 24 | -------------------------------------------------------------------------------- /server/server: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViRb3/z3-python-ctf/0aca1874e5f7d642384201aa40e16530c15fe350/server/server -------------------------------------------------------------------------------- /server/solve.py: -------------------------------------------------------------------------------- 1 | from z3 import * 2 | from typing import * 3 | 4 | solver = Solver() 5 | 6 | 7 | def FilledArray(name: str, items: List[int]): 8 | arr = Array(name, BitVecSort(8), BitVecSort(8)) 9 | exp = Store(arr, 0, BitVecVal(items[0], 8)) 10 | for i in range(1, len(items)): 11 | exp = Store(exp, i, BitVecVal(items[i], 8)) 12 | return exp 13 | 14 | 15 | def UnknownArray(name: str, len: int): 16 | arr = Array(name, BitVecSort(8), BitVecSort(8)) 17 | return arr 18 | 19 | 20 | def BitVecList(name: str, items: List[int]): 21 | exp = [BitVec(f"name_{i}", 8) for i in range(0, len(items))] 22 | for i in range(0, len(items)): 23 | solver.add(exp[i] == items[i]) 24 | return exp 25 | 26 | 27 | def BitVecListUnk(name: str, len: int): 28 | exp = [BitVec(f"name_{i}", 8) for i in range(0, len)] 29 | return exp 30 | 31 | 32 | arrs_raw = [ 33 | "0D 02 0B 13 1B 09 0A 00 10 06 07 1A 05 12 04 19 11 0E 17 16 0F 1C 1D 18 08 15 01 03 1F 0C 1E 14", 34 | "A8 5F 43 DF 90 15 A2 F5 77 48 49 6C 67 20 0E CD B6 C8 4A E7 89 2F A1 A6 E8 B7 E1 C6 58 A9 D4 5A 4D 9E 34 05 53 C2 76 D3 C5 B3 BF C9 AF 98 25 68 D9 2D E6 65 D7 59 D6 0A 31 8F 99 AA 7C C0 35 B5 ED 4B EB D5 8E 6B 9D 37 2E 62 0F 07 9B 87 B8 BD DE 69 C7 CF 66 46 60 04 D0 A7 F8 70 7E FA 9A 03 08 C4 F6 8B 79 33 23 DD DA C1 13 CE 16 EE 93 63 12 6F 83 0D 71 64 4C 51 00 BA EF 95 6E 22 E5 94 30 FB 14 41 7A 1C 2A 56 B9 38 42 F0 44 F3 F2 9F 52 4E D8 CB 24 32 BE 0C A3 09 85 01 1D A5 28 45 F4 47 CC AE C3 AB A0 92 72 57 AC 3E E3 B4 74 1B 81 4F DC 2B 50 02 27 B2 6D F1 54 FE 80 5E 3B 36 E2 FF 11 EA FD 1A 97 86 26 73 B1 D2 3A 1E 5D 39 7F 1F A4 91 5C 55 EC E4 29 8C F7 7D 18 82 BC 2C 75 40 BB 17 8D F9 D1 E9 0B 7B 10 CA 6A FC 19 3C 8A B0 AD 21 96 5B 06 61 3D 3F 88 78 DB 84 9C E0", 35 | "42 33 21 68 00 00 00 00 00 00 00 00 00 00 00 00", 36 | "50 21 50 EB 86 B0 44 65 4F 3E 44 0D 41 EA A2 EB 13 E4 B2 0C 4F FD F6 9E C9 30 45 0D 54 30 D7 11", 37 | ] 38 | arrs_raw = [[int(x) for x in bytearray.fromhex(b)] for b in arrs_raw] 39 | 40 | arrs = [ 41 | FilledArray("bytes0", arrs_raw[0]), 42 | FilledArray("bytes1", arrs_raw[1]), 43 | FilledArray("bytes2", arrs_raw[2]), 44 | FilledArray("bytes3", arrs_raw[3]), 45 | ] 46 | 47 | steps = [ 48 | BitVecListUnk("step1", 32), 49 | BitVecListUnk("step2", 32), 50 | BitVecListUnk("step3", 32), 51 | ] 52 | 53 | for i in range(32): 54 | steps[1][i] = steps[0][arrs_raw[0][i]] 55 | for i in range(32): 56 | steps[2][i] = arrs[1][steps[1][i]] 57 | for i in range(32): 58 | steps[2][i] = steps[2][i] ^ arrs_raw[2][i % 4] 59 | for i in range(32): 60 | solver.add(steps[2][i] == arrs_raw[3][i]) 61 | 62 | if not solver.check(): 63 | print("No solution") 64 | exit(0) 65 | 66 | m = solver.model() 67 | print(m) 68 | -------------------------------------------------------------------------------- /sonda/README.md: -------------------------------------------------------------------------------- 1 | # Sonda 2 | 3 | > You can solve it from an iPad if u want :3 4 | 5 | Sonda was a reverse-engineering challenge from `The Game` CTF at [HackUPC](https://hackupc.com/) 2019 6 | 7 | This paper explains how the algorithm works through reverse-engineering and how to use [Z3](https://github.com/Z3Prover/z3) in Python to find the flag without doing any maths by hand 8 | 9 | ## Try challenge 10 | 11 | You can download the binary [here](sonda) 12 | 13 | ## Solution 14 | 15 | - ### [Analysis](SOLUTION_1.md) 16 | - ### [Solving](SOLUTION_2.md) 17 | -------------------------------------------------------------------------------- /sonda/SOLUTION_1.md: -------------------------------------------------------------------------------- 1 | # Analysis 2 | 3 | ## Decompiling 4 | We land in the `main` method, which is also the only place we are interested in. Decompile it (requires [IDA Pro](https://www.hex-rays.com/products/ida/index.shtml)): 5 | - Hide casts (hotkey: `\`) 6 | - Create new struct type for `ptr` 7 | 8 | We end up with [sonda.c](sonda.c) 9 | 10 | ## Understanding the algorithm 11 | ```c 12 | printf("Give me the magic number: ", argv, envp); 13 | __isoc99_scanf("%d", &seed); 14 | if ( seed % 17 || seed > 20 ) 15 | { 16 | puts("BAD..."); 17 | result = 1; 18 | } 19 | ``` 20 | The only valid numbers that don't produce a remainder are `0` and `17` 21 | 22 | ```c 23 | s = malloc(seed); 24 | printf("Tell me more: "); 25 | __isoc99_scanf("%s", s); 26 | v4 = strlen(s); 27 | if ( v4 <= seed ) 28 | { 29 | /* ... */ 30 | } 31 | else 32 | { 33 | puts("WTF is wrong with u?"); 34 | free(s); 35 | result = 1; 36 | } 37 | ``` 38 | 39 | `s` is our flag. We see that it must be the same length as the value of `seed`. Since a flag with length `0` doesn't make sense, we deduce that seed _must_ be `17`, and so must be the length of the flag 40 | 41 | 42 | ```c 43 | srand(seed); 44 | ``` 45 | 46 | This seeds the random number generator. Since the seed is always `17`, the produced random numbers will be identical between runs, but still different for each call during the same run 47 | 48 | ```c 49 | ptr = malloc(4LL * seed); 50 | *ptr = 2 * seed + rand() % (5 * seed); 51 | for ( i = 1; i < seed; ++i ) 52 | { 53 | v5 = ptr[i - 1]; 54 | ptr[i] = v5 + rand() % 94 + 33; 55 | } 56 | ``` 57 | 58 | `ptr` is initialized as an array of `4 byte` ints. All elements are filled based on the seed and the produced random numbers 59 | 60 | ```c 61 | for ( j = 0; j < seed; ++j ) 62 | { 63 | v9 = 0; 64 | for ( k = 0; k <= j; ++k ) 65 | v9 += s[k]; 66 | if ( v9 != ptr[j] ) 67 | { 68 | puts("NOOB! Keep trying..."); 69 | free(s); 70 | free(ptr); 71 | return 1; 72 | } 73 | } 74 | ``` 75 | 76 | For each character of the flag, `v9` (`4 byte` int) is calculated: 77 | - the values of all characters before and including the current character are added together 78 | 79 | `v9` must be equal to the number at the same position in `ptr` 80 | 81 | --- 82 | ## [Next](SOLUTION_2.md) >> 83 | -------------------------------------------------------------------------------- /sonda/SOLUTION_2.md: -------------------------------------------------------------------------------- 1 | # Solving 2 | 3 | ## Random number generator 4 | The only thing from the algorithm that we can't reproduce in Python is the random number generator. We can work around that using a dummy C program: 5 | ```c 6 | int seed = 17; 7 | srand(seed); 8 | for (int i = 0; i < seed; i++) 9 | printf("%d\n", rand()); 10 | ``` 11 | Record the produced numbers to a Python list (`rands`) 12 | 13 | ## [Final solution (solver.py)](solver.py) 14 | The `flag` and `ptr` arrays will be printed to `stdout` 15 | 16 | ## [Key formatter (key-formatter.py)](key-formatter.py) 17 | Using the `flag` array from above, decode it to ASCII and print to `stdout` 18 | 19 | --- 20 | ## << [Previous](SOLUTION_1.md) 21 | -------------------------------------------------------------------------------- /sonda/key-formatter.py: -------------------------------------------------------------------------------- 1 | flag = [89,117,123,69,74,36,92,102,62,54,34,86,48,76,124,110,54] 2 | flag = flag[::-1] 3 | 4 | for ch in flag: 5 | print(chr(ch), end='') 6 | print() 7 | 8 | # 6n|L0V"6>f\$JE{uY -------------------------------------------------------------------------------- /sonda/solver.py: -------------------------------------------------------------------------------- 1 | from z3 import * 2 | 3 | rands = [1227918265, 3978157, 263514239, 1969574147, 1833982879, 4 | 488658959, 231688945, 1043863911, 1421669753, 1942003127, 5 | 1343955001, 461983965, 602354579, 726141576, 1746455982, 6 | 1641023978, 1153484208, 945487677, 1559964282, 1484758023] 7 | seed = 17 8 | 9 | s = Solver() 10 | 11 | flag = [BitVec(f"flag_{i}", 8) for i in range(0, seed)] 12 | ptr = [BitVec(f"ptr_{i}", 32) for i in range(0, seed)] 13 | 14 | s.add(ptr[0] == 2 * seed + rands[0] % (5 * seed)) 15 | 16 | for i in range(1, seed): 17 | v5 = ptr[i-1] 18 | s.add(ptr[i] == v5 + rands[i] % 94 + 33) 19 | 20 | for j in range(0, seed): 21 | v9 = BitVecVal(0, 32) 22 | for k in range(0, j+1): 23 | v9 += ZeroExt(24, flag[k]) 24 | s.add(ptr[j] == v9) 25 | 26 | print(s.check()) 27 | model = s.model() 28 | print(model) 29 | -------------------------------------------------------------------------------- /sonda/sonda: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViRb3/z3-python-ctf/0aca1874e5f7d642384201aa40e16530c15fe350/sonda/sonda -------------------------------------------------------------------------------- /sonda/sonda.c: -------------------------------------------------------------------------------- 1 | int __cdecl main(int argc, const char **argv, const char **envp) 2 | { 3 | int result; // eax 4 | size_t v4; // rax 5 | int v5; // ebx 6 | unsigned int seed; // [rsp+4h] [rbp-3Ch] 7 | int i; // [rsp+8h] [rbp-38h] 8 | int j; // [rsp+Ch] [rbp-34h] 9 | int v9; // [rsp+10h] [rbp-30h] 10 | int k; // [rsp+14h] [rbp-2Ch] 11 | char *s; // [rsp+18h] [rbp-28h] 12 | _DWORD *ptr; // [rsp+20h] [rbp-20h] 13 | unsigned __int64 v13; // [rsp+28h] [rbp-18h] 14 | 15 | v13 = __readfsqword(0x28u); 16 | printf("Give me the magic number: ", argv, envp); 17 | __isoc99_scanf("%d", &seed); 18 | if ( seed % 17 || seed > 20 ) 19 | { 20 | puts("BAD..."); 21 | result = 1; 22 | } 23 | else 24 | { 25 | s = malloc(seed); 26 | printf("Tell me more: "); 27 | __isoc99_scanf("%s", s); 28 | v4 = strlen(s); 29 | if ( v4 <= seed ) 30 | { 31 | srand(seed); 32 | ptr = malloc(4LL * seed); 33 | *ptr = 2 * seed + rand() % (5 * seed); 34 | for ( i = 1; i < seed; ++i ) 35 | { 36 | v5 = ptr[i - 1]; 37 | ptr[i] = v5 + rand() % 94 + 33; 38 | } 39 | for ( j = 0; j < seed; ++j ) 40 | { 41 | v9 = 0; 42 | for ( k = 0; k <= j; ++k ) 43 | v9 += s[k]; 44 | if ( v9 != ptr[j] ) 45 | { 46 | puts("NOOB! Keep trying..."); 47 | free(s); 48 | free(ptr); 49 | return 1; 50 | } 51 | } 52 | printf("flag{%s}\n", s); 53 | free(s); 54 | result = 0; 55 | } 56 | else 57 | { 58 | puts("WTF is wrong with u?"); 59 | free(s); 60 | result = 1; 61 | } 62 | } 63 | return result; 64 | } -------------------------------------------------------------------------------- /sweet/solve.py: -------------------------------------------------------------------------------- 1 | from z3 import * 2 | 3 | for input_len in range(1, 32): 4 | s = Solver() 5 | input = [BitVec(f"i_{i}", 8) for i in range(input_len)] 6 | output = BitVecVal(0, 64) 7 | 8 | for i in range(input_len): 9 | output += ZeroExt(64 - 8, input[i]) 10 | output <<= 1 11 | 12 | s.add(output == 0x2D64A) 13 | 14 | if s.check() == sat: 15 | m = s.model() 16 | solution = sorted([(d, m[d]) for d in m], key=lambda x: str(x[0])) 17 | flag = "".join([f"{int(str(x[1]), 10):x}" for x in solution]) 18 | 19 | # print(solution) 20 | print(flag) 21 | break 22 | -------------------------------------------------------------------------------- /sweet/sweet.c: -------------------------------------------------------------------------------- 1 | int main() { 2 | char input[32]; 3 | puts("Input password:"); 4 | fgets(input, 32, stdin); 5 | 6 | long long sum = 0; 7 | for (int i = 0; i < strlen(input); i++) { 8 | char c = input[i]; 9 | sum += (int)c; 10 | sum <<= 1; 11 | } 12 | 13 | if (sum != 0x2d64a) { 14 | puts("Bad password!"); 15 | } else { 16 | puts("Well done!"); 17 | } 18 | 19 | return 0; 20 | } --------------------------------------------------------------------------------