ps_parser/
lib.rs

1mod parser;
2
3pub(crate) use parser::NEWLINE;
4pub use parser::{PowerShellSession, PsValue, ScriptResult, Token, Variables};
5
6#[cfg(test)]
7mod tests {
8    use std::collections::HashMap;
9
10    use super::*;
11    use crate::Token;
12
13    #[test]
14    fn obfuscation_1() {
15        let input = r#"
16$ilryNQSTt="System.$([cHAR]([ByTE]0x4d)+[ChAR]([byte]0x61)+[chAr](110)+[cHar]([byTE]0x61)+[cHaR](103)+[cHar](101*64/64)+[chaR]([byTE]0x6d)+[cHAr](101)+[CHAr]([byTE]0x6e)+[Char](116*103/103)).$([Char]([ByTe]0x41)+[Char](117+70-70)+[CHAr]([ByTE]0x74)+[CHar]([bYte]0x6f)+[CHar]([bytE]0x6d)+[ChaR]([ByTe]0x61)+[CHar]([bYte]0x74)+[CHAR]([byte]0x69)+[Char](111*26/26)+[chAr]([BYTe]0x6e)).$(('Ârmí'+'Ùtìl'+'s').NORmalizE([ChAR](44+26)+[chAR](111*9/9)+[cHar](82+32)+[ChaR](109*34/34)+[cHaR](68+24-24)) -replace [ChAr](92)+[CHaR]([BYTe]0x70)+[Char]([BytE]0x7b)+[CHaR]([BYTe]0x4d)+[chAR](110)+[ChAr](15+110))";$ilryNQSTt
17"#;
18
19        let mut p = PowerShellSession::new();
20        assert_eq!(
21            p.safe_eval(input).unwrap().as_str(),
22            "System.Management.Automation.ArmiUtils"
23        );
24    }
25
26    #[test]
27    fn obfuscation_2() {
28        let input = r#"
29$(('W'+'r'+'î'+'t'+'é'+'Í'+'n'+'t'+'3'+'2').NormAlIzE([chaR]([bYTE]0x46)+[CHAR](111)+[ChAR]([Byte]0x72)+[CHAR]([BytE]0x6d)+[CHAr](64+4)) -replace [cHAr]([BytE]0x5c)+[char]([bYtE]0x70)+[ChAR]([byTe]0x7b)+[cHar]([bYtE]0x4d)+[Char]([bYte]0x6e)+[CHAR](125))
30"#;
31
32        let mut p = PowerShellSession::new();
33        assert_eq!(p.safe_eval(input).unwrap().as_str(), "WriteInt32");
34    }
35
36    #[test]
37    fn obfuscation_3() {
38        let input = r#"
39$([cHar]([BYte]0x65)+[chAr]([bYTE]0x6d)+[CHaR]([ByTe]0x73)+[char](105)+[CHAR]([bYTE]0x43)+[cHaR](111)+[chaR]([bYTE]0x6e)+[cHAr]([bYTe]0x74)+[cHAr](32+69)+[cHaR](120+30-30)+[cHAR]([bYte]0x74))
40"#;
41
42        let mut p = PowerShellSession::new();
43        assert_eq!(p.safe_eval(input).unwrap().as_str(), "emsiContext");
44    }
45
46    #[test]
47    fn obfuscation_4() {
48        let input = r#"
49[syStem.texT.EncoDInG]::unIcoDe.geTstRiNg([SYSTem.cOnVERT]::froMbasE64striNg("WwBjAGgAYQByAF0AKABbAGkAbgB0AF0AKAAiADkAZQA0AGUAIgAgAC0AcgBlAHAAbABhAGMAZQAgACIAZQAiACkAKwAzACkA"))"#;
50
51        let mut p = PowerShellSession::new();
52        assert_eq!(
53            p.safe_eval(input).unwrap().as_str(),
54            r#"[char]([int]("9e4e" -replace "e")+3)"#
55        );
56    }
57
58    #[test]
59    fn deobfuscation() {
60        // assign variable and print it to screen
61        let mut p = PowerShellSession::new();
62        let input = r#" $global:var = [char]([int]("9e4e" -replace "e")+3); [int]'a';$var"#;
63        let script_res = p.parse_input(input).unwrap();
64        assert_eq!(script_res.result(), 'a'.into());
65        assert_eq!(
66            script_res.deobfuscated(),
67            vec!["$var = 'a'", "[int]'a'"].join(NEWLINE)
68        );
69        assert_eq!(script_res.errors().len(), 1);
70        assert_eq!(
71            script_res.errors()[0].to_string(),
72            "ValError: Cannot convert value \"String\" to type \"Int\""
73        );
74
75        // the same but do it in two parts
76        let mut p = PowerShellSession::new();
77        let input = r#" $global:var = [char]([int]("9e4e" -replace "e")+3) "#;
78        let script_res = p.parse_input(input).unwrap();
79
80        assert_eq!(script_res.errors().len(), 0);
81
82        let script_res = p.parse_input(" [int]'a';$var ").unwrap();
83        assert_eq!(script_res.deobfuscated(), vec!["[int]'a'"].join(NEWLINE));
84        assert_eq!(script_res.output(), vec!["a"].join(NEWLINE));
85        assert_eq!(script_res.errors().len(), 1);
86        assert_eq!(
87            script_res.errors()[0].to_string(),
88            "ValError: Cannot convert value \"String\" to type \"Int\""
89        );
90    }
91
92    #[test]
93    fn deobfuscation_non_existing_value() {
94        // assign not existing value, without forcing evaluation
95        let mut p = PowerShellSession::new();
96        let input = r#" $local:var = $env:programfiles;[int]'a';$var"#;
97        let script_res = p.parse_input(input).unwrap();
98        assert_eq!(script_res.result(), PsValue::Null);
99        assert_eq!(
100            script_res.deobfuscated(),
101            vec!["$local:var = $env:programfiles", "[int]'a'", "$var"].join(NEWLINE)
102        );
103        assert_eq!(script_res.errors().len(), 3);
104        assert_eq!(
105            script_res.errors()[0].to_string(),
106            "VariableError: Variable \"programfiles\" is not defined"
107        );
108        assert_eq!(
109            script_res.errors()[1].to_string(),
110            "ValError: Cannot convert value \"String\" to type \"Int\""
111        );
112        assert_eq!(
113            script_res.errors()[2].to_string(),
114            "VariableError: Variable \"var\" is not defined"
115        );
116
117        // assign not existing value, forcing evaluation
118        let mut p = PowerShellSession::new().with_variables(Variables::force_eval());
119        let input = r#" $global:var = $env:programfiles;[int]'a';$var"#;
120        let script_res = p.parse_input(input).unwrap();
121        assert_eq!(script_res.result(), PsValue::Null);
122        assert_eq!(
123            script_res.deobfuscated(),
124            vec!["$var = $null", "[int]'a'"].join(NEWLINE)
125        );
126        assert_eq!(script_res.errors().len(), 1);
127    }
128
129    #[test]
130    fn deobfuscation_env_value() {
131        // assign not existing value, without forcing evaluation
132        let mut p = PowerShellSession::new().with_variables(Variables::env());
133        let input = r#" $global:var = $env:programfiles;$var"#;
134        let script_res = p.parse_input(input).unwrap();
135        assert_eq!(
136            script_res.result(),
137            PsValue::String(std::env::var("PROGRAMFILES").unwrap())
138        );
139        assert_eq!(
140            script_res.deobfuscated(),
141            vec![format!(
142                "$var = '{}'",
143                std::env::var("PROGRAMFILES").unwrap()
144            )]
145            .join(NEWLINE)
146        );
147        assert_eq!(script_res.errors().len(), 0);
148    }
149
150    #[test]
151    fn hash_table() {
152        // assign not existing value, without forcing evaluation
153        let mut p = PowerShellSession::new().with_variables(Variables::env());
154        let input = r#" 
155$nestedData = @{
156    Users = @(
157        @{ Name = "Alice"; Age = 30; Skills = @("PowerShell", "Python") }
158        @{ Name = "Bob"; Age = 25; Skills = @("Java", "C#") }
159    )
160    Settings = @{
161        Theme = "Dark"
162        Language = "en-US"
163    }
164}
165"$nestedData"
166        "#;
167        let script_res = p.parse_input(input).unwrap();
168        assert_eq!(
169            script_res.result(),
170            PsValue::String("System.Collections.Hashtable".to_string())
171        );
172
173        assert_eq!(
174            p.parse_input("$nesteddata.settings").unwrap().result(),
175            PsValue::HashTable(HashMap::from([
176                ("language".to_string(), PsValue::String("en-US".to_string())),
177                ("theme".to_string(), PsValue::String("Dark".to_string())),
178            ]))
179        );
180
181        assert_eq!(
182            p.safe_eval("$nesteddata.settings.theme").unwrap(),
183            "Dark".to_string()
184        );
185
186        assert_eq!(
187            p.parse_input("$nesteddata.users[0]").unwrap().result(),
188            PsValue::HashTable(HashMap::from([
189                (
190                    "skills".to_string(),
191                    PsValue::Array(vec![
192                        PsValue::String("PowerShell".to_string()),
193                        PsValue::String("Python".to_string().into())
194                    ])
195                ),
196                ("name".to_string(), PsValue::String("Alice".to_string())),
197                ("age".to_string(), PsValue::Int(30)),
198            ]))
199        );
200
201        assert_eq!(
202            p.safe_eval("$nesteddata.users[0]['name']").unwrap(),
203            "Alice".to_string()
204        );
205
206        assert_eq!(
207            p.safe_eval("$nesteddata.users[0].NAME").unwrap(),
208            "Alice".to_string()
209        );
210    }
211
212    #[test]
213    fn test_simple_arithmetic() {
214        let input = r#"
215Write-Host "=== Test 3: Arithmetic Operations ===" -ForegroundColor Green
216$a = 10
217$b = 5
218Write-Output "Addition: $(($a + $b))"
219Write-Output "Subtraction: $(($a - $b))"
220Write-Output "Multiplication: $(($a * $b))"
221Write-Output "Division: $(($a / $b))"
222Write-Output "Modulo: $(($a % $b))"
223"#;
224
225        let script_result = PowerShellSession::new().parse_input(input).unwrap();
226
227        assert_eq!(script_result.result(), PsValue::String("Modulo: 0".into()));
228        assert_eq!(
229            script_result.output(),
230            vec![
231                r#"=== Test 3: Arithmetic Operations ==="#,
232                r#"Addition: 15"#,
233                r#"Subtraction: 5"#,
234                r#"Multiplication: 50"#,
235                r#"Division: 2"#,
236                r#"Modulo: 0"#
237            ]
238            .join(NEWLINE)
239        );
240        assert_eq!(script_result.errors().len(), 0);
241        assert_eq!(script_result.tokens().strings(), vec![]);
242        assert_eq!(script_result.tokens().expandable_strings().len(), 6);
243        assert_eq!(
244            script_result.tokens().expandable_strings()[1],
245            Token::StringExpandable(
246                "\"Addition: $(($a + $b))\"".to_string(),
247                "Addition: 15".to_string()
248            )
249        );
250        assert_eq!(script_result.tokens().expression().len(), 12);
251        assert_eq!(
252            script_result.tokens().expression()[2],
253            Token::Expression("$a + $b".to_string(), PsValue::Int(15))
254        );
255    }
256
257    #[test]
258    fn test_scripts() {
259        use std::fs;
260        let Ok(entries) = fs::read_dir("test_scripts") else {
261            panic!("Failed to read test files");
262        };
263        for entry in entries {
264            let dir_entry = entry.unwrap();
265            if std::fs::FileType::is_dir(&dir_entry.file_type().unwrap()) {
266                // If it's a directory, we can read the files inside it
267                let input_script = dir_entry.path().join("input.ps1");
268                let deobfuscated = dir_entry.path().join("deobfuscated.txt");
269                let output = dir_entry.path().join("output.txt");
270
271                let Ok(content) = fs::read_to_string(&input_script) else {
272                    panic!("Failed to read test files");
273                };
274
275                let Ok(deobfuscated) = fs::read_to_string(&deobfuscated) else {
276                    panic!("Failed to read test files");
277                };
278
279                let Ok(output) = fs::read_to_string(&output) else {
280                    panic!("Failed to read test files");
281                };
282
283                let script_result = PowerShellSession::new()
284                    .with_variables(Variables::env())
285                    .parse_input(&content)
286                    .unwrap();
287
288                let deobfuscated_vec = deobfuscated
289                    .lines()
290                    .map(|s| s.trim_end())
291                    .collect::<Vec<&str>>();
292
293                let script_deobfuscated = script_result.deobfuscated();
294
295                let output_vec = output.lines().map(|s| s.trim_end()).collect::<Vec<&str>>();
296
297                let script_output = script_result.output();
298
299                //std::fs::write(format!("{}.txt",
300                // dir_entry.path().components().last().unwrap().as_os_str().to_string_lossy()),
301                // script_output.clone()).unwrap();
302                let script_deobfuscated_vec = script_deobfuscated
303                    .lines()
304                    .map(|s| s.trim_end())
305                    .collect::<Vec<&str>>();
306
307                let script_output_vec = script_output
308                    .lines()
309                    .map(|s| s.trim_end())
310                    .collect::<Vec<&str>>();
311
312                for i in 0..deobfuscated_vec.len() {
313                    assert_eq!(deobfuscated_vec[i], script_deobfuscated_vec[i]);
314                }
315
316                for i in 0..output_vec.len() {
317                    assert_eq!(output_vec[i], script_output_vec[i]);
318                }
319            }
320        }
321    }
322
323    #[test]
324    fn test_range() {
325        // Test for even numbers
326        let mut p = PowerShellSession::new().with_variables(Variables::env());
327        let input = r#" $numbers = 1..10; $numbers"#;
328        let script_res = p.parse_input(input).unwrap();
329        assert_eq!(
330            script_res.deobfuscated(),
331            vec!["$numbers = @(1,2,3,4,5,6,7,8,9,10)"].join(NEWLINE)
332        );
333        assert_eq!(script_res.errors().len(), 0);
334    }
335
336    #[test]
337    fn even_numbers() {
338        // Test for even numbers
339        let mut p = PowerShellSession::new().with_variables(Variables::env());
340        let input = r#" $numbers = 1..10; $evenNumbers = $numbers | Where-Object { $_ % 2 -eq 0 }; $evenNumbers"#;
341        let script_res = p.parse_input(input).unwrap();
342        assert_eq!(
343            script_res.result(),
344            PsValue::Array(vec![
345                PsValue::Int(2),
346                PsValue::Int(4),
347                PsValue::Int(6),
348                PsValue::Int(8),
349                PsValue::Int(10)
350            ])
351        );
352        assert_eq!(
353            script_res.deobfuscated(),
354            vec![
355                "$numbers = @(1,2,3,4,5,6,7,8,9,10)",
356                "$evennumbers = @(2,4,6,8,10)"
357            ]
358            .join(NEWLINE)
359        );
360        assert_eq!(script_res.errors().len(), 0);
361    }
362}