|
10 | 10 | from qiling.const import QL_VERBOSE |
11 | 11 | from qiling.extensions import pipe |
12 | 12 |
|
13 | | -def instruction_count(ql: Qiling, address: int, size: int, user_data): |
14 | | - user_data[0] += 1 |
15 | | - |
16 | | -def get_count(flag): |
17 | | - ql = Qiling(["rootfs/x86_windows/bin/crackme.exe"], "rootfs/x86_windows", verbose=QL_VERBOSE.OFF) |
18 | | - |
19 | | - ql.os.stdin = pipe.SimpleStringBuffer() |
20 | | - ql.os.stdout = pipe.SimpleStringBuffer() |
21 | | - |
22 | | - ql.os.stdin.write(bytes("".join(flag) + "\n", 'utf-8')) |
23 | | - count = [0] |
24 | | - |
25 | | - ql.hook_code(instruction_count, count) |
26 | | - ql.run() |
27 | | - |
28 | | - print(ql.os.stdout.read().decode('utf-8'), end='') |
29 | | - print(" ============ count: %d ============ " % count[0]) |
30 | | - |
31 | | - return count[0] |
32 | | - |
33 | | -def solve(): |
34 | | - # BJWXB_CTF{C5307D46-E70E-4038-B6F9-8C3F698B7C53} |
35 | | - prefix = list("BJWXB_CTF{C5307D46-E70E-4038-B6F9-8C3F698B7C") |
36 | | - flag = list("\x00" * 100) |
37 | | - base = get_count(prefix + flag) |
38 | | - i = 0 |
39 | | - |
40 | | - try: |
41 | | - for i in range(len(flag)): |
42 | | - for j in "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-{}": |
43 | | - flag[i] = j |
44 | | - data = get_count(prefix + flag) |
45 | | - if data > base: |
46 | | - base = data |
47 | | - print("\n\n\n>>> FLAG: " + "".join(prefix + flag) + "\n\n\n") |
48 | | - break |
49 | | - if flag[i] == "}": |
| 13 | +ROOTFS = r"rootfs/x86_windows" |
| 14 | + |
| 15 | +class Solver: |
| 16 | + def __init__(self, invalid: bytes): |
| 17 | + # create a silent qiling instance |
| 18 | + self.ql = Qiling([rf"{ROOTFS}/bin/crackme.exe"], ROOTFS, verbose=QL_VERBOSE.OFF) |
| 19 | + |
| 20 | + self.ql.os.stdin = pipe.SimpleInStream(sys.stdin.fileno()) # take over the input to the program using a fake stdin |
| 21 | + self.ql.os.stdout = pipe.NullOutStream(sys.stdout.fileno()) # disregard program output |
| 22 | + |
| 23 | + # execute program until it reaches the part that asks for input |
| 24 | + self.ql.run(end=0x401030) |
| 25 | + |
| 26 | + # record replay starting and ending points. |
| 27 | + # |
| 28 | + # since the emulation halted upon entering a function, its return address is there on |
| 29 | + # the stack. we use it to limit the emulation till function returns |
| 30 | + self.replay_starts = self.ql.arch.regs.arch_pc |
| 31 | + self.replay_ends = self.ql.stack_read(0) |
| 32 | + |
| 33 | + # instead of restarting the whole program every time a new flag character is guessed, |
| 34 | + # we will restore its state to the latest point possible, fast-forwarding a good |
| 35 | + # amount of start-up code that is not affected by the input. |
| 36 | + # |
| 37 | + # here we save the state just when the read input function is about to be called so we |
| 38 | + # could use it to jumpstart the initialization part and get to the read input immediately |
| 39 | + self.jumpstart = self.ql.save() or {} |
| 40 | + |
| 41 | + # calibrate the replay instruction count by running the code with an invalid input |
| 42 | + # first. the instruction count returned from the calibration process will be then |
| 43 | + # used as a baseline for consequent replays |
| 44 | + self.best_icount = self.__run(invalid) |
| 45 | + |
| 46 | + def __run(self, input: bytes) -> int: |
| 47 | + icount = [0] |
| 48 | + |
| 49 | + def __count_instructions(ql: Qiling, address: int, size: int): |
| 50 | + icount[0] += 1 |
| 51 | + |
| 52 | + # set a hook to fire up every time an instruction is about to execute |
| 53 | + hobj = self.ql.hook_code(__count_instructions) |
| 54 | + |
| 55 | + # feed stdin with input |
| 56 | + self.ql.os.stdin.write(input + b'\n') |
| 57 | + |
| 58 | + # resume emulation till function returns |
| 59 | + self.ql.run(begin=self.replay_starts, end=self.replay_ends) |
| 60 | + |
| 61 | + hobj.remove() |
| 62 | + |
| 63 | + return icount[0] |
| 64 | + |
| 65 | + def replay(self, input: bytes) -> bool: |
| 66 | + """Restore state and replay with a new input. |
| 67 | +
|
| 68 | + Returns an indication to execution progress: `True` if a progress |
| 69 | + was made, `False` otherwise |
| 70 | + """ |
| 71 | + |
| 72 | + # restore program's state back to the starting point |
| 73 | + self.ql.restore(self.jumpstart) |
| 74 | + |
| 75 | + # resume emulation and count emulated instructions |
| 76 | + curr_icount = self.__run(input) |
| 77 | + |
| 78 | + # the larger part of the input is correct, the more instructions are expected to be executed. this is true |
| 79 | + # for traditional loop-based validations like strcmp or memcmp which bails as soon as a mismatch is found: |
| 80 | + # more correct characters mean more loop iterations - thus more executed instructions. |
| 81 | + # |
| 82 | + # if we got a higher instruction count, it means we made a progress in the right direction |
| 83 | + if curr_icount > self.best_icount: |
| 84 | + self.best_icount = curr_icount |
| 85 | + |
| 86 | + return True |
| 87 | + |
| 88 | + return False |
| 89 | + |
| 90 | +def progress(msg: str) -> None: |
| 91 | + print(msg, end='\r', file=sys.stderr, flush=True) |
| 92 | + |
| 93 | +def main(): |
| 94 | + flag = bytearray(b'BJWXB_CTF{********-****-****-****-************}') |
| 95 | + indices = (i for i, ch in enumerate(flag) if ch == ord('*')) |
| 96 | + |
| 97 | + # uppercase hex digits |
| 98 | + charset = '0123456789ABCDEF' |
| 99 | + |
| 100 | + progress('Initializing...') |
| 101 | + solver = Solver(flag) |
| 102 | + |
| 103 | + for i in indices: |
| 104 | + for ch in charset: |
| 105 | + flag[i] = ord(ch) |
| 106 | + |
| 107 | + progress(f'Guessing... {flag.decode()}') |
| 108 | + |
| 109 | + if solver.replay(flag): |
50 | 110 | break |
51 | | - print("SOLVED!!!") |
52 | | - except KeyboardInterrupt: |
53 | | - print("STOP: KeyboardInterrupt") |
| 111 | + |
| 112 | + else: |
| 113 | + raise RuntimeError('no match found') |
| 114 | + |
| 115 | + print(f'\nFlag found!') |
54 | 116 |
|
55 | 117 | if __name__ == "__main__": |
56 | | - solve() |
| 118 | + main() |
| 119 | + |
| 120 | +# expected flag: BJWXB_CTF{C5307D46-E70E-4038-B6F9-8C3F698B7C53} |
0 commit comments