├── .gitignore
├── LICENSE
├── OCaml_crackmes
├── README.md
├── baby
├── imgs
│ ├── 1.png
│ ├── 10.png
│ ├── 2.png
│ ├── 3.png
│ ├── 4.png
│ ├── 5.png
│ ├── 6.png
│ ├── 7.png
│ ├── 8.png
│ └── 9.png
├── solve.py
└── teenager
├── README.md
├── ReverseMe3
├── README.md
├── ReverseMe3.EXE
├── ReverseMe3_vm_fixed.exe
├── imgs
│ ├── 1.png
│ ├── 10.png
│ ├── 11.png
│ ├── 12.png
│ ├── 13.png
│ ├── 14.png
│ ├── 15.png
│ ├── 16.png
│ ├── 17.png
│ ├── 18.png
│ ├── 19.png
│ ├── 2.png
│ ├── 20.png
│ ├── 21.png
│ ├── 22.png
│ ├── 23.png
│ ├── 3.png
│ ├── 4.png
│ ├── 5.png
│ ├── 6.png
│ ├── 7.png
│ ├── 8.png
│ └── 9.png
└── name_hash.py
├── armageddon
├── angr-solve.py
├── armageddon
├── imgs
│ ├── 1.png
│ ├── 2.png
│ └── 3.png
├── qiling_emulate.py
├── solve.md
└── z3_solve.py
├── automating-gdb
├── README.md
├── creator-writeup.rar
├── gdb_solve.py
├── imgs
│ ├── 1.png
│ ├── 2.png
│ ├── 3.png
│ ├── 4.png
│ ├── 5.png
│ ├── 6.png
│ ├── binaryninja-recursion.bndb-20ab.html
│ └── binaryninja-recursion.bndb-code_buffer.html
├── recursion.elf
└── solution.txt
├── client_houseplant_ctf_2020
├── client.apk
├── imgs
│ ├── 1.png
│ ├── 2.png
│ ├── 3.png
│ ├── 4.png
│ └── video.png
├── mitm-solve.py
└── solve.md
├── elf_format
├── README.md
├── binaries
│ ├── dumped-elf
│ ├── dumped-elf_fixed
│ ├── dumped-elf_fixed_patched
│ └── tricky-crackme
├── imgs
│ ├── 1.png
│ ├── 10.png
│ ├── 11.png
│ ├── 12.png
│ ├── 13.png
│ ├── 2.png
│ ├── 3.png
│ ├── 4.png
│ ├── 5.png
│ ├── 6.png
│ ├── 7.png
│ ├── 8.png
│ └── 9.png
└── test_lazy_binding
│ ├── .gdb_history
│ ├── simple
│ ├── simple.c
│ ├── simple_4
│ ├── simple_non_lazy
│ ├── simple_patched
│ └── simple_patched_2
├── how_to_not_write_a_bad_crackme
└── how_to_not_write_a_bad_crackme.md
├── obfuscation
├── README.md
├── imgs
│ ├── 1.png
│ ├── 10.png
│ ├── 2.png
│ ├── 3.png
│ ├── 4.png
│ ├── 5.png
│ ├── 6.png
│ ├── 7.png
│ ├── 8.png
│ └── 9.png
├── keygenme4.exe
└── solution
│ ├── de-obfuscate
│ ├── __init__.py
│ └── de-obfuscate.py
│ └── keygen.py
├── quarkslab_android_crackme
├── imgs
│ ├── analyze_me.png
│ ├── app.png
│ ├── country_code.png
│ ├── data.png
│ ├── jni.png
│ ├── key.png
│ ├── loop.png
│ ├── loop_body.png
│ ├── phone_func.png
│ ├── wrong.png
│ └── wrong_number.png
└── main.md
├── reverse_engineering_and_fixing_a_fan
├── imgs
│ ├── img-1.jpg
│ ├── img-2.jpg
│ ├── img-3.jpg
│ └── img-4.jpg
└── main.md
├── shl_undefined_behavior
├── imgs
│ ├── 1.png
│ ├── 2.png
│ └── 3.png
├── src_and_binaries
│ ├── test
│ ├── test.c
│ ├── test_O2
│ ├── test_missing_UL
│ ├── test_missing_UL.c
│ └── test_missing_UL_O2
└── writeup.md
└── x86
├── README.md
├── imgs
└── 1.png
├── source
├── Makefile
├── code.h
├── generator.py
└── main.c
├── x86
└── z3_solve.py
/.gitignore:
--------------------------------------------------------------------------------
1 | *.bndb
2 | .vscode
--------------------------------------------------------------------------------
/OCaml_crackmes/README.md:
--------------------------------------------------------------------------------
1 | # Solving Two OCaml Crackmes Without Knowing Much about OCaml
2 |
3 | Earlier this year, my friend [Towel](https://crackmes.one/user/Towel) uploaded two OCaml crackmes to crackmes.one. One of them is [`Baby OCaml`](https://crackmes.one/crackme/5f600af333c5d4357b3b01d6), and the other one is called [`Teenager OCaml`](https://crackmes.one/crackme/5f600b9933c5d4357b3b01d7). Well, interesting names!
4 |
5 | This is not the first time Towel came up with OCaml crackmes. [`Qt Scanner`](https://crackmes.one/crackme/5ec1b82133c5d449d91ae539), rated as level 5, is a hard challenge. I attempted that, but have not succeeded yet. So, when I first saw these two new OCaml challenges, I am not very eager to try them, despite they are rated as level 1 and 3. Nevertheless, we cannot hide from challenges forever, so I decided to try it last week. And the outcome is good, I managed to solve them without digging deep into the OCaml runtime.
6 |
7 | ## Baby OCaml
8 |
9 | OCaml is an interpretive language, but it can be compiled to native code. This is in contrast to Python/PyInstaller, where the script is just packaged into the generated binary and we can restore the original source of it. The OCaml compiler generates native code based on the source code, and the source is not present within the generated binary. Worse still, when we deal with new programming languages, e.g., OCaml, Go, Rust, we are likely to encounter some novel things we do not expect. For example, Rust has a very different way of passing parameters and return values of a function. We need to first get familiar with it, then start reversing the actual code logic.
10 |
11 | The Baby binary is 2.0 MB in size, which is HUGE for a crackme. The OCaml runtime will occupy lots of space in it, so we need to find the code that we are interested in. Opening the binary in BinaryNinja reveals that it is a statically linked binary:
12 |
13 |
14 |
15 | OK, so even libc functions are not easy to find. But the entry point looks so familiar to me that I can still recognize the `call 0x470980` at 0x401c48 is `libc_start_main`, and `sub_401770` at 0x401c41 is the `main` function. However, the `main` function is mostly initializing the OCaml runtime, and I cannot find the actual entry point to the code.
16 |
17 | Then I decided to run the binary and see if I can get any clue from it:
18 |
19 | ```Bash
20 | $ ./baby
21 | -= Montrehack =-
22 | Baby OCaml
23 |
24 | [!] Nope, try again.
25 | ```
26 |
27 | Ok, it does not ask for input, so the input should probably be supplied as a command line argument. I tried to find the strings it prints but failed. Well, the strings must be encrypted or otherwise obfuscated. Now I cannot quickly find the logic that checks the input, so again this is a dead end.
28 |
29 | I tried to reverse the binary for a half-day but cannot make a breakthrough. The call stack is deep and lots of function pointers are used. I was lost and put the binary aside for a while until one day Towel poked me to try his challenges. I told him that I cannot even solve the baby one, thanks to the string obfuscation. We chatted about the challenges a little bit, and I decided to give it a try again.
30 |
31 | This time, I have to admit, that I am super lucky. I browsed the string list and spotted something unusual in the first few:
32 |
33 |
34 |
35 | Looks readable, right? I navigated to the location and the code seems to be comparing strings:
36 |
37 |
38 |
39 | I am pretty sure the code is checking whether the ASCII string at `rax` is `Getting_Warmed_Up`. Note, the last char, `'p'`, with an ASCII value 0x70, is checked against 0x600000000000070. Well, due to little-endian, this will be effectively checking the lowest byte in the qword, but I have no idea what the 0x60 means. So OCaml runtime does have some weird things that are quite unusual.
40 |
41 | Anyways, I solved the challenge:
42 |
43 | ```Bash
44 | $ ./baby Getting_Warmed_Up
45 | -= Montrehack =-
46 | Baby OCaml
47 |
48 | [+] Success!
49 |
50 | FLAG-c34bc2bd73fdb06799061a8e76f62664
51 | ```
52 |
53 | ## Tennager OCaml
54 |
55 | Although I did not solve the last challenge decently, I cannot wait to start working on the Teenager one. This binary is 1.9 MB in size. So, yeah, the size is mostly static libraries + OCaml runtime, and the size of the actual logic is almost negligible within it.
56 |
57 | This time it does not use string obfuscation so I can easily locate the place where the binary asks for input:
58 |
59 |
60 |
61 | The control flow seems quite obvious, in the first node it asks for input, there there are two checks, and we must get to the lower left node to pass the check. I was pretty relieved when I saw this since there aren't many functions in this graph. However, it turns out I am naive and too optimistic about it.
62 |
63 | The first thing that I cannot understand is.... the first check.
64 |
65 |
66 |
67 | At 0x403168, `rbx` must be 0x2b, from which we can deduce that `rbx` must be 0x15 at 0x403163. And tracing back, it becomes weird. From debugging I noticed at 0x40314b, `rax` actually holds the ASCII string of the input. What could be located at `rax-0x8`? Well, I am not sure, but it is highly likely to be something related to the string's length. However, reading the code I cannot make any sense of it. I tried inputs with different lengths and the value does not change according to the input length.
68 |
69 | Furthermore, at 0x40315b there is a `movzx rdi, byte [rax+rbx]`. We know `rax` is the string, if this is one of the input char, then this check is very strange. The length will be checked against one particular char, and the result must be 0x15.
70 |
71 | Luckily, I debugged the code more and find after code at 0x403160, `rbx` always holds the length of the input. So this one is checking whether the string length is 0x15. The OCaml is yet unsolved, but I managed to get some information out of it.
72 |
73 | Now, there are only three functions ahead, but I cannot trace the execution easily. The code uses lots of function pointers and I quickly get lost. A patient reverser would study the OCaml compiler to figure out how the code is generated, but I still have one thing to try: hardware breakpoint on the input string.
74 |
75 | The plan is simple, we now know the string is held in `rax` at 0x40314b, then we can set a hardware breakpoint on it and see who accesses it. If everything goes well, we can find the code that reads the input, which is very likely to be also the checking logic code.
76 |
77 | I set a breakpoint at 0x40317f, and supplied the input string "111111111111111111111" (which is just '1' * 0x15). It hits! Not bad, at least we are correct on the length check. The pwndbg shows `rax` does point to the input string:
78 |
79 | ```Bash
80 | RAX 0x7ffff7ff9b90 ◂— '111111111111111111111'
81 | ```
82 |
83 | Then I set a hardware read breakpoint:
84 |
85 | ```Bash
86 | pwndbg> rwatch *0x7ffff7ff9b95
87 | Hardware read watchpoint 3: *0x7ffff7ff9b95
88 | ```
89 |
90 | Note, the string starts at 0x7ffff7ff9b95, but I set a breakpoint at 0x7ffff7ff9b95, which is the 6th char of the input. This is a personal habit since there could potentially be more places that access the first char than we are interested in. On the other hand, the code that reads a char in the middle is more likely to be interesting and worth checking out.
91 |
92 | The hardware breakpoint is hit at 0x402c07, and the instruction above it is reading the 6th char of the input:
93 |
94 |
95 |
96 | This function (sub_4024f0) looks like:
97 |
98 |
99 |
100 | So it is very likely that the function is checking very char one by one. This function has no xref to it at all, so I probably will not be able to find it easily, if I do not use hardware breakpoint. Inspecting the stack gives me the actual caller, 0x402410, and I have to say it is not easy to find the actual callee without debugging. The good news is if I were to reverse OCaml in the future, I know where to look at and with the help of debugging, I can hopefully find the callee and sort out the execution flow.
101 |
102 |
103 |
104 |
105 | I notice if the check passes, the return value is set to 0xa7. Remember the check at 0x4031e6? 0xd9f is quite a strange value, but it could be related to the 0xa7 here.
106 |
107 |
108 |
109 | ```Python
110 | >>> hex(0xd9f/0x15)
111 | '0xa6'
112 | ```
113 |
114 | So, there is some code, which I have not discovered, that minus 1 from the return value and then sum everything up. Now we know the check for each char, and it should not be hard to dump the constraints and solve it with z3.
115 |
116 | I am lazy and do not wish to manually transcript the constraints and z3 syntax. However, angr does not easily work with it, thanks to the OCaml runtime, which angr does not understand. So I need to combine the power of BinaryNinja API to simplify the binary and enable angr to work with it.
117 |
118 | ## Solving with BinaryNinja and Angr
119 |
120 |
121 |
122 | If we look at the basic block at 0x402d4c, there are two inputs to it: 1) the ASCII string in `rax`, 2) the value of `rbx` set at 0x402d24. We also need to extract the char index from the instruction at 0x402d4c (0x3 for in this screenshot). To get the initial value of `rbx`, we do not need to search for the instruction at 0x402d24. Instead, we can use the possible value set of `rbx` to get it. To enable angr, we also need to get the target address of the true/false branch of the conditional at 0x402d64.
123 |
124 | ### Getting True/False Branch Address
125 |
126 | To get the good/bad branch, we first get the `outgoing_edges` of a basic block and check the `edge.type`:
127 |
128 | ```Python
129 | bbl = bv.get_basic_blocks_at(addr)[0]
130 | edges = bbl.outgoing_edges
131 | for edge in edges:
132 | if edge.type == BranchType.TrueBranch:
133 | good_addr = edge.target.start
134 | elif edge.type == BranchType.FalseBranch:
135 | bad_addr = edge.target.start
136 | ```
137 |
138 | ### Parsing LLIL and Getting Char Index
139 |
140 | For each constraint, we need to know the index of the char being checked. For example, for instruction `movzx rax, byte [rax+0x3]`, we need to get 0x3 from it. This requires us to walk the LLIL instruction and find its value.
141 |
142 | ```Python
143 | def find_llil_basic_block(llil_basic_blocks, addr):
144 | for llil_bbl in llil_basic_blocks:
145 | if llil_bbl[0].address == addr:
146 | return llil_bbl
147 |
148 | func = bv.get_functions_containing(addr)[0]
149 | llil_basic_blocks = list(func.llil_basic_blocks)
150 | llil_bbl = find_llil_basic_block(llil_basic_blocks, addr)
151 | src = llil_bbl[0].operands[1].operands[0].operands[0]
152 |
153 | char_idx = 0
154 | if src.operation == LowLevelILOperation.LLIL_ADD:
155 | char_idx = src.operands[1].value.value
156 | ```
157 |
158 | Note, the above code might not be very reader-friendly, e.g., `src = llil_bbl[0].operands[1].operands[0].operands[0]`. This is because LLIL is essentially a tree, and we are travelling down it.
159 |
160 |
161 | ### Getting the Possible Value of rbx
162 |
163 | To get the possible value of rbx when the execution enters the basic block, we need to use the `get_possible_reg_values` API.
164 |
165 | ```Python
166 | rbx_value = 0
167 | value_set = llil_bbl[0].get_possible_reg_values('rbx')
168 | if value_set.type == RegisterValueType.ConstantValue:
169 | rbx_value = value_set.value
170 | rbx_value &= 0xffffffffffffffff
171 | ```
172 |
173 | Note, not all of the check uses rbx. For them, the `value_set.type` will be `UnderterminedValue`, and rbx_value will remain 0x0. This has no side effect on solving.
174 |
175 | ### Angr Time
176 |
177 | The last step is to solve it with angr:
178 |
179 | ```Python
180 | def angr_solve(addr, good_addr, bad_addr, char_idx, rbx_value):
181 | proj = angr.Project('./teenager')
182 | state = proj.factory.entry_state(addr = addr)
183 | # suppose the input string (ASCII) is stored at 0xaa000000
184 | input_addr = 0xaa000000
185 | state.regs.rax = input_addr
186 | state.regs.rbx = rbx_value
187 | flag = state.solver.BVS('flag', 8)
188 | state.memory.store(input_addr + char_idx, flag)
189 | simgr = proj.factory.simgr(state)
190 | simgr.explore(find = good_addr, avoid = [bad_addr])
191 | if simgr.found:
192 | solution_state = simgr.found[0]
193 | char_solution = solution_state.solver.eval(flag, cast_to = bytes)
194 | return True, char_solution
195 | else:
196 | False, None
197 | ```
198 |
199 | Note, the above script is not super robust, since we really expect the solving to succeed.
200 |
201 | We still need to manually collect the 0x15 address of the basic blocks. Although it is possible to automatically collect them, I feel the time to make it work will be longer than just select and copy 0x15 addresses.
202 |
203 | The script returns `0CamL_Ints_Ar3_W4rped`, and feeding it to the challenge gives me:
204 |
205 | ```Bash
206 | $ ./teenager
207 | -= Montrehack =-
208 | Teenager
209 |
210 | Enter Password: 0CamL_Ints_Ar3_W4rped
211 |
212 | [+] Success!
213 | FLAG-221fddd2bbf810be10d156b060b0eda5
214 | ```
215 |
216 | This reminds me of the description of the challenge:
217 |
218 | ```
219 | A slightly harder OCaml challenge to get practice with OCaml integer representations.
220 | ```
221 |
222 | So, it seems that I solved without knowing anything about OCaml integer representation.
--------------------------------------------------------------------------------
/OCaml_crackmes/baby:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeffli678/writeups/2bfd17d87af0b4e356b94d28c5f66fe91f529701/OCaml_crackmes/baby
--------------------------------------------------------------------------------
/OCaml_crackmes/imgs/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeffli678/writeups/2bfd17d87af0b4e356b94d28c5f66fe91f529701/OCaml_crackmes/imgs/1.png
--------------------------------------------------------------------------------
/OCaml_crackmes/imgs/10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeffli678/writeups/2bfd17d87af0b4e356b94d28c5f66fe91f529701/OCaml_crackmes/imgs/10.png
--------------------------------------------------------------------------------
/OCaml_crackmes/imgs/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeffli678/writeups/2bfd17d87af0b4e356b94d28c5f66fe91f529701/OCaml_crackmes/imgs/2.png
--------------------------------------------------------------------------------
/OCaml_crackmes/imgs/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeffli678/writeups/2bfd17d87af0b4e356b94d28c5f66fe91f529701/OCaml_crackmes/imgs/3.png
--------------------------------------------------------------------------------
/OCaml_crackmes/imgs/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeffli678/writeups/2bfd17d87af0b4e356b94d28c5f66fe91f529701/OCaml_crackmes/imgs/4.png
--------------------------------------------------------------------------------
/OCaml_crackmes/imgs/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeffli678/writeups/2bfd17d87af0b4e356b94d28c5f66fe91f529701/OCaml_crackmes/imgs/5.png
--------------------------------------------------------------------------------
/OCaml_crackmes/imgs/6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeffli678/writeups/2bfd17d87af0b4e356b94d28c5f66fe91f529701/OCaml_crackmes/imgs/6.png
--------------------------------------------------------------------------------
/OCaml_crackmes/imgs/7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeffli678/writeups/2bfd17d87af0b4e356b94d28c5f66fe91f529701/OCaml_crackmes/imgs/7.png
--------------------------------------------------------------------------------
/OCaml_crackmes/imgs/8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeffli678/writeups/2bfd17d87af0b4e356b94d28c5f66fe91f529701/OCaml_crackmes/imgs/8.png
--------------------------------------------------------------------------------
/OCaml_crackmes/imgs/9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeffli678/writeups/2bfd17d87af0b4e356b94d28c5f66fe91f529701/OCaml_crackmes/imgs/9.png
--------------------------------------------------------------------------------
/OCaml_crackmes/solve.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | from binaryninja import *
3 | import angr
4 |
5 | bv = BinaryViewType.get_view_of_file('teenager.bndb')
6 |
7 | basic_block_addrs = [
8 | 0x402d4c,
9 | 0x402cdc,
10 | 0x402c6c,
11 | 0x402c02,
12 | 0x402ba2,
13 | 0x402b3c,
14 | 0x402ad2,
15 | 0x402a6c,
16 | 0x4029fc,
17 | 0x40298c,
18 | 0x402922,
19 | 0x4028b2,
20 | 0x402842,
21 | 0x4027e2,
22 | 0x402782,
23 | 0x40271c,
24 | 0x4026b2,
25 | 0x40264c,
26 | 0x4025e2,
27 | 0x402582,
28 | 0x402512
29 | ]
30 |
31 | def find_llil_basic_block(llil_basic_blocks, addr):
32 | for llil_bbl in llil_basic_blocks:
33 | if llil_bbl[0].address == addr:
34 | return llil_bbl
35 |
36 | def collect_info(bv, addr):
37 |
38 | bbl = bv.get_basic_blocks_at(addr)[0]
39 |
40 | edges = bbl.outgoing_edges
41 | good_addr = 0
42 | bad_addr = 0
43 | for edge in edges:
44 | if edge.type == BranchType.TrueBranch:
45 | good_addr = edge.target.start
46 | elif edge.type == BranchType.FalseBranch:
47 | bad_addr = edge.target.start
48 |
49 | func = bv.get_functions_containing(addr)[0]
50 | llil_basic_blocks = list(func.llil_basic_blocks)
51 | llil_bbl = find_llil_basic_block(llil_basic_blocks, addr)
52 | src = llil_bbl[0].operands[1].operands[0].operands[0]
53 |
54 | char_idx = 0
55 | if src.operation == LowLevelILOperation.LLIL_ADD:
56 | char_idx = src.operands[1].value.value
57 |
58 | rbx_value = 0
59 | value_set = llil_bbl[0].get_possible_reg_values('rbx')
60 | if value_set.type == RegisterValueType.ConstantValue:
61 | rbx_value = value_set.value
62 | rbx_value &= 0xffffffffffffffff
63 |
64 | return good_addr, bad_addr, char_idx, rbx_value
65 |
66 | def angr_solve(addr, good_addr, bad_addr, char_idx, rbx_value):
67 | proj = angr.Project('./teenager')
68 | state = proj.factory.entry_state(addr = addr)
69 | # suppose the input string (ASCII) is stored at 0xaa000000
70 | input_addr = 0xaa000000
71 | state.regs.rax = input_addr
72 | state.regs.rbx = rbx_value
73 | flag = state.solver.BVS('flag', 8)
74 | state.memory.store(input_addr + char_idx, flag)
75 | simgr = proj.factory.simgr(state)
76 | simgr.explore(find = good_addr, avoid = [bad_addr])
77 | if simgr.found:
78 | solution_state = simgr.found[0]
79 | char_solution = solution_state.solver.eval(flag, cast_to = bytes)
80 | return True, char_solution
81 | else:
82 | False, None
83 |
84 | def main():
85 | solution = [0] * 0x15
86 | for addr in basic_block_addrs:
87 | good_addr, bad_addr, char_idx, rbx_value = collect_info(bv, addr)
88 | print(hex(addr), hex(good_addr), hex(bad_addr), hex(char_idx), hex(rbx_value))
89 | solved, output = angr_solve(addr, good_addr, bad_addr, char_idx, rbx_value)
90 | if solved:
91 | print(hex(char_idx), output)
92 | solution[char_idx] = output.decode('ascii')
93 |
94 | flag = ''.join(solution)
95 | print(flag)
96 |
97 | if __name__ == '__main__':
98 | main()
99 |
100 | # 0CamL_Ints_Ar3_W4rped
101 |
102 | # $ ./teenager
103 | # -= Montrehack =-
104 | # Teenager
105 |
106 | # Enter Password: 0CamL_Ints_Ar3_W4rped
107 |
108 | # [+] Success!
109 | # FLAG-221fddd2bbf810be10d156b060b0eda5
--------------------------------------------------------------------------------
/OCaml_crackmes/teenager:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeffli678/writeups/2bfd17d87af0b4e356b94d28c5f66fe91f529701/OCaml_crackmes/teenager
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # writeups
2 | writeups for CTFs and other stuff
3 |
--------------------------------------------------------------------------------
/ReverseMe3/ReverseMe3.EXE:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeffli678/writeups/2bfd17d87af0b4e356b94d28c5f66fe91f529701/ReverseMe3/ReverseMe3.EXE
--------------------------------------------------------------------------------
/ReverseMe3/ReverseMe3_vm_fixed.exe:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeffli678/writeups/2bfd17d87af0b4e356b94d28c5f66fe91f529701/ReverseMe3/ReverseMe3_vm_fixed.exe
--------------------------------------------------------------------------------
/ReverseMe3/imgs/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeffli678/writeups/2bfd17d87af0b4e356b94d28c5f66fe91f529701/ReverseMe3/imgs/1.png
--------------------------------------------------------------------------------
/ReverseMe3/imgs/10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeffli678/writeups/2bfd17d87af0b4e356b94d28c5f66fe91f529701/ReverseMe3/imgs/10.png
--------------------------------------------------------------------------------
/ReverseMe3/imgs/11.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeffli678/writeups/2bfd17d87af0b4e356b94d28c5f66fe91f529701/ReverseMe3/imgs/11.png
--------------------------------------------------------------------------------
/ReverseMe3/imgs/12.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeffli678/writeups/2bfd17d87af0b4e356b94d28c5f66fe91f529701/ReverseMe3/imgs/12.png
--------------------------------------------------------------------------------
/ReverseMe3/imgs/13.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeffli678/writeups/2bfd17d87af0b4e356b94d28c5f66fe91f529701/ReverseMe3/imgs/13.png
--------------------------------------------------------------------------------
/ReverseMe3/imgs/14.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeffli678/writeups/2bfd17d87af0b4e356b94d28c5f66fe91f529701/ReverseMe3/imgs/14.png
--------------------------------------------------------------------------------
/ReverseMe3/imgs/15.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeffli678/writeups/2bfd17d87af0b4e356b94d28c5f66fe91f529701/ReverseMe3/imgs/15.png
--------------------------------------------------------------------------------
/ReverseMe3/imgs/16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeffli678/writeups/2bfd17d87af0b4e356b94d28c5f66fe91f529701/ReverseMe3/imgs/16.png
--------------------------------------------------------------------------------
/ReverseMe3/imgs/17.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeffli678/writeups/2bfd17d87af0b4e356b94d28c5f66fe91f529701/ReverseMe3/imgs/17.png
--------------------------------------------------------------------------------
/ReverseMe3/imgs/18.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeffli678/writeups/2bfd17d87af0b4e356b94d28c5f66fe91f529701/ReverseMe3/imgs/18.png
--------------------------------------------------------------------------------
/ReverseMe3/imgs/19.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeffli678/writeups/2bfd17d87af0b4e356b94d28c5f66fe91f529701/ReverseMe3/imgs/19.png
--------------------------------------------------------------------------------
/ReverseMe3/imgs/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeffli678/writeups/2bfd17d87af0b4e356b94d28c5f66fe91f529701/ReverseMe3/imgs/2.png
--------------------------------------------------------------------------------
/ReverseMe3/imgs/20.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeffli678/writeups/2bfd17d87af0b4e356b94d28c5f66fe91f529701/ReverseMe3/imgs/20.png
--------------------------------------------------------------------------------
/ReverseMe3/imgs/21.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeffli678/writeups/2bfd17d87af0b4e356b94d28c5f66fe91f529701/ReverseMe3/imgs/21.png
--------------------------------------------------------------------------------
/ReverseMe3/imgs/22.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeffli678/writeups/2bfd17d87af0b4e356b94d28c5f66fe91f529701/ReverseMe3/imgs/22.png
--------------------------------------------------------------------------------
/ReverseMe3/imgs/23.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeffli678/writeups/2bfd17d87af0b4e356b94d28c5f66fe91f529701/ReverseMe3/imgs/23.png
--------------------------------------------------------------------------------
/ReverseMe3/imgs/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeffli678/writeups/2bfd17d87af0b4e356b94d28c5f66fe91f529701/ReverseMe3/imgs/3.png
--------------------------------------------------------------------------------
/ReverseMe3/imgs/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeffli678/writeups/2bfd17d87af0b4e356b94d28c5f66fe91f529701/ReverseMe3/imgs/4.png
--------------------------------------------------------------------------------
/ReverseMe3/imgs/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeffli678/writeups/2bfd17d87af0b4e356b94d28c5f66fe91f529701/ReverseMe3/imgs/5.png
--------------------------------------------------------------------------------
/ReverseMe3/imgs/6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeffli678/writeups/2bfd17d87af0b4e356b94d28c5f66fe91f529701/ReverseMe3/imgs/6.png
--------------------------------------------------------------------------------
/ReverseMe3/imgs/7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeffli678/writeups/2bfd17d87af0b4e356b94d28c5f66fe91f529701/ReverseMe3/imgs/7.png
--------------------------------------------------------------------------------
/ReverseMe3/imgs/8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeffli678/writeups/2bfd17d87af0b4e356b94d28c5f66fe91f529701/ReverseMe3/imgs/8.png
--------------------------------------------------------------------------------
/ReverseMe3/imgs/9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeffli678/writeups/2bfd17d87af0b4e356b94d28c5f66fe91f529701/ReverseMe3/imgs/9.png
--------------------------------------------------------------------------------
/ReverseMe3/name_hash.py:
--------------------------------------------------------------------------------
1 | def rol(val, n):
2 | bin_str = bin(val)[2:]
3 | bin_str = '0' * (32 - len(bin_str)) + bin_str
4 | bin_str = bin_str[n : ] + bin_str[ : n]
5 | return int(bin_str, 2)
6 |
7 | def calc_hash(name):
8 | val = 0
9 | for c in name:
10 | val += ord(c)
11 | val &= 0xffffffff
12 | val = rol(val, ord(c) & 31)
13 | return val
14 |
15 | def main():
16 | hash_1 = calc_hash('LoadLibraryA')
17 | print(hex(hash_1))
18 | hash_2 = calc_hash('MessageBoxA')
19 | print(hex(hash_2))
20 |
21 | main()
--------------------------------------------------------------------------------
/armageddon/angr-solve.py:
--------------------------------------------------------------------------------
1 | import angr
2 | import claripy
3 |
4 | proj = angr.Project('./armageddon')
5 | print(hex(proj.entry))
6 | start_address = 0x14a88
7 | state = proj.factory.entry_state(addr = start_address)
8 |
9 | input_addr = 0xaa000000
10 | r11 = input_addr + 0x34
11 | state.regs.r11 = r11
12 |
13 | n = 42
14 | flag = state.solver.BVS('flag', n * 8)
15 | state.memory.store(input_addr, flag)
16 |
17 | simgr = proj.factory.simgr(state)
18 | good = 0x1504c
19 | simgr.explore(find = good,
20 | avoid = [
21 | 0x10674,
22 | 0x107c8,
23 | 0x109ac,
24 | 0x10b6c,
25 | 0x10cf0,
26 | 0x10ea4,
27 | 0x11010,
28 | 0x11190,
29 | 0x11308,
30 | 0x114a4,
31 | 0x116a8,
32 | 0x1185c,
33 | 0x119c8,
34 | 0x11b84,
35 | 0x11d38,
36 | 0x11f10,
37 | 0x120c4,
38 | 0x122e4,
39 | 0x124c8,
40 | 0x1264c,
41 | 0x12800,
42 | 0x12948,
43 | 0x12b1c,
44 | 0x12d30,
45 | 0x12e9c,
46 | 0x13070,
47 | 0x13248,
48 | 0x133e0,
49 | 0x135f0,
50 | 0x137d4,
51 | 0x13970,
52 | 0x13b50,
53 | 0x13cbc,
54 | 0x13e6c,
55 | 0x14014,
56 | 0x141c8,
57 | 0x1434c,
58 | 0x144c4,
59 | 0x14648,
60 | 0x1485c,
61 | 0x149a0
62 | ]
63 | )
64 |
65 | if simgr.found:
66 | solution_state = simgr.found[0]
67 | input1 = solution_state.solver.eval(flag, cast_to = bytes)
68 | print('flag: ', input1)
69 | else:
70 | print('Counld not find flag')
71 |
72 |
--------------------------------------------------------------------------------
/armageddon/armageddon:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeffli678/writeups/2bfd17d87af0b4e356b94d28c5f66fe91f529701/armageddon/armageddon
--------------------------------------------------------------------------------
/armageddon/imgs/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeffli678/writeups/2bfd17d87af0b4e356b94d28c5f66fe91f529701/armageddon/imgs/1.png
--------------------------------------------------------------------------------
/armageddon/imgs/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeffli678/writeups/2bfd17d87af0b4e356b94d28c5f66fe91f529701/armageddon/imgs/2.png
--------------------------------------------------------------------------------
/armageddon/imgs/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeffli678/writeups/2bfd17d87af0b4e356b94d28c5f66fe91f529701/armageddon/imgs/3.png
--------------------------------------------------------------------------------
/armageddon/qiling_emulate.py:
--------------------------------------------------------------------------------
1 | from qiling import *
2 |
3 | if __name__ == "__main__":
4 | ql = Qiling(["./armageddon"], "/mnt/F/reversing/qiling/examples/rootfs/arm_linux")
5 | ql.run()
6 |
--------------------------------------------------------------------------------
/armageddon/solve.md:
--------------------------------------------------------------------------------
1 | # Solving an ARM challenge with z3
2 |
3 | ## First Impression
4 |
5 | The last week's challenge is hosted at https://crackmes.one/crackme/5edb0b8533c5d449d91ae73b. It is authored by Towel and it is a real challenge in UMDCTF 2019.
6 |
7 | Loading it into BinaryNinja reveals that it is an ARM binary. Not very surprised as its name is `armageddon`. ARM is no longer special for me as I gradually become familiar with the ISA. After all, it is simpler than the x86 and those frequently used instructions are easy to understand and remember.
8 |
9 | BinaryNinja has no problem recognizing the `__libc_start_main` and I can get to `main` easily. The first thing I find is that `main` is a **long** function.
10 |
11 |
12 |
13 | Well, a long function is not necessarily hard to analyze. It probably leverages certain obfuscation and/or its code is pretty repetitive. I started browsing the code from the beginning.
14 |
15 |
16 |
17 | It first prints a welcoming message and then asks the user to type the input. After that, it calls `scanf` with `"%41s"` which reads at most 41 chars from the terminal. Not bad, we now know it accepts a string as the input and we know the maximum length of it.
18 |
19 | We also notice that the basic blocks are split into quite short ones. This is probably an obfuscation technique. Nevetheless BinaryNinja kind of automatically accounts for it so we are not bothered by it. If a disassembler does not correctly inline the blocks after the jump (`b`), it could be harder to analyze.
20 |
21 | After reading the input, the code becomes repetitive: each time, a function is called with the user input as its only parameter. The pattern is repeated until near the bottom of the function, where a loop is found. The loop could be decrypting the flag based on the correct user input. And the checks on the input is obviously inside these called functions.
22 |
23 | I followed the first check function and it looks like this:
24 |
25 |
26 |
27 | Near the bottom of the function we see the comparison and if the comparison is not equal, an error message is printed. After analyzing the algorithm, I find the constraint is:
28 |
29 | ```
30 | passwd[1] * passwd[0x27] * passwd[0x15] + passwd[0x11] + passwd[0x13] * passwd[0x1e] == 0xdb11e
31 | ```
32 |
33 | I browsed several other check functions and they all look similar. Now it becomes obvious: There are a series of constraints and the correct input must satisfy all of them.
34 |
35 |
36 | ## Round One: Failure of angr
37 |
38 | This challenge is very suitable for tools like `angr` or `z3`. In fact angr also uses z3 as its constraint solving backend. However, angr can automatically extract constraints from the binary, which could save a lot of time for reversers. So I decided to first give angr a try.
39 |
40 | The code is not hard to write -- especially they all look similar for different binaries.
41 |
42 | Code for [angr-solve.py](angr-solve.py)
43 |
44 | ```Python
45 | import angr
46 | import claripy
47 |
48 | proj = angr.Project('./armageddon')
49 | print(hex(proj.entry))
50 | start_address = 0x14a88
51 | state = proj.factory.entry_state(addr = start_address)
52 |
53 | input_addr = 0xaa000000
54 | r11 = input_addr + 0x34
55 | state.regs.r11 = r11
56 |
57 | n = 42
58 | flag = state.solver.BVS('flag', n * 8)
59 | state.memory.store(input_addr, flag)
60 |
61 | simgr = proj.factory.simgr(state)
62 | good = 0x1504c
63 | simgr.explore(find = good,
64 | avoid = [
65 | 0x10674,
66 | 0x107c8,
67 | 0x109ac,
68 | 0x10b6c,
69 | 0x10cf0,
70 | 0x10ea4,
71 | 0x11010,
72 | 0x11190,
73 | 0x11308,
74 | 0x114a4,
75 | 0x116a8,
76 | 0x1185c,
77 | 0x119c8,
78 | 0x11b84,
79 | 0x11d38,
80 | 0x11f10,
81 | 0x120c4,
82 | 0x122e4,
83 | 0x124c8,
84 | 0x1264c,
85 | 0x12800,
86 | 0x12948,
87 | 0x12b1c,
88 | 0x12d30,
89 | 0x12e9c,
90 | 0x13070,
91 | 0x13248,
92 | 0x133e0,
93 | 0x135f0,
94 | 0x137d4,
95 | 0x13970,
96 | 0x13b50,
97 | 0x13cbc,
98 | 0x13e6c,
99 | 0x14014,
100 | 0x141c8,
101 | 0x1434c,
102 | 0x144c4,
103 | 0x14648,
104 | 0x1485c,
105 | 0x149a0
106 | ]
107 | )
108 |
109 | if simgr.found:
110 | solution_state = simgr.found[0]
111 | input1 = solution_state.solver.eval(flag, cast_to = bytes)
112 | print('flag: ', input1)
113 | else:
114 | print('Cound not find flag')
115 |
116 | ```
117 |
118 | We tell angr where is the input, and specify a good address to reach, as well as an (optional) list of addresses to avoid. Those addresses to be avoided are those printing error messages.
119 |
120 | This, in theory, should work. However, after running for several minutes angr tells me there is no solution. This is a little bit surprising as I assume as long as there is a solution, angr either returns it or keeps running. There could be multiple reasons for it, e.g., a bug in angr, or the constraints are not properly lifted, etc. We could output the constraints that angr is solving and troubleshot what went wrong. But please allow me to save it as future work.
121 |
122 | ## Round Two: Conquering it with z3
123 |
124 | The next option is to convert the constraints into Python syntax and solve it with z3. The transcribing is arduous work and prone to error. It is better done in an automated or semi-automated way.
125 |
126 | I opened the challenge binary in Ghidra and found that the decompilation is generally good:
127 |
128 | ```C
129 | int FUN_000104fc(int param_1)
130 |
131 | {
132 | if ((uint)*(byte *)(param_1 + 1) *
133 | (uint)*(byte *)(param_1 + 0x27) * (uint)*(byte *)(param_1 + 0x15) +
134 | (uint)*(byte *)(param_1 + 0x11) +
135 | (uint)*(byte *)(param_1 + 0x13) * (uint)*(byte *)(param_1 + 0x1e) != 0xdb11e) {
136 | puts("\n[!] Code did not validate! :(\n");
137 | /* WARNING: Subroutine does not return */
138 | exit(0);
139 | }
140 | return param_1;
141 | }
142 | ```
143 | Then I copy-and-pasted all the constraints into a temp script and converted it into Python syntax. Note this work is still quite repetitive, so I decided to convert the code with a regular expression.
144 |
145 | I did it in VS Code. I searched for
146 |
147 | ```Python
148 | \(uint\)\*\(byte \*\)\(param_1 \+ ((0x)?[0-9a-f]+)\)
149 | ```
150 |
151 | and replaced them with
152 |
153 | ```
154 | passwd[$1]
155 | ```
156 |
157 | Basically, this will convert `(uint)*(byte *)(param_1 + 1)` to `passwd[1]`. There are still manual works needed, like removing the `if`, etc. But those are not hard to do.
158 |
159 | Eventually, the solving script looks like this [(z3_solve.py)](z3_solve.py.py):
160 |
161 | ```Python
162 | from z3 import *
163 |
164 | n = 41
165 | passwd = [BitVec('s_%d' % i, 32) for i in range(n)]
166 |
167 | s = Solver()
168 | for i in range(n):
169 | s.add(passwd[i] >= 0x21)
170 | s.add(passwd[i] <= 127)
171 |
172 | s.add(passwd[1] * passwd[0x27] * passwd[0x15] + passwd[0x11] + passwd[0x13] * passwd[0x1e] == 0xdb11e)
173 | s.add(passwd[0x25] - passwd[0x13] * passwd[0xc] == -0xc0c)
174 | s.add(((passwd[2] - passwd[0x1f]) + passwd[0x21] * passwd[0xd] * passwd[0x14]) - passwd[0x11] == 0xebd1d)
175 | s.add((passwd[7] + passwd[0x24] * passwd[0xf]) - passwd[0x1d] * passwd[0x22] == 0x18e5)
176 | s.add((passwd[0x15] - passwd[0x1b] * passwd[0xf]) - passwd[0x11] == -0x2e3b)
177 | s.add(((passwd[0xf] - passwd[0x25] * passwd[8]) - passwd[5]) - passwd[6] == -0x19a5)
178 | s.add(((passwd[0x23] + passwd[0x1d]) - passwd[0x14]) + passwd[0x1a] == 0xc4)
179 | s.add(passwd[7] * passwd[0x20] + passwd[0x1f] * passwd[0xb] == 0x45ca)
180 | s.add(passwd[0x1d] * passwd[0x18] * passwd[0x24] + passwd[0x25] == 0xac3fb)
181 | s.add(((passwd[8] - passwd[0x10]) - passwd[0xc]) + passwd[0x28] + passwd[0xf] == 0xd0)
182 | s.add((passwd[0x23] * passwd[0x11] * passwd[0x0] - passwd[0xb]) + passwd[0xc] * passwd[7] * passwd[0x26] == 0x172e48)
183 | s.add(((passwd[0x1a] - passwd[0xd]) + passwd[3] * passwd[8]) - passwd[5] == 0x10b8)
184 | s.add(passwd[3] + passwd[0x11] + passwd[0x24] + passwd[0x14] == 0x160)
185 | s.add((passwd[0x1a] - passwd[0x15] * passwd[0x12]) + passwd[0x1b] * passwd[0x19] == 0x8a2)
186 | s.add((passwd[0x22] - passwd[0xe]) + passwd[5] * passwd[0x21] + passwd[0x23] == 0x1bd8)
187 | s.add(passwd[5] * passwd[8] * passwd[0x26] * passwd[0x19] + passwd[0x15] + passwd[0x23] == 0x2ca6988)
188 | s.add((passwd[8] * passwd[8] + passwd[0x15] * passwd[0xc]) - passwd[0x24] == 0x2430)
189 | s.add((((passwd[0x23] + passwd[2]) - passwd[7]) - passwd[9] * passwd[0x12]) + passwd[2] * passwd[0x27] == 0x2de)
190 | s.add(((passwd[5] * (passwd[0x11] - 1) - passwd[6]) - passwd[0x14]) - passwd[0x22] * passwd[0x17] == -0x11d5)
191 | s.add((passwd[0x22] - passwd[0xb]) + passwd[0xb] * passwd[0xd] == 0x2aba)
192 | s.add((passwd[0x22] - passwd[0xb]) + passwd[0xb] * passwd[0xd] == 0x2aba)
193 | s.add(passwd[0x1b] + passwd[0x12] * passwd[0xf] + passwd[0x20] + passwd[9] == 0x2668)
194 | s.add(passwd[0x15] - passwd[0xe] * passwd[0x1d] == -0x1400)
195 | s.add((((passwd[9] * passwd[9] - passwd[10]) + passwd[0xd]) - passwd[0x24]) - passwd[0x14] == 0x19ac)
196 | s.add(((passwd[0xc] + passwd[2] + passwd[0x22]) - passwd[4] * passwd[0x14] * passwd[0x17]) + passwd[0x16] == -0xafa0c)
197 | s.add(((passwd[4] + passwd[5]) - passwd[10]) + passwd[0x1b] == 0xb4)
198 | s.add(((passwd[0xf] - passwd[0x1c]) - passwd[0x25]) - passwd[0x18] * passwd[0x12] * passwd[0x0] == -0xd06e8)
199 | s.add(((passwd[4] * passwd[0x23] + passwd[0x19]) - passwd[0x15]) - passwd[0x18] * passwd[0x14] == -0x1f8)
200 | s.add((((passwd[0x19] + passwd[10]) - passwd[0xf]) + passwd[0x1c]) - passwd[0x21] == 0x3e)
201 | s.add((((passwd[6] - passwd[0x19]) + passwd[2]) - passwd[0x19]) + passwd[1] + passwd[0x12] * passwd[0x1c] == 0x1eb9)
202 | s.add(passwd[0xb] * (passwd[5] + passwd[0x22] * passwd[0x16]) + passwd[0xc] + passwd[0x22] == 0x121b93)
203 | s.add((((passwd[3] + passwd[0xe]) - passwd[0x26]) - passwd[0xd]) - passwd[1] == -0x80)
204 | s.add((((passwd[0x1e] + passwd[0x15]) - passwd[0x11]) - passwd[0x17] * passwd[5]) + passwd[0x21] == -0x1afd)
205 | s.add((passwd[7] - passwd[0xe]) + passwd[0x11] + passwd[0x21] == 0xdf)
206 | s.add((passwd[8] - passwd[3]) + passwd[2] * passwd[10] * passwd[10] == 0x626e2)
207 | s.add(((passwd[0x25] + passwd[7]) - passwd[0x13]) + passwd[0xc] + passwd[0xb] == 0x12f)
208 | s.add(passwd[1] + passwd[8] * passwd[0x14] + passwd[0x20] + passwd[0xf] == 0x167a)
209 | s.add((passwd[0x11] - passwd[4]) - passwd[0x1d] * passwd[0x12] == -0x11ca)
210 | s.add((passwd[0xd] * passwd[0x16] - passwd[10]) - passwd[0x23] == 0x32e9)
211 | s.add(passwd[0xd] + passwd[0xb] + passwd[0x1d] * passwd[0x13] == 0xec9)
212 | s.add((((passwd[0x19] + passwd[0x26] * passwd[0xf]) - passwd[0xb]) + passwd[0x20]) - passwd[0x15] * passwd[0x22] == 0x2a)
213 | s.add(passwd[6] * passwd[9] + passwd[0x23] == 0xedd)
214 |
215 | if s.check() == sat:
216 | print('solved!')
217 | m = s.model()
218 | flag = ''
219 | for i in range(n):
220 | c = m[passwd[i]].as_long()
221 | flag += chr(c)
222 | print(flag)
223 | else:
224 | print('failed')
225 | ```
226 |
227 | One thing to mention here is although the individual chars of passwd are only 8 bits wide, we declare them to be 32-bit wide. Otherwise, it could cause a problem to the `==` at the end of the line. Obviously, we have to add the constraint `passwd[i] >= 0x21` and `passwd[i] <= 127`, to actually enforce they are printable ASCII chars.
228 |
229 | Running this immediately returns the flag:
230 |
231 | ```
232 | UMDCTF-{ARM_1s_s0_SATisfying_7y8fdlsjebn}
233 | ```
234 |
235 | ## Epilog
236 |
237 | Despite z3 returns a result and it looks quite convincing, there are still some code below the last constraint. Typically, in CTF, this means the correct input that passes the constraints is NOT the actual flag; rather the input is used to decrypt the flag to be submitted. However, the above code is already in a good flag format. This confuses me so I decide to run the binary to see what happens. There is an excellent tool for this situation: the process level emulator -- [`Qiling`](https://www.qiling.io/).
238 |
239 | Qiling is an emulator based on Unicorn. It is simpler than Qemu since it only emulates the process that we are interested in. So there is no need to set up a bulky OS to run it. The code is extremely simple [(qiling_emulate.py)](qiling_emulate.py):
240 |
241 | ```Python
242 | from qiling import *
243 |
244 | if __name__ == "__main__":
245 | ql = Qiling(["./armageddon"], "QILING_INSTALL_PATH/examples/rootfs/arm_linux")
246 | ql.run()
247 | ```
248 |
249 | Since major system calls are implemented by Qiling, the program executes properly. Below is an excerpt of the output:
250 |
251 | ```
252 | write(1,27008,16) = 0
253 | [+] write() CONTENT: bytearray(b'[+] Enter Code: ')
254 | [+] Enter Code: UMDCTF-{ARM_1s_s0_SATisfying_7y8fdlsjebn}
255 | read(0, 0x29010, 0x2000) = 42
256 | write(1,27008,1) = 0
257 | [+] write() CONTENT: bytearray(b'\n')
258 |
259 | write(1,27008,33) = 0
260 | [+] write() CONTENT: bytearray(b'[+] Code validated successfully!\n')
261 | [+] Code validated successfully!
262 | write(1,27008,1) = 0
263 | [+] write() CONTENT: bytearray(b'\n')
264 |
265 | [!] 0xf7ca9be8: syscall number = 0x8c(140) not implemented
266 | exit_group(0)
267 | ```
268 |
269 | So after we supply the correct code, it simply prints `Code validated successfully!\n`.
270 |
271 | LoL! I forget that it does not tell us the code is correct yet. Well, not bad, since playing with Qiling is quite painless.
--------------------------------------------------------------------------------
/armageddon/z3_solve.py:
--------------------------------------------------------------------------------
1 | from z3 import *
2 |
3 | n = 41
4 | passwd = [BitVec('s_%d' % i, 32) for i in range(n)]
5 |
6 | s = Solver()
7 | for i in range(n):
8 | s.add(passwd[i] >= 0x21)
9 | s.add(passwd[i] <= 127)
10 |
11 | s.add(passwd[1] * passwd[0x27] * passwd[0x15] + passwd[0x11] + passwd[0x13] * passwd[0x1e] == 0xdb11e)
12 | s.add(passwd[0x25] - passwd[0x13] * passwd[0xc] == -0xc0c)
13 | s.add(((passwd[2] - passwd[0x1f]) + passwd[0x21] * passwd[0xd] * passwd[0x14]) - passwd[0x11] == 0xebd1d)
14 | s.add((passwd[7] + passwd[0x24] * passwd[0xf]) - passwd[0x1d] * passwd[0x22] == 0x18e5)
15 | s.add((passwd[0x15] - passwd[0x1b] * passwd[0xf]) - passwd[0x11] == -0x2e3b)
16 | s.add(((passwd[0xf] - passwd[0x25] * passwd[8]) - passwd[5]) - passwd[6] == -0x19a5)
17 | s.add(((passwd[0x23] + passwd[0x1d]) - passwd[0x14]) + passwd[0x1a] == 0xc4)
18 | s.add(passwd[7] * passwd[0x20] + passwd[0x1f] * passwd[0xb] == 0x45ca)
19 | s.add(passwd[0x1d] * passwd[0x18] * passwd[0x24] + passwd[0x25] == 0xac3fb)
20 | s.add(((passwd[8] - passwd[0x10]) - passwd[0xc]) + passwd[0x28] + passwd[0xf] == 0xd0)
21 | s.add((passwd[0x23] * passwd[0x11] * passwd[0x0] - passwd[0xb]) + passwd[0xc] * passwd[7] * passwd[0x26] == 0x172e48)
22 | s.add(((passwd[0x1a] - passwd[0xd]) + passwd[3] * passwd[8]) - passwd[5] == 0x10b8)
23 | s.add(passwd[3] + passwd[0x11] + passwd[0x24] + passwd[0x14] == 0x160)
24 | s.add((passwd[0x1a] - passwd[0x15] * passwd[0x12]) + passwd[0x1b] * passwd[0x19] == 0x8a2)
25 | s.add((passwd[0x22] - passwd[0xe]) + passwd[5] * passwd[0x21] + passwd[0x23] == 0x1bd8)
26 | s.add(passwd[5] * passwd[8] * passwd[0x26] * passwd[0x19] + passwd[0x15] + passwd[0x23] == 0x2ca6988)
27 | s.add((passwd[8] * passwd[8] + passwd[0x15] * passwd[0xc]) - passwd[0x24] == 0x2430)
28 | s.add((((passwd[0x23] + passwd[2]) - passwd[7]) - passwd[9] * passwd[0x12]) + passwd[2] * passwd[0x27] == 0x2de)
29 | s.add(((passwd[5] * (passwd[0x11] - 1) - passwd[6]) - passwd[0x14]) - passwd[0x22] * passwd[0x17] == -0x11d5)
30 | s.add((passwd[0x22] - passwd[0xb]) + passwd[0xb] * passwd[0xd] == 0x2aba)
31 | s.add((passwd[0x22] - passwd[0xb]) + passwd[0xb] * passwd[0xd] == 0x2aba)
32 | s.add(passwd[0x1b] + passwd[0x12] * passwd[0xf] + passwd[0x20] + passwd[9] == 0x2668)
33 | s.add(passwd[0x15] - passwd[0xe] * passwd[0x1d] == -0x1400)
34 | s.add((((passwd[9] * passwd[9] - passwd[10]) + passwd[0xd]) - passwd[0x24]) - passwd[0x14] == 0x19ac)
35 | s.add(((passwd[0xc] + passwd[2] + passwd[0x22]) - passwd[4] * passwd[0x14] * passwd[0x17]) + passwd[0x16] == -0xafa0c)
36 | s.add(((passwd[4] + passwd[5]) - passwd[10]) + passwd[0x1b] == 0xb4)
37 | s.add(((passwd[0xf] - passwd[0x1c]) - passwd[0x25]) - passwd[0x18] * passwd[0x12] * passwd[0x0] == -0xd06e8)
38 | s.add(((passwd[4] * passwd[0x23] + passwd[0x19]) - passwd[0x15]) - passwd[0x18] * passwd[0x14] == -0x1f8)
39 | s.add((((passwd[0x19] + passwd[10]) - passwd[0xf]) + passwd[0x1c]) - passwd[0x21] == 0x3e)
40 | s.add((((passwd[6] - passwd[0x19]) + passwd[2]) - passwd[0x19]) + passwd[1] + passwd[0x12] * passwd[0x1c] == 0x1eb9)
41 | s.add(passwd[0xb] * (passwd[5] + passwd[0x22] * passwd[0x16]) + passwd[0xc] + passwd[0x22] == 0x121b93)
42 | s.add((((passwd[3] + passwd[0xe]) - passwd[0x26]) - passwd[0xd]) - passwd[1] == -0x80)
43 | s.add((((passwd[0x1e] + passwd[0x15]) - passwd[0x11]) - passwd[0x17] * passwd[5]) + passwd[0x21] == -0x1afd)
44 | s.add((passwd[7] - passwd[0xe]) + passwd[0x11] + passwd[0x21] == 0xdf)
45 | s.add((passwd[8] - passwd[3]) + passwd[2] * passwd[10] * passwd[10] == 0x626e2)
46 | s.add(((passwd[0x25] + passwd[7]) - passwd[0x13]) + passwd[0xc] + passwd[0xb] == 0x12f)
47 | s.add(passwd[1] + passwd[8] * passwd[0x14] + passwd[0x20] + passwd[0xf] == 0x167a)
48 | s.add((passwd[0x11] - passwd[4]) - passwd[0x1d] * passwd[0x12] == -0x11ca)
49 | s.add((passwd[0xd] * passwd[0x16] - passwd[10]) - passwd[0x23] == 0x32e9)
50 | s.add(passwd[0xd] + passwd[0xb] + passwd[0x1d] * passwd[0x13] == 0xec9)
51 | s.add((((passwd[0x19] + passwd[0x26] * passwd[0xf]) - passwd[0xb]) + passwd[0x20]) - passwd[0x15] * passwd[0x22] == 0x2a)
52 | s.add(passwd[6] * passwd[9] + passwd[0x23] == 0xedd)
53 |
54 | if s.check() == sat:
55 | print('solved!')
56 | m = s.model()
57 | flag = ''
58 | for i in range(n):
59 | c = m[passwd[i]].as_long()
60 | flag += chr(c)
61 | print(flag)
62 | else:
63 | print('failed')
64 |
65 | # UMDCTF-{ARM_1s_s0_SATisfying_7y8fdlsjebn}
--------------------------------------------------------------------------------
/automating-gdb/README.md:
--------------------------------------------------------------------------------
1 | # Solving a Recursive Crackme by Automating GDB
2 |
3 | The last week's challenge is called [`Recursion`](https://0x00sec.org/t/reverseme-recursion/21802). From the name we already expect to do some automation -- manually solving stuff recursively is not a wise idea.
4 |
5 | ## First Impression
6 |
7 | The forum probably does not allow users to post binary files, so challenges are all posted as base64 encoded. There are too many ways to restore the binary, but Binary Ninja saves you from remembering the command: Just copy the encoded text, create a new empty binary, and then click "Paste From" -> "Base64". Then you are done!
8 |
9 |
10 |
11 | We get a 14.5 kB ELF file. There is some mild obfuscation in the start of the `main`, which does not pose a serious challenge. In the middle of the `main` we see the program is reading input and checking length:
12 |
13 |
14 |
15 | The first thing I notice is that the input must be exactly 0x50 chars, which is quite unusual. Not it reads at most 0x50 chars and checks if the chars read are at least 0x50 chars, which means it must be 0x50 chars.
16 |
17 | Besides, after the length check, we see it calls `mmap`. For reversing challenges, once we see a `mmap` in it, probably there is a self-modifying code.
18 |
19 |
20 |
21 | Moving downward we see that the program copies a 0xae4-byte buffer into the newly allocated buffer, and then calls it. A strange thing here is the user input is moved into register `r12`. Typically, no compilers will use register `r12` to pass function argument, so this code might be hand-crafted.
22 |
23 | After the `call rdx`, the program tells if the flag is correct based on the return value. Now the next step is obvious, we need to define a function on that code buffer and see what it has.
24 |
25 | ## Decryption routine
26 |
27 |
28 |
29 | The function looks like this. The loop decrypts another buffer at `data_20ab`, whose size is 0xa59. The decryption is just xor with 0x9f. Note the code_size variable sits right after this function, and right before the next data buffer to be decrypted. Meanwhile, the loop calculates a checksum of the next data buffer, and compare it with the dword at register `r12`. What is it? It is the user input! So the user input must match the checksum value.
30 |
31 | If the checksum matches, the program continues to execute the second newly decrypted buffer. Here, we can use Binary Ninja's transformation to transform the data in place, after which we define a function at the start of it.
32 |
33 |
34 |
35 | The newly defined function looks like this:
36 |
37 |
38 |
39 | It looks almost the same as the previous one, except for some small mutations. The xor key is different and it is 0xb6 this time. The buffer size is 0x9ce this time, which is smaller than the previous one. And that indicates we are probably recursively decrypting this buffer and each time we only decrypt the first part of it, which forms a function.
40 |
41 | I tried to repeat the process a few times and it just repeats. RECURSION. That is probably a good reason for the name.
42 |
43 | The first way to solve this is to solve it statically. We only need to get the xor key and the buffer size, to decrypt the buffer and calculate the checksum. However, due to the mutation, it is not that easy to get it correct. It is, though, definitely possible, but not optimal. So I come up with a dynamic approach.
44 |
45 | ## Using Hardware Breakpoints and Automating GDB
46 |
47 | I did not rewrite the checksum algorithm by myself, despite it is super simple. Even if it is super complex and I cannot reverse/rewrite it, I can still solve this challenge. Why?
48 |
49 | Because we can wait at the line where the dword from the user input is compared with the correct checksum. Particularly, it is the `cmp esi, edi` line. the register `esi` holds our input, which, during debugging, is trash. Register `edi` holds the correct checksum. If we set a breakpoint here and examine the value of `edi`, we directly get the correct checksum.
50 |
51 | However, this approach cannot easily scale to the entire challenge. The problem here is we do not know where to set the next breakpoint before we decrypt the code. However, manually decrypting the code is arduous and error-prone, so we would better automate the solution.
52 |
53 | Note the address of the user input buffer is moved into r12 and never changed. If one checksum matches, the program executes `add r12, 0x4` to move to the next dword. So we can use a hardware breakpoint to catch the program when it reads the buffer `r12`, and read the value of `edi`. Then we remove the current hardware breakpoint, set a new one on the next address, and wait for the program to break again.
54 |
55 | Automating GDB is easier said than done. I have known it is possible for a long time, though I have never done it before. After duckduckgo-ing a little bit, I found there are two ways to do it. The first one is to implement a GDB command in Python; the second way is to use pygdbmi to interact with GDB's machine interface.
56 |
57 | Both methods allow us to execute gdb commands as if we directly use GDB, and get the output from GDB afterward. However, I found the pygdbmi approach is much harder to use for the current purpose. First of all, it runs GDB headlessly. So if there is an error in the script, it is hard to find it. Conversely, if we take the first approach, since we register ourselves as a new command (`solve` in particular) after we run the stuff we are still in GDB. We can see the commands we executed and see the outputs from GDB, which allows painless debugging. Also, despite the name machine interface, it does not automatically parse the string output from GDB. For example, if we examine the value of `rdi` by executing
58 |
59 | ```p/x $rdi```
60 |
61 | The GDB returns something like:
62 |
63 | ```$1 = 0x555555557e90```
64 |
65 | I would expect the pygdbmi to parse the value for me. However, it does nothing for this and directly returns the string output. We get the very same thing in the first approach. So obviously it is the better way to do it.
66 |
67 | Note that I am not saying gdbmi is not good. It is used by various projects, e.g., gdbgui, which is a browser-based GDB frontend. If you have not tried it, I strongly recommend you to experiment with it. It is just using gdbmi will require more development work and it is not suitable for reversing challenge, where we care more about getting things rolling faster.
68 |
69 | Ok, so much for the comparison. It is time to get to the code. The code is not fancy -- it just requires some effort to write it correctly.
70 |
71 | ```Python
72 | import gdb
73 | import struct
74 |
75 | def get_reg_value(response):
76 | response = response.split()[2]
77 | value = int(response, 16)
78 | return value
79 |
80 | class Solve(gdb.Command):
81 | def __init__(self):
82 | # This registers our class as "solve"
83 | super(Solve, self).__init__("solve", gdb.COMMAND_DATA)
84 |
85 | def invoke(self, arg, from_tty):
86 | # When we call "solve" from gdb, this is the method
87 | # that will be called.
88 |
89 | dummy_input = open('input.txt', 'wb')
90 | dummy_input.write(b'1' * 0x50)
91 | dummy_input.close()
92 |
93 | solution = bytes()
94 |
95 | inferiors = gdb.inferiors()
96 | inferior = inferiors[0]
97 | gdb.execute('del')
98 | gdb.execute('file crackme.elf')
99 | gdb.execute('set breakpoint pending on')
100 | gdb.execute('b __libc_start_main')
101 | gdb.execute('r < input.txt')
102 | response = gdb.execute('p/x $rdi', to_string = True)
103 | main_addr = get_reg_value(response)
104 | main_addr_raw = 0x1229
105 | print(main_addr)
106 | base = main_addr - main_addr_raw
107 |
108 | gdb.execute('b *%d' % (base + 0x1399))
109 | gdb.execute('c')
110 |
111 | response = gdb.execute('p/x $rax', to_string = True)
112 | input_addr = get_reg_value(response)
113 | print('input_addr', hex(input_addr))
114 |
115 | i = 0
116 | while True:
117 | try:
118 | gdb.execute('del')
119 | gdb.execute('rwatch *%d' % (input_addr + i * 4))
120 | gdb.execute('c')
121 |
122 | response = gdb.execute('p/x $edi', to_string = True)
123 | checksum = get_reg_value(response)
124 | print('checksum', hex(checksum))
125 | solution += struct.pack('
2 |
3 |
79 |
80 |
81 |
This CFG generated by Binary Ninja from recursion.bndb on Wed 29 Jul 2020 02:39:15 PM CST showing 20ab as Assembly.