summaryrefslogtreecommitdiff
path: root/tests/analysis/scan/pyapi.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/analysis/scan/pyapi.py')
-rw-r--r--tests/analysis/scan/pyapi.py297
1 files changed, 297 insertions, 0 deletions
diff --git a/tests/analysis/scan/pyapi.py b/tests/analysis/scan/pyapi.py
new file mode 100644
index 0000000..7a697b3
--- /dev/null
+++ b/tests/analysis/scan/pyapi.py
@@ -0,0 +1,297 @@
+
+import binascii
+import struct
+
+from chrysacase import ChrysalideTestCase
+from gi._constants import TYPE_INVALID
+from pychrysalide.analysis.scan import ScanExpression
+from pychrysalide.analysis.scan import ScanOptions
+from pychrysalide.analysis.scan import find_token_modifiers_for_name
+from pychrysalide.glibext import ComparableItem
+
+
+class TestRostPythonAPI(ChrysalideTestCase):
+ """TestCase for the ROST Python API."""
+
+ def testEmptyOptions(self):
+ """Check default scan options."""
+
+ ops = ScanOptions()
+
+ self.assertEqual(ops.backend_for_data, TYPE_INVALID)
+
+
+ def testDirectInstancesOfExpression(self):
+ """Reject direct instances of ROST expressions."""
+
+ with self.assertRaisesRegex(RuntimeError, 'pychrysalide.analysis.scan.ScanExpression is an abstract class'):
+
+ e = ScanExpression()
+
+
+ def testBooleanComparison(self):
+ """Compare custom scan expressions."""
+
+ class StrLenExpr(ScanExpression):
+
+ def __init__(self, value):
+ super().__init__(ScanExpression.ScanReductionState.REDUCED)
+ self._value = value
+
+ def _cmp_rich(self, other, op):
+
+ if op == ComparableItem.RichCmpOperation.EQ:
+ return len(self._value) == len(other._value)
+
+
+ e0 = StrLenExpr('00000000000')
+
+ e1 = StrLenExpr('00000000000')
+
+ e2 = StrLenExpr('000000000000000000000000000')
+
+ self.assertTrue(e0 == e1)
+
+ # !?
+ # Python teste e0 != e1 (non implémenté), puis e1 != e0 (pareil) et en déduit une différence !
+ # self.assertFalse(e0 != e1)
+
+ self.assertFalse(e0 == e2)
+
+ # TypeError: '<' not supported between instances of 'StrLenExpr' and 'StrLenExpr'
+ with self.assertRaisesRegex(TypeError, '\'<\' not supported between instances'):
+ self.assertTrue(e0 < e1)
+
+
+ def testBytePatternModifiers(self):
+ """Validate the bytes produced by modifiers."""
+
+ mod = find_token_modifiers_for_name('plain')
+ self.assertIsNotNone(mod)
+
+ source = b'ABC'
+ transformed = mod.transform(source)
+
+ self.assertEqual(source, transformed[0])
+
+
+ mod = find_token_modifiers_for_name('hex')
+ self.assertIsNotNone(mod)
+
+ source = b'ABC'
+ transformed = mod.transform(source)
+
+ self.assertEqual(binascii.hexlify(source), transformed[0])
+
+
+ mod = find_token_modifiers_for_name('rev')
+ self.assertIsNotNone(mod)
+
+ source = b'ABC'
+ transformed = mod.transform(source)
+
+ self.assertEqual(source[::-1], transformed[0])
+
+
+ mod = find_token_modifiers_for_name('lower')
+ self.assertIsNotNone(mod)
+
+ source = b'AbC'
+ transformed = mod.transform(source)
+
+ self.assertEqual(source.lower(), transformed[0])
+
+
+ mod = find_token_modifiers_for_name('upper')
+ self.assertIsNotNone(mod)
+
+ source = b'AbC'
+ transformed = mod.transform(source)
+
+ self.assertEqual(source.upper(), transformed[0])
+
+
+ mod = find_token_modifiers_for_name('wide')
+ self.assertIsNotNone(mod)
+
+ source = b'ABC'
+ transformed = mod.transform(source)
+
+ self.assertEqual(source.decode('ascii'), transformed[0].decode('utf-16-le'))
+
+
+ mod = find_token_modifiers_for_name('base64')
+ self.assertIsNotNone(mod)
+
+ source = b'ABC'
+ transformed = mod.transform(source)
+
+ self.assertEqual(len(transformed), 3)
+ self.assertEqual(transformed[0], b'QUJD')
+ self.assertEqual(transformed[1], b'FCQ')
+ self.assertEqual(transformed[2], b'BQk')
+
+
+ def testClassicalAPIHashing(self):
+ """Reproduce classical API Hashing results."""
+
+ def b2i(t):
+ return struct.unpack('<I', t)[0]
+
+
+ # Example:
+ # - PlugX (2020) - https://vms.drweb.fr/virus/?i=21512304
+
+ mod = find_token_modifiers_for_name('crc32')
+ self.assertIsNotNone(mod)
+
+ source = b'GetCurrentProcess\x00'
+ transformed = mod.transform(source)
+
+ self.assertEqual(b2i(transformed[0]), 0x3690e66)
+
+
+ # Example:
+ # - GuLoader (2020) - https://www.crowdstrike.com/blog/guloader-malware-analysis/
+
+ mod = find_token_modifiers_for_name('djb2')
+ self.assertIsNotNone(mod)
+
+ source = b'GetProcAddress'
+ transformed = mod.transform(source)
+
+ self.assertEqual(b2i(transformed[0]), 0xcf31bb1f)
+
+
+ def testCustomAPIHashing(self):
+ """Reproduce custom API Hashing results."""
+
+ def b2i(t):
+ return struct.unpack('<I', t)[0]
+
+
+ # Example:
+ # Underminer Exploit Kit (2019) - https://jsac.jpcert.or.jp/archive/2019/pdf/JSAC2019_1_koike-nakajima_jp.pdf
+
+ mod = find_token_modifiers_for_name('add1505-shl5')
+ self.assertIsNotNone(mod)
+
+ source = b'LoadLibraryA'
+ transformed = mod.transform(source)
+
+ self.assertEqual(b2i(transformed[0]), 0x5fbff0fb)
+
+
+ # Example:
+ # Enigma Stealer (2023) https://www.trendmicro.com/es_mx/research/23/b/enigma-stealer-targets-cryptocurrency-industry-with-fake-jobs.html
+
+ mod = find_token_modifiers_for_name('enigma-murmur')
+ self.assertIsNotNone(mod)
+
+ source = b'CreateMutexW'
+ transformed = mod.transform(source)
+
+ self.assertEqual(b2i(transformed[0]), 0xfd43765a)
+
+
+ # Examples:
+ # - ShadowHammer (2019) - https://blog.f-secure.com/analysis-shadowhammer-asus-attack-first-stage-payload/
+ # - ShadowHammer (2019) - https://securelist.com/operation-shadowhammer-a-high-profile-supply-chain-attack/90380/
+
+ mod = find_token_modifiers_for_name('imul21-add')
+ self.assertIsNotNone(mod)
+
+ source = b'VirtualAlloc'
+ transformed = mod.transform(source)
+
+ self.assertEqual(b2i(transformed[0]), 0xdf894b12)
+
+
+ # Examples:
+ # - Bottle Exploit Kit (2019) - https://nao-sec.org/2019/12/say-hello-to-bottle-exploit-kit.html
+ # - ShadowHammer (2019) - https://securelist.com/operation-shadowhammer-a-high-profile-supply-chain-attack/90380/
+
+ mod = find_token_modifiers_for_name('imul83-add')
+ self.assertIsNotNone(mod)
+
+ source = b'GetProcAddress'
+ transformed = mod.transform(source)
+
+ self.assertEqual(b2i(transformed[0]), 0x9ab9b854)
+
+
+ # Examples:
+ # - ?? (2021) - https://www.threatspike.com/blogs/reflective-dll-injection
+ # - Mustang Panda (2022) - https://blog.talosintelligence.com/mustang-panda-targets-europe/
+
+ mod = find_token_modifiers_for_name('ror13')
+ self.assertIsNotNone(mod)
+
+ source = b'GetProcAddress'
+ transformed = mod.transform(source)
+
+ self.assertEqual(b2i(transformed[0]), 0x7c0dfcaa)
+
+ source = b'VirtualAlloc'
+ transformed = mod.transform(source)
+
+ self.assertEqual(b2i(transformed[0]), 0x91afca54)
+
+
+ # Example:
+ # - Energetic Bear (2019) - https://insights.sei.cmu.edu/blog/api-hashing-tool-imagine-that/
+
+ mod = find_token_modifiers_for_name('sll1-add-hash32')
+ self.assertIsNotNone(mod)
+
+ source = b'LoadLibraryA'
+ transformed = mod.transform(source)
+
+ self.assertEqual(b2i(transformed[0]), 0x000d5786)
+
+
+ # Example:
+ # - SideWinder/WarHawk (2022) - https://www.zscaler.com/blogs/security-research/warhawk-new-backdoor-arsenal-sidewinder-apt-group
+
+ mod = find_token_modifiers_for_name('sub42')
+ self.assertIsNotNone(mod)
+
+ source = b'LoadLibraryA'
+ transformed = mod.transform(source)
+
+ self.assertEqual(transformed[0], b'\x8e\xb1\xa3\xa6\x8e\xab\xa4\xb4\xa3\xb4\xbb\x83')
+
+
+ # Example:
+ # - TrickBot (2021) - https://medium.com/walmartglobaltech/trickbot-crews-new-cobaltstrike-loader-32c72b78e81c
+
+ mod = find_token_modifiers_for_name('sub-index1')
+ self.assertIsNotNone(mod)
+
+ source = b'raw.githubusercontent.com'
+ transformed = mod.transform(source)
+
+ self.assertEqual(transformed[0], b'\x73\x63\x7a\x32\x6c\x6f\x7b\x70\x7e\x6c\x80\x7f\x72\x80\x72\x7f\x7f\x86\x78\x82\x89\x44\x7a\x87\x86')
+
+
+ def testBytePatternModifiersAPI(self):
+ """Validate the API for pattern modifiers."""
+
+ mod = find_token_modifiers_for_name('plain')
+ self.assertIsNotNone(mod)
+
+ source = [ b'ABC', b'01234' ]
+ transformed = mod.transform(source)
+
+ self.assertEqual(len(source), len(transformed))
+ self.assertEqual(source[0], transformed[0])
+ self.assertEqual(source[1], transformed[1])
+
+
+ mod = find_token_modifiers_for_name('xor')
+ self.assertIsNotNone(mod)
+
+ source = [ b'ABC' ]
+ transformed = mod.transform(source, 0x20)
+
+ self.assertEqual(transformed[0], b'abc')