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
//! This is a heuristic that inspects operands to existing instructions
//! for references to likely code.
use std::{collections::BTreeSet, vec};
use anyhow::Result;
use byteorder::ByteOrder;
use log::debug;
use crate::{
analysis::{
cfg::{self, read_insn_with_cache, CachingPageReader},
dis, heuristics,
},
aspace::AddressSpace,
module::{Module, Permissions},
VA,
};
pub fn find_executable_pointers(module: &Module) -> Result<Vec<VA>> {
// list of candidates: (address of pointer, address pointed to)
let mut candidates: Vec<(VA, VA)> = vec![];
let min_addr = module.address_space.base_address;
let max_addr = module
.sections
.iter()
.map(|section| section.virtual_range.end)
.max()
.unwrap();
// look for hardcoded pointers into the executable section of the module.
// note: this often finds jump tables, too. more filtering is below.
// note: also finds many exception handlers. see filtering below.
for section in module.sections.iter() {
let vstart: VA = section.virtual_range.start;
let vsize = (section.virtual_range.end - section.virtual_range.start) as usize;
let sec_buf = module.address_space.read_bytes(vstart, vsize)?;
debug!(
"code references: scanning section {:#x}-{:#x}",
section.virtual_range.start, section.virtual_range.end
);
if let crate::arch::Arch::X64 = module.arch {
candidates.extend(
sec_buf
// using windows for unaligned pointers,
// rather than chunks for aligned pointers.
.windows(std::mem::size_of::<u64>())
.map(|b| byteorder::LittleEndian::read_u64(b) as VA)
.enumerate()
// naive range filter that is very fast
.filter(|&(_, va)| va >= min_addr && va < max_addr)
.filter(|&(_, va)| module.probe_va(va, Permissions::X))
.map(|(i, va)| (vstart + (i as u64), va)),
)
} else {
candidates.extend(
sec_buf
// using windows for unaligned pointers
// rather than chunks for aligned pointers.
.windows(std::mem::size_of::<u32>())
.map(|b| byteorder::LittleEndian::read_u32(b) as VA)
.enumerate()
// naive range filter that is very fast
.filter(|&(_, va)| va >= min_addr && va < max_addr)
.filter(|&(_, va)| module.probe_va(va, Permissions::X))
.map(|(i, va)| (vstart + (i as u64), va)),
)
}
}
Ok(candidates
.into_iter()
.map(|(src, dst)| {
debug!(
"code references: candidate pointer: {:#x} points to valid content at {:#x}",
src, dst
);
dst
})
.collect::<BTreeSet<VA>>()
.into_iter()
.collect())
}
pub fn find_new_code_references(module: &Module, insns: &cfg::InstructionIndex) -> Result<Vec<VA>> {
let decoder = dis::get_disassembler(module)?;
// we prefer to read via a page cache,
// assuming that when we read instructions ordered by address,
// fetches will often be localized within one page.
let mut reader: CachingPageReader = Default::default();
let mut new_code: BTreeSet<VA> = Default::default();
for &va in insns.insns_by_address.keys() {
if let Ok(Some(insn)) = read_insn_with_cache(&mut reader, &module.address_space, va, &decoder) {
for op in dis::get_operands(&insn) {
if let Ok(Some(xref)) = dis::get_operand_xref(module, va, &insn, op) {
let target = match xref {
dis::Target::Direct(target) => target,
dis::Target::Indirect(target) => target,
};
if insns.insns_by_address.contains_key(&target) {
// this is already code.
continue;
}
if heuristics::is_probably_code(module, &decoder, target) {
// finally, we think we have some new code.
log::debug!("code references: found new likely code at {:#x}", target);
new_code.insert(target);
}
}
}
}
}
// TODO: do additional passes on the newly found code
Ok(new_code.into_iter().collect())
}
#[cfg(test)]
mod tests {
use crate::{
analysis::cfg::{code_references::*, InstructionIndex},
rsrc::*,
};
#[test]
fn push_function_pointer() -> Result<()> {
// recognize a function pointer being pushed onto the stack
// such as a call to CreateThread
//
// in this case, we have function sub_4010E0
// that is referenced at 0x41FA0C:
//
// ```
// mov edi, [ebp+arg_0]
// push offset sub_40116C
// push offset sub_4010E0 ; @ 0x41FA0C
// push 10h
// push 4
// lea eax, [edi+8]
// push eax
// call ??_L@YGXPAXIHP6EX0@Z1@Z ;
// ```
let buf = get_buf(Rsrc::DED0);
let pe = crate::loader::pe::PE::from_bytes(&buf)?;
let ptrs = find_executable_pointers(&pe.module)?;
assert!(ptrs.contains(&0x4010E0));
let existing = crate::analysis::pe::find_function_starts(&pe)?;
assert!(!existing.contains(&0x4010E0));
let mut insns: InstructionIndex = Default::default();
for &function in existing.iter() {
insns.build_index(&pe.module, function)?;
}
let found = find_new_code_references(&pe.module, &insns)?;
assert!(found.contains(&0x4010E0));
Ok(())
}
}