dotscope 0.6.0

A high-performance, cross-platform framework for analyzing and reverse engineering .NET PE executables
Documentation
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
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
//! Method summaries for interprocedural analysis.
//!
//! This module provides types for summarizing method behavior, enabling
//! cross-method optimization like constant propagation, dead method detection,
//! and inlining decisions.

use crate::{
    analysis::{ConstValue, MethodPurity, ReturnInfo},
    metadata::{signatures::TypeSignature, token::Token},
};

/// Summary of a method's behavior for interprocedural analysis.
#[derive(Debug, Clone)]
pub struct MethodSummary {
    /// The method token.
    pub token: Token,

    /// What this method returns.
    pub return_info: ReturnInfo,

    /// Side effect classification.
    pub purity: MethodPurity,

    /// Parameter analysis.
    pub parameters: Vec<ParameterSummary>,

    /// Is this method a string decryptor?
    pub is_string_decryptor: bool,

    /// Is this method part of control flow obfuscation (dispatcher)?
    pub is_dispatcher: bool,

    /// Should this method be considered for inlining?
    pub inline_candidate: bool,

    /// Approximate instruction count (for inlining heuristics).
    pub instruction_count: usize,

    /// Number of call sites (how many places call this method).
    pub call_site_count: usize,

    /// Is this method an entry point (Main, event handler, etc.)?
    pub is_entry_point: bool,

    /// Return type signature (from method metadata).
    pub return_type: Option<TypeSignature>,

    /// Parameter type signatures (from method metadata).
    pub parameter_types: Vec<TypeSignature>,

    /// Contains XOR operations (common in string decryption).
    pub has_xor_operations: bool,

    /// Contains array element access (common in string decryption).
    pub has_array_access: bool,

    /// Contains encoding/decoding method calls.
    pub has_encoding_calls: bool,

    /// Number of distinct constant values passed at call sites.
    pub distinct_arg_values: usize,
}

impl MethodSummary {
    /// Creates a new method summary with default values.
    ///
    /// All analysis properties start as `Unknown` or false until actual analysis is performed.
    ///
    /// # Arguments
    ///
    /// * `token` - The metadata token identifying the method.
    ///
    /// # Returns
    ///
    /// A new `MethodSummary` with default values.
    #[must_use]
    pub fn new(token: Token) -> Self {
        Self {
            token,
            return_info: ReturnInfo::Unknown,
            purity: MethodPurity::Unknown,
            parameters: Vec::new(),
            is_string_decryptor: false,
            is_dispatcher: false,
            inline_candidate: false,
            instruction_count: 0,
            call_site_count: 0,
            is_entry_point: false,
            return_type: None,
            parameter_types: Vec::new(),
            has_xor_operations: false,
            has_array_access: false,
            has_encoding_calls: false,
            distinct_arg_values: 0,
        }
    }

    /// Checks if this method is dead (no callers and not an entry point).
    ///
    /// # Returns
    ///
    /// `true` if the method has no call sites and is not marked as an entry point.
    #[must_use]
    pub fn is_dead(&self) -> bool {
        self.call_site_count == 0 && !self.is_entry_point
    }

    /// Returns the constant return value if this method always returns the same value.
    ///
    /// # Returns
    ///
    /// A reference to the constant value if return is constant, `None` otherwise.
    #[must_use]
    pub fn returns_constant(&self) -> Option<&ConstValue> {
        match &self.return_info {
            ReturnInfo::Constant(v) => Some(v),
            _ => None,
        }
    }

    /// Checks if this method is pure (has no side effects).
    ///
    /// # Returns
    ///
    /// `true` if the method has been determined to be pure.
    #[must_use]
    pub fn is_pure(&self) -> bool {
        matches!(self.purity, MethodPurity::Pure)
    }

    /// Checks if this method only reads state (no mutations).
    ///
    /// # Returns
    ///
    /// `true` if the method is pure or read-only.
    #[must_use]
    pub fn is_read_only(&self) -> bool {
        matches!(self.purity, MethodPurity::Pure | MethodPurity::ReadOnly)
    }

    /// Returns the constant value for a parameter if all call sites pass the same value.
    ///
    /// # Arguments
    ///
    /// * `index` - The zero-based parameter index.
    ///
    /// # Returns
    ///
    /// A reference to the constant value if all call sites agree, `None` otherwise.
    #[must_use]
    pub fn parameter_constant(&self, index: usize) -> Option<&ConstValue> {
        self.parameters
            .get(index)
            .and_then(|p| p.constant_value.as_ref())
    }

