1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
|
#!/usr/bin/env python3
"""Query classification for Ctrl+F: is that text, or is it bytes?
The whole risk of a mode-guessing search is guessing wrong in the direction
that loses work: deciding a word like `dead` or `add` is a byte pattern, and
silently searching the image instead of the disassembly. These checks pin that
asymmetry down. Pure -- no IDA, no worker.
"""
#: pure: stdlib only.
#: Read by tests/run.py (--fast skips every NEEDS_IDA file).
NEEDS_IDA = False
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from idatui.search import ( # noqa: E402
BYTES,
TEXT,
classify,
looks_like_bytes,
normalise_pattern,
pattern_problem,
probably_meant_bytes,
)
PASS = FAIL = 0
def check(name, cond, detail=""):
global PASS, FAIL
if cond:
PASS += 1
print(f" ok {name}")
else:
FAIL += 1
print(f" FAIL {name} {detail}")
def main() -> int:
# -- the asymmetry: hex-looking WORDS must stay text --------------------- #
for word in ("add", "dead", "beef", "cafe", "ff", "0", "abcdef", "decode", "face"):
check(
f"{word!r} searches text, not bytes",
classify(word)[0] == TEXT,
classify(word),
)
# -- unambiguous byte patterns ------------------------------------------ #
for pat in (
"48 8b ?? c3",
"B8 ? ? ? ? 90",
"48,8b,05",
"de ad be ef",
"48 8? ?? 24",
"??",
):
check(f"{pat!r} searches bytes", classify(pat)[0] == BYTES, classify(pat))
check("a quoted literal is a byte pattern", classify('"Hello", 0')[0] == BYTES)
# A TYPO in a byte pattern must stay a byte pattern, so it can be refused
# with a reason. Falling back to text answers "no match", which is
# indistinguishable from "those bytes are not in this binary".
check(
"a typo'd byte pattern is still a byte pattern",
classify("48 zz c3")[0] == BYTES,
classify("48 zz c3"),
)
check("and it is refused by name", "'zz'" in (pattern_problem("48 zz c3") or ""))
check(
"but a word among bytes is prose",
classify("add ff")[0] == TEXT and classify("mov rdi, rax")[0] == TEXT,
classify("add ff"),
)
check("prose stays text", classify("mov rdi, rax")[0] == TEXT)
check("a call target stays text", classify("call cs:__isoc99_scanf")[0] == TEXT)
check(
"an empty query is text (nothing to search yet)",
classify("")[0] == TEXT and not looks_like_bytes(""),
)
# -- explicit wins over any guess --------------------------------------- #
check("hex: forces bytes", classify("hex: dead") == (BYTES, "dead"))
check("bytes: forces bytes too", classify("bytes:dead") == (BYTES, "dead"))
check("text: forces text", classify("text: 48 8b c3") == (TEXT, "48 8b c3"))
check(
"F2's forced mode beats the shape",
classify("dead", forced=BYTES) == (BYTES, "dead")
and classify("48 8b c3", forced=TEXT) == (TEXT, "48 8b c3"),
)
check(
"a prefix beats even the forced mode",
classify("text:48 8b c3", forced=BYTES)[0] == TEXT,
)
# -- the shapes people paste -------------------------------------------- #
check("commas become spaces", normalise_pattern("48,8b,05") == "48 8b 05")
check(
"a run with no separators is split into bytes",
normalise_pattern("488B05C3") == "48 8B 05 C3",
)
check("whitespace is squeezed", normalise_pattern(" 48 8b\t05 ") == "48 8b 05")
check(
"a quoted literal keeps its own spacing",
normalise_pattern('"Hello, world", 0') == '"Hello, world", 0',
)
# -- refusing a bad pattern with a reason -------------------------------- #
check("an empty pattern says what to type", "48 8b" in (pattern_problem("") or ""))
check(
"a non-hex token is named",
"'zz'" in (pattern_problem("48 zz c3") or ""),
pattern_problem("48 zz c3"),
)
check(
"a good pattern has no complaint",
pattern_problem("48 8b ?? c3") is None and pattern_problem('"Hi", 0') is None,
)
check(
"an odd-length run is refused rather than silently split",
pattern_problem("488B0") is not None,
pattern_problem("488B0"),
)
print(f"\n{PASS} passed, {FAIL} failed")
return 1 if FAIL else 0
if __name__ == "__main__":
raise SystemExit(main())
|