-
Notifications
You must be signed in to change notification settings - Fork 12
/
flirt.rs
214 lines (195 loc) · 9.32 KB
/
flirt.rs
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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
use std::collections::BTreeMap;
use anyhow::Result;
use log::debug;
use crate::{
analysis::dis,
aspace::AddressSpace,
module::{Module, Permissions},
VA,
};
use lancelot_flirt::*;
const EMPTY_CONTEXT: zydis::ffi::RegisterContext = zydis::ffi::RegisterContext { values: [0u64; 257] };
/// make a best guess for the reference target, found at `ref_offset` from `va`.
///
/// flirt uses references `(offset, name)` to describe that a function contains
/// a pointer to some known name (usually also matched by flirt).
/// the offset is relative to the start of the function, and may point in the
/// middle of an instruction!
/// (this is probably an artifact of extracting via relocations in lib files.)
///
/// its a problem because we don't know where the instruction,
/// so its hard to inspect operands for pointers.
/// we could either disassemble the entire function and find instruction ranges,
/// or scan backwards looking for potential instructions.
/// we do the latter here, as it works well in practice.
///
/// from the given address `ref_offset`, we disassemble backwards, up to -4
/// bytes. at each location, we inspect the operands. if it could be a pointer,
/// then we use metadata provided by zydis for the offset of the operand data.
/// (this is a great feature of zydis!)
/// the offset of the operand pointer data should match the number of bytes
/// we've disassembled backwards, that is, the pointer is found at the
/// reference address.
///
/// in practice, many references look like `call FOO` which is `E8 ?? ?? ?? ??`
/// and we recover the reference on the first try.
fn guess_reference_target(
module: &Module,
decoder: &zydis::Decoder,
va: VA,
ref_offset: u64,
perms: Permissions,
) -> Option<VA> {
// scan from -1 to -4 bytes backwards from the reference
for i in (1..=4u64).rev() {
let candidate_insn_va = va + ref_offset - i;
let mut insn_buf = [0u8; 16];
if module.address_space.read_into(candidate_insn_va, &mut insn_buf).is_ok() {
if let Ok(Some(insn)) = decoder.decode(&insn_buf) {
let explicit_operands = insn
.operands
.iter()
.filter(|op| matches!(op.visibility, zydis::OperandVisibility::EXPLICIT));
// we assume the pointer will be found in the first two explicit operands,
// which works well for x86.
for (j, op) in explicit_operands.take(2).enumerate() {
match op.ty {
zydis::OperandType::MEMORY => {
if (op.mem.base == zydis::Register::NONE || op.mem.base == zydis::Register::RIP)
&& op.mem.disp.has_displacement
&& insn.raw.disp_offset == i as u8
{
if let Ok(target) = insn.calc_absolute_address_ex(candidate_insn_va, op, &EMPTY_CONTEXT)
{
if module.probe_va(target, perms) {
return Some(target);
}
}
}
continue;
}
zydis::OperandType::IMMEDIATE => {
if insn.raw.imm[j].offset == i as u8 {
if let Ok(target) = insn.calc_absolute_address(candidate_insn_va, op) {
if module.probe_va(target, perms) {
return Some(target);
}
}
}
continue;
}
zydis::OperandType::POINTER => continue,
zydis::OperandType::REGISTER => continue,
zydis::OperandType::UNUSED => continue,
}
}
}
}
}
None
}
/// match the given flirt signatures at the given address.
/// returns a list of the signatures that match.
pub fn match_flirt(module: &Module, sigs: &FlirtSignatureSet, va: VA) -> Result<Vec<FlirtSignature>> {
fn match_flirt_inner(
module: &Module,
sigs: &FlirtSignatureSet,
decoder: &zydis::Decoder,
va: VA,
cache: &mut BTreeMap<VA, Vec<FlirtSignature>>,
) -> Result<Vec<FlirtSignature>> {
let sec = module
.sections
.iter()
.find(|sec| sec.virtual_range.start <= va && va < sec.virtual_range.end)
.unwrap();
let size = sec.virtual_range.end - va;
let buf = module.address_space.read_bytes(va, size as usize)?;
debug!("flirt: matching: {:#x}", va);
Ok(sigs
.r#match(&buf)
.iter()
.filter(|sig| {
let mut does_match_references = true;
debug!("flirt: {:#x}: candidate: {:?}", va, sig);
'names: for name in sig.names.iter() {
if let Symbol::Reference(Name {
offset,
name: wanted_name,
}) = name
{
// i dont know what this means.
assert!(*offset >= 0, "negative offset");
if wanted_name == "." {
// special case: name "." matches any data?
// not exactly sure if this should only match special data `ctype`?
// see: https://github.com/williballenthin/lancelot/issues/112#issuecomment-802379966
if guess_reference_target(module, &decoder, va, *offset as u64, Permissions::R).is_some() {
continue;
} else {
does_match_references = false;
break;
}
}
// guess the reference target, then match flirt signatures there,
// and see if the wanted name matches a name recovered by flirt.
//
// we use the cache to record whether a negative match was encountered.
// this drastically when we have many nested references (like CALL wrappers).
// see: https://github.com/fireeye/capa/issues/448
if let Some(target) =
guess_reference_target(module, &decoder, va, *offset as u64, Permissions::X)
{
// this is just:
// target_sigs = cached(match_flirt_inner(...target...))
//
// can't use entry API because of mutable cache used to create cache entry.
#[allow(clippy::map_entry)]
if !cache.contains_key(&target) {
let target_sigs = match_flirt_inner(module, sigs, decoder, target, cache)
.unwrap_or_else(|_| Default::default());
cache.insert(target, target_sigs);
}
let target_sigs = cache.get(&target).unwrap();
let mut does_name_match = false;
'sigs: for target_sig in target_sigs.iter() {
debug!("flirt: {:#x}: found reference: {:?} @ {:#x}", va, target_sig, offset);
for name in target_sig.names.iter() {
match name {
Symbol::Reference(_) => continue,
Symbol::Local(Name {
name: target_name,
offset,
})
| Symbol::Public(Name {
name: target_name,
offset,
}) => {
if *offset == 0 && target_name == wanted_name {
does_name_match = true;
break 'sigs;
}
}
}
}
}
if !does_name_match {
does_match_references = false;
break 'names;
}
} else {
does_match_references = false;
break;
}
}
}
does_match_references
})
.cloned()
.cloned()
.collect::<Vec<_>>())
}
let decoder = dis::get_disassembler(module)?;
let mut cache = Default::default();
match_flirt_inner(module, sigs, &decoder, va, &mut cache)
}