Skip to content

Commit 17d5898

Browse files
authored
Merge pull request #891 from elicn/misc_improv
Re-implemented Linux crackme example
2 parents 19f662d + 7a312c5 commit 17d5898

File tree

4 files changed

+105
-63
lines changed

4 files changed

+105
-63
lines changed

examples/crackme_x86_linux.py

Lines changed: 92 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -6,91 +6,129 @@
66
import sys
77
sys.path.append("..")
88

9-
import os
9+
import string
10+
from typing import TextIO
11+
1012
from qiling import Qiling
13+
from qiling.os.posix import stat
14+
15+
ROOTFS = r"rootfs/x86_linux"
1116

12-
class MyPipe():
17+
class MyPipe(TextIO):
1318
def __init__(self):
14-
self.buf = b''
19+
self.buf = bytearray()
1520

16-
def write(self, s):
17-
self.buf += s
21+
def write(self, s: bytes):
22+
self.buf.extend(s)
1823

19-
def read(self, size):
20-
if size <= len(self.buf):
21-
ret = self.buf[: size]
22-
self.buf = self.buf[size:]
23-
else:
24-
ret = self.buf
25-
self.buf = ''
26-
return ret
24+
def read(self, size: int) -> bytes:
25+
ret = self.buf[:size]
26+
self.buf = self.buf[size:]
27+
28+
return bytes(ret)
2729

28-
def fileno(self):
29-
return 0
30+
def fileno(self) -> int:
31+
return sys.stdin.fileno()
3032

31-
def show(self):
32-
pass
33+
def fstat(self):
34+
return stat.Fstat(self.fileno())
3335

34-
def clear(self):
35-
pass
36+
class Solver:
37+
def __init__(self, invalid: bytes):
38+
# create a silent qiling instance
39+
self.ql = Qiling([rf"{ROOTFS}/bin/crackme_linux"], ROOTFS,
40+
console=False, # thwart qiling logger output
41+
stdin=MyPipe(), # take over the input to the program using a fake stdin
42+
stdout=sys.stdout) # thwart program output
3643

37-
def flush(self):
38-
pass
44+
# execute program until it reaches the 'main' function
45+
self.ql.run(end=0x0804851b)
3946

40-
def close(self):
41-
self.outpipe.close()
47+
# record replay starting and ending points.
48+
#
49+
# since the emulation halted upon entering 'main', its return address is there on
50+
# the stack. we use it to limit the emulation till function returns
51+
self.replay_starts = self.ql.reg.arch_pc
52+
self.replay_ends = self.ql.stack_read(0)
4253

43-
def fstat(self):
44-
return os.fstat(sys.stdin.fileno())
54+
# instead of restarting the whole program every time a new flag character is guessed,
55+
# we will restore its state to the latest point possible, fast-forwarding a good
56+
# amount of start-up code that is not affected by the input.
57+
#
58+
# here we save the state just when 'main' is about to be called so we could use it
59+
# to jumpstart the initialization part and get to 'main' immediately
60+
self.jumpstart = self.ql.save() or {}
61+
62+
# calibrate the replay instruction count by running the code with an invalid input
63+
# first. the instruction count returned from the calibration process will be then
64+
# used as a baseline for consequent replays
65+
self.best_icount = self.__run(invalid)
66+
67+
def __run(self, input: bytes) -> int:
68+
icount = [0]
4569

46-
def instruction_count(ql: Qiling, address: int, size: int, user_data):
47-
user_data[0] += 1
70+
def __count_instructions(ql: Qiling, address: int, size: int):
71+
icount[0] += 1
4872

49-
def my__llseek(ql, *args, **kw):
50-
pass
73+
# set a hook to fire up every time an instruction is about to execute
74+
hobj = self.ql.hook_code(__count_instructions)
5175

52-
def run_one_round(payload: bytes):
53-
stdin = MyPipe()
76+
# feed stdin with input
77+
self.ql.stdin.write(input + b'\n')
5478

55-
ql = Qiling(["rootfs/x86_linux/bin/crackme_linux"], "rootfs/x86_linux",
56-
console=False, # thwart qiling logger output
57-
stdin=stdin, # take over the input to the program
58-
stdout=sys.stdout) # thwart program output
79+
# resume emulation till function returns
80+
self.ql.run(begin=self.replay_starts, end=self.replay_ends)
5981

60-
ins_count = [0]
61-
ql.hook_code(instruction_count, ins_count)
62-
ql.set_syscall("_llseek", my__llseek)
82+
hobj.remove()
6383

64-
stdin.write(payload + b'\n')
65-
ql.run()
84+
return icount[0]
6685

67-
del stdin
68-
del ql
86+
def replay(self, input: bytes) -> bool:
87+
"""Restore state and replay with a new input.
6988
70-
return ins_count[0]
89+
Returns an indication to execution progress: `True` if a progress
90+
was made, `False` otherwise
91+
"""
7192