    /// Checks if this method returns a string type.
    ///
    /// # Returns
    ///
    /// `true` if the method returns `System.String`.
    #[must_use]
    pub fn returns_string(&self) -> bool {
        matches!(self.return_type, Some(TypeSignature::String))
    }

    /// Checks if any parameter accepts an integer type.
    ///
    /// Integer parameters are common in string decryptors where the
    /// integer is used as an index or decryption key.
    ///
    /// # Returns
    ///
    /// `true` if any parameter is an integer type (I1, U1, I2, U2, I4, U4, I8, U8).
    #[must_use]
    pub fn has_integer_parameter(&self) -> bool {
        self.parameter_types.iter().any(|t| {
            matches!(
                t,
                TypeSignature::I1
                    | TypeSignature::U1
                    | TypeSignature::I2
                    | TypeSignature::U2
                    | TypeSignature::I4
                    | TypeSignature::U4
                    | TypeSignature::I8
                    | TypeSignature::U8
                    | TypeSignature::I
                    | TypeSignature::U
            )
        })
    }

    /// Checks if any parameter is a byte array.
    ///
    /// Byte array parameters are common in string decryptors where
    /// the array contains encrypted string data.
    ///
    /// # Returns
    ///
    /// `true` if any parameter is `byte[]` or similar array type.
    #[must_use]
    pub fn has_byte_array_parameter(&self) -> bool {
        self.parameter_types.iter().any(|t| {
            if let TypeSignature::SzArray(arr) = t {
                matches!(*arr.base, TypeSignature::U1 | TypeSignature::I1)
            } else {
                false
            }
        })
    }

    /// Computes a heuristic score for how likely this method is a string decryptor.
    ///
    /// Higher scores indicate higher confidence. Considers:
    /// - Return type (string = +30)
    /// - Parameter types (int/byte[] = +20)
    /// - XOR operations (+15)
    /// - Array access (+10)
    /// - Encoding calls (+20)
    /// - Called with many distinct constants (+15)
    /// - Small method size (+5)
    ///
    /// # Returns
    ///
    /// A score from 0-100 indicating likelihood of being a string decryptor.
    #[must_use]
    pub fn string_decryptor_score(&self) -> u32 {
        let mut score = 0u32;

        // Return type is string
        if self.returns_string() {
            score += 30;
        }

        // Has int or byte[] parameter
        if self.has_integer_parameter() {
            score += 15;
        }
        if self.has_byte_array_parameter() {
            score += 15;
        }

        // Contains XOR operations
        if self.has_xor_operations {
            score += 15;
        }

        // Contains array access
        if self.has_array_access {
            score += 10;
        }

        // Contains encoding calls
        if self.has_encoding_calls {
            score += 20;
        }

        // Called with many distinct constant values
        if self.distinct_arg_values >= 5 {
            score += 15;
        } else if self.distinct_arg_values >= 2 {
            score += 5;
        }

        // Small method (decryptors tend to be compact)
        if self.instruction_count > 0 && self.instruction_count <= 100 {
            score += 5;
        }

        score.min(100)
    }
}

/// Information about how a parameter is used.
#[derive(Debug, Clone)]
pub struct ParameterSummary {
    /// Parameter index.
    pub index: usize,

    /// Parameter name (if available from metadata).
    pub name: Option<String>,

    /// Whether this parameter is used in the method body.
    pub is_used: bool,

    /// Whether this parameter is only used in pure operations.
    pub pure_usage_only: bool,

    /// If ALL call sites pass the same constant, it's stored here.
    pub constant_value: Option<ConstValue>,

    /// Number of uses within the method.
    pub use_count: usize,
}

impl ParameterSummary {
    /// Creates a new parameter summary with default values.
    ///
    /// # Arguments
    ///
    /// * `index` - The zero-based parameter index.
    ///
    /// # Returns
    ///
    /// A new `ParameterSummary` with default values (unused, no constant).
    #[must_use]
    pub fn new(index: usize) -> Self {
        Self {
            index,
            name: None,
            is_used: false,
            pure_usage_only: true,
            constant_value: None,
            use_count: 0,
        }
    }

