1use std::collections::{HashMap, HashSet};
23use std::time::{Duration, Instant};
24
25use idakit::decompiler::ctree::{Ctree, ExpressionId, LocalId, NodeRef};
26use idakit::prelude::*;
27
28const SOURCES: &[&str] = &["recv", "read", "fgets", "getenv", "scanf", "gets"];
30const SINKS: &[&str] = &[
32 "memcpy", "memmove", "strcpy", "strcat", "sprintf", "system", "malloc", "alloca", "exec",
33];
34
35fn matches(name: &str, set: &[&str]) -> bool {
36 set.iter().any(|n| name.contains(n))
37}
38
39struct TaintInput {
46 tree: Ctree,
47 callees: HashMap<ExpressionId, String>,
49}
50
51fn callee_name(tree: &Ctree, callee: ExpressionId) -> Option<String> {
56 let kind = tree.kind(callee);
57 if let Some((_, name)) = kind.as_obj() {
58 return name.map(str::to_owned);
59 }
60 kind.as_helper().map(str::to_owned)
61}
62
63fn resolve_callees(tree: &Ctree) -> HashMap<ExpressionId, String> {
68 let mut map = HashMap::new();
69 for (id, callee, _) in tree.calls() {
70 if let Some(name) = callee_name(tree, callee) {
71 map.insert(id, name);
72 }
73 }
74 map
75}
76
77fn expression_tainted(img: &TaintInput, e: ExpressionId, tainted: &HashSet<u32>) -> bool {
81 img.tree
82 .expression_descendants(NodeRef::Expression(e))
83 .any(|id| match img.tree.kind(id).as_var() {
84 Some(LocalId(i)) => tainted.contains(&i),
85 None => img.callees.get(&id).is_some_and(|n| matches(n, SOURCES)),
86 })
87}
88
89fn analyze(img: &TaintInput) -> usize {
92 let defs: Vec<(u32, ExpressionId)> = img
94 .tree
95 .assigns()
96 .filter_map(|(_, _, x, y)| {
97 let LocalId(i) = img.tree.kind(x).as_var()?;
98 Some((i, y))
99 })
100 .collect();
101
102 let mut tainted: HashSet<u32> = HashSet::new();
104 loop {
105 let before = tainted.len();
106 for &(lv, rhs) in &defs {
107 if !tainted.contains(&lv) && expression_tainted(img, rhs, &tainted) {
108 tainted.insert(lv);
109 }
110 }
111 if tainted.len() == before {
112 break;
113 }
114 }
115
116 img.tree
118 .calls()
119 .filter(|(id, _, args)| {
120 img.callees.get(id).is_some_and(|n| matches(n, SINKS))
121 && args.iter().any(|a| expression_tainted(img, *a, &tainted))
122 })
123 .count()
124}
125
126#[derive(Default)]
127struct Totals {
128 decompile: Duration,
129 extract: Duration,
130 resolve: Duration,
131 analyze: Duration,
132 funcs: usize,
133 decompile_failed: usize,
134 extract_failed: usize,
135 flows: usize,
136 nodes: u64,
137}
138
139fn run(idb: &mut Database, db: &str) -> Result<(), Error> {
140 idb.open(db).call()?;
141
142 let limit = std::env::var("TAINT_LIMIT")
143 .ok()
144 .and_then(|s| s.parse::<usize>().ok())
145 .unwrap_or(usize::MAX);
146
147 let eas: Vec<Address> = idb.functions().map(|f| f.address()).take(limit).collect();
148 println!("[taint] sweeping {} functions", eas.len());
149
150 let mut t = Totals::default();
151 let wall = Instant::now();
152
153 for (i, &address) in eas.iter().enumerate() {
154 let started = Instant::now();
155 let Ok(cf) = idb.decompile(address) else {
156 t.decompile_failed += 1;
157 continue;
158 };
159 t.decompile += started.elapsed();
160
161 let started = Instant::now();
162 let Ok(tree) = cf.ctree() else {
163 t.extract_failed += 1;
164 continue;
165 };
166 t.extract += started.elapsed();
167
168 let started = Instant::now();
169 let callees = resolve_callees(&tree);
170 t.resolve += started.elapsed();
171
172 t.nodes += (tree.expressions().count() + tree.statements().count()) as u64;
173 let img = TaintInput { tree, callees };
174
175 let started = Instant::now();
176 t.flows += analyze(&img);
177 t.analyze += started.elapsed();
178
179 t.funcs += 1;
180 if (i + 1) % 5000 == 0 {
181 println!("[taint] {} / {} ...", i + 1, eas.len());
182 }
183 }
184
185 report(&t, wall.elapsed());
186 idb.close(false);
187 Ok(())
188}
189
190fn report(t: &Totals, wall: Duration) {
191 let kernel = t.decompile + t.extract + t.resolve;
192 let pct = |d: Duration| 100.0 * d.as_secs_f64() / wall.as_secs_f64().max(f64::EPSILON);
193 let per = |d: Duration| {
194 if t.funcs == 0 {
195 0.0
196 } else {
197 d.as_secs_f64() * 1e6 / t.funcs as f64
198 }
199 };
200
201 println!("\n=== taint sweep ===");
202 println!(
203 "functions analyzed: {} (decompile-failed {}, extract-failed {})",
204 t.funcs, t.decompile_failed, t.extract_failed
205 );
206 println!("ctree nodes total: {}", t.nodes);
207 println!("source->sink flows: {}", t.flows);
208 println!("wall: {:.2}s", wall.as_secs_f64());
209 println!(
210 " decompile {:>7.2}s {:>5.1}% {:>7.1} us/fn",
211 t.decompile.as_secs_f64(),
212 pct(t.decompile),
213 per(t.decompile)
214 );
215 println!(
216 " extract {:>7.2}s {:>5.1}% {:>7.1} us/fn",
217 t.extract.as_secs_f64(),
218 pct(t.extract),
219 per(t.extract)
220 );
221 println!(
222 " resolve {:>7.2}s {:>5.1}% {:>7.1} us/fn",
223 t.resolve.as_secs_f64(),
224 pct(t.resolve),
225 per(t.resolve)
226 );
227 println!(
228 " analyze {:>7.2}s {:>5.1}% {:>7.1} us/fn (the only parallelizable part)",
229 t.analyze.as_secs_f64(),
230 pct(t.analyze),
231 per(t.analyze)
232 );
233 println!(
234 "kernel-bound (decompile+extract+resolve): {:.1}% of wall -- the serial floor",
235 pct(kernel)
236 );
237}
238
239fn main() -> Result<(), Box<dyn std::error::Error>> {
240 let db = std::env::args()
241 .nth(1)
242 .expect("usage: taint <db.i64> (set TAINT_LIMIT to cap the sweep)");
243
244 Ida::run(move |ida| -> Result<(), Error> { ida.call(move |idb| run(idb, &db))? })??;
245
246 Ok(())
247}