72-
def solve():
93+
# restore program's state back to the starting point
94+
self.ql.restore(self.jumpstart)
95+
96+
# resume emulation and count emulated instructions
97+
curr_icount = self.__run(input)
98+
99+
# the larger part of the input is correct, the more instructions are expected to be executed. this is true
100+
# for traditional loop-based validations like strcmp or memcmp which bails as soon as a mismatch is found:
101+
# more correct characters mean more loop iterations - thus more executed instructions.
102+
#
103+
# if we got a higher instruction count, it means we made a progress in the right direction
104+
if curr_icount > self.best_icount:
105+
self.best_icount = curr_icount
106+
107+
return True
108+
109+
return False
110+
111+
def main():
73112
idx_list = (1, 4, 2, 0, 3)
74113
flag = [0] * len(idx_list)
75114

76-
prev_ic = run_one_round(bytes(flag))
115+
solver = Solver(bytes(flag))
116+
77117
for idx in idx_list:
78118

79119
# bruteforce all possible flag characters
80-
for ch in '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ ':
120+
for ch in string.printable:
81121
flag[idx] = ord(ch)
82122

83-
print(f'\rguessing char at {idx}: {ch}... ', end='', flush=True)
84-
ic = run_one_round(bytes(flag))
123+
print(f'\rGuessing... [{"".join(chr(ch) if ch else "_" for ch in flag)}]', end='', file=sys.stderr, flush=True)
85124

86-
if ic > prev_ic:
87-
print(f'ok')
88-
prev_ic = ic
125+
if solver.replay(bytes(flag)):
89126
break
127+
90128
else:
91-
print(f'no match found')
129+
print(f'No match found')
92130

93-
print(f'flag: "{"".join(chr(ch) for ch in flag)}"')
131+
print(f'\nFlag found!')
94132

95133
if __name__ == "__main__":
96-
solve()
134+
main()

qiling/core.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
# See https://stackoverflow.com/questions/39740632/python-type-hinting-without-cyclic-imports
1010
from typing import Dict, List, Union
1111
from typing import TYPE_CHECKING
12+
13+
from unicorn.unicorn import Uc
1214
if TYPE_CHECKING:
1315
from .arch.register import QlRegisterManager
1416
from .arch.arch import QlArch
@@ -654,7 +656,7 @@ def filter(self, ft):
654656
self._log_filter.update_filter(ft)
655657

656658
@property
657-
def uc(self):
659+
def uc(self) -> Uc:
658660
""" Raw uc instance.
659661
660662
Type: Uc

qiling/os/posix/posix.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# Cross Platform and Multi Architecture Advanced Binary Emulation Framework
44
#
55

6-
from inspect import signature
6+
from inspect import signature, Parameter
77
from typing import Union, Callable
88

99
from unicorn.arm64_const import UC_ARM64_REG_X8, UC_ARM64_REG_X16
@@ -13,7 +13,7 @@
1313

1414
from qiling import Qiling
1515
from qiling.cc import QlCC, intel, arm, mips
16-
from qiling.const import QL_ARCH, QL_OS, QL_INTERCEPT, QL_CALL_BLOCK, QL_VERBOSE
16+
from qiling.const import QL_ARCH, QL_OS, QL_INTERCEPT
1717
from qiling.exception import QlErrorSyscallNotFound
1818
from qiling.os.os import QlOs
1919
from qiling.os.posix.const import errors, NR_OPEN
@@ -209,15 +209,15 @@ def load_syscall(self):
209209
args = []
210210

211211
# ignore first arg, which is 'ql'
212-
arg_names = tuple(signature(syscall_hook).parameters.values())[1:]
212+
args_info = tuple(signature(syscall_hook).parameters.values())[1:]
213213

214-
for name, value in zip(arg_names, params):
215-
name = str(name)
216-
217-
# ignore python special args
218-
if name in ('*args', '**kw', '**kwargs'):
214+
for info, value in zip(args_info, params):
215+
# skip python special args, like: *args and **kwargs
216+
if info.kind != Parameter.POSITIONAL_OR_KEYWORD:
219217
continue
220218

219+
name = info.name
220+
221221
# cut the first part of the arg if it is of form fstatat64_fd
222222
if name.startswith(f'{syscall_basename}_'):
223223
name = name.partition('_')[-1]

qiling/os/windows/api.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
from qiling.os.const import *
77

8+
# See: https://docs.microsoft.com/en-us/windows/win32/winprog/windows-data-types
9+
810
LONG = PARAM_INTN
911
ULONG = PARAM_INTN
1012
CHAR = PARAM_INT8

0 commit comments

Comments
 (0)