    /// Creates a parameter summary with a name.
    ///
    /// # Arguments
    ///
    /// * `index` - The zero-based parameter index.
    /// * `name` - The parameter name from metadata.
    ///
    /// # Returns
    ///
    /// A new `ParameterSummary` with the specified name.
    #[must_use]
    pub fn with_name(index: usize, name: impl Into<String>) -> Self {
        Self {
            name: Some(name.into()),
            ..Self::new(index)
        }
    }

    /// Checks if this parameter is dead (unused in the method body).
    ///
    /// # Returns
    ///
    /// `true` if the parameter is never used.
    #[must_use]
    pub fn is_dead(&self) -> bool {
        !self.is_used
    }

    /// Checks if this parameter has a known constant value at all call sites.
    ///
    /// # Returns
    ///
    /// `true` if all call sites pass the same constant value for this parameter.
    #[must_use]
    pub fn has_constant(&self) -> bool {
        self.constant_value.is_some()
    }
}

/// Information about a call site for interprocedural analysis.
#[derive(Debug, Clone)]
pub struct CallSiteInfo {
    /// The calling method.
    pub caller: Token,

    /// Offset within the caller where the call occurs.
    pub offset: usize,

    /// Argument values at this call site (None if unknown).
    pub arguments: Vec<Option<ConstValue>>,

    /// Is this call site in live code?
    pub is_live: bool,
}

impl CallSiteInfo {
    /// Creates a new call site info with default values.
    ///
    /// # Arguments
    ///
    /// * `caller` - The metadata token of the calling method.
    /// * `offset` - The instruction offset within the caller.
    ///
    /// # Returns
    ///
    /// A new `CallSiteInfo` marked as live with no argument information.
    #[must_use]
    pub fn new(caller: Token, offset: usize) -> Self {
        Self {
            caller,
            offset,
            arguments: Vec::new(),
            is_live: true,
        }
    }

    /// Returns the constant value for an argument if known.
    ///
    /// # Arguments
    ///
    /// * `index` - The zero-based argument index.
    ///
    /// # Returns
    ///
    /// A reference to the constant value if known, `None` otherwise.
    #[must_use]
    pub fn argument_constant(&self, index: usize) -> Option<&ConstValue> {
        self.arguments.get(index).and_then(|v| v.as_ref())
    }

    /// Checks if all arguments have known constant values.
    ///
    /// # Returns
    ///
    /// `true` if every argument at this call site is a known constant.
    #[must_use]
    pub fn all_arguments_constant(&self) -> bool {
        self.arguments.iter().all(Option::is_some)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_method_summary_default() {
        let summary = MethodSummary::new(Token::new(0x06000001));
        // Default summary: no callers (call_site_count=0), not an entry point
        // So is_dead() is true for a new summary
        assert!(summary.is_dead());
        assert!(!summary.is_pure());
        assert!(summary.returns_constant().is_none());
    }

    #[test]
    fn test_return_info() {
        assert!(ReturnInfo::Void.is_known());
        assert!(ReturnInfo::Constant(ConstValue::I32(42)).is_known());
        assert!(!ReturnInfo::Dynamic.is_known());

        assert!(ReturnInfo::PassThrough(0).is_potentially_foldable());
        assert!(ReturnInfo::PureComputation.is_potentially_foldable());
        assert!(!ReturnInfo::Dynamic.is_potentially_foldable());
    }

    #[test]
    fn test_method_purity() {
        assert!(MethodPurity::Pure.can_eliminate_if_unused());
        assert!(MethodPurity::ReadOnly.can_eliminate_if_unused());
        assert!(!MethodPurity::Impure.can_eliminate_if_unused());

        assert!(MethodPurity::Pure.can_inline());
        assert!(MethodPurity::LocalMutation.can_inline());
        assert!(!MethodPurity::Impure.can_inline());
    }

    #[test]
    fn test_parameter_summary() {
        let mut param = ParameterSummary::new(0);
        assert!(param.is_dead());
        assert!(!param.has_constant());

        param.is_used = true;
        param.constant_value = Some(ConstValue::I32(42));

        assert!(!param.is_dead());
        assert!(param.has_constant());
    }

    #[test]
    fn test_call_site_info() {
        let mut call_site = CallSiteInfo::new(Token::new(0x06000001), 0x20);
        call_site.arguments = vec![Some(ConstValue::I32(1)), None, Some(ConstValue::I32(3))];

        assert_eq!(call_site.argument_constant(0), Some(&ConstValue::I32(1)));
        assert_eq!(call_site.argument_constant(1), None);
        assert!(!call_site.all_arguments_constant());
    }
}