mirror of
https://github.com/mandiant/capa.git
synced 2025-12-12 23:59:48 -08:00
Compare commits
622 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c4a46b7b4 | ||
|
|
8fe88f601f | ||
|
|
d46cf5b519 | ||
|
|
29682cf767 | ||
|
|
42df936336 | ||
|
|
fe6117e87a | ||
|
|
04ca770545 | ||
|
|
43f3f31d69 | ||
|
|
acd0020413 | ||
|
|
0002b05418 | ||
|
|
545e198257 | ||
|
|
d4b83e3f8a | ||
|
|
efcc2e0dd4 | ||
|
|
5e0d6176a1 | ||
|
|
e240372a90 | ||
|
|
a64a88981f | ||
|
|
bc8df09be5 | ||
|
|
b09e3e69f2 | ||
|
|
43128404be | ||
|
|
28e85aa548 | ||
|
|
30c14210ed | ||
|
|
d2fc740278 | ||
|
|
cbe30199ff | ||
|
|
3f5d9c79f9 | ||
|
|
59332c2e94 | ||
|
|
d230780443 | ||
|
|
7387c073fb | ||
|
|
535ba622ae | ||
|
|
c6b634f3ae | ||
|
|
386baec3c5 | ||
|
|
b2ead45ad4 | ||
|
|
74284e9dad | ||
|
|
270077bc73 | ||
|
|
367a0c483c | ||
|
|
8a272e92c7 | ||
|
|
2d1105dba9 | ||
|
|
c798996f6e | ||
|
|
ef0e4bd4fd | ||
|
|
bfaee2c402 | ||
|
|
1f6cd807a4 | ||
|
|
6f416dfefb | ||
|
|
06c71a7f2b | ||
|
|
270350f8d1 | ||
|
|
c603b92bc5 | ||
|
|
59be399dac | ||
|
|
7f39cb1bc3 | ||
|
|
d09e1c8ee2 | ||
|
|
c1735b6033 | ||
|
|
1921961cff | ||
|
|
3cd766630f | ||
|
|
fac548a76e | ||
|
|
24f4ebef23 | ||
|
|
99ee317fd0 | ||
|
|
456f6e0003 | ||
|
|
1ccd2c4d0f | ||
|
|
f42b5b1088 | ||
|
|
1b90a28acd | ||
|
|
cd0e0ce4d1 | ||
|
|
7cb4ea9273 | ||
|
|
66e374a343 | ||
|
|
5e8262d3c0 | ||
|
|
6bb14d0874 | ||
|
|
c3fdab8ec5 | ||
|
|
237554d84a | ||
|
|
6ed7aca5be | ||
|
|
a13ce094b3 | ||
|
|
6806b8f5a7 | ||
|
|
e3d9386239 | ||
|
|
fbdf92367e | ||
|
|
2ec96d7f13 | ||
|
|
1c457d3428 | ||
|
|
fe1193f374 | ||
|
|
abbf3db2ac | ||
|
|
5a1009520d | ||
|
|
b49fb7fcf9 | ||
|
|
9e12c563bc | ||
|
|
530e28cbc3 | ||
|
|
637dd6bf0a | ||
|
|
fdc9530352 | ||
|
|
4990f7a2c8 | ||
|
|
b5f274bf56 | ||
|
|
ac2d01a60a | ||
|
|
95bdaf072b | ||
|
|
af1500825a | ||
|
|
cd2ef15a8a | ||
|
|
02359e5e84 | ||
|
|
d873cc0257 | ||
|
|
ea2acea668 | ||
|
|
4a40732cad | ||
|
|
cd9f32ced5 | ||
|
|
2bedc6b181 | ||
|
|
e26deb472e | ||
|
|
78d0111a6c | ||
|
|
d61c85c171 | ||
|
|
03f0034d33 | ||
|
|
3f2e698684 | ||
|
|
259aa53de4 | ||
|
|
7915fb3fb6 | ||
|
|
fbb348bc82 | ||
|
|
a8552e6b96 | ||
|
|
4be3fe1628 | ||
|
|
a087045322 | ||
|
|
248229a383 | ||
|
|
0ff22d319f | ||
|
|
a1dfcc73dd | ||
|
|
3e98115dc2 | ||
|
|
ddc52fa21c | ||
|
|
986e2e6057 | ||
|
|
793057c202 | ||
|
|
3bf9cacaec | ||
|
|
bed4593d04 | ||
|
|
e8082173ad | ||
|
|
b1f4035530 | ||
|
|
0d4a92a351 | ||
|
|
89803e7523 | ||
|
|
613ce92cfd | ||
|
|
8bde277be2 | ||
|
|
3be7bbbf88 | ||
|
|
d8aa276f25 | ||
|
|
dcddef09dc | ||
|
|
ad442aaae3 | ||
|
|
21ecc7618a | ||
|
|
8f8a0b118f | ||
|
|
0358b46fcd | ||
|
|
1a29077b45 | ||
|
|
c249b841e8 | ||
|
|
7d12942cf7 | ||
|
|
c52b0a22e0 | ||
|
|
840145f947 | ||
|
|
10d6e55d62 | ||
|
|
80112bac64 | ||
|
|
49ff9d5a7c | ||
|
|
1044709803 | ||
|
|
252f5cebb7 | ||
|
|
e8ddee4782 | ||
|
|
8daa1c032c | ||
|
|
beccf28d09 | ||
|
|
5ac3414490 | ||
|
|
5d49f5a1d2 | ||
|
|
41bf5f0926 | ||
|
|
4c5a16a1db | ||
|
|
85fb9aa99f | ||
|
|
57d34087dd | ||
|
|
2d65b4b2a1 | ||
|
|
d068faa35e | ||
|
|
1c33cd4470 | ||
|
|
21e410cc77 | ||
|
|
68ebd87127 | ||
|
|
62069e9e59 | ||
|
|
14a2088606 | ||
|
|
114c3854e7 | ||
|
|
26ca593fad | ||
|
|
ec785f9d6d | ||
|
|
f54ef35a7a | ||
|
|
e0b57fc74e | ||
|
|
4754a84a8a | ||
|
|
02fdf41969 | ||
|
|
92e75ee89b | ||
|
|
7c2b6a3161 | ||
|
|
26a8647444 | ||
|
|
cae7c4d0a7 | ||
|
|
27a5e17a3e | ||
|
|
a9ba133506 | ||
|
|
eb20724d78 | ||
|
|
1b9e486c49 | ||
|
|
7ef167fcd0 | ||
|
|
9db106e3f0 | ||
|
|
b4052e5a64 | ||
|
|
9a77f18ced | ||
|
|
03996f2b82 | ||
|
|
53ca96fcee | ||
|
|
c1ca4ab703 | ||
|
|
43bcf401b2 | ||
|
|
f1c495dc0a | ||
|
|
98eb28704c | ||
|
|
1f3582c9c3 | ||
|
|
62f7bddd4d | ||
|
|
b097569607 | ||
|
|
da6f72c20a | ||
|
|
00e94d976a | ||
|
|
d1d6db877d | ||
|
|
da3e3c6bb4 | ||
|
|
e57be09823 | ||
|
|
7598a97888 | ||
|
|
ebaf51ce56 | ||
|
|
0cf8b154a4 | ||
|
|
b420d6bbb2 | ||
|
|
6086cc5e18 | ||
|
|
c3ed12d8d4 | ||
|
|
2d98c9e3c4 | ||
|
|
0933040d0b | ||
|
|
12046e698e | ||
|
|
73ac83bd06 | ||
|
|
631685472d | ||
|
|
32bcf999b8 | ||
|
|
008f6d1839 | ||
|
|
1746a640cc | ||
|
|
d5937e4af5 | ||
|
|
1336796c0c | ||
|
|
2efcfcf239 | ||
|
|
8f2ffe8526 | ||
|
|
8cf74759a6 | ||
|
|
22a1a8e41f | ||
|
|
74009eb4a4 | ||
|
|
5932358f9d | ||
|
|
1ad5364fec | ||
|
|
201330295c | ||
|
|
a7b7f643a5 | ||
|
|
4fd6f17ced | ||
|
|
e67679658a | ||
|
|
d67f924b73 | ||
|
|
961daf6c36 | ||
|
|
748e7641ef | ||
|
|
6321adc411 | ||
|
|
02e451a2b1 | ||
|
|
8cac47038c | ||
|
|
59ab8e0b04 | ||
|
|
577d96c026 | ||
|
|
7031c68a85 | ||
|
|
3a7326726e | ||
|
|
f01d79df46 | ||
|
|
df6de3446c | ||
|
|
eaeef59583 | ||
|
|
f9c7ca2941 | ||
|
|
50935372ca | ||
|
|
d8f89d49d4 | ||
|
|
7e823057b9 | ||
|
|
e4d69984d3 | ||
|
|
acd04e7181 | ||
|
|
22a53bb1dc | ||
|
|
aaef16f51b | ||
|
|
8613c88a60 | ||
|
|
6070bd562e | ||
|
|
01c4ac822c | ||
|
|
05dbdd4473 | ||
|
|
64323b394a | ||
|
|
70f6f1cd03 | ||
|
|
e9d4a23dad | ||
|
|
3cdbc66375 | ||
|
|
5128638071 | ||
|
|
1f80791f8f | ||
|
|
44d8e693b0 | ||
|
|
3bdc61f5ee | ||
|
|
a7e4d265e2 | ||
|
|
0ac497ab59 | ||
|
|
dbb0200147 | ||
|
|
ff7a93f364 | ||
|
|
8f6a660f3d | ||
|
|
64c542502b | ||
|
|
b4974a80bb | ||
|
|
95f23dafe5 | ||
|
|
02dc42154b | ||
|
|
4047780c08 | ||
|
|
c648af2cb4 | ||
|
|
4a698ffdff | ||
|
|
1babdb069f | ||
|
|
b49213bef6 | ||
|
|
42e877671b | ||
|
|
14c18727db | ||
|
|
aacfcaaa23 | ||
|
|
9f3428e1c3 | ||
|
|
52de09a032 | ||
|
|
be6bb879f3 | ||
|
|
f7371c4a9f | ||
|
|
bd7cf8cdd1 | ||
|
|
70b39cbd2c | ||
|
|
199a5cff4b | ||
|
|
501e213dce | ||
|
|
d663007e60 | ||
|
|
a07ca443f0 | ||
|
|
84df8baa5f | ||
|
|
241c0aeedd | ||
|
|
ae85399193 | ||
|
|
17f70bb87c | ||
|
|
7a1f2f4b3b | ||
|
|
599d3ac92c | ||
|
|
02f8e57e66 | ||
|
|
b6ac6d2959 | ||
|
|
c681175685 | ||
|
|
5e600d02a8 | ||
|
|
b9edb6dbc9 | ||
|
|
6e5302e5ec | ||
|
|
4b472c8564 | ||
|
|
4ccf6f0e69 | ||
|
|
eac3d8336d | ||
|
|
53475c9643 | ||
|
|
3c0361fd5c | ||
|
|
0d14c168a4 | ||
|
|
00ecfe7a80 | ||
|
|
fd64b2c5d5 | ||
|
|
099cd868ae | ||
|
|
3071394ef4 | ||
|
|
d1b4e59e7d | ||
|
|
50750a59d9 | ||
|
|
e41afbee58 | ||
|
|
9ea2aca9cb | ||
|
|
c7ab89507e | ||
|
|
c197fd5086 | ||
|
|
b6e607f60e | ||
|
|
38d8b7f501 | ||
|
|
514b4929b3 | ||
|
|
e8cef536f6 | ||
|
|
4ea3475d2b | ||
|
|
15a276e3a5 | ||
|
|
f6e58ea212 | ||
|
|
1b191b5aea | ||
|
|
c2346f41cb | ||
|
|
3f40f47104 | ||
|
|
3dfb7beb6b | ||
|
|
6a222a6139 | ||
|
|
b34864c55e | ||
|
|
26655315c7 | ||
|
|
8aaa8809e6 | ||
|
|
cbac0e0d3b | ||
|
|
22b8c594b8 | ||
|
|
7a8065b2bb | ||
|
|
6070479e0a | ||
|
|
fd70dc24df | ||
|
|
8cb8cfdb46 | ||
|
|
79f25ec0a3 | ||
|
|
2235417a25 | ||
|
|
ce449790df | ||
|
|
79e36ab11d | ||
|
|
dde3abdfa0 | ||
|
|
7ea166f98c | ||
|
|
faceca6fec | ||
|
|
6589b2044b | ||
|
|
f00e44aba6 | ||
|
|
6591b574a0 | ||
|
|
ca91051d1a | ||
|
|
29f24de5d5 | ||
|
|
2014c64732 | ||
|
|
b5c6cdeaa1 | ||
|
|
bf7c569060 | ||
|
|
bbc0afd083 | ||
|
|
8857f92f7c | ||
|
|
70f568b1cc | ||
|
|
c586166006 | ||
|
|
96f266ce5e | ||
|
|
e5549d6ce8 | ||
|
|
b60717bb8c | ||
|
|
83eefd343c | ||
|
|
03e8be6368 | ||
|
|
a58e9e4df3 | ||
|
|
0a78187c69 | ||
|
|
61112c2527 | ||
|
|
67cfefd2df | ||
|
|
3dfd16c033 | ||
|
|
67b9d2e1c0 | ||
|
|
a076a0c44e | ||
|
|
f152729c79 | ||
|
|
3c0e36d5d4 | ||
|
|
887f37b72c | ||
|
|
e30dd08dec | ||
|
|
2d1bbeda0c | ||
|
|
68603a9cc7 | ||
|
|
6c83db9977 | ||
|
|
6d16cafbc8 | ||
|
|
e503cedd8f | ||
|
|
1a498d1afc | ||
|
|
33a46cc633 | ||
|
|
b3b9ec11dd | ||
|
|
a7afdec2e1 | ||
|
|
56a0bedac9 | ||
|
|
f451fe68e1 | ||
|
|
946816e377 | ||
|
|
99af09fce5 | ||
|
|
0888e5ad69 | ||
|
|
c423ccec67 | ||
|
|
03f72f498e | ||
|
|
fbd7c566f4 | ||
|
|
e09d35bbb9 | ||
|
|
e644775ad1 | ||
|
|
6ad471a914 | ||
|
|
476ffabae9 | ||
|
|
4b7a9e149f | ||
|
|
49c18bd83d | ||
|
|
67717761bd | ||
|
|
b10196cdac | ||
|
|
fa0ddba436 | ||
|
|
0fb3be359f | ||
|
|
26662e99de | ||
|
|
5513d4ca43 | ||
|
|
2b07ec925c | ||
|
|
efb4c9d540 | ||
|
|
b8de9625ee | ||
|
|
607daa345e | ||
|
|
35e6df6f6b | ||
|
|
cb1ef965d0 | ||
|
|
2ab057a24d | ||
|
|
12f8588c03 | ||
|
|
3571f35578 | ||
|
|
803fe321d1 | ||
|
|
cf42670e97 | ||
|
|
ac36b9d328 | ||
|
|
9a9f72f07a | ||
|
|
4b9a844c92 | ||
|
|
a273ad31d4 | ||
|
|
16f3164865 | ||
|
|
5fb9de775f | ||
|
|
05879dc02a | ||
|
|
d5cb36151f | ||
|
|
b6fd95c7b8 | ||
|
|
8ce570cea7 | ||
|
|
5b82ed2fd9 | ||
|
|
37a4dbf822 | ||
|
|
ef86160d88 | ||
|
|
5f31bdbb3e | ||
|
|
810e2d70d3 | ||
|
|
85dd065f91 | ||
|
|
2a61e357de | ||
|
|
e34fdfae1a | ||
|
|
58e94a35cb | ||
|
|
93acf9feb4 | ||
|
|
0362148989 | ||
|
|
985ea5ebdc | ||
|
|
64ebf14256 | ||
|
|
cfebe5a5ba | ||
|
|
99e0e45bfc | ||
|
|
83845078a7 | ||
|
|
7c102509bd | ||
|
|
1af90b9db3 | ||
|
|
d4de650f90 | ||
|
|
5de0324441 | ||
|
|
5fa2a87747 | ||
|
|
68ef9d7858 | ||
|
|
a286e066d1 | ||
|
|
94a712b820 | ||
|
|
c8aa73ac18 | ||
|
|
a74b8e6328 | ||
|
|
ff773695d0 | ||
|
|
c4ebb0a31d | ||
|
|
f9b3d6304c | ||
|
|
1c85f530b1 | ||
|
|
d65d7bcd7e | ||
|
|
c11633c5db | ||
|
|
ea0a708f35 | ||
|
|
00254b93dc | ||
|
|
6932df3564 | ||
|
|
9e3a48aa8d | ||
|
|
6e17462bd0 | ||
|
|
d29e7e6f3a | ||
|
|
049e222e88 | ||
|
|
caef7812a3 | ||
|
|
68efa7316b | ||
|
|
5396d5f99e | ||
|
|
4576cbd0a1 | ||
|
|
1fa9180fee | ||
|
|
801c80d7a2 | ||
|
|
eba1989c9f | ||
|
|
90591811df | ||
|
|
c959506ae9 | ||
|
|
25f9029a82 | ||
|
|
4f75b3d9f6 | ||
|
|
974d79f2be | ||
|
|
c0a8a91281 | ||
|
|
2219139605 | ||
|
|
966e38babf | ||
|
|
5f39083df6 | ||
|
|
565b002bfe | ||
|
|
1dd5a8dbf2 | ||
|
|
7ef17b8dee | ||
|
|
d01a0e022d | ||
|
|
3258556d5d | ||
|
|
5f77200108 | ||
|
|
b12865f1e5 | ||
|
|
ee90fc8761 | ||
|
|
e6585ee526 | ||
|
|
b68be0c2ce | ||
|
|
3b95ed0b5a | ||
|
|
50490e6a93 | ||
|
|
d466345e4e | ||
|
|
4ece47c64c | ||
|
|
2b85af0f88 | ||
|
|
e0491097b0 | ||
|
|
fa3d658f33 | ||
|
|
6dcd115765 | ||
|
|
88cffee902 | ||
|
|
b12d526a60 | ||
|
|
3af7fe0b08 | ||
|
|
d7548c0b20 | ||
|
|
f79e16d1a6 | ||
|
|
ad47ea3bab | ||
|
|
505910edb5 | ||
|
|
aee0ec8016 | ||
|
|
613c185428 | ||
|
|
501227f23f | ||
|
|
56d075fd32 | ||
|
|
9ae908c741 | ||
|
|
81500a4d1d | ||
|
|
b819033da0 | ||
|
|
35243ef7a6 | ||
|
|
655c45d43f | ||
|
|
34c4809f68 | ||
|
|
f9b6800831 | ||
|
|
b5254e3662 | ||
|
|
148cb71839 | ||
|
|
62700ca5d1 | ||
|
|
b1d6fcd6c8 | ||
|
|
8afebc1f17 | ||
|
|
447cd95bc5 | ||
|
|
5224380947 | ||
|
|
7aeb685412 | ||
|
|
b6911f8ad2 | ||
|
|
a7d06275c1 | ||
|
|
d581eefcdf | ||
|
|
47f58162c5 | ||
|
|
ee72ed4b53 | ||
|
|
5cd7f33d00 | ||
|
|
d6674c7548 | ||
|
|
a46d7b3262 | ||
|
|
0f902124d1 | ||
|
|
d4a218e268 | ||
|
|
22bef146f8 | ||
|
|
b26ed47ab8 | ||
|
|
7ba08edffa | ||
|
|
c958a6a286 | ||
|
|
1583fedba2 | ||
|
|
307a6fad4f | ||
|
|
958d5bcc6a | ||
|
|
c5a9aa21bf | ||
|
|
13b5d7c179 | ||
|
|
bd84ee83a5 | ||
|
|
97f633312f | ||
|
|
b290690b19 | ||
|
|
fc57ed76a0 | ||
|
|
a6fdb71178 | ||
|
|
fe2f668306 | ||
|
|
45d007fa9a | ||
|
|
662ec11031 | ||
|
|
1d8a3486cd | ||
|
|
c195afa0b3 | ||
|
|
63e0d9b3f3 | ||
|
|
659cbedc3c | ||
|
|
0ebba2cd15 | ||
|
|
1f091a4ccd | ||
|
|
d1aafa3764 | ||
|
|
faefe41ad5 | ||
|
|
473d0daf58 | ||
|
|
a10abfebde | ||
|
|
78172b5f5b | ||
|
|
1caeb248ca | ||
|
|
8527d02dc8 | ||
|
|
0e73f26e88 | ||
|
|
ed24db4460 | ||
|
|
127886144b | ||
|
|
c83877ec74 | ||
|
|
8d6fcd9939 | ||
|
|
1dc5e40308 | ||
|
|
cc832d26aa | ||
|
|
9fcb70387d | ||
|
|
236ad883d4 | ||
|
|
12c9c466c7 | ||
|
|
5a1cb0e48d | ||
|
|
5196caabb5 | ||
|
|
0f99592903 | ||
|
|
56e9645700 | ||
|
|
0d8c6cc0fd | ||
|
|
20c7949be3 | ||
|
|
7cc6773bf8 | ||
|
|
055700a5d1 | ||
|
|
85b14075cd | ||
|
|
149c3989f1 | ||
|
|
3b5a34f331 | ||
|
|
b4fe2d8592 | ||
|
|
67d06c73e0 | ||
|
|
81a942d7a1 | ||
|
|
521473cd81 | ||
|
|
676d422511 | ||
|
|
f2dbb531fe | ||
|
|
84fce86152 | ||
|
|
8307c66256 | ||
|
|
ac71676d79 | ||
|
|
70e6d83259 | ||
|
|
3bbac4a35f | ||
|
|
87455ed6dd | ||
|
|
e1735f0a5e | ||
|
|
8521f85742 | ||
|
|
b1b15e2eef | ||
|
|
36e304839b | ||
|
|
5a14a6d0cc | ||
|
|
85901893a0 | ||
|
|
49d7f2a88f | ||
|
|
8d8c5f99c1 | ||
|
|
4069515cad | ||
|
|
3c1cd67f60 | ||
|
|
580948e46b | ||
|
|
4ffd7b89f3 | ||
|
|
2441c18a85 | ||
|
|
ee89fa45b6 | ||
|
|
3976e5858d | ||
|
|
4e542f9cff | ||
|
|
ce1ecfad4d | ||
|
|
d9d5aaffa1 | ||
|
|
21809350f7 | ||
|
|
418b063067 | ||
|
|
dcf838872c | ||
|
|
456b32e6a8 | ||
|
|
acad9c5570 | ||
|
|
4b2cfb4825 | ||
|
|
7733562587 | ||
|
|
eaa70fa80f | ||
|
|
44843ea977 | ||
|
|
cac041b869 | ||
|
|
49684e4c25 | ||
|
|
47268c2344 | ||
|
|
da0a1e7903 | ||
|
|
eca1582678 | ||
|
|
2049058b45 | ||
|
|
c2b5e7116d | ||
|
|
9c1b076a5f | ||
|
|
51f7e10cb6 | ||
|
|
25ad6446ba | ||
|
|
1af5255501 | ||
|
|
49d61db8f9 | ||
|
|
601471c1e6 | ||
|
|
3c4141589d | ||
|
|
c5f768accc | ||
|
|
2e6671ff91 | ||
|
|
f4171c32cf | ||
|
|
449c64d80b | ||
|
|
735cb57b10 |
2
.github/CONTRIBUTING.md
vendored
2
.github/CONTRIBUTING.md
vendored
@@ -31,7 +31,7 @@ This project and everyone participating in it is governed by the [Capa Code of C
|
||||
|
||||
### Capa and its repositories
|
||||
|
||||
We host the capa project as three Github repositories:
|
||||
We host the capa project as three GitHub repositories:
|
||||
- [capa](https://github.com/mandiant/capa)
|
||||
- [capa-rules](https://github.com/mandiant/capa-rules)
|
||||
- [capa-testfiles](https://github.com/mandiant/capa-testfiles)
|
||||
|
||||
14
.github/mypy/mypy.ini
vendored
14
.github/mypy/mypy.ini
vendored
@@ -21,9 +21,6 @@ ignore_missing_imports = True
|
||||
[mypy-flirt.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-smda.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-lief.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
@@ -48,6 +45,9 @@ ignore_missing_imports = True
|
||||
[mypy-ida_bytes.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-ida_nalt.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-ida_kernwin.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
@@ -60,6 +60,9 @@ ignore_missing_imports = True
|
||||
[mypy-ida_loader.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-ida_segment.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-PyQt5.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
@@ -76,4 +79,7 @@ ignore_missing_imports = True
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-dncil.*]
|
||||
ignore_missing_imports = True
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-netnode.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
5
.github/pyinstaller/hooks/hook-smda.py
vendored
5
.github/pyinstaller/hooks/hook-smda.py
vendored
@@ -1,5 +0,0 @@
|
||||
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
|
||||
import PyInstaller.utils.hooks
|
||||
|
||||
# ref: https://groups.google.com/g/pyinstaller/c/amWi0-66uZI/m/miPoKfWjBAAJ
|
||||
binaries = PyInstaller.utils.hooks.collect_dynamic_libs("capstone")
|
||||
71
.github/pyinstaller/pyinstaller.spec
vendored
71
.github/pyinstaller/pyinstaller.spec
vendored
@@ -6,51 +6,35 @@ import subprocess
|
||||
import wcwidth
|
||||
|
||||
|
||||
# when invoking pyinstaller from the project root,
|
||||
# this gets run from the project root.
|
||||
with open('./capa/version.py', 'wb') as f:
|
||||
# git output will look like:
|
||||
#
|
||||
# tags/v1.0.0-0-g3af38dc
|
||||
# ------- tag
|
||||
# - commits since
|
||||
# g------- git hash fragment
|
||||
version = (subprocess.check_output(["git", "describe", "--always", "--tags", "--long"])
|
||||
.decode("utf-8")
|
||||
.strip()
|
||||
.replace("tags/", ""))
|
||||
f.write(("__version__ = '%s'" % version).encode("utf-8"))
|
||||
|
||||
a = Analysis(
|
||||
# when invoking pyinstaller from the project root,
|
||||
# this gets invoked from the directory of the spec file,
|
||||
# i.e. ./.github/pyinstaller
|
||||
['../../capa/main.py'],
|
||||
pathex=['capa'],
|
||||
["../../capa/main.py"],
|
||||
pathex=["capa"],
|
||||
binaries=None,
|
||||
datas=[
|
||||
# when invoking pyinstaller from the project root,
|
||||
# this gets invoked from the directory of the spec file,
|
||||
# i.e. ./.github/pyinstaller
|
||||
('../../rules', 'rules'),
|
||||
('../../sigs', 'sigs'),
|
||||
|
||||
("../../rules", "rules"),
|
||||
("../../sigs", "sigs"),
|
||||
("../../cache", "cache"),
|
||||
# capa.render.default uses tabulate that depends on wcwidth.
|
||||
# it seems wcwidth uses a json file `version.json`
|
||||
# and this doesn't get picked up by pyinstaller automatically.
|
||||
# so we manually embed the wcwidth resources here.
|
||||
#
|
||||
# ref: https://stackoverflow.com/a/62278462/87207
|
||||
(os.path.dirname(wcwidth.__file__), 'wcwidth')
|
||||
(os.path.dirname(wcwidth.__file__), "wcwidth"),
|
||||
],
|
||||
# when invoking pyinstaller from the project root,
|
||||
# this gets run from the project root.
|
||||
hookspath=['.github/pyinstaller/hooks'],
|
||||
hookspath=[".github/pyinstaller/hooks"],
|
||||
runtime_hooks=None,
|
||||
excludes=[
|
||||
# ignore packages that would otherwise be bundled with the .exe.
|
||||
# review: build/pyinstaller/xref-pyinstaller.html
|
||||
|
||||
# we don't do any GUI stuff, so ignore these modules
|
||||
"tkinter",
|
||||
"_tkinter",
|
||||
@@ -60,7 +44,6 @@ a = Analysis(
|
||||
# since we don't spawn a notebook, we can safely remove these.
|
||||
"IPython",
|
||||
"ipywidgets",
|
||||
|
||||
# these are pulled in by networkx
|
||||
# but we don't need to compute the strongly connected components.
|
||||
"numpy",
|
||||
@@ -68,7 +51,6 @@ a = Analysis(
|
||||
"matplotlib",
|
||||
"pandas",
|
||||
"pytest",
|
||||
|
||||
# deps from viv that we don't use.
|
||||
# this duplicates the entries in `hook-vivisect`,
|
||||
# but works better this way.
|
||||
@@ -78,32 +60,33 @@ a = Analysis(
|
||||
"PyQt5",
|
||||
"qt5",
|
||||
"pyqtwebengine",
|
||||
"pyasn1"
|
||||
])
|
||||
"pyasn1",
|
||||
"binaryninja",
|
||||
],
|
||||
)
|
||||
|
||||
a.binaries = a.binaries - TOC([
|
||||
('tcl85.dll', None, None),
|
||||
('tk85.dll', None, None),
|
||||
('_tkinter', None, None)])
|
||||
a.binaries = a.binaries - TOC([("tcl85.dll", None, None), ("tk85.dll", None, None), ("_tkinter", None, None)])
|
||||
|
||||
pyz = PYZ(a.pure, a.zipped_data)
|
||||
|
||||
exe = EXE(pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
exclude_binaries=False,
|
||||
name='capa',
|
||||
icon='logo.ico',
|
||||
debug=False,
|
||||
strip=None,
|
||||
upx=True,
|
||||
console=True )
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
exclude_binaries=False,
|
||||
name="capa",
|
||||
icon="logo.ico",
|
||||
debug=False,
|
||||
strip=None,
|
||||
upx=True,
|
||||
console=True,
|
||||
)
|
||||
|
||||
# enable the following to debug the contents of the .exe
|
||||
#
|
||||
#coll = COLLECT(exe,
|
||||
# coll = COLLECT(exe,
|
||||
# a.binaries,
|
||||
# a.zipfiles,
|
||||
# a.datas,
|
||||
|
||||
29
.github/workflows/build.yml
vendored
29
.github/workflows/build.yml
vendored
@@ -15,33 +15,35 @@ jobs:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-18.04
|
||||
- os: ubuntu-20.04
|
||||
# use old linux so that the shared library versioning is more portable
|
||||
artifact_name: capa
|
||||
asset_name: linux
|
||||
- os: windows-2022
|
||||
- os: windows-2019
|
||||
artifact_name: capa.exe
|
||||
asset_name: windows
|
||||
- os: macos-10.15
|
||||
- os: macos-11
|
||||
# use older macOS for assumed better portability
|
||||
artifact_name: capa
|
||||
asset_name: macos
|
||||
steps:
|
||||
- name: Checkout capa
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
with:
|
||||
submodules: true
|
||||
# using Python 3.8 to support running across multiple operating systems including Windows 7
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435 # v4.5.0
|
||||
with:
|
||||
python-version: 3.8
|
||||
- if: matrix.os == 'ubuntu-18.04'
|
||||
- if: matrix.os == 'ubuntu-20.04'
|
||||
run: sudo apt-get install -y libyaml-dev
|
||||
- name: Upgrade pip, setuptools
|
||||
run: python -m pip install --upgrade pip setuptools
|
||||
- name: Install capa with build requirements
|
||||
run: pip install -e .[build]
|
||||
- name: Cache the rule set
|
||||
run: python ./scripts/cache-ruleset.py ./rules/ ./cache/
|
||||
- name: Build standalone executable
|
||||
run: pyinstaller --log-level DEBUG .github/pyinstaller/pyinstaller.spec
|
||||
- name: Does it run (PE)?
|
||||
@@ -50,7 +52,7 @@ jobs:
|
||||
run: dist/capa "tests/data/499c2a85f6e8142c3f48d4251c9c7cd6.raw32"
|
||||
- name: Does it run (ELF)?
|
||||
run: dist/capa "tests/data/7351f8a40c5450557b24622417fc478d.elf_"
|
||||
- uses: actions/upload-artifact@v2
|
||||
- uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
|
||||
with:
|
||||
name: ${{ matrix.asset_name }}
|
||||
path: dist/${{ matrix.artifact_name }}
|
||||
@@ -63,10 +65,7 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
# OSs not already tested above
|
||||
- os: ubuntu-18.04
|
||||
artifact_name: capa
|
||||
asset_name: linux
|
||||
- os: ubuntu-20.04
|
||||
- os: ubuntu-22.04
|
||||
artifact_name: capa
|
||||
asset_name: linux
|
||||
- os: windows-2022
|
||||
@@ -74,11 +73,11 @@ jobs:
|
||||
asset_name: windows
|
||||
steps:
|
||||
- name: Download ${{ matrix.asset_name }}
|
||||
uses: actions/download-artifact@v2
|
||||
uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
|
||||
with:
|
||||
name: ${{ matrix.asset_name }}
|
||||
- name: Set executable flag
|
||||
if: matrix.os != 'windows-2022'
|
||||
if: matrix.os != 'windows-2022'
|
||||
run: chmod +x ${{ matrix.artifact_name }}
|
||||
- name: Run capa
|
||||
run: ./${{ matrix.artifact_name }} -h
|
||||
@@ -100,7 +99,7 @@ jobs:
|
||||
artifact_name: capa
|
||||
steps:
|
||||
- name: Download ${{ matrix.asset_name }}
|
||||
uses: actions/download-artifact@v2
|
||||
uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
|
||||
with:
|
||||
name: ${{ matrix.asset_name }}
|
||||
- name: Set executable flag
|
||||
@@ -110,7 +109,7 @@ jobs:
|
||||
- name: Zip ${{ matrix.artifact_name }} into ${{ env.zip_name }}
|
||||
run: zip ${{ env.zip_name }} ${{ matrix.artifact_name }}
|
||||
- name: Upload ${{ env.zip_name }} to GH Release
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
uses: svenstaro/upload-release-action@2728235f7dc9ff598bd86ce3c274b74f802d2208 # v2
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN}}
|
||||
file: ${{ env.zip_name }}
|
||||
|
||||
6
.github/workflows/changelog.yml
vendored
6
.github/workflows/changelog.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
steps:
|
||||
- name: Get changed files
|
||||
id: files
|
||||
uses: Ana06/get-changed-files@v1.2
|
||||
uses: Ana06/get-changed-files@e0c398b7065a8d84700c471b6afc4116d1ba4e96 # v2.2.0
|
||||
- name: check changelog updated
|
||||
id: changelog_updated
|
||||
env:
|
||||
@@ -27,14 +27,14 @@ jobs:
|
||||
echo $FILES | grep -qF 'CHANGELOG.md' || echo $PR_BODY | grep -qiF "$NO_CHANGELOG"
|
||||
- name: Reject pull request if no CHANGELOG update
|
||||
if: ${{ always() && steps.changelog_updated.outcome == 'failure' }}
|
||||
uses: Ana06/automatic-pull-request-review@v0.1.0
|
||||
uses: Ana06/automatic-pull-request-review@0cf4e8a17ba79344ed3fdd7fed6dd0311d08a9d4 # v0.1.0
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
event: REQUEST_CHANGES
|
||||
body: "Please add bug fixes, new features, breaking changes and anything else you think is worthwhile mentioning to the `master (unreleased)` section of CHANGELOG.md. If no CHANGELOG update is needed add the following to the PR description: `${{ env.NO_CHANGELOG }}`"
|
||||
allow_duplicate: false
|
||||
- name: Dismiss previous review if CHANGELOG update
|
||||
uses: Ana06/automatic-pull-request-review@v0.1.0
|
||||
uses: Ana06/automatic-pull-request-review@0cf4e8a17ba79344ed3fdd7fed6dd0311d08a9d4 # v0.1.0
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
event: DISMISS
|
||||
|
||||
5
.github/workflows/publish.yml
vendored
5
.github/workflows/publish.yml
vendored
@@ -11,9 +11,9 @@ jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435 # v4.5.0
|
||||
with:
|
||||
python-version: '3.7'
|
||||
- name: Install dependencies
|
||||
@@ -27,4 +27,3 @@ jobs:
|
||||
run: |
|
||||
python setup.py sdist bdist_wheel
|
||||
twine upload --skip-existing dist/*
|
||||
|
||||
|
||||
72
.github/workflows/scorecard.yml
vendored
Normal file
72
.github/workflows/scorecard.yml
vendored
Normal file
@@ -0,0 +1,72 @@
|
||||
# This workflow uses actions that are not certified by GitHub. They are provided
|
||||
# by a third-party and are governed by separate terms of service, privacy
|
||||
# policy, and support documentation.
|
||||
|
||||
name: Scorecard supply-chain security
|
||||
on:
|
||||
# For Branch-Protection check. Only the default branch is supported. See
|
||||
# https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection
|
||||
branch_protection_rule:
|
||||
# To guarantee Maintained check is occasionally updated. See
|
||||
# https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained
|
||||
schedule:
|
||||
- cron: '43 4 * * 3'
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
|
||||
# Declare default permissions as read only.
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
analysis:
|
||||
name: Scorecard analysis
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
# Needed to upload the results to code-scanning dashboard.
|
||||
security-events: write
|
||||
# Needed to publish results and get a badge (see publish_results below).
|
||||
id-token: write
|
||||
# Uncomment the permissions below if installing in a private repository.
|
||||
# contents: read
|
||||
# actions: read
|
||||
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # v3.1.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: "Run analysis"
|
||||
uses: ossf/scorecard-action@99c53751e09b9529366343771cc321ec74e9bd3d # v2.0.6
|
||||
with:
|
||||
results_file: results.sarif
|
||||
results_format: sarif
|
||||
# (Optional) "write" PAT token. Uncomment the `repo_token` line below if:
|
||||
# - you want to enable the Branch-Protection check on a *public* repository, or
|
||||
# - you are installing Scorecard on a *private* repository
|
||||
# To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat.
|
||||
# repo_token: ${{ secrets.SCORECARD_TOKEN }}
|
||||
|
||||
# Public repositories:
|
||||
# - Publish results to OpenSSF REST API for easy access by consumers
|
||||
# - Allows the repository to include the Scorecard badge.
|
||||
# - See https://github.com/ossf/scorecard-action#publishing-results.
|
||||
# For private repositories:
|
||||
# - `publish_results` will always be set to `false`, regardless
|
||||
# of the value entered here.
|
||||
publish_results: true
|
||||
|
||||
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
|
||||
# format to the repository Actions tab.
|
||||
- name: "Upload artifact"
|
||||
uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 # v3.1.0
|
||||
with:
|
||||
name: SARIF file
|
||||
path: results.sarif
|
||||
retention-days: 5
|
||||
|
||||
# Upload the results to GitHub's code scanning dashboard.
|
||||
- name: "Upload to code-scanning"
|
||||
uses: github/codeql-action/upload-sarif@807578363a7869ca324a79039e6db9c843e0e100 # v2.1.27
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
4
.github/workflows/tag.yml
vendored
4
.github/workflows/tag.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout capa-rules
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
with:
|
||||
repository: mandiant/capa-rules
|
||||
token: ${{ secrets.CAPA_TOKEN }}
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
git tag $name -m "https://github.com/mandiant/capa/releases/$name"
|
||||
# TODO update branch name-major=${name%%.*}
|
||||
- name: Push tag to capa-rules
|
||||
uses: ad-m/github-push-action@master
|
||||
uses: ad-m/github-push-action@0fafdd62b84042d49ec0cb92d9cac7f7ce4ec79e # master
|
||||
with:
|
||||
repository: mandiant/capa-rules
|
||||
github_token: ${{ secrets.CAPA_TOKEN }}
|
||||
|
||||
61
.github/workflows/tests.yml
vendored
61
.github/workflows/tests.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout capa
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
# The sync GH action in capa-rules relies on a single '- *$' in the CHANGELOG file
|
||||
- name: Ensure CHANGELOG has '- *$'
|
||||
run: |
|
||||
@@ -26,31 +26,31 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout capa
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435 # v4.5.0
|
||||
with:
|
||||
python-version: "3.8"
|
||||
- name: Install dependencies
|
||||
run: pip install -e .[dev]
|
||||
- name: Lint with isort
|
||||
run: isort --profile black --length-sort --line-width 120 -c .
|
||||
run: isort --profile black --length-sort --line-width 120 --skip-glob "*_pb2.py" -c .
|
||||
- name: Lint with black
|
||||
run: black -l 120 --check .
|
||||
run: black -l 120 --extend-exclude ".*_pb2.py" --check .
|
||||
- name: Lint with pycodestyle
|
||||
run: pycodestyle --show-source capa/ scripts/ tests/
|
||||
run: pycodestyle --exclude="*_pb2.py" --show-source capa/ scripts/ tests/
|
||||
- name: Check types with mypy
|
||||
run: mypy --config-file .github/mypy/mypy.ini capa/ scripts/ tests/
|
||||
run: mypy --config-file .github/mypy/mypy.ini --check-untyped-defs capa/ scripts/ tests/
|
||||
|
||||
rule_linter:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout capa with submodules
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435 # v4.5.0
|
||||
with:
|
||||
python-version: "3.8"
|
||||
- name: Install capa
|
||||
@@ -67,20 +67,22 @@ jobs:
|
||||
matrix:
|
||||
os: [ubuntu-20.04, windows-2019, macos-11]
|
||||
# across all operating systems
|
||||
python-version: ["3.7", "3.10"]
|
||||
python-version: ["3.7", "3.11"]
|
||||
include:
|
||||
# on Ubuntu run these as well
|
||||
- os: ubuntu-20.04
|
||||
python-version: "3.8"
|
||||
- os: ubuntu-20.04
|
||||
python-version: "3.9"
|
||||
- os: ubuntu-20.04
|
||||
python-version: "3.10"
|
||||
steps:
|
||||
- name: Checkout capa with submodules
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435 # v4.5.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install pyyaml
|
||||
@@ -90,3 +92,38 @@ jobs:
|
||||
run: pip install -e .[dev]
|
||||
- name: Run tests
|
||||
run: pytest -v tests/
|
||||
|
||||
binja-tests:
|
||||
name: Binary Ninja tests for ${{ matrix.python-version }} on ${{ matrix.os }}
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [code_style, rule_linter]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.7", "3.11"]
|
||||
steps:
|
||||
- name: Checkout capa with submodules
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435 # v4.5.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install pyyaml
|
||||
run: sudo apt-get install -y libyaml-dev
|
||||
- name: Install capa
|
||||
run: pip install -e .[dev]
|
||||
- name: install Binary Ninja
|
||||
env:
|
||||
BN_SERIAL: ${{ secrets.BN_SERIAL }}
|
||||
run: |
|
||||
mkdir ./.github/binja
|
||||
curl "https://raw.githubusercontent.com/Vector35/binaryninja-api/6812c97/scripts/download_headless.py" -o ./.github/binja/download_headless.py
|
||||
python ./.github/binja/download_headless.py --serial $BN_SERIAL --output .github/binja/BinaryNinja-headless.zip
|
||||
unzip .github/binja/BinaryNinja-headless.zip -d .github/binja/
|
||||
python .github/binja/binaryninja/scripts/install_api.py --install-on-root --silent
|
||||
- name: Run tests
|
||||
env:
|
||||
BN_LICENSE: ${{ secrets.BN_LICENSE }}
|
||||
run: pytest -v tests/test_binja_features.py # explicitly refer to the binja tests for performance. other tests run above.
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -118,7 +118,12 @@ rule-linter-output.log
|
||||
scripts/perf/*.txt
|
||||
scripts/perf/*.svg
|
||||
scripts/perf/*.zip
|
||||
|
||||
.direnv
|
||||
.envrc
|
||||
.DS_Store
|
||||
*/.DS_Store
|
||||
Pipfile
|
||||
Pipfile.lock
|
||||
/cache/
|
||||
.github/binja/binaryninja
|
||||
|
||||
226
CHANGELOG.md
226
CHANGELOG.md
@@ -8,7 +8,7 @@
|
||||
|
||||
### New Rules (0)
|
||||
|
||||
-
|
||||
-
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -17,10 +17,226 @@
|
||||
### Development
|
||||
|
||||
### Raw diffs
|
||||
- [capa v4.0.0...master](https://github.com/mandiant/capa/compare/v4.0.0...master)
|
||||
- [capa-rules v4.0.0...master](https://github.com/mandiant/capa-rules/compare/v4.0.0...master)
|
||||
- [capa v5.1.0...master](https://github.com/mandiant/capa/compare/v5.1.0...master)
|
||||
- [capa-rules v5.1.0...master](https://github.com/mandiant/capa-rules/compare/v5.1.0...master)
|
||||
|
||||
## v4.0.0 (2022-07-XX)
|
||||
## v5.1.0
|
||||
capa version 5.1.0 adds a Protocol Buffers (protobuf) format for result documents. Additionally, the [Vector35](https://vector35.com/) team contributed a new feature extractor using Binary Ninja. Other new features are a new CLI flag to override the detected operating system, functionality to read and render existing result documents, and a output color format that's easier to read.
|
||||
|
||||
Over 25 capa rules have been added and improved.
|
||||
|
||||
Thanks for all the support, especially to @xusheng6, @captainGeech42, @ggold7046, @manasghandat, @ooprathamm, @linpeiyu164, @yelhamer, @HongThatCong, @naikordian, @stevemk14ebr, @emtuls, @raymondlleong, @bkojusner, @joren485, and everyone else who submitted bugs and provided feedback!
|
||||
|
||||
### New Features
|
||||
- add protobuf format for result documents #1219 @williballenthin @mr-tz
|
||||
- extractor: add Binary Ninja feature extractor @xusheng6
|
||||
- new cli flag `--os` to override auto-detected operating system for a sample @captainGeech42
|
||||
- change colour/highlight to "cyan" instead of "blue" for better readability #1384 @ggold7046
|
||||
- add new format to parse output json back to capa #1396 @ooprathamm
|
||||
- parse ELF symbols' names to guess OS #1403 @yelhamer
|
||||
|
||||
### New Rules (26)
|
||||
|
||||
- persistence/scheduled-tasks/schedule-task-via-at joren485
|
||||
- data-manipulation/prng/generate-random-numbers-via-rtlgenrandom william.ballenthin@mandiant.com
|
||||
- communication/ip/convert-ip-address-from-string @mr-tz
|
||||
- data-manipulation/compression/compress-data-via-zlib-inflate-or-deflate blas.kojusner@mandiant.com
|
||||
- executable/installer/dotnet/packaged-as-single-file-dotnet-application michael.hunhoff@mandiant.com
|
||||
- communication/socket/create-raw-socket blas.kojusner@mandiant.com
|
||||
- communication/http/reference-http-user-agent-string @mr-tz
|
||||
- communication/http/get-http-content-length william.ballenthin@mandiant.com
|
||||
- nursery/move-directory michael.hunhoff@mandiant.com
|
||||
- nursery/get-http-request-uri william.ballenthin@mandiant.com
|
||||
- nursery/create-zip-archive-in-dotnet michael.hunhoff@mandiant.com
|
||||
- nursery/extract-zip-archive-in-dotnet anushka.virgaonkar@mandiant.com michael.hunhoff@mandiant.com
|
||||
- data-manipulation/encryption/tea/decrypt-data-using-tea william.ballenthin@mandiant.com raymond.leong@mandiant.com
|
||||
- data-manipulation/encryption/tea/encrypt-data-using-tea william.ballenthin@mandiant.com raymond.leong@mandiant.com
|
||||
- data-manipulation/encryption/xtea/encrypt-data-using-xtea raymond.leong@mandiant.com
|
||||
- data-manipulation/encryption/xxtea/encrypt-data-using-xxtea raymond.leong@mandiant.com
|
||||
- nursery/hash-data-using-ripemd128 raymond.leong@mandiant.com
|
||||
- nursery/hash-data-using-ripemd256 raymond.leong@mandiant.com
|
||||
- nursery/hash-data-using-ripemd320 raymond.leong@mandiant.com
|
||||
- nursery/set-web-proxy-in-dotnet michael.hunhoff@mandiant.com
|
||||
- nursery/check-for-windows-sandbox-via-subdirectory echernofsky@google.com
|
||||
- nursery/enumerate-pe-sections-in-dotnet @mr-tz
|
||||
- nursery/destroy-software-breakpoint-capability echernofsky@google.com
|
||||
- nursery/send-data-to-internet michael.hunhoff@mandiant.com
|
||||
- nursery/compiled-with-cx_freeze @mr-tz
|
||||
- nursery/contain-a-thread-local-storage-tls-section-in-dotnet michael.hunhoff@mandiant.com
|
||||
|
||||
### Bug Fixes
|
||||
- extractor: removed '.dynsym' as the library name for ELF imports #1318 @stevemk14ebr
|
||||
- extractor: fix vivisect loop detection corner case #1310 @mr-tz
|
||||
- match: extend OS characteristic to match OS_ANY to all supported OSes #1324 @mike-hunhoff
|
||||
- extractor: fix IDA and vivisect string and bytes features overlap and tests #1327 #1336 @xusheng6
|
||||
|
||||
### capa explorer IDA Pro plugin
|
||||
- fix exception when plugin loaded in IDA hosted under idat #1341 @mike-hunhoff
|
||||
- improve embedded PE detection performance and reduce FP potential #1344 @mike-hunhoff
|
||||
|
||||
### Raw diffs
|
||||
- [capa v5.0.0...v5.1.0](https://github.com/mandiant/capa/compare/v5.0.0...v5.1.0)
|
||||
- [capa-rules v5.0.0...v5.1.0](https://github.com/mandiant/capa-rules/compare/v5.0.0...v5.1.0)
|
||||
|
||||
|
||||
## v5.0.0 (2023-02-08)
|
||||
This capa version comes with major improvements and additions to better handle .NET binaries. To showcase this we've updated and added over 30 .NET rules.
|
||||
|
||||
Additionally, capa now caches its rule set for better performance. The capa explorer also caches its analysis results, so that multiple IDA Pro or plugin invocations don't need to repeat the same analysis.
|
||||
|
||||
We have removed the SMDA backend and changed the program return codes to be positive numbers.
|
||||
|
||||
Other improvements to highlight include better ELF OS detection, various rendering bug fixes, and enhancements to the feature extraction. We've also added support for Python 3.11.
|
||||
|
||||
Thanks for all the support, especially to @jsoref, @bkojusner, @edeca, @richardweiss80, @joren485, @ryantxu1, @mwilliams31, @anushkavirgaonkar, @MalwareMechanic, @Still34, @dzbeck, @johnk3r, and everyone else who submitted bugs and provided feedback!
|
||||
|
||||
### New Features
|
||||
- verify rule metadata format on load #1160 @mr-tz
|
||||
- dotnet: emit property features #1168 @anushkavirgaonkar
|
||||
- dotnet: emit API features for objects created via the newobj instruction #1186 @mike-hunhoff
|
||||
- dotnet: emit API features for generic methods #1231 @mike-hunhoff
|
||||
- Python 3.11 support #1192 @williballenthin
|
||||
- dotnet: emit calls to/from MethodDef methods #1236 @mike-hunhoff
|
||||
- dotnet: emit namespace/class features for ldvirtftn/ldftn instructions #1241 @mike-hunhoff
|
||||
- dotnet: emit namespace/class features for type references #1242 @mike-hunhoff
|
||||
- dotnet: extract dotnet and pe format #1187 @mr-tz
|
||||
- don't render all library rule matches in vverbose output #1174 @mr-tz
|
||||
- cache the rule set across invocations for better performance #1212 @williballenthin
|
||||
- update ATT&CK/MBC data for linting #1297 @mr-tz
|
||||
|
||||
### Breaking Changes
|
||||
- remove SMDA backend #1062 @williballenthin
|
||||
- error return codes are now positive numbers #1269 @mr-tz
|
||||
|
||||
### New Rules (77)
|
||||
|
||||
- collection/use-dotnet-library-sharpclipboard @johnk3r
|
||||
- data-manipulation/encryption/aes/use-dotnet-library-encryptdecryptutils @johnk3r
|
||||
- data-manipulation/json/use-dotnet-library-newtonsoftjson @johnk3r
|
||||
- data-manipulation/svg/use-dotnet-library-sharpvectors @johnk3r
|
||||
- executable/resource/embed-dependencies-as-resources-using-fodycostura @johnk3r @mr-tz
|
||||
- communication/ftp/send/send-file-using-ftp michael.hunhof@mandiant.com anushka.virgaonkar@mandiant.com
|
||||
- nursery/extract-zip-archive anushka.virgaonkar@mandiant.com
|
||||
- nursery/allocate-unmanaged-memory-in-dotnet michael.hunhoff@mandiant.com
|
||||
- nursery/check-file-extension-in-dotnet michael.hunhoff@mandiant.com
|
||||
- nursery/decode-data-using-base64-in-dotnet michael.hunhoff@mandiant.com
|
||||
- nursery/deserialize-json-in-dotnet michael.hunhoff@mandiant.com
|
||||
- nursery/find-data-using-regex-in-dotnet michael.hunhoff@mandiant.com
|
||||
- nursery/generate-random-filename-in-dotnet michael.hunhoff@mandiant.com
|
||||
- nursery/get-os-version-in-dotnet michael.hunhoff@mandiant.com
|
||||
- nursery/load-xml-in-dotnet michael.hunhoff@mandiant.com
|
||||
- nursery/manipulate-unmanaged-memory-in-dotnet michael.hunhoff@mandiant.com
|
||||
- nursery/save-image-in-dotnet michael.hunhoff@mandiant.com
|
||||
- nursery/send-email-in-dotnet michael.hunhoff@mandiant.com
|
||||
- nursery/serialize-json-in-dotnet michael.hunhoff@mandiant.com
|
||||
- nursery/set-http-user-agent-in-dotnet michael.hunhoff@mandiant.com
|
||||
- nursery/compile-csharp-in-dotnet michael.hunhoff@mandiant.com
|
||||
- nursery/compile-visual-basic-in-dotnet michael.hunhoff@mandiant.com
|
||||
- nursery/compress-data-using-gzip-in-dotnet michael.hunhoff@mandiant.com
|
||||
- nursery/execute-sqlite-statement-in-dotnet michael.hunhoff@mandiant.com
|
||||
- nursery/execute-via-asynchronous-task-in-dotnet michael.hunhoff@mandiant.com
|
||||
- nursery/execute-via-timer-in-dotnet michael.hunhoff@mandiant.com
|
||||
- nursery/execute-wmi-query-in-dotnet michael.hunhoff@mandiant.com
|
||||
- nursery/manipulate-network-credentials-in-dotnet michael.hunhoff@mandiant.com
|
||||
- nursery/encrypt-data-using-aes william.ballenthin@mandiant.com Ivan Kwiatkowski (@JusticeRage)
|
||||
- host-interaction/uac/bypass/bypass-uac-via-rpc david.cannings@pwc.com david@edeca.net
|
||||
- nursery/check-for-vm-using-instruction-vpcext richard.weiss@mandiant.com
|
||||
- nursery/get-windows-directory-from-kuser_shared_data david.cannings@pwc.com
|
||||
- nursery/encrypt-data-using-openssl-dsa Ana06
|
||||
- nursery/encrypt-data-using-openssl-ecdsa Ana06
|
||||
- nursery/encrypt-data-using-openssl-rsa Ana06
|
||||
- runtime/dotnet/execute-via-dotnet-startup-hook william.ballenthin@mandiant.com
|
||||
- host-interaction/console/manipulate-console-buffer william.ballenthin@mandiant.com michael.hunhoff@mandiant.com
|
||||
- nursery/access-wmi-data-in-dotnet michael.hunhoff@mandiant.com
|
||||
- nursery/allocate-unmanaged-memory-via-dotnet michael.hunhoff@mandiant.com
|
||||
- nursery/generate-random-bytes-in-dotnet michael.hunhoff@mandiant.com
|
||||
- nursery/manipulate-console-window michael.hunhoff@mandiant.com
|
||||
- nursery/obfuscated-with-koivm michael.hunhoff@mandiant.com
|
||||
- nursery/implement-com-dll moritz.raabe@mandiant.com
|
||||
- nursery/linked-against-libsodium @mr-tz
|
||||
- compiler/nuitka/compiled-with-nuitka @williballenthin
|
||||
- nursery/authenticate-data-with-md5-mac william.ballenthin@mandiant.com
|
||||
- nursery/resolve-function-by-djb2-hash still@teamt5.org
|
||||
- host-interaction/mutex/create-semaphore-on-linux @ramen0x3f
|
||||
- host-interaction/mutex/lock-semaphore-on-linux @ramen0x3f
|
||||
- host-interaction/mutex/unlock-semaphore-on-linux @ramen0x3f
|
||||
- data-manipulation/hashing/sha384/hash-data-using-sha384 william.ballenthin@mandiant.com
|
||||
- data-manipulation/hashing/sha512/hash-data-using-sha512 william.ballenthin@mandiant.com
|
||||
- nursery/decode-data-using-url-encoding michael.hunhoff@mandiant.com
|
||||
- nursery/manipulate-user-privileges michael.hunhoff@mandiant.com
|
||||
- lib/get-os-version @mr-tz
|
||||
- nursery/decrypt-data-using-tea william.ballenthin@mandiant.com
|
||||
- nursery/encrypt-data-using-tea william.ballenthin@mandiant.com
|
||||
- nursery/hash-data-using-whirlpool william.ballenthin@mandiant.com
|
||||
- nursery/reference-base58-string william.ballenthin@mandiant.com
|
||||
- communication/mailslot/create-mailslot william.ballenthin@mandiant.com
|
||||
- executable/resource/access-dotnet-resource @mr-tz
|
||||
- linking/static/linked-against-cpp-standard-library @mr-tz
|
||||
- data-manipulation/compression/compress-data-using-lzo david@edeca.net david.cannings@pwc.com
|
||||
- data-manipulation/compression/decompress-data-using-lzo david@edeca.net david.cannings@pwc.com
|
||||
- communication/socket/tcp/create-tcp-socket-via-raw-afd-driver william.ballenthin@mandiant.com
|
||||
- host-interaction/process/map-section-object william.ballenthin@mandiant.com
|
||||
- lib/create-or-open-section-object william.ballenthin@mandiant.com
|
||||
- load-code/dotnet/execute-dotnet-assembly-via-clr-host blas.kojusner@mandiant.com
|
||||
- load-code/execute-vbscript-javascript-or-jscript-in-memory blas.kojusner@mandiant.com
|
||||
- host-interaction/file-system/reference-absolute-stream-path-on-windows blas.kojusner@mandiant.com
|
||||
- nursery/generate-method-via-reflection-in-dotnet michael.hunhoff@mandiant.com
|
||||
- nursery/unmanaged-call-via-dynamic-pinvoke-in-dotnet michael.hunhoff@mandiant.com
|
||||
|
||||
### Bug Fixes
|
||||
- render: convert feature attributes to aliased dictionary for vverbose #1152 @mike-hunhoff
|
||||
- decouple Token dependency / extractor and features #1139 @mr-tz
|
||||
- update pydantic model to guarantee type coercion #1176 @mike-hunhoff
|
||||
- do not overwrite version in version.py during PyInstaller build #1169 @mr-tz
|
||||
- render: fix vverbose rendering of offsets #1215 @williballenthin
|
||||
- elf: better detect OS via GLIBC ABI version needed and dependencies #1221 @williballenthin
|
||||
- dotnet: address unhandled exceptions with improved type checking #1230 @mike-hunhoff
|
||||
- fix import-to-ida script formatting #1208 @williballenthin
|
||||
- render: fix verbose rendering of scopes #1263 @williballenthin
|
||||
- rules: better detect invalid rules #1282 @williballenthin
|
||||
- show-features: better render strings with embedded whitespace #1267 @williballenthin
|
||||
- handle vivisect bug around strings at instruction level, use min length 4 #1271 @williballenthin @mr-tz
|
||||
- extractor: guard against invalid "calls from" features #1177 @mr-tz
|
||||
- extractor: add format to global features #1258 @mr-tz
|
||||
- extractor: discover all strings with length >= 4 #1280 @mr-tz
|
||||
- extractor: don't extract byte features for strings #1293 @mr-tz
|
||||
|
||||
### capa explorer IDA Pro plugin
|
||||
- fix: display instruction items #1154 @mr-tz
|
||||
- fix: accept only plaintext pasted content #1194 @williballenthin
|
||||
- fix: UnboundLocalError #1217 @williballenthin
|
||||
- extractor: add support for COFF files and extern functions #1223 @mike-hunhoff
|
||||
- doc: improve error messaging and documentation related to capa rule set #1249 @mike-hunhoff
|
||||
- fix: assume 32-bit displacement for offsets #1250 @mike-hunhoff
|
||||
- generator: refactor caching and matching #1251 @mike-hunhoff
|
||||
- fix: improve exception handling to prevent IDA from locking up when errors occur #1262 @mike-hunhoff
|
||||
- verify rule metadata using Pydantic #1167 @mr-tz
|
||||
- extractor: make read consistent with file object behavior #1254 @mr-tz
|
||||
- fix: UnboundLocalError x2 #1302 @mike-hunhoff
|
||||
- cache capa results across IDA sessions #1279 @mr-tz
|
||||
|
||||
### Raw diffs
|
||||
- [capa v4.0.1...v5.0.0](https://github.com/mandiant/capa/compare/v4.0.1...v5.0.0)
|
||||
- [capa-rules v4.0.1...v5.0.0](https://github.com/mandiant/capa-rules/compare/v4.0.1...v5.0.0)
|
||||
|
||||
|
||||
## v4.0.1 (2022-08-15)
|
||||
Some rules contained invalid metadata fields that caused an error when rendering rule hits. We've updated all rules and enhanced the rule linter to catch such issues.
|
||||
|
||||
### New Rules (1)
|
||||
|
||||
- anti-analysis/obfuscation/obfuscated-with-vs-obfuscation jakub.jozwiak@mandiant.com
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
- linter: use pydantic to validate rule metadata #1141 @mike-hunhoff
|
||||
- build binaries using PyInstaller no longer overwrites functions in version.py #1136 @mr-tz
|
||||
|
||||
### Raw diffs
|
||||
- [capa v4.0.0...v4.0.1](https://github.com/mandiant/capa/compare/v4.0.0...v4.0.1)
|
||||
- [capa-rules v4.0.0...v4.0.1](https://github.com/mandiant/capa-rules/compare/v4.0.0...v4.0.1)
|
||||
|
||||
## v4.0.0 (2022-08-10)
|
||||
Version 4 adds support for analyzing .NET executables. capa will autodetect .NET modules, or you can explicitly invoke the new feature extractor via `--format dotnet`. We've also extended the rule syntax for .NET features including `namespace` and `class`.
|
||||
|
||||
Additionally, new `instruction` scope and `operand` features enable users to create more explicit rules. These features are not backwards compatible. We removed the previously used `/x32` and `/x64` flavors of number and operand features.
|
||||
@@ -1227,7 +1443,7 @@ Download a standalone binary below and checkout the readme [here on GitHub](http
|
||||
- setup: pin vivisect version @williballenthin
|
||||
- setup: bump vivisect dependency version @williballenthin
|
||||
- setup: set Python project name to `flare-capa` @williballenthin
|
||||
- ci: run tests and linter via Github Actions @Ana06
|
||||
- ci: run tests and linter via GitHub Actions @Ana06
|
||||
- hooks: run style checkers and hide stashed output @Ana06
|
||||
- linter: ignore period in rule filename @williballenthin
|
||||
- linter: warn on nursery rule with no changes needed @williballenthin
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[](https://pypi.org/project/flare-capa)
|
||||
[](https://github.com/mandiant/capa/releases)
|
||||
[](https://github.com/mandiant/capa-rules)
|
||||
[](https://github.com/mandiant/capa-rules)
|
||||
[](https://github.com/mandiant/capa/actions?query=workflow%3ACI+event%3Apush+branch%3Amaster)
|
||||
[](https://github.com/mandiant/capa/releases)
|
||||
[](LICENSE.txt)
|
||||
@@ -15,7 +15,7 @@ Check out:
|
||||
- the overview in our first [capa blog post](https://www.mandiant.com/resources/capa-automatically-identify-malware-capabilities)
|
||||
- the major version 2.0 updates described in our [second blog post](https://www.mandiant.com/resources/capa-2-better-stronger-faster)
|
||||
- the major version 3.0 (ELF support) described in the [third blog post](https://www.mandiant.com/resources/elfant-in-the-room-capa-v3)
|
||||
- the major version 4.0 (.NET support) described in the TODO
|
||||
- the major version 4.0 (.NET support) described in the [fourth blog post](https://www.mandiant.com/resources/blog/capa-v4-casting-wider-net)
|
||||
|
||||
```
|
||||
$ capa.exe suspicious.exe
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import copy
|
||||
import collections
|
||||
from typing import TYPE_CHECKING, Set, Dict, List, Tuple, Mapping, Iterable
|
||||
from typing import TYPE_CHECKING, Set, Dict, List, Tuple, Union, Mapping, Iterable, Iterator, cast
|
||||
|
||||
import capa.perf
|
||||
import capa.features.common
|
||||
@@ -38,15 +38,17 @@ class Statement:
|
||||
"""
|
||||
|
||||
def __init__(self, description=None):
|
||||
super(Statement, self).__init__()
|
||||
super().__init__()
|
||||
self.name = self.__class__.__name__
|
||||
self.description = description
|
||||
|
||||
def __str__(self):
|
||||
name = self.name.lower()
|
||||
children = ",".join(map(str, self.get_children()))
|
||||
if self.description:
|
||||
return "%s(%s = %s)" % (self.name.lower(), ",".join(map(str, self.get_children())), self.description)
|
||||
return f"{name}({children} = {self.description})"
|
||||
else:
|
||||
return "%s(%s)" % (self.name.lower(), ",".join(map(str, self.get_children())))
|
||||
return f"{name}({children})"
|
||||
|
||||
def __repr__(self):
|
||||
return str(self)
|
||||
@@ -60,17 +62,24 @@ class Statement:
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_children(self):
|
||||
def get_children(self) -> Iterator[Union["Statement", Feature]]:
|
||||
if hasattr(self, "child"):
|
||||
yield self.child
|
||||
# this really confuses mypy because the property may not exist
|
||||
# since its defined in the subclasses.
|
||||
child = self.child # type: ignore
|
||||
assert isinstance(child, (Statement, Feature))
|
||||
yield child
|
||||
|
||||
if hasattr(self, "children"):
|
||||
for child in getattr(self, "children"):
|
||||
assert isinstance(child, (Statement, Feature))
|
||||
yield child
|
||||
|
||||
def replace_child(self, existing, new):
|
||||
if hasattr(self, "child"):
|
||||
if self.child is existing:
|
||||
# this really confuses mypy because the property may not exist
|
||||
# since its defined in the subclasses.
|
||||
if self.child is existing: # type: ignore
|
||||
self.child = new
|
||||
|
||||
if hasattr(self, "children"):
|
||||
@@ -90,7 +99,7 @@ class And(Statement):
|
||||
"""
|
||||
|
||||
def __init__(self, children, description=None):
|
||||
super(And, self).__init__(description=description)
|
||||
super().__init__(description=description)
|
||||
self.children = children
|
||||
|
||||
def evaluate(self, ctx, short_circuit=True):
|
||||
@@ -123,7 +132,7 @@ class Or(Statement):
|
||||
"""
|
||||
|
||||
def __init__(self, children, description=None):
|
||||
super(Or, self).__init__(description=description)
|
||||
super().__init__(description=description)
|
||||
self.children = children
|
||||
|
||||
def evaluate(self, ctx, short_circuit=True):
|
||||
@@ -150,7 +159,7 @@ class Not(Statement):
|
||||
"""match only if the child evaluates to False."""
|
||||
|
||||
def __init__(self, child, description=None):
|
||||
super(Not, self).__init__(description=description)
|
||||
super().__init__(description=description)
|
||||
self.child = child
|
||||
|
||||
def evaluate(self, ctx, short_circuit=True):
|
||||
@@ -172,7 +181,7 @@ class Some(Statement):
|
||||
"""
|
||||
|
||||
def __init__(self, count, children, description=None):
|
||||
super(Some, self).__init__(description=description)
|
||||
super().__init__(description=description)
|
||||
self.count = count
|
||||
self.children = children
|
||||
|
||||
@@ -208,7 +217,7 @@ class Range(Statement):
|
||||
"""match if the child is contained in the ctx set with a count in the given range."""
|
||||
|
||||
def __init__(self, child, min=None, max=None, description=None):
|
||||
super(Range, self).__init__(description=description)
|
||||
super().__init__(description=description)
|
||||
self.child = child
|
||||
self.min = min if min is not None else 0
|
||||
self.max = max if max is not None else (1 << 64 - 1)
|
||||
@@ -225,9 +234,9 @@ class Range(Statement):
|
||||
|
||||
def __str__(self):
|
||||
if self.max == (1 << 64 - 1):
|
||||
return "range(%s, min=%d, max=infinity)" % (str(self.child), self.min)
|
||||
return f"range({str(self.child)}, min={self.min}, max=infinity)"
|
||||
else:
|
||||
return "range(%s, min=%d, max=%d)" % (str(self.child), self.min, self.max)
|
||||
return f"range({str(self.child)}, min={self.min}, max={self.max})"
|
||||
|
||||
|
||||
class Subscope(Statement):
|
||||
@@ -237,7 +246,7 @@ class Subscope(Statement):
|
||||
"""
|
||||
|
||||
def __init__(self, scope, child, description=None):
|
||||
super(Subscope, self).__init__(description=description)
|
||||
super().__init__(description=description)
|
||||
self.scope = scope
|
||||
self.child = child
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import abc
|
||||
|
||||
from dncil.clr.token import Token
|
||||
|
||||
|
||||
class Address(abc.ABC):
|
||||
@abc.abstractmethod
|
||||
@@ -34,6 +32,9 @@ class AbsoluteVirtualAddress(int, Address):
|
||||
def __repr__(self):
|
||||
return f"absolute(0x{self:x})"
|
||||
|
||||
def __hash__(self):
|
||||
return int.__hash__(self)
|
||||
|
||||
|
||||
class RelativeVirtualAddress(int, Address):
|
||||
"""a memory address relative to a base address"""
|
||||
@@ -41,6 +42,9 @@ class RelativeVirtualAddress(int, Address):
|
||||
def __repr__(self):
|
||||
return f"relative(0x{self:x})"
|
||||
|
||||
def __hash__(self):
|
||||
return int.__hash__(self)
|
||||
|
||||
|
||||
class FileOffsetAddress(int, Address):
|
||||
"""an address relative to the start of a file"""
|
||||
@@ -52,52 +56,45 @@ class FileOffsetAddress(int, Address):
|
||||
def __repr__(self):
|
||||
return f"file(0x{self:x})"
|
||||
|
||||
def __hash__(self):
|
||||
return int.__hash__(self)
|
||||
|
||||
class DNTokenAddress(Address):
|
||||
|
||||
class DNTokenAddress(int, Address):
|
||||
"""a .NET token"""
|
||||
|
||||
def __init__(self, token: Token):
|
||||
self.token = token
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.token.value == other.token.value
|
||||
|
||||
def __lt__(self, other):
|
||||
return self.token.value < other.token.value
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.token.value)
|
||||
def __new__(cls, token: int):
|
||||
return int.__new__(cls, token)
|
||||
|
||||
def __repr__(self):
|
||||
return f"token(0x{self.token.value:x})"
|
||||
return f"token(0x{self:x})"
|
||||
|
||||
def __index__(self):
|
||||
# returns the object converted to an integer
|
||||
return self.token.value
|
||||
def __hash__(self):
|
||||
return int.__hash__(self)
|
||||
|
||||
|
||||
class DNTokenOffsetAddress(Address):
|
||||
"""an offset into an object specified by a .NET token"""
|
||||
|
||||
def __init__(self, token: Token, offset: int):
|
||||
def __init__(self, token: int, offset: int):
|
||||
assert offset >= 0
|
||||
self.token = token
|
||||
self.offset = offset
|
||||
|
||||
def __eq__(self, other):
|
||||
return (self.token.value, self.offset) == (other.token.value, other.offset)
|
||||
return (self.token, self.offset) == (other.token, other.offset)
|
||||
|
||||
def __lt__(self, other):
|
||||
return (self.token.value, self.offset) < (other.token.value, other.offset)
|
||||
return (self.token, self.offset) < (other.token, other.offset)
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.token.value, self.offset))
|
||||
return hash((self.token, self.offset))
|
||||
|
||||
def __repr__(self):
|
||||
return f"token(0x{self.token.value:x})+(0x{self.offset:x})"
|
||||
return f"token(0x{self.token:x})+(0x{self.offset:x})"
|
||||
|
||||
def __index__(self):
|
||||
return self.token.value + self.offset
|
||||
return self.token + self.offset
|
||||
|
||||
|
||||
class _NoAddress(Address):
|
||||
|
||||
@@ -11,7 +11,7 @@ from capa.features.common import Feature
|
||||
|
||||
class BasicBlock(Feature):
|
||||
def __init__(self, description=None):
|
||||
super(BasicBlock, self).__init__(None, description=description)
|
||||
super().__init__(0, description=description)
|
||||
|
||||
def __str__(self):
|
||||
return "basic block"
|
||||
|
||||
@@ -9,9 +9,10 @@
|
||||
import re
|
||||
import abc
|
||||
import codecs
|
||||
import typing
|
||||
import logging
|
||||
import collections
|
||||
from typing import TYPE_CHECKING, Set, Dict, List, Union, Optional, Sequence
|
||||
from typing import TYPE_CHECKING, Set, Dict, List, Union, Optional
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# circular import, otherwise
|
||||
@@ -29,6 +30,14 @@ MAX_BYTES_FEATURE_SIZE = 0x100
|
||||
THUNK_CHAIN_DEPTH_DELTA = 5
|
||||
|
||||
|
||||
class FeatureAccess:
|
||||
READ = "read"
|
||||
WRITE = "write"
|
||||
|
||||
|
||||
VALID_FEATURE_ACCESS = (FeatureAccess.READ, FeatureAccess.WRITE)
|
||||
|
||||
|
||||
def bytes_to_str(b: bytes) -> str:
|
||||
return str(codecs.encode(b, "hex").decode("utf-8"))
|
||||
|
||||
@@ -73,7 +82,7 @@ class Result:
|
||||
children: List["Result"],
|
||||
locations: Optional[Set[Address]] = None,
|
||||
):
|
||||
super(Result, self).__init__()
|
||||
super().__init__()
|
||||
self.success = success
|
||||
self.statement = statement
|
||||
self.children = children
|
||||
@@ -92,15 +101,19 @@ class Result:
|
||||
|
||||
|
||||
class Feature(abc.ABC):
|
||||
def __init__(self, value: Union[str, int, float, bytes], description=None):
|
||||
def __init__(
|
||||
self,
|
||||
value: Union[str, int, float, bytes],
|
||||
description: Optional[str] = None,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
value (any): the value of the feature, such as the number or string.
|
||||
description (str): a human-readable description that explains the feature value.
|
||||
"""
|
||||
super(Feature, self).__init__()
|
||||
self.name = self.__class__.__name__.lower()
|
||||
super().__init__()
|
||||
|
||||
self.name = self.__class__.__name__.lower()
|
||||
self.value = value
|
||||
self.description = description
|
||||
|
||||
@@ -119,23 +132,28 @@ class Feature(abc.ABC):
|
||||
< capa.features.freeze.features.feature_from_capa(other).json()
|
||||
)
|
||||
|
||||
def get_name_str(self) -> str:
|
||||
"""
|
||||
render the name of this feature, for use by `__str__` and friends.
|
||||
subclasses should override to customize the rendering.
|
||||
"""
|
||||
return self.name
|
||||
|
||||
def get_value_str(self) -> str:
|
||||
"""
|
||||
render the value of this feature, for use by `__str__` and friends.
|
||||
subclasses should override to customize the rendering.
|
||||
|
||||
Returns: any
|
||||
"""
|
||||
return str(self.value)
|
||||
|
||||
def __str__(self):
|
||||
if self.value is not None:
|
||||
if self.description:
|
||||
return "%s(%s = %s)" % (self.name, self.get_value_str(), self.description)
|
||||
return f"{self.get_name_str()}({self.get_value_str()} = {self.description})"
|
||||
else:
|
||||
return "%s(%s)" % (self.name, self.get_value_str())
|
||||
return f"{self.get_name_str()}({self.get_value_str()})"
|
||||
else:
|
||||
return "%s" % self.name
|
||||
return f"{self.get_name_str()}"
|
||||
|
||||
def __repr__(self):
|
||||
return str(self)
|
||||
@@ -148,33 +166,37 @@ class Feature(abc.ABC):
|
||||
|
||||
class MatchedRule(Feature):
|
||||
def __init__(self, value: str, description=None):
|
||||
super(MatchedRule, self).__init__(value, description=description)
|
||||
super().__init__(value, description=description)
|
||||
self.name = "match"
|
||||
|
||||
|
||||
class Characteristic(Feature):
|
||||
def __init__(self, value: str, description=None):
|
||||
super(Characteristic, self).__init__(value, description=description)
|
||||
super().__init__(value, description=description)
|
||||
|
||||
|
||||
class String(Feature):
|
||||
def __init__(self, value: str, description=None):
|
||||
super(String, self).__init__(value, description=description)
|
||||
super().__init__(value, description=description)
|
||||
|
||||
def get_value_str(self) -> str:
|
||||
assert isinstance(self.value, str)
|
||||
return escape_string(self.value)
|
||||
|
||||
|
||||
class Class(Feature):
|
||||
def __init__(self, value: str, description=None):
|
||||
super(Class, self).__init__(value, description=description)
|
||||
super().__init__(value, description=description)
|
||||
|
||||
|
||||
class Namespace(Feature):
|
||||
def __init__(self, value: str, description=None):
|
||||
super(Namespace, self).__init__(value, description=description)
|
||||
super().__init__(value, description=description)
|
||||
|
||||
|
||||
class Substring(String):
|
||||
def __init__(self, value: str, description=None):
|
||||
super(Substring, self).__init__(value, description=description)
|
||||
super().__init__(value, description=description)
|
||||
self.value = value
|
||||
|
||||
def evaluate(self, ctx, short_circuit=True):
|
||||
@@ -183,8 +205,9 @@ class Substring(String):
|
||||
|
||||
# mapping from string value to list of locations.
|
||||
# will unique the locations later on.
|
||||
matches = collections.defaultdict(list)
|
||||
matches: typing.DefaultDict[str, Set[Address]] = collections.defaultdict(set)
|
||||
|
||||
assert isinstance(self.value, str)
|
||||
for feature, locations in ctx.items():
|
||||
if not isinstance(feature, (String,)):
|
||||
continue
|
||||
@@ -194,32 +217,32 @@ class Substring(String):
|
||||
raise ValueError("unexpected feature value type")
|
||||
|
||||
if self.value in feature.value:
|
||||
matches[feature.value].extend(locations)
|
||||
matches[feature.value].update(locations)
|
||||
if short_circuit:
|
||||
# we found one matching string, thats sufficient to match.
|
||||
# don't collect other matching strings in this mode.
|
||||
break
|
||||
|
||||
if matches:
|
||||
# finalize: defaultdict -> dict
|
||||
# which makes json serialization easier
|
||||
matches = dict(matches)
|
||||
|
||||
# collect all locations
|
||||
locations = set()
|
||||
for s in matches.keys():
|
||||
matches[s] = list(set(matches[s]))
|
||||
locations.update(matches[s])
|
||||
for locs in matches.values():
|
||||
locations.update(locs)
|
||||
|
||||
# unlike other features, we cannot return put a reference to `self` directly in a `Result`.
|
||||
# this is because `self` may match on many strings, so we can't stuff the matched value into it.
|
||||
# instead, return a new instance that has a reference to both the substring and the matched values.
|
||||
return Result(True, _MatchedSubstring(self, matches), [], locations=locations)
|
||||
return Result(True, _MatchedSubstring(self, dict(matches)), [], locations=locations)
|
||||
else:
|
||||
return Result(False, _MatchedSubstring(self, {}), [])
|
||||
|
||||
def get_value_str(self) -> str:
|
||||
assert isinstance(self.value, str)
|
||||
return escape_string(self.value)
|
||||
|
||||
def __str__(self):
|
||||
return "substring(%s)" % self.value
|
||||
assert isinstance(self.value, str)
|
||||
return f"substring({escape_string(self.value)})"
|
||||
|
||||
|
||||
class _MatchedSubstring(Substring):
|
||||
@@ -236,7 +259,7 @@ class _MatchedSubstring(Substring):
|
||||
substring: the substring feature that matches.
|
||||
match: mapping from matching string to its locations.
|
||||
"""
|
||||
super(_MatchedSubstring, self).__init__(str(substring.value), description=substring.description)
|
||||
super().__init__(str(substring.value), description=substring.description)
|
||||
# we want this to collide with the name of `Substring` above,
|
||||
# so that it works nicely with the renderers.
|
||||
self.name = "substring"
|
||||
@@ -244,15 +267,14 @@ class _MatchedSubstring(Substring):
|
||||
self.matches = matches
|
||||
|
||||
def __str__(self):
|
||||
return 'substring("%s", matches = %s)' % (
|
||||
self.value,
|
||||
", ".join(map(lambda s: '"' + s + '"', (self.matches or {}).keys())),
|
||||
)
|
||||
matches = ", ".join(map(lambda s: '"' + s + '"', (self.matches or {}).keys()))
|
||||
assert isinstance(self.value, str)
|
||||
return f'substring("{self.value}", matches = {matches})'
|
||||
|
||||
|
||||
class Regex(String):
|
||||
def __init__(self, value: str, description=None):
|
||||
super(Regex, self).__init__(value, description=description)
|
||||
super().__init__(value, description=description)
|
||||
self.value = value
|
||||
|
||||
pat = self.value[len("/") : -len("/")]
|
||||
@@ -262,12 +284,12 @@ class Regex(String):
|
||||
flags |= re.IGNORECASE
|
||||
try:
|
||||
self.re = re.compile(pat, flags)
|
||||
except re.error:
|
||||
except re.error as exc:
|
||||
if value.endswith("/i"):
|
||||
value = value[: -len("i")]
|
||||
raise ValueError(
|
||||
"invalid regular expression: %s it should use Python syntax, try it at https://pythex.org" % value
|
||||
)
|
||||
f"invalid regular expression: {value} it should use Python syntax, try it at https://pythex.org"
|
||||
) from exc
|
||||
|
||||
def evaluate(self, ctx, short_circuit=True):
|
||||
capa.perf.counters["evaluate.feature"] += 1
|
||||
@@ -275,7 +297,7 @@ class Regex(String):
|
||||
|
||||
# mapping from string value to list of locations.
|
||||
# will unique the locations later on.
|
||||
matches = collections.defaultdict(list)
|
||||
matches: typing.DefaultDict[str, Set[Address]] = collections.defaultdict(set)
|
||||
|
||||
for feature, locations in ctx.items():
|
||||
if not isinstance(feature, (String,)):
|
||||
@@ -290,33 +312,29 @@ class Regex(String):
|
||||
# using this mode cleans is more convenient for rule authors,
|
||||
# so that they don't have to prefix/suffix their terms like: /.*foo.*/.
|
||||
if self.re.search(feature.value):
|
||||
matches[feature.value].extend(locations)
|
||||
matches[feature.value].update(locations)
|
||||
if short_circuit:
|
||||
# we found one matching string, thats sufficient to match.
|
||||
# don't collect other matching strings in this mode.
|
||||
break
|
||||
|
||||
if matches:
|
||||
# finalize: defaultdict -> dict
|
||||
# which makes json serialization easier
|
||||
matches = dict(matches)
|
||||
|
||||
# collect all locations
|
||||
locations = set()
|
||||
for s in matches.keys():
|
||||
matches[s] = list(set(matches[s]))
|
||||
locations.update(matches[s])
|
||||
for locs in matches.values():
|
||||
locations.update(locs)
|
||||
|
||||
# unlike other features, we cannot return put a reference to `self` directly in a `Result`.
|
||||
# this is because `self` may match on many strings, so we can't stuff the matched value into it.
|
||||
# instead, return a new instance that has a reference to both the regex and the matched values.
|
||||
# see #262.
|
||||
return Result(True, _MatchedRegex(self, matches), [], locations=locations)
|
||||
return Result(True, _MatchedRegex(self, dict(matches)), [], locations=locations)
|
||||
else:
|
||||
return Result(False, _MatchedRegex(self, {}), [])
|
||||
|
||||
def __str__(self):
|
||||
return "regex(string =~ %s)" % self.value
|
||||
assert isinstance(self.value, str)
|
||||
return f"regex(string =~ {self.value})"
|
||||
|
||||
|
||||
class _MatchedRegex(Regex):
|
||||
@@ -333,7 +351,7 @@ class _MatchedRegex(Regex):
|
||||
regex: the regex feature that matches.
|
||||
matches: mapping from matching string to its locations.
|
||||
"""
|
||||
super(_MatchedRegex, self).__init__(str(regex.value), description=regex.description)
|
||||
super().__init__(str(regex.value), description=regex.description)
|
||||
# we want this to collide with the name of `Regex` above,
|
||||
# so that it works nicely with the renderers.
|
||||
self.name = "regex"
|
||||
@@ -341,10 +359,9 @@ class _MatchedRegex(Regex):
|
||||
self.matches = matches
|
||||
|
||||
def __str__(self):
|
||||
return "regex(string =~ %s, matches = %s)" % (
|
||||
self.value,
|
||||
", ".join(map(lambda s: '"' + s + '"', (self.matches or {}).keys())),
|
||||
)
|
||||
matches = ", ".join(map(lambda s: '"' + s + '"', (self.matches or {}).keys()))
|
||||
assert isinstance(self.value, str)
|
||||
return f"regex(string =~ {self.value}, matches = {matches})"
|
||||
|
||||
|
||||
class StringFactory:
|
||||
@@ -356,23 +373,26 @@ class StringFactory:
|
||||
|
||||
class Bytes(Feature):
|
||||
def __init__(self, value: bytes, description=None):
|
||||
super(Bytes, self).__init__(value, description=description)
|
||||
super().__init__(value, description=description)
|
||||
self.value = value
|
||||
|
||||
def evaluate(self, ctx, **kwargs):
|
||||
capa.perf.counters["evaluate.feature"] += 1
|
||||
capa.perf.counters["evaluate.feature.bytes"] += 1
|
||||
|
||||
assert isinstance(self.value, bytes)
|
||||
for feature, locations in ctx.items():
|
||||
if not isinstance(feature, (Bytes,)):
|
||||
continue
|
||||
|
||||
assert isinstance(feature.value, bytes)
|
||||
if feature.value.startswith(self.value):
|
||||
return Result(True, self, [], locations=locations)
|
||||
|
||||
return Result(False, self, [])
|
||||
|
||||
def get_value_str(self):
|
||||
assert isinstance(self.value, bytes)
|
||||
return hex_string(bytes_to_str(self.value))
|
||||
|
||||
|
||||
@@ -386,7 +406,7 @@ VALID_ARCH = (ARCH_I386, ARCH_AMD64, ARCH_ANY)
|
||||
|
||||
class Arch(Feature):
|
||||
def __init__(self, value: str, description=None):
|
||||
super(Arch, self).__init__(value, description=description)
|
||||
super().__init__(value, description=description)
|
||||
self.name = "arch"
|
||||
|
||||
|
||||
@@ -397,13 +417,29 @@ OS_MACOS = "macos"
|
||||
OS_ANY = "any"
|
||||
VALID_OS = {os.value for os in capa.features.extractors.elf.OS}
|
||||
VALID_OS.update({OS_WINDOWS, OS_LINUX, OS_MACOS, OS_ANY})
|
||||
# internal only, not to be used in rules
|
||||
OS_AUTO = "auto"
|
||||
|
||||
|
||||
class OS(Feature):
|
||||
def __init__(self, value: str, description=None):
|
||||
super(OS, self).__init__(value, description=description)
|
||||
super().__init__(value, description=description)
|
||||
self.name = "os"
|
||||
|
||||
def evaluate(self, ctx, **kwargs):
|
||||
capa.perf.counters["evaluate.feature"] += 1
|
||||
capa.perf.counters["evaluate.feature." + self.name] += 1
|
||||
|
||||
for feature, locations in ctx.items():
|
||||
if not isinstance(feature, (OS,)):
|
||||
continue
|
||||
|
||||
assert isinstance(feature.value, str)
|
||||
if OS_ANY in (self.value, feature.value) or self.value == feature.value:
|
||||
return Result(True, self, [], locations=locations)
|
||||
|
||||
return Result(False, self, [])
|
||||
|
||||
|
||||
FORMAT_PE = "pe"
|
||||
FORMAT_ELF = "elf"
|
||||
@@ -414,12 +450,13 @@ FORMAT_AUTO = "auto"
|
||||
FORMAT_SC32 = "sc32"
|
||||
FORMAT_SC64 = "sc64"
|
||||
FORMAT_FREEZE = "freeze"
|
||||
FORMAT_RESULT = "result"
|
||||
FORMAT_UNKNOWN = "unknown"
|
||||
|
||||
|
||||
class Format(Feature):
|
||||
def __init__(self, value: str, description=None):
|
||||
super(Format, self).__init__(value, description=description)
|
||||
super().__init__(value, description=description)
|
||||
self.name = "format"
|
||||
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ class FeatureExtractor:
|
||||
# for example, the Vivisect feature extract might require the vw and/or path.
|
||||
# this base class doesn't know what to do with that info, though.
|
||||
#
|
||||
super(FeatureExtractor, self).__init__()
|
||||
super().__init__()
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_base_address(self) -> Union[AbsoluteVirtualAddress, capa.features.address._NoAddress]:
|
||||
|
||||
146
capa/features/extractors/binja/basicblock.py
Normal file
146
capa/features/extractors/binja/basicblock.py
Normal file
@@ -0,0 +1,146 @@
|
||||
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import sys
|
||||
import string
|
||||
import struct
|
||||
from typing import Tuple, Iterator
|
||||
|
||||
from binaryninja import Function
|
||||
from binaryninja import BasicBlock as BinjaBasicBlock
|
||||
from binaryninja import (
|
||||
BinaryView,
|
||||
VariableSourceType,
|
||||
MediumLevelILSetVar,
|
||||
MediumLevelILOperation,
|
||||
MediumLevelILBasicBlock,
|
||||
MediumLevelILInstruction,
|
||||
)
|
||||
|
||||
from capa.features.common import Feature, Characteristic
|
||||
from capa.features.address import Address, AbsoluteVirtualAddress
|
||||
from capa.features.basicblock import BasicBlock
|
||||
from capa.features.extractors.helpers import MIN_STACKSTRING_LEN
|
||||
from capa.features.extractors.base_extractor import BBHandle, FunctionHandle
|
||||
|
||||
|
||||
def get_printable_len(il: MediumLevelILSetVar) -> int:
|
||||
"""Return string length if all operand bytes are ascii or utf16-le printable"""
|
||||
width = il.dest.type.width
|
||||
value = il.src.value.value
|
||||
|
||||
if width == 1:
|
||||
chars = struct.pack("<B", value & 0xFF)
|
||||
elif width == 2:
|
||||
chars = struct.pack("<H", value & 0xFFFF)
|
||||
elif width == 4:
|
||||
chars = struct.pack("<I", value & 0xFFFFFFFF)
|
||||
elif width == 8:
|
||||
chars = struct.pack("<Q", value & 0xFFFFFFFFFFFFFFFF)
|
||||
else:
|
||||
return 0
|
||||
|
||||
def is_printable_ascii(chars_: bytes):
|
||||
return all(c < 127 and chr(c) in string.printable for c in chars_)
|
||||
|
||||
def is_printable_utf16le(chars_: bytes):
|
||||
if all(c == 0x00 for c in chars_[1::2]):
|
||||
return is_printable_ascii(chars_[::2])
|
||||
|
||||
if is_printable_ascii(chars):
|
||||
return width
|
||||
|
||||
if is_printable_utf16le(chars):
|
||||
return width // 2
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def is_mov_imm_to_stack(il: MediumLevelILInstruction) -> bool:
|
||||
"""verify instruction moves immediate onto stack"""
|
||||
if il.operation != MediumLevelILOperation.MLIL_SET_VAR:
|
||||
return False
|
||||
|
||||
if il.src.operation != MediumLevelILOperation.MLIL_CONST:
|
||||
return False
|
||||
|
||||
if not il.dest.source_type == VariableSourceType.StackVariableSourceType:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def bb_contains_stackstring(f: Function, bb: MediumLevelILBasicBlock) -> bool:
|
||||
"""check basic block for stackstring indicators
|
||||
|
||||
true if basic block contains enough moves of constant bytes to the stack
|
||||
"""
|
||||
count = 0
|
||||
for il in bb:
|
||||
if is_mov_imm_to_stack(il):
|
||||
count += get_printable_len(il)
|
||||
|
||||
if count > MIN_STACKSTRING_LEN:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def extract_bb_stackstring(fh: FunctionHandle, bbh: BBHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""extract stackstring indicators from basic block"""
|
||||
bb: Tuple[BinjaBasicBlock, MediumLevelILBasicBlock] = bbh.inner
|
||||
if bb[1] is not None and bb_contains_stackstring(fh.inner, bb[1]):
|
||||
yield Characteristic("stack string"), bbh.address
|
||||
|
||||
|
||||
def extract_bb_tight_loop(fh: FunctionHandle, bbh: BBHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""extract tight loop indicators from a basic block"""
|
||||
bb: Tuple[BinjaBasicBlock, MediumLevelILBasicBlock] = bbh.inner
|
||||
for edge in bb[0].outgoing_edges:
|
||||
if edge.target.start == bb[0].start:
|
||||
yield Characteristic("tight loop"), bbh.address
|
||||
|
||||
|
||||
def extract_features(fh: FunctionHandle, bbh: BBHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""extract basic block features"""
|
||||
for bb_handler in BASIC_BLOCK_HANDLERS:
|
||||
for feature, addr in bb_handler(fh, bbh):
|
||||
yield feature, addr
|
||||
yield BasicBlock(), bbh.address
|
||||
|
||||
|
||||
BASIC_BLOCK_HANDLERS = (
|
||||
extract_bb_tight_loop,
|
||||
extract_bb_stackstring,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
return
|
||||
|
||||
from binaryninja import BinaryViewType
|
||||
|
||||
from capa.features.extractors.binja.extractor import BinjaFeatureExtractor
|
||||
|
||||
bv: BinaryView = BinaryViewType.get_view_of_file(sys.argv[1])
|
||||
if bv is None:
|
||||
return
|
||||
|
||||
features = []
|
||||
extractor = BinjaFeatureExtractor(bv)
|
||||
for fh in extractor.get_functions():
|
||||
for bbh in extractor.get_basic_blocks(fh):
|
||||
features.extend(list(extract_features(fh, bbh)))
|
||||
|
||||
import pprint
|
||||
|
||||
pprint.pprint(features)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
77
capa/features/extractors/binja/extractor.py
Normal file
77
capa/features/extractors/binja/extractor.py
Normal file
@@ -0,0 +1,77 @@
|
||||
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
from typing import List, Tuple, Iterator
|
||||
|
||||
import binaryninja as binja
|
||||
|
||||
import capa.features.extractors.elf
|
||||
import capa.features.extractors.binja.file
|
||||
import capa.features.extractors.binja.insn
|
||||
import capa.features.extractors.binja.global_
|
||||
import capa.features.extractors.binja.function
|
||||
import capa.features.extractors.binja.basicblock
|
||||
from capa.features.common import Feature
|
||||
from capa.features.address import Address, AbsoluteVirtualAddress
|
||||
from capa.features.extractors.base_extractor import BBHandle, InsnHandle, FunctionHandle, FeatureExtractor
|
||||
|
||||
|
||||
class BinjaFeatureExtractor(FeatureExtractor):
|
||||
def __init__(self, bv: binja.BinaryView):
|
||||
super().__init__()
|
||||
self.bv = bv
|
||||
self.global_features: List[Tuple[Feature, Address]] = []
|
||||
self.global_features.extend(capa.features.extractors.binja.file.extract_file_format(self.bv))
|
||||
self.global_features.extend(capa.features.extractors.binja.global_.extract_os(self.bv))
|
||||
self.global_features.extend(capa.features.extractors.binja.global_.extract_arch(self.bv))
|
||||
|
||||
def get_base_address(self):
|
||||
return AbsoluteVirtualAddress(self.bv.start)
|
||||
|
||||
def extract_global_features(self):
|
||||
yield from self.global_features
|
||||
|
||||
def extract_file_features(self):
|
||||
yield from capa.features.extractors.binja.file.extract_features(self.bv)
|
||||
|
||||
def get_functions(self) -> Iterator[FunctionHandle]:
|
||||
for f in self.bv.functions:
|
||||
yield FunctionHandle(address=AbsoluteVirtualAddress(f.start), inner=f)
|
||||
|
||||
def extract_function_features(self, fh: FunctionHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
yield from capa.features.extractors.binja.function.extract_features(fh)
|
||||
|
||||
def get_basic_blocks(self, fh: FunctionHandle) -> Iterator[BBHandle]:
|
||||
f: binja.Function = fh.inner
|
||||
# Set up a MLIL basic block dict look up to associate the disassembly basic block with its MLIL basic block
|
||||
mlil_lookup = {}
|
||||
for mlil_bb in f.mlil.basic_blocks:
|
||||
mlil_lookup[mlil_bb.source_block.start] = mlil_bb
|
||||
|
||||
for bb in f.basic_blocks:
|
||||
mlil_bb = None
|
||||
if bb.start in mlil_lookup:
|
||||
mlil_bb = mlil_lookup[bb.start]
|
||||
|
||||
yield BBHandle(address=AbsoluteVirtualAddress(bb.start), inner=(bb, mlil_bb))
|
||||
|
||||
def extract_basic_block_features(self, fh: FunctionHandle, bbh: BBHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
yield from capa.features.extractors.binja.basicblock.extract_features(fh, bbh)
|
||||
|
||||
def get_instructions(self, fh: FunctionHandle, bbh: BBHandle) -> Iterator[InsnHandle]:
|
||||
import capa.features.extractors.binja.helpers as binja_helpers
|
||||
|
||||
bb: Tuple[binja.BasicBlock, binja.MediumLevelILBasicBlock] = bbh.inner
|
||||
addr = bb[0].start
|
||||
|
||||
for text, length in bb[0]:
|
||||
insn = binja_helpers.DisassemblyInstruction(addr, length, text)
|
||||
yield InsnHandle(address=AbsoluteVirtualAddress(addr), inner=insn)
|
||||
addr += length
|
||||
|
||||
def extract_insn_features(self, fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle):
|
||||
yield from capa.features.extractors.binja.insn.extract_features(fh, bbh, ih)
|
||||
188
capa/features/extractors/binja/file.py
Normal file
188
capa/features/extractors/binja/file.py
Normal file
@@ -0,0 +1,188 @@
|
||||
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import sys
|
||||
import struct
|
||||
from typing import Tuple, Iterator
|
||||
|
||||
from binaryninja import Symbol, Segment, BinaryView, SymbolType, SymbolBinding
|
||||
|
||||
import capa.features.extractors.common
|
||||
import capa.features.extractors.helpers
|
||||
import capa.features.extractors.strings
|
||||
from capa.features.file import Export, Import, Section, FunctionName
|
||||
from capa.features.common import FORMAT_PE, FORMAT_ELF, Format, String, Feature, Characteristic
|
||||
from capa.features.address import NO_ADDRESS, Address, FileOffsetAddress, AbsoluteVirtualAddress
|
||||
from capa.features.extractors.binja.helpers import unmangle_c_name
|
||||
|
||||
|
||||
def check_segment_for_pe(bv: BinaryView, seg: Segment) -> Iterator[Tuple[int, int]]:
|
||||
"""check segment for embedded PE
|
||||
|
||||
adapted for binja from:
|
||||
https://github.com/vivisect/vivisect/blob/7be4037b1cecc4551b397f840405a1fc606f9b53/PE/carve.py#L19
|
||||
"""
|
||||
mz_xor = [
|
||||
(
|
||||
capa.features.extractors.helpers.xor_static(b"MZ", i),
|
||||
capa.features.extractors.helpers.xor_static(b"PE", i),
|
||||
i,
|
||||
)
|
||||
for i in range(256)
|
||||
]
|
||||
|
||||
todo = []
|
||||
# If this is the first segment of the binary, skip the first bytes. Otherwise, there will always be a matched
|
||||
# PE at the start of the binaryview.
|
||||
start = seg.start
|
||||
if bv.view_type == "PE" and start == bv.start:
|
||||
start += 1
|
||||
|
||||
for mzx, pex, i in mz_xor:
|
||||
for off, _ in bv.find_all_data(start, seg.end, mzx):
|
||||
todo.append((off, mzx, pex, i))
|
||||
|
||||
while len(todo):
|
||||
off, mzx, pex, i = todo.pop()
|
||||
|
||||
# The MZ header has one field we will check e_lfanew is at 0x3c
|
||||
e_lfanew = off + 0x3C
|
||||
|
||||
if seg.end < (e_lfanew + 4):
|
||||
continue
|
||||
|
||||
newoff = struct.unpack("<I", capa.features.extractors.helpers.xor_static(bv.read(e_lfanew, 4), i))[0]
|
||||
|
||||
peoff = off + newoff
|
||||
if seg.end < (peoff + 2):
|
||||
continue
|
||||
|
||||
if bv.read(peoff, 2) == pex:
|
||||
yield off, i
|
||||
|
||||
|
||||
def extract_file_embedded_pe(bv: BinaryView) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""extract embedded PE features"""
|
||||
for seg in bv.segments:
|
||||
for ea, _ in check_segment_for_pe(bv, seg):
|
||||
yield Characteristic("embedded pe"), FileOffsetAddress(ea)
|
||||
|
||||
|
||||
def extract_file_export_names(bv: BinaryView) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""extract function exports"""
|
||||
for sym in bv.get_symbols_of_type(SymbolType.FunctionSymbol):
|
||||
if sym.binding in [SymbolBinding.GlobalBinding, SymbolBinding.WeakBinding]:
|
||||
name = sym.short_name
|
||||
yield Export(name), AbsoluteVirtualAddress(sym.address)
|
||||
unmangled_name = unmangle_c_name(name)
|
||||
if name != unmangled_name:
|
||||
yield Export(unmangled_name), AbsoluteVirtualAddress(sym.address)
|
||||
|
||||
|
||||
def extract_file_import_names(bv: BinaryView) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""extract function imports
|
||||
|
||||
1. imports by ordinal:
|
||||
- modulename.#ordinal
|
||||
|
||||
2. imports by name, results in two features to support importname-only
|
||||
matching:
|
||||
- modulename.importname
|
||||
- importname
|
||||
"""
|
||||
for sym in bv.get_symbols_of_type(SymbolType.ImportAddressSymbol):
|
||||
lib_name = str(sym.namespace)
|
||||
addr = AbsoluteVirtualAddress(sym.address)
|
||||
for name in capa.features.extractors.helpers.generate_symbols(lib_name, sym.short_name):
|
||||
yield Import(name), addr
|
||||
|
||||
ordinal = sym.ordinal
|
||||
if ordinal != 0 and (lib_name != ""):
|
||||
ordinal_name = f"#{ordinal}"
|
||||
for name in capa.features.extractors.helpers.generate_symbols(lib_name, ordinal_name):
|
||||
yield Import(name), addr
|
||||
|
||||
|
||||
def extract_file_section_names(bv: BinaryView) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""extract section names"""
|
||||
for name, section in bv.sections.items():
|
||||
yield Section(name), AbsoluteVirtualAddress(section.start)
|
||||
|
||||
|
||||
def extract_file_strings(bv: BinaryView) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""extract ASCII and UTF-16 LE strings"""
|
||||
for s in bv.strings:
|
||||
yield String(s.value), FileOffsetAddress(s.start)
|
||||
|
||||
|
||||
def extract_file_function_names(bv: BinaryView) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
extract the names of statically-linked library functions.
|
||||
"""
|
||||
for sym_name in bv.symbols:
|
||||
for sym in bv.symbols[sym_name]:
|
||||
if sym.type == SymbolType.LibraryFunctionSymbol:
|
||||
name = sym.short_name
|
||||
yield FunctionName(name), sym.address
|
||||
if name.startswith("_"):
|
||||
# some linkers may prefix linked routines with a `_` to avoid name collisions.
|
||||
# extract features for both the mangled and un-mangled representations.
|
||||
# e.g. `_fwrite` -> `fwrite`
|
||||
# see: https://stackoverflow.com/a/2628384/87207
|
||||
yield FunctionName(name[1:]), sym.address
|
||||
|
||||
|
||||
def extract_file_format(bv: BinaryView) -> Iterator[Tuple[Feature, Address]]:
|
||||
view_type = bv.view_type
|
||||
if view_type in ["PE", "COFF"]:
|
||||
yield Format(FORMAT_PE), NO_ADDRESS
|
||||
elif view_type == "ELF":
|
||||
yield Format(FORMAT_ELF), NO_ADDRESS
|
||||
elif view_type == "Raw":
|
||||
# no file type to return when processing a binary file, but we want to continue processing
|
||||
return
|
||||
else:
|
||||
raise NotImplementedError(f"unexpected file format: {view_type}")
|
||||
|
||||
|
||||
def extract_features(bv: BinaryView) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""extract file features"""
|
||||
for file_handler in FILE_HANDLERS:
|
||||
for feature, addr in file_handler(bv):
|
||||
yield feature, addr
|
||||
|
||||
|
||||
FILE_HANDLERS = (
|
||||
extract_file_export_names,
|
||||
extract_file_import_names,
|
||||
extract_file_strings,
|
||||
extract_file_section_names,
|
||||
extract_file_embedded_pe,
|
||||
extract_file_function_names,
|
||||
extract_file_format,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
""" """
|
||||
if len(sys.argv) < 2:
|
||||
return
|
||||
|
||||
from binaryninja import BinaryViewType
|
||||
|
||||
bv: BinaryView = BinaryViewType.get_view_of_file(sys.argv[1])
|
||||
if bv is None:
|
||||
return
|
||||
|
||||
import pprint
|
||||
|
||||
pprint.pprint(list(extract_features(bv)))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
34
capa/features/extractors/binja/find_binja_api.py
Normal file
34
capa/features/extractors/binja/find_binja_api.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
import subprocess
|
||||
|
||||
# When the script gets executed as a standalone executable (via PyInstaller), `import binaryninja` does not work because
|
||||
# we have excluded the binaryninja module in `pyinstaller.spec`. The trick here is to call the system Python and try
|
||||
# to find out the path of the binaryninja module that has been installed.
|
||||
# Note, including the binaryninja module in the `pyintaller.spec` would not work, since the binaryninja module tries to
|
||||
# find the binaryninja core e.g., `libbinaryninjacore.dylib`, using a relative path. And this does not work when the
|
||||
# binaryninja module is extracted by the PyInstaller.
|
||||
code = r"""
|
||||
from pathlib import Path
|
||||
import importlib
|
||||
spec = importlib.util.find_spec('binaryninja')
|
||||
if spec is not None:
|
||||
if len(spec.submodule_search_locations) > 0:
|
||||
path = Path(spec.submodule_search_locations[0])
|
||||
# encode the path with utf8 then convert to hex, make sure it can be read and restored properly
|
||||
print(str(path.parent).encode('utf8').hex())
|
||||
"""
|
||||
|
||||
|
||||
def find_binja_path() -> str:
|
||||
raw_output = subprocess.check_output(["python", "-c", code]).decode("ascii").strip()
|
||||
return bytes.fromhex(raw_output).decode("utf8")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(find_binja_path())
|
||||
97
capa/features/extractors/binja/function.py
Normal file
97
capa/features/extractors/binja/function.py
Normal file
@@ -0,0 +1,97 @@
|
||||
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
import sys
|
||||
from typing import Tuple, Iterator
|
||||
|
||||
from binaryninja import Function, BinaryView, LowLevelILOperation
|
||||
|
||||
from capa.features.common import Feature, Characteristic
|
||||
from capa.features.address import Address, AbsoluteVirtualAddress
|
||||
from capa.features.extractors import loops
|
||||
from capa.features.extractors.base_extractor import FunctionHandle
|
||||
|
||||
|
||||
def extract_function_calls_to(fh: FunctionHandle):
|
||||
"""extract callers to a function"""
|
||||
func: Function = fh.inner
|
||||
bv: BinaryView = func.view
|
||||
|
||||
for caller in func.caller_sites:
|
||||
# Everything that is a code reference to the current function is considered a caller, which actually includes
|
||||
# many other references that are NOT a caller. For example, an instruction `push function_start` will also be
|
||||
# considered a caller to the function
|
||||
if caller.llil.operation in [
|
||||
LowLevelILOperation.LLIL_CALL,
|
||||
LowLevelILOperation.LLIL_CALL_STACK_ADJUST,
|
||||
LowLevelILOperation.LLIL_JUMP,
|
||||
LowLevelILOperation.LLIL_TAILCALL,
|
||||
]:
|
||||
yield Characteristic("calls to"), AbsoluteVirtualAddress(caller.address)
|
||||
|
||||
|
||||
def extract_function_loop(fh: FunctionHandle):
|
||||
"""extract loop indicators from a function"""
|
||||
func: Function = fh.inner
|
||||
|
||||
edges = []
|
||||
|
||||
# construct control flow graph
|
||||
for bb in func.basic_blocks:
|
||||
for edge in bb.outgoing_edges:
|
||||
edges.append((bb.start, edge.target.start))
|
||||
|
||||
if loops.has_loop(edges):
|
||||
yield Characteristic("loop"), fh.address
|
||||
|
||||
|
||||
def extract_recursive_call(fh: FunctionHandle):
|
||||
"""extract recursive function call"""
|
||||
func: Function = fh.inner
|
||||
bv: BinaryView = func.view
|
||||
if bv is None:
|
||||
return
|
||||
|
||||
for ref in bv.get_code_refs(func.start):
|
||||
if ref.function == func:
|
||||
yield Characteristic("recursive call"), fh.address
|
||||
|
||||
|
||||
def extract_features(fh: FunctionHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
for func_handler in FUNCTION_HANDLERS:
|
||||
for feature, addr in func_handler(fh):
|
||||
yield feature, addr
|
||||
|
||||
|
||||
FUNCTION_HANDLERS = (extract_function_calls_to, extract_function_loop, extract_recursive_call)
|
||||
|
||||
|
||||
def main():
|
||||
""" """
|
||||
if len(sys.argv) < 2:
|
||||
return
|
||||
|
||||
from binaryninja import BinaryViewType
|
||||
|
||||
from capa.features.extractors.binja.extractor import BinjaFeatureExtractor
|
||||
|
||||
bv: BinaryView = BinaryViewType.get_view_of_file(sys.argv[1])
|
||||
if bv is None:
|
||||
return
|
||||
|
||||
features = []
|
||||
extractor = BinjaFeatureExtractor(bv)
|
||||
for fh in extractor.get_functions():
|
||||
features.extend(list(extract_features(fh)))
|
||||
|
||||
import pprint
|
||||
|
||||
pprint.pprint(features)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
55
capa/features/extractors/binja/global_.py
Normal file
55
capa/features/extractors/binja/global_.py
Normal file
@@ -0,0 +1,55 @@
|
||||
import logging
|
||||
import contextlib
|
||||
from typing import Tuple, Iterator
|
||||
|
||||
from binaryninja import BinaryView
|
||||
|
||||
import capa.features.extractors.elf
|
||||
from capa.features.common import OS, OS_MACOS, ARCH_I386, ARCH_AMD64, OS_WINDOWS, Arch, Feature
|
||||
from capa.features.address import NO_ADDRESS, Address
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def extract_os(bv: BinaryView) -> Iterator[Tuple[Feature, Address]]:
|
||||
name = bv.platform.name
|
||||
if "-" in name:
|
||||
name = name.split("-")[0]
|
||||
|
||||
if name == "windows":
|
||||
yield OS(OS_WINDOWS), NO_ADDRESS
|
||||
|
||||
elif name == "macos":
|
||||
yield OS(OS_MACOS), NO_ADDRESS
|
||||
|
||||
elif name in ["linux", "freebsd", "decree"]:
|
||||
yield OS(name), NO_ADDRESS
|
||||
|
||||
else:
|
||||
# we likely end up here:
|
||||
# 1. handling shellcode, or
|
||||
# 2. handling a new file format (e.g. macho)
|
||||
#
|
||||
# for (1) we can't do much - its shellcode and all bets are off.
|
||||
# we could maybe accept a further CLI argument to specify the OS,
|
||||
# but i think this would be rarely used.
|
||||
# rules that rely on OS conditions will fail to match on shellcode.
|
||||
#
|
||||
# for (2), this logic will need to be updated as the format is implemented.
|
||||
logger.debug("unsupported file format: %s, will not guess OS", name)
|
||||
return
|
||||
|
||||
|
||||
def extract_arch(bv: BinaryView) -> Iterator[Tuple[Feature, Address]]:
|
||||
arch = bv.arch.name
|
||||
if arch == "x86_64":
|
||||
yield Arch(ARCH_AMD64), NO_ADDRESS
|
||||
elif arch == "x86":
|
||||
yield Arch(ARCH_I386), NO_ADDRESS
|
||||
else:
|
||||
# we likely end up here:
|
||||
# 1. handling a new architecture (e.g. aarch64)
|
||||
#
|
||||
# for (1), this logic will need to be updated as the format is implemented.
|
||||
logger.debug("unsupported architecture: %s", arch)
|
||||
return
|
||||
50
capa/features/extractors/binja/helpers.py
Normal file
50
capa/features/extractors/binja/helpers.py
Normal file
@@ -0,0 +1,50 @@
|
||||
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
import re
|
||||
from typing import List, Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
from binaryninja import LowLevelILInstruction
|
||||
from binaryninja.architecture import InstructionTextToken
|
||||
|
||||
|
||||
@dataclass
|
||||
class DisassemblyInstruction:
|
||||
address: int
|
||||
length: int
|
||||
text: List[InstructionTextToken]
|
||||
|
||||
|
||||
LLIL_VISITOR = Callable[[LowLevelILInstruction, LowLevelILInstruction, int], bool]
|
||||
|
||||
|
||||
def visit_llil_exprs(il: LowLevelILInstruction, func: LLIL_VISITOR):
|
||||
# BN does not really support operand index at the disassembly level, so use the LLIL operand index as a substitute.
|
||||
# Note, this is NOT always guaranteed to be the same as disassembly operand.
|
||||
for i, op in enumerate(il.operands):
|
||||
if isinstance(op, LowLevelILInstruction) and func(op, il, i):
|
||||
visit_llil_exprs(op, func)
|
||||
|
||||
|
||||
def unmangle_c_name(name: str) -> str:
|
||||
# https://learn.microsoft.com/en-us/cpp/build/reference/decorated-names?view=msvc-170#FormatC
|
||||
# Possible variations for BaseThreadInitThunk:
|
||||
# @BaseThreadInitThunk@12
|
||||
# _BaseThreadInitThunk
|
||||
# _BaseThreadInitThunk@12
|
||||
# It is also possible for a function to have a `Stub` appended to its name:
|
||||
# _lstrlenWStub@4
|
||||
|
||||
# A small optimization to avoid running the regex too many times
|
||||
# TODO: this still increases the unit test execution time from 170s to 200s, should be able to accelerate it
|
||||
if name[0] in ["@", "_"]:
|
||||
match = re.match(r"^[@|_](.*?)(Stub)?(@\d+)?$", name)
|
||||
if match:
|
||||
return match.group(1)
|
||||
|
||||
return name
|
||||
630
capa/features/extractors/binja/insn.py
Normal file
630
capa/features/extractors/binja/insn.py
Normal file
@@ -0,0 +1,630 @@
|
||||
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
import sys
|
||||
from typing import Any, Dict, List, Tuple, Iterator, Optional
|
||||
|
||||
from binaryninja import Function
|
||||
from binaryninja import BasicBlock as BinjaBasicBlock
|
||||
from binaryninja import (
|
||||
BinaryView,
|
||||
ILRegister,
|
||||
SymbolType,
|
||||
BinaryReader,
|
||||
RegisterValueType,
|
||||
LowLevelILOperation,
|
||||
LowLevelILInstruction,
|
||||
InstructionTextTokenType,
|
||||
)
|
||||
|
||||
import capa.features.extractors.helpers
|
||||
from capa.features.insn import API, MAX_STRUCTURE_SIZE, Number, Offset, Mnemonic, OperandNumber, OperandOffset
|
||||
from capa.features.common import MAX_BYTES_FEATURE_SIZE, THUNK_CHAIN_DEPTH_DELTA, Bytes, String, Feature, Characteristic
|
||||
from capa.features.address import Address, AbsoluteVirtualAddress
|
||||
from capa.features.extractors.binja.helpers import DisassemblyInstruction, visit_llil_exprs
|
||||
from capa.features.extractors.base_extractor import BBHandle, InsnHandle, FunctionHandle
|
||||
|
||||
# security cookie checks may perform non-zeroing XORs, these are expected within a certain
|
||||
# byte range within the first and returning basic blocks, this helps to reduce FP features
|
||||
SECURITY_COOKIE_BYTES_DELTA = 0x40
|
||||
|
||||
|
||||
# check if a function is a stub function to another function/symbol. The criteria is:
|
||||
# 1. The function must only have one basic block
|
||||
# 2. The function must only make one call/jump to another address
|
||||
# If the function being checked is a stub function, returns the target address. Otherwise, return None.
|
||||
def is_stub_function(bv: BinaryView, addr: int) -> Optional[int]:
|
||||
funcs = bv.get_functions_at(addr)
|
||||
for func in funcs:
|
||||
if len(func.basic_blocks) != 1:
|
||||
continue
|
||||
|
||||
call_count = 0
|
||||
call_target = None
|
||||
for il in func.llil.instructions:
|
||||
if il.operation in [
|
||||
LowLevelILOperation.LLIL_CALL,
|
||||
LowLevelILOperation.LLIL_CALL_STACK_ADJUST,
|
||||
LowLevelILOperation.LLIL_JUMP,
|
||||
LowLevelILOperation.LLIL_TAILCALL,
|
||||
]:
|
||||
call_count += 1
|
||||
if il.dest.value.type in [
|
||||
RegisterValueType.ImportedAddressValue,
|
||||
RegisterValueType.ConstantValue,
|
||||
RegisterValueType.ConstantPointerValue,
|
||||
]:
|
||||
call_target = il.dest.value.value
|
||||
|
||||
if call_count == 1 and call_target is not None:
|
||||
return call_target
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def extract_insn_api_features(fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
parse instruction API features
|
||||
|
||||
example:
|
||||
call dword [0x00473038]
|
||||
"""
|
||||
insn: DisassemblyInstruction = ih.inner
|
||||
func: Function = fh.inner
|
||||
bv: BinaryView = func.view
|
||||
|
||||
for llil in func.get_llils_at(ih.address):
|
||||
if llil.operation in [
|
||||
LowLevelILOperation.LLIL_CALL,
|
||||
LowLevelILOperation.LLIL_CALL_STACK_ADJUST,
|
||||
LowLevelILOperation.LLIL_JUMP,
|
||||
LowLevelILOperation.LLIL_TAILCALL,
|
||||
]:
|
||||
if llil.dest.value.type not in [
|
||||
RegisterValueType.ImportedAddressValue,
|
||||
RegisterValueType.ConstantValue,
|
||||
RegisterValueType.ConstantPointerValue,
|
||||
]:
|
||||
continue
|
||||
address = llil.dest.value.value
|
||||
candidate_addrs = [address]
|
||||
stub_addr = is_stub_function(bv, address)
|
||||
if stub_addr is not None:
|
||||
candidate_addrs.append(stub_addr)
|
||||
|
||||
for address in candidate_addrs:
|
||||
sym = func.view.get_symbol_at(address)
|
||||
if sym is None or sym.type not in [SymbolType.ImportAddressSymbol, SymbolType.ImportedFunctionSymbol]:
|
||||
continue
|
||||
|
||||
sym_name = sym.short_name
|
||||
|
||||
lib_name = ""
|
||||
import_lib = bv.lookup_imported_object_library(sym.address)
|
||||
if import_lib is not None:
|
||||
lib_name = import_lib[0].name
|
||||
if lib_name.endswith(".dll"):
|
||||
lib_name = lib_name[:-4]
|
||||
elif lib_name.endswith(".so"):
|
||||
lib_name = lib_name[:-3]
|
||||
|
||||
for name in capa.features.extractors.helpers.generate_symbols(lib_name, sym_name):
|
||||
yield API(name), ih.address
|
||||
|
||||
if sym_name.startswith("_"):
|
||||
for name in capa.features.extractors.helpers.generate_symbols(lib_name, sym_name[1:]):
|
||||
yield API(name), ih.address
|
||||
|
||||
|
||||
def extract_insn_number_features(
|
||||
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
|
||||
) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
parse instruction number features
|
||||
example:
|
||||
push 3136B0h ; dwControlCode
|
||||
"""
|
||||
insn: DisassemblyInstruction = ih.inner
|
||||
func: Function = fh.inner
|
||||
bv: BinaryView = func.view
|
||||
|
||||
results: List[Tuple[Any[Number, OperandNumber], Address]] = []
|
||||
address_size = func.view.arch.address_size * 8
|
||||
|
||||
def llil_checker(il: LowLevelILInstruction, parent: LowLevelILInstruction, index: int) -> bool:
|
||||
if il.operation == LowLevelILOperation.LLIL_LOAD:
|
||||
return False
|
||||
|
||||
if il.operation not in [LowLevelILOperation.LLIL_CONST, LowLevelILOperation.LLIL_CONST_PTR]:
|
||||
return True
|
||||
|
||||
for op in parent.operands:
|
||||
if isinstance(op, ILRegister) and op.name in ["esp", "ebp", "rsp", "rbp", "sp"]:
|
||||
return False
|
||||
elif isinstance(op, LowLevelILInstruction) and op.operation == LowLevelILOperation.LLIL_REG:
|
||||
if op.src.name in ["esp", "ebp", "rsp", "rbp", "sp"]:
|
||||
return False
|
||||
|
||||
raw_value = il.value.value
|
||||
if parent.operation == LowLevelILOperation.LLIL_SUB:
|
||||
raw_value = -raw_value
|
||||
|
||||
results.append((Number(raw_value), ih.address))
|
||||
results.append((OperandNumber(index, raw_value), ih.address))
|
||||
|
||||
return False
|
||||
|
||||
for llil in func.get_llils_at(ih.address):
|
||||
visit_llil_exprs(llil, llil_checker)
|
||||
|
||||
for result in results:
|
||||
yield result
|
||||
|
||||
|
||||
def extract_insn_bytes_features(fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
parse referenced byte sequences
|
||||
example:
|
||||
push offset iid_004118d4_IShellLinkA ; riid
|
||||
"""
|
||||
insn: DisassemblyInstruction = ih.inner
|
||||
func: Function = fh.inner
|
||||
bv: BinaryView = func.view
|
||||
|
||||
candidate_addrs = set()
|
||||
|
||||
llil = func.get_llil_at(ih.address)
|
||||
if llil is None or llil.operation in [LowLevelILOperation.LLIL_CALL, LowLevelILOperation.LLIL_CALL_STACK_ADJUST]:
|
||||
return
|
||||
|
||||
for ref in bv.get_code_refs_from(ih.address):
|
||||
if ref == ih.address:
|
||||
continue
|
||||
|
||||
if len(bv.get_functions_containing(ref)) > 0:
|
||||
continue
|
||||
|
||||
candidate_addrs.add(ref)
|
||||
|
||||
# collect candidate address by enumerating all integers, https://github.com/Vector35/binaryninja-api/issues/3966
|
||||
def llil_checker(il: LowLevelILInstruction, parent: LowLevelILInstruction, index: int) -> bool:
|
||||
if il.operation in [LowLevelILOperation.LLIL_CONST, LowLevelILOperation.LLIL_CONST_PTR]:
|
||||
value = il.value.value
|
||||
if value > 0:
|
||||
candidate_addrs.add(value)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
for llil in func.get_llils_at(ih.address):
|
||||
visit_llil_exprs(llil, llil_checker)
|
||||
|
||||
for addr in candidate_addrs:
|
||||
extracted_bytes = bv.read(addr, MAX_BYTES_FEATURE_SIZE)
|
||||
if extracted_bytes and not capa.features.extractors.helpers.all_zeros(extracted_bytes):
|
||||
if bv.get_string_at(addr) is None:
|
||||
# don't extract byte features for obvious strings
|
||||
yield Bytes(extracted_bytes), ih.address
|
||||
|
||||
|
||||
def extract_insn_string_features(
|
||||
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
|
||||
) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
parse instruction string features
|
||||
|
||||
example:
|
||||
push offset aAcr ; "ACR > "
|
||||
"""
|
||||
insn: DisassemblyInstruction = ih.inner
|
||||
func: Function = fh.inner
|
||||
bv: BinaryView = func.view
|
||||
|
||||
candidate_addrs = set()
|
||||
|
||||
# collect candidate address from code refs directly
|
||||
for ref in bv.get_code_refs_from(ih.address):
|
||||
if ref == ih.address:
|
||||
continue
|
||||
|
||||
if len(bv.get_functions_containing(ref)) > 0:
|
||||
continue
|
||||
|
||||
candidate_addrs.add(ref)
|
||||
|
||||
# collect candidate address by enumerating all integers, https://github.com/Vector35/binaryninja-api/issues/3966
|
||||
def llil_checker(il: LowLevelILInstruction, parent: LowLevelILInstruction, index: int) -> bool:
|
||||
if il.operation in [LowLevelILOperation.LLIL_CONST, LowLevelILOperation.LLIL_CONST_PTR]:
|
||||
value = il.value.value
|
||||
if value > 0:
|
||||
candidate_addrs.add(value)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
for llil in func.get_llils_at(ih.address):
|
||||
visit_llil_exprs(llil, llil_checker)
|
||||
|
||||
# Now we have all the candidate address, check them for string or pointer to string
|
||||
br = BinaryReader(bv)
|
||||
for addr in candidate_addrs:
|
||||
found = bv.get_string_at(addr)
|
||||
if found:
|
||||
yield String(found.value), ih.address
|
||||
|
||||
br.seek(addr)
|
||||
pointer = None
|
||||
if bv.arch.address_size == 4:
|
||||
pointer = br.read32()
|
||||
elif bv.arch.address_size == 8:
|
||||
pointer = br.read64()
|
||||
|
||||
if pointer is not None:
|
||||
found = bv.get_string_at(pointer)
|
||||
if found:
|
||||
yield String(found.value), ih.address
|
||||
|
||||
|
||||
def extract_insn_offset_features(
|
||||
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
|
||||
) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
parse instruction structure offset features
|
||||
|
||||
example:
|
||||
.text:0040112F cmp [esi+4], ebx
|
||||
"""
|
||||
insn: DisassemblyInstruction = ih.inner
|
||||
func: Function = fh.inner
|
||||
|
||||
results: List[Tuple[Any[Offset, OperandOffset], Address]] = []
|
||||
address_size = func.view.arch.address_size * 8
|
||||
|
||||
def llil_checker(il: LowLevelILInstruction, parent: LowLevelILInstruction, index: int) -> bool:
|
||||
# The most common case, read/write dereference to something like `dword [eax+0x28]`
|
||||
if il.operation in [LowLevelILOperation.LLIL_ADD, LowLevelILOperation.LLIL_SUB]:
|
||||
left = il.left
|
||||
right = il.right
|
||||
# Exclude offsets based on stack/franme pointers
|
||||
if left.operation == LowLevelILOperation.LLIL_REG and left.src.name in ["esp", "ebp", "rsp", "rbp", "sp"]:
|
||||
return True
|
||||
|
||||
if right.operation != LowLevelILOperation.LLIL_CONST:
|
||||
return True
|
||||
|
||||
raw_value = right.value.value
|
||||
# If this is not a dereference, then this must be an add and the offset must be in the range \
|
||||
# [0, MAX_STRUCTURE_SIZE]. For example,
|
||||
# add eax, 0x10,
|
||||
# lea ebx, [eax + 1]
|
||||
if parent.operation not in [LowLevelILOperation.LLIL_LOAD, LowLevelILOperation.LLIL_STORE]:
|
||||
if il.operation != LowLevelILOperation.LLIL_ADD or (not 0 < raw_value < MAX_STRUCTURE_SIZE):
|
||||
return False
|
||||
|
||||
if address_size > 0:
|
||||
# BN also encodes the constant value as two's complement, we need to restore its original value
|
||||
value = capa.features.extractors.helpers.twos_complement(raw_value, address_size)
|
||||
else:
|
||||
value = raw_value
|
||||
|
||||
results.append((Offset(value), ih.address))
|
||||
results.append((OperandOffset(index, value), ih.address))
|
||||
return False
|
||||
|
||||
# An edge case: for code like `push dword [esi]`, we need to generate a feature for offset 0x0
|
||||
elif il.operation in [LowLevelILOperation.LLIL_LOAD, LowLevelILOperation.LLIL_STORE]:
|
||||
if il.operands[0].operation == LowLevelILOperation.LLIL_REG:
|
||||
results.append((Offset(0), ih.address))
|
||||
results.append((OperandOffset(index, 0), ih.address))
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
for llil in func.get_llils_at(ih.address):
|
||||
visit_llil_exprs(llil, llil_checker)
|
||||
|
||||
for result in results:
|
||||
yield result
|
||||
|
||||
|
||||
def is_nzxor_stack_cookie(f: Function, bb: BinjaBasicBlock, llil: LowLevelILInstruction) -> bool:
|
||||
"""check if nzxor exists within stack cookie delta"""
|
||||
# TODO: we can do a much accurate analysi using LLIL SSA
|
||||
|
||||
reg_names = []
|
||||
if llil.left.operation == LowLevelILOperation.LLIL_REG:
|
||||
reg_names.append(llil.left.src.name)
|
||||
|
||||
if llil.right.operation == LowLevelILOperation.LLIL_REG:
|
||||
reg_names.append(llil.right.src.name)
|
||||
|
||||
# stack cookie reg should be stack/frame pointer
|
||||
if not any(reg in ["ebp", "esp", "rbp", "rsp", "sp"] for reg in reg_names):
|
||||
return False
|
||||
|
||||
# expect security cookie init in first basic block within first bytes (instructions)
|
||||
if len(bb.incoming_edges) == 0 and llil.address < (bb.start + SECURITY_COOKIE_BYTES_DELTA):
|
||||
return True
|
||||
|
||||
# ... or within last bytes (instructions) before a return
|
||||
if len(bb.outgoing_edges) == 0 and llil.address > (bb.end - SECURITY_COOKIE_BYTES_DELTA):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def extract_insn_nzxor_characteristic_features(
|
||||
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
|
||||
) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
parse instruction non-zeroing XOR instruction
|
||||
ignore expected non-zeroing XORs, e.g. security cookies
|
||||
"""
|
||||
insn: DisassemblyInstruction = ih.inner
|
||||
func: Function = fh.inner
|
||||
|
||||
results = []
|
||||
|
||||
def llil_checker(il: LowLevelILInstruction, parent: LowLevelILInstruction, index: int) -> bool:
|
||||
# If the two operands of the xor instruction are the same, the LLIL will be translated to other instructions,
|
||||
# e.g., <llil: eax = 0>, (LLIL_SET_REG). So we do not need to check whether the two operands are the same.
|
||||
if il.operation == LowLevelILOperation.LLIL_XOR:
|
||||
# Exclude cases related to the stack cookie
|
||||
if is_nzxor_stack_cookie(fh.inner, bbh.inner[0], il):
|
||||
return False
|
||||
results.append((Characteristic("nzxor"), ih.address))
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
for llil in func.get_llils_at(ih.address):
|
||||
visit_llil_exprs(llil, llil_checker)
|
||||
|
||||
for result in results:
|
||||
yield result
|
||||
|
||||
|
||||
def extract_insn_mnemonic_features(
|
||||
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
|
||||
) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""parse instruction mnemonic features"""
|
||||
insn: DisassemblyInstruction = ih.inner
|
||||
yield Mnemonic(insn.text[0].text), ih.address
|
||||
|
||||
|
||||
def extract_insn_obfs_call_plus_5_characteristic_features(
|
||||
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
|
||||
) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
parse call $+5 instruction from the given instruction.
|
||||
"""
|
||||
insn: DisassemblyInstruction = ih.inner
|
||||
if insn.text[0].text == "call" and insn.text[2].text == "$+5" and insn.length == 5:
|
||||
yield Characteristic("call $+5"), ih.address
|
||||
|
||||
|
||||
def extract_insn_peb_access_characteristic_features(
|
||||
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
|
||||
) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""parse instruction peb access
|
||||
|
||||
fs:[0x30] on x86, gs:[0x60] on x64
|
||||
"""
|
||||
insn: DisassemblyInstruction = ih.inner
|
||||
func: Function = fh.inner
|
||||
|
||||
results = []
|
||||
|
||||
def llil_checker(il: LowLevelILInstruction, parent: LowLevelILOperation, index: int) -> bool:
|
||||
if il.operation != LowLevelILOperation.LLIL_LOAD:
|
||||
return True
|
||||
|
||||
src = il.src
|
||||
if src.operation != LowLevelILOperation.LLIL_ADD:
|
||||
return True
|
||||
|
||||
left = src.left
|
||||
right = src.right
|
||||
|
||||
if left.operation != LowLevelILOperation.LLIL_REG:
|
||||
return True
|
||||
|
||||
reg = left.src.name
|
||||
|
||||
if right.operation != LowLevelILOperation.LLIL_CONST:
|
||||
return True
|
||||
|
||||
value = right.value.value
|
||||
if not (reg, value) in (("fsbase", 0x30), ("gsbase", 0x60)):
|
||||
return True
|
||||
|
||||
results.append((Characteristic("peb access"), ih.address))
|
||||
return False
|
||||
|
||||
for llil in func.get_llils_at(ih.address):
|
||||
visit_llil_exprs(llil, llil_checker)
|
||||
|
||||
for result in results:
|
||||
yield result
|
||||
|
||||
|
||||
def extract_insn_segment_access_features(
|
||||
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
|
||||
) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""parse instruction fs or gs access"""
|
||||
insn: DisassemblyInstruction = ih.inner
|
||||
func: Function = fh.inner
|
||||
|
||||
results = []
|
||||
|
||||
def llil_checker(il: LowLevelILInstruction, parent: LowLevelILInstruction, index: int) -> bool:
|
||||
if il.operation == LowLevelILOperation.LLIL_REG:
|
||||
reg = il.src.name
|
||||
if reg == "fsbase":
|
||||
results.append((Characteristic("fs access"), ih.address))
|
||||
return False
|
||||
elif reg == "gsbase":
|
||||
results.append((Characteristic("gs access"), ih.address))
|
||||
return False
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
for llil in func.get_llils_at(ih.address):
|
||||
visit_llil_exprs(llil, llil_checker)
|
||||
|
||||
for result in results:
|
||||
yield result
|
||||
|
||||
|
||||
def extract_insn_cross_section_cflow(
|
||||
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
|
||||
) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""inspect the instruction for a CALL or JMP that crosses section boundaries"""
|
||||
insn: DisassemblyInstruction = ih.inner
|
||||
func: Function = fh.inner
|
||||
bv: BinaryView = func.view
|
||||
|
||||
if bv is None:
|
||||
return
|
||||
|
||||
seg1 = bv.get_segment_at(ih.address)
|
||||
sections1 = bv.get_sections_at(ih.address)
|
||||
for ref in bv.get_code_refs_from(ih.address):
|
||||
if len(bv.get_functions_at(ref)) == 0:
|
||||
continue
|
||||
|
||||
seg2 = bv.get_segment_at(ref)
|
||||
sections2 = bv.get_sections_at(ref)
|
||||
if seg1 != seg2 or sections1 != sections2:
|
||||
yield Characteristic("cross section flow"), ih.address
|
||||
|
||||
|
||||
def extract_function_calls_from(fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""extract functions calls from features
|
||||
|
||||
most relevant at the function scope, however, its most efficient to extract at the instruction scope
|
||||
"""
|
||||
insn: DisassemblyInstruction = ih.inner
|
||||
func: Function = fh.inner
|
||||
bv: BinaryView = func.view
|
||||
|
||||
if bv is None:
|
||||
return
|
||||
|
||||
for il in func.get_llils_at(ih.address):
|
||||
if il.operation not in [
|
||||
LowLevelILOperation.LLIL_CALL,
|
||||
LowLevelILOperation.LLIL_CALL_STACK_ADJUST,
|
||||
LowLevelILOperation.LLIL_TAILCALL,
|
||||
]:
|
||||
continue
|
||||
|
||||
dest = il.dest
|
||||
if dest.operation == LowLevelILOperation.LLIL_CONST_PTR:
|
||||
value = dest.value.value
|
||||
yield Characteristic("calls from"), AbsoluteVirtualAddress(value)
|
||||
elif dest.operation == LowLevelILOperation.LLIL_CONST:
|
||||
yield Characteristic("calls from"), AbsoluteVirtualAddress(dest.value)
|
||||
elif dest.operation == LowLevelILOperation.LLIL_LOAD:
|
||||
indirect_src = dest.src
|
||||
if indirect_src.operation == LowLevelILOperation.LLIL_CONST_PTR:
|
||||
value = indirect_src.value.value
|
||||
yield Characteristic("calls from"), AbsoluteVirtualAddress(value)
|
||||
elif indirect_src.operation == LowLevelILOperation.LLIL_CONST:
|
||||
yield Characteristic("calls from"), AbsoluteVirtualAddress(indirect_src.value)
|
||||
elif dest.operation == LowLevelILOperation.LLIL_REG:
|
||||
if dest.value.type in [
|
||||
RegisterValueType.ImportedAddressValue,
|
||||
RegisterValueType.ConstantValue,
|
||||
RegisterValueType.ConstantPointerValue,
|
||||
]:
|
||||
yield Characteristic("calls from"), AbsoluteVirtualAddress(dest.value.value)
|
||||
|
||||
|
||||
def extract_function_indirect_call_characteristic_features(
|
||||
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
|
||||
) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""extract indirect function calls (e.g., call eax or call dword ptr [edx+4])
|
||||
does not include calls like => call ds:dword_ABD4974
|
||||
|
||||
most relevant at the function or basic block scope;
|
||||
however, its most efficient to extract at the instruction scope
|
||||
"""
|
||||
insn: DisassemblyInstruction = ih.inner
|
||||
func: Function = fh.inner
|
||||
|
||||
llil = func.get_llil_at(ih.address)
|
||||
if llil is None or llil.operation not in [
|
||||
LowLevelILOperation.LLIL_CALL,
|
||||
LowLevelILOperation.LLIL_CALL_STACK_ADJUST,
|
||||
LowLevelILOperation.LLIL_TAILCALL,
|
||||
]:
|
||||
return
|
||||
|
||||
if llil.dest.operation in [LowLevelILOperation.LLIL_CONST, LowLevelILOperation.LLIL_CONST_PTR]:
|
||||
return
|
||||
|
||||
if llil.dest.operation == LowLevelILOperation.LLIL_LOAD:
|
||||
src = llil.dest.src
|
||||
if src.operation in [LowLevelILOperation.LLIL_CONST, LowLevelILOperation.LLIL_CONST_PTR]:
|
||||
return
|
||||
|
||||
yield Characteristic("indirect call"), ih.address
|
||||
|
||||
|
||||
def extract_features(f: FunctionHandle, bbh: BBHandle, insn: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""extract instruction features"""
|
||||
for inst_handler in INSTRUCTION_HANDLERS:
|
||||
for feature, ea in inst_handler(f, bbh, insn):
|
||||
yield feature, ea
|
||||
|
||||
|
||||
INSTRUCTION_HANDLERS = (
|
||||
extract_insn_api_features,
|
||||
extract_insn_number_features,
|
||||
extract_insn_bytes_features,
|
||||
extract_insn_string_features,
|
||||
extract_insn_offset_features,
|
||||
extract_insn_nzxor_characteristic_features,
|
||||
extract_insn_mnemonic_features,
|
||||
extract_insn_obfs_call_plus_5_characteristic_features,
|
||||
extract_insn_peb_access_characteristic_features,
|
||||
extract_insn_cross_section_cflow,
|
||||
extract_insn_segment_access_features,
|
||||
extract_function_calls_from,
|
||||
extract_function_indirect_call_characteristic_features,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
""" """
|
||||
if len(sys.argv) < 2:
|
||||
return
|
||||
|
||||
from binaryninja import BinaryViewType
|
||||
|
||||
from capa.features.extractors.binja.extractor import BinjaFeatureExtractor
|
||||
|
||||
bv: BinaryView = BinaryViewType.get_view_of_file(sys.argv[1])
|
||||
if bv is None:
|
||||
return
|
||||
|
||||
features = []
|
||||
extractor = BinjaFeatureExtractor(bv)
|
||||
for fh in extractor.get_functions():
|
||||
for bbh in extractor.get_basic_blocks(fh):
|
||||
for insn in extractor.get_instructions(fh, bbh):
|
||||
features.extend(list(extract_features(fh, bbh, insn)))
|
||||
|
||||
import pprint
|
||||
|
||||
pprint.pprint(features)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -9,12 +9,32 @@ import pefile
|
||||
import capa.features
|
||||
import capa.features.extractors.elf
|
||||
import capa.features.extractors.pefile
|
||||
from capa.features.common import OS, FORMAT_PE, FORMAT_ELF, OS_WINDOWS, FORMAT_FREEZE, Arch, Format, String, Feature
|
||||
import capa.features.extractors.strings
|
||||
from capa.features.common import (
|
||||
OS,
|
||||
OS_ANY,
|
||||
OS_AUTO,
|
||||
ARCH_ANY,
|
||||
FORMAT_PE,
|
||||
FORMAT_ELF,
|
||||
OS_WINDOWS,
|
||||
FORMAT_FREEZE,
|
||||
FORMAT_RESULT,
|
||||
Arch,
|
||||
Format,
|
||||
String,
|
||||
Feature,
|
||||
)
|
||||
from capa.features.freeze import is_freeze
|
||||
from capa.features.address import NO_ADDRESS, Address, FileOffsetAddress
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# match strings for formats
|
||||
MATCH_PE = b"MZ"
|
||||
MATCH_ELF = b"\x7fELF"
|
||||
MATCH_RESULT = b'{"meta":'
|
||||
|
||||
|
||||
def extract_file_strings(buf, **kwargs) -> Iterator[Tuple[String, Address]]:
|
||||
"""
|
||||
@@ -28,12 +48,14 @@ def extract_file_strings(buf, **kwargs) -> Iterator[Tuple[String, Address]]:
|
||||
|
||||
|
||||
def extract_format(buf) -> Iterator[Tuple[Feature, Address]]:
|
||||
if buf.startswith(b"MZ"):
|
||||
if buf.startswith(MATCH_PE):
|
||||
yield Format(FORMAT_PE), NO_ADDRESS
|
||||
elif buf.startswith(b"\x7fELF"):
|
||||
elif buf.startswith(MATCH_ELF):
|
||||
yield Format(FORMAT_ELF), NO_ADDRESS
|
||||
elif is_freeze(buf):
|
||||
yield Format(FORMAT_FREEZE), NO_ADDRESS
|
||||
elif buf.startswith(MATCH_RESULT):
|
||||
yield Format(FORMAT_RESULT), NO_ADDRESS
|
||||
else:
|
||||
# we likely end up here:
|
||||
# 1. handling a file format (e.g. macho)
|
||||
@@ -44,10 +66,13 @@ def extract_format(buf) -> Iterator[Tuple[Feature, Address]]:
|
||||
|
||||
|
||||
def extract_arch(buf) -> Iterator[Tuple[Feature, Address]]:
|
||||
if buf.startswith(b"MZ"):
|
||||
if buf.startswith(MATCH_PE):
|
||||
yield from capa.features.extractors.pefile.extract_file_arch(pe=pefile.PE(data=buf))
|
||||
|
||||
elif buf.startswith(b"\x7fELF"):
|
||||
elif buf.startswith(MATCH_RESULT):
|
||||
yield Arch(ARCH_ANY), NO_ADDRESS
|
||||
|
||||
elif buf.startswith(MATCH_ELF):
|
||||
with contextlib.closing(io.BytesIO(buf)) as f:
|
||||
arch = capa.features.extractors.elf.detect_elf_arch(f)
|
||||
|
||||
@@ -63,7 +88,7 @@ def extract_arch(buf) -> Iterator[Tuple[Feature, Address]]:
|
||||
# 2. handling a new file format (e.g. macho)
|
||||
#
|
||||
# for (1) we can't do much - its shellcode and all bets are off.
|
||||
# we could maybe accept a futher CLI argument to specify the arch,
|
||||
# we could maybe accept a further CLI argument to specify the arch,
|
||||
# but i think this would be rarely used.
|
||||
# rules that rely on arch conditions will fail to match on shellcode.
|
||||
#
|
||||
@@ -72,10 +97,15 @@ def extract_arch(buf) -> Iterator[Tuple[Feature, Address]]:
|
||||
return
|
||||
|
||||
|
||||
def extract_os(buf) -> Iterator[Tuple[Feature, Address]]:
|
||||
if buf.startswith(b"MZ"):
|
||||
def extract_os(buf, os=OS_AUTO) -> Iterator[Tuple[Feature, Address]]:
|
||||
if os != OS_AUTO:
|
||||
yield OS(os), NO_ADDRESS
|
||||
|
||||
if buf.startswith(MATCH_PE):
|
||||
yield OS(OS_WINDOWS), NO_ADDRESS
|
||||
elif buf.startswith(b"\x7fELF"):
|
||||
elif buf.startswith(MATCH_RESULT):
|
||||
yield OS(OS_ANY), NO_ADDRESS
|
||||
elif buf.startswith(MATCH_ELF):
|
||||
with contextlib.closing(io.BytesIO(buf)) as f:
|
||||
os = capa.features.extractors.elf.detect_elf_os(f)
|
||||
|
||||
@@ -91,8 +121,6 @@ def extract_os(buf) -> Iterator[Tuple[Feature, Address]]:
|
||||
# 2. handling a new file format (e.g. macho)
|
||||
#
|
||||
# for (1) we can't do much - its shellcode and all bets are off.
|
||||
# we could maybe accept a futher CLI argument to specify the OS,
|
||||
# but i think this would be rarely used.
|
||||
# rules that rely on OS conditions will fail to match on shellcode.
|
||||
#
|
||||
# for (2), this logic will need to be updated as the format is implemented.
|
||||
|
||||
@@ -8,27 +8,77 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, Tuple, Iterator
|
||||
from typing import Dict, List, Tuple, Union, Iterator, Optional
|
||||
|
||||
import dnfile
|
||||
from dncil.clr.token import Token
|
||||
from dncil.cil.opcode import OpCodes
|
||||
|
||||
import capa.features.extractors
|
||||
import capa.features.extractors.dotnetfile
|
||||
import capa.features.extractors.dnfile.file
|
||||
import capa.features.extractors.dnfile.insn
|
||||
import capa.features.extractors.dnfile.function
|
||||
from capa.features.common import Feature
|
||||
from capa.features.address import NO_ADDRESS, Address, DNTokenAddress, DNTokenOffsetAddress, AbsoluteVirtualAddress
|
||||
from capa.features.address import NO_ADDRESS, Address, DNTokenAddress, DNTokenOffsetAddress
|
||||
from capa.features.extractors.dnfile.types import DnType, DnUnmanagedMethod
|
||||
from capa.features.extractors.base_extractor import BBHandle, InsnHandle, FunctionHandle, FeatureExtractor
|
||||
from capa.features.extractors.dnfile.helpers import get_dotnet_managed_method_bodies
|
||||
from capa.features.extractors.dnfile.helpers import (
|
||||
get_dotnet_types,
|
||||
get_dotnet_fields,
|
||||
get_dotnet_managed_imports,
|
||||
get_dotnet_managed_methods,
|
||||
get_dotnet_unmanaged_imports,
|
||||
get_dotnet_managed_method_bodies,
|
||||
)
|
||||
|
||||
|
||||
class DnFileFeatureExtractorCache:
|
||||
def __init__(self, pe: dnfile.dnPE):
|
||||
self.imports: Dict[int, Union[DnType, DnUnmanagedMethod]] = {}
|
||||
self.native_imports: Dict[int, Union[DnType, DnUnmanagedMethod]] = {}
|
||||
self.methods: Dict[int, Union[DnType, DnUnmanagedMethod]] = {}
|
||||
self.fields: Dict[int, Union[DnType, DnUnmanagedMethod]] = {}
|
||||
self.types: Dict[int, Union[DnType, DnUnmanagedMethod]] = {}
|
||||
|
||||
for import_ in get_dotnet_managed_imports(pe):
|
||||
self.imports[import_.token] = import_
|
||||
for native_import in get_dotnet_unmanaged_imports(pe):
|
||||
self.native_imports[native_import.token] = native_import
|
||||
for method in get_dotnet_managed_methods(pe):
|
||||
self.methods[method.token] = method
|
||||
for field in get_dotnet_fields(pe):
|
||||
self.fields[field.token] = field
|
||||
for type_ in get_dotnet_types(pe):
|
||||
self.types[type_.token] = type_
|
||||
|
||||
def get_import(self, token: int) -> Optional[Union[DnType, DnUnmanagedMethod]]:
|
||||
return self.imports.get(token, None)
|
||||
|
||||
def get_native_import(self, token: int) -> Optional[Union[DnType, DnUnmanagedMethod]]:
|
||||
return self.native_imports.get(token, None)
|
||||
|
||||
def get_method(self, token: int) -> Optional[Union[DnType, DnUnmanagedMethod]]:
|
||||
return self.methods.get(token, None)
|
||||
|
||||
def get_field(self, token: int) -> Optional[Union[DnType, DnUnmanagedMethod]]:
|
||||
return self.fields.get(token, None)
|
||||
|
||||
def get_type(self, token: int) -> Optional[Union[DnType, DnUnmanagedMethod]]:
|
||||
return self.types.get(token, None)
|
||||
|
||||
|
||||
class DnfileFeatureExtractor(FeatureExtractor):
|
||||
def __init__(self, path: str):
|
||||
super(DnfileFeatureExtractor, self).__init__()
|
||||
super().__init__()
|
||||
self.pe: dnfile.dnPE = dnfile.dnPE(path)
|
||||
|
||||
# pre-compute .NET token lookup tables; each .NET method has access to this cache for feature extraction
|
||||
# most relevant at instruction scope
|
||||
self.token_cache: DnFileFeatureExtractorCache = DnFileFeatureExtractorCache(self.pe)
|
||||
|
||||
# pre-compute these because we'll yield them at *every* scope.
|
||||
self.global_features: List[Tuple[Feature, Address]] = []
|
||||
self.global_features.extend(capa.features.extractors.dotnetfile.extract_file_format())
|
||||
self.global_features.extend(capa.features.extractors.dotnetfile.extract_file_os(pe=self.pe))
|
||||
self.global_features.extend(capa.features.extractors.dotnetfile.extract_file_arch(pe=self.pe))
|
||||
|
||||
@@ -42,12 +92,45 @@ class DnfileFeatureExtractor(FeatureExtractor):
|
||||
yield from capa.features.extractors.dnfile.file.extract_features(self.pe)
|
||||
|
||||
def get_functions(self) -> Iterator[FunctionHandle]:
|
||||
for token, f in get_dotnet_managed_method_bodies(self.pe):
|
||||
yield FunctionHandle(address=DNTokenAddress(Token(token)), inner=f, ctx={"pe": self.pe})
|
||||
# create a method lookup table
|
||||
methods: Dict[Address, FunctionHandle] = {}
|
||||
for token, method in get_dotnet_managed_method_bodies(self.pe):
|
||||
fh: FunctionHandle = FunctionHandle(
|
||||
address=DNTokenAddress(token),
|
||||
inner=method,
|
||||
ctx={"pe": self.pe, "calls_from": set(), "calls_to": set(), "cache": self.token_cache},
|
||||
)
|
||||
|
||||
def extract_function_features(self, f):
|
||||
# TODO
|
||||
yield from []
|
||||
# method tokens should be unique
|
||||
assert fh.address not in methods.keys()
|
||||
methods[fh.address] = fh
|
||||
|
||||
# calculate unique calls to/from each method
|
||||
for fh in methods.values():
|
||||
for insn in fh.inner.instructions:
|
||||
if insn.opcode not in (
|
||||
OpCodes.Call,
|
||||
OpCodes.Callvirt,
|
||||
OpCodes.Jmp,
|
||||
OpCodes.Newobj,
|
||||
):
|
||||
continue
|
||||
|
||||
address: DNTokenAddress = DNTokenAddress(insn.operand.value)
|
||||
|
||||
# record call to destination method; note: we only consider MethodDef methods for destinations
|
||||
dest: Optional[FunctionHandle] = methods.get(address, None)
|
||||
if dest is not None:
|
||||
dest.ctx["calls_to"].add(fh.address)
|
||||
|
||||
# record call from source method; note: we record all unique calls from a MethodDef method, not just
|
||||
# those calls to other MethodDef methods e.g. calls to imported MemberRef methods
|
||||
fh.ctx["calls_from"].add(address)
|
||||
|
||||
yield from methods.values()
|
||||
|
||||
def extract_function_features(self, fh) -> Iterator[Tuple[Feature, Address]]:
|
||||
yield from capa.features.extractors.dnfile.function.extract_features(fh)
|
||||
|
||||
def get_basic_blocks(self, f) -> Iterator[BBHandle]:
|
||||
# each dotnet method is considered 1 basic block
|
||||
@@ -63,7 +146,7 @@ class DnfileFeatureExtractor(FeatureExtractor):
|
||||
def get_instructions(self, fh, bbh):
|
||||
for insn in bbh.inner.instructions:
|
||||
yield InsnHandle(
|
||||
address=DNTokenOffsetAddress(bbh.address.token, insn.offset - (fh.inner.offset + fh.inner.header_size)),
|
||||
address=DNTokenOffsetAddress(bbh.address, insn.offset - (fh.inner.offset + fh.inner.header_size)),
|
||||
inner=insn,
|
||||
)
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ def extract_file_class_features(pe: dnfile.dnPE) -> Iterator[Tuple[Class, Addres
|
||||
|
||||
def extract_features(pe: dnfile.dnPE) -> Iterator[Tuple[Feature, Address]]:
|
||||
for file_handler in FILE_HANDLERS:
|
||||
for (feature, address) in file_handler(pe):
|
||||
for feature, address in file_handler(pe):
|
||||
yield feature, address
|
||||
|
||||
|
||||
|
||||
50
capa/features/extractors/dnfile/function.py
Normal file
50
capa/features/extractors/dnfile/function.py
Normal file
@@ -0,0 +1,50 @@
|
||||
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Tuple, Iterator
|
||||
|
||||
from capa.features.common import Feature, Characteristic
|
||||
from capa.features.address import Address
|
||||
from capa.features.extractors.base_extractor import FunctionHandle
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def extract_function_calls_to(fh: FunctionHandle) -> Iterator[Tuple[Characteristic, Address]]:
|
||||
"""extract callers to a function"""
|
||||
for dest in fh.ctx["calls_to"]:
|
||||
yield Characteristic("calls to"), dest
|
||||
|
||||
|
||||
def extract_function_calls_from(fh: FunctionHandle) -> Iterator[Tuple[Characteristic, Address]]:
|
||||
"""extract callers from a function"""
|
||||
for src in fh.ctx["calls_from"]:
|
||||
yield Characteristic("calls from"), src
|
||||
|
||||
|
||||
def extract_recursive_call(fh: FunctionHandle) -> Iterator[Tuple[Characteristic, Address]]:
|
||||
"""extract recursive function call"""
|
||||
if fh.address in fh.ctx["calls_to"]:
|
||||
yield Characteristic("recursive call"), fh.address
|
||||
|
||||
|
||||
def extract_function_loop(fh: FunctionHandle) -> Iterator[Tuple[Characteristic, Address]]:
|
||||
"""extract loop indicators from a function"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def extract_features(fh: FunctionHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
for func_handler in FUNCTION_HANDLERS:
|
||||
for feature, addr in func_handler(fh):
|
||||
yield feature, addr
|
||||
|
||||
|
||||
FUNCTION_HANDLERS = (extract_function_calls_to, extract_function_calls_from, extract_recursive_call)
|
||||
@@ -9,7 +9,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any, Tuple, Iterator, Optional
|
||||
from typing import Dict, Tuple, Union, Iterator, Optional
|
||||
|
||||
import dnfile
|
||||
from dncil.cil.body import CilMethodBody
|
||||
@@ -17,10 +17,10 @@ from dncil.cil.error import MethodBodyFormatError
|
||||
from dncil.clr.token import Token, StringToken, InvalidToken
|
||||
from dncil.cil.body.reader import CilMethodBodyReaderBase
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from capa.features.common import FeatureAccess
|
||||
from capa.features.extractors.dnfile.types import DnType, DnUnmanagedMethod
|
||||
|
||||
# key indexes to dotnet metadata tables
|
||||
DOTNET_META_TABLES_BY_INDEX = {table.value: table.name for table in dnfile.enums.MetadataTables}
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DnfileMethodBodyReader(CilMethodBodyReaderBase):
|
||||
@@ -41,90 +41,20 @@ class DnfileMethodBodyReader(CilMethodBodyReaderBase):
|
||||
return self.offset
|
||||
|
||||
|
||||
class DnClass(object):
|
||||
def __init__(self, token: int, namespace: str, classname: str):
|
||||
self.token: int = token
|
||||
self.namespace: str = namespace
|
||||
self.classname: str = classname
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.token,))
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.token == other.token
|
||||
|
||||
def __str__(self):
|
||||
return DnClass.format_name(self.namespace, self.classname)
|
||||
|
||||
def __repr__(self):
|
||||
return str(self)
|
||||
|
||||
@staticmethod
|
||||
def format_name(namespace: str, classname: str):
|
||||
name: str = classname
|
||||
if namespace:
|
||||
# like System.IO.File::OpenRead
|
||||
name = f"{namespace}.{name}"
|
||||
return name
|
||||
|
||||
|
||||
class DnMethod(DnClass):
|
||||
def __init__(self, token: int, namespace: str, classname: str, methodname: str):
|
||||
super(DnMethod, self).__init__(token, namespace, classname)
|
||||
self.methodname: str = methodname
|
||||
|
||||
def __str__(self):
|
||||
return DnMethod.format_name(self.namespace, self.classname, self.methodname)
|
||||
|
||||
@staticmethod
|
||||
def format_name(namespace: str, classname: str, methodname: str): # type: ignore
|
||||
# like File::OpenRead
|
||||
name: str = f"{classname}::{methodname}"
|
||||
if namespace:
|
||||
# like System.IO.File::OpenRead
|
||||
name = f"{namespace}.{name}"
|
||||
return name
|
||||
|
||||
|
||||
class DnUnmanagedMethod:
|
||||
def __init__(self, token: int, modulename: str, methodname: str):
|
||||
self.token: int = token
|
||||
self.modulename: str = modulename
|
||||
self.methodname: str = methodname
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.token,))
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.token == other.token
|
||||
|
||||
def __str__(self):
|
||||
return DnUnmanagedMethod.format_name(self.modulename, self.methodname)
|
||||
|
||||
def __repr__(self):
|
||||
return str(self)
|
||||
|
||||
@staticmethod
|
||||
def format_name(modulename, methodname):
|
||||
return f"{modulename}.{methodname}"
|
||||
|
||||
|
||||
def resolve_dotnet_token(pe: dnfile.dnPE, token: Token) -> Any:
|
||||
def resolve_dotnet_token(pe: dnfile.dnPE, token: Token) -> Union[dnfile.base.MDTableRow, InvalidToken, str]:
|
||||
"""map generic token to string or table row"""
|
||||
assert pe.net is not None
|
||||
assert pe.net.mdtables is not None
|
||||
|
||||
if isinstance(token, StringToken):
|
||||
user_string: Optional[str] = read_dotnet_user_string(pe, token)
|
||||
if user_string is None:
|
||||
return InvalidToken(token.value)
|
||||
return user_string
|
||||
|
||||
table_name: str = DOTNET_META_TABLES_BY_INDEX.get(token.table, "")
|
||||
if not table_name:
|
||||
# table_index is not valid
|
||||
return InvalidToken(token.value)
|
||||
|
||||
table: Any = getattr(pe.net.mdtables, table_name, None)
|
||||
table: Optional[dnfile.base.ClrMetaDataTable] = pe.net.mdtables.tables.get(token.table, None)
|
||||
if table is None:
|
||||
# table index is valid but table is not present
|
||||
# table index is not valid
|
||||
return InvalidToken(token.value)
|
||||
|
||||
try:
|
||||
@@ -139,16 +69,23 @@ def read_dotnet_method_body(pe: dnfile.dnPE, row: dnfile.mdtable.MethodDefRow) -
|
||||
try:
|
||||
return CilMethodBody(DnfileMethodBodyReader(pe, row))
|
||||
except MethodBodyFormatError as e:
|
||||
logger.warn("failed to parse managed method body @ 0x%08x (%s)" % (row.Rva, e))
|
||||
logger.debug("failed to parse managed method body @ 0x%08x (%s)", row.Rva, e)
|
||||
return None
|
||||
|
||||
|
||||
def read_dotnet_user_string(pe: dnfile.dnPE, token: StringToken) -> Optional[str]:
|
||||
"""read user string from #US stream"""
|
||||
assert pe.net is not None
|
||||
|
||||
if pe.net.user_strings is None:
|
||||
# stream may not exist (seen in obfuscated .NET)
|
||||
logger.debug("#US stream does not exist for stream index 0x%08x", token.rid)
|
||||
return None
|
||||
|
||||
try:
|
||||
user_string: Optional[dnfile.stream.UserString] = pe.net.user_strings.get_us(token.rid)
|
||||
except UnicodeDecodeError as e:
|
||||
logger.warn("failed to decode #US stream index 0x%08x (%s)" % (token.rid, e))
|
||||
logger.debug("failed to decode #US stream index 0x%08x (%s)", token.rid, e)
|
||||
return None
|
||||
|
||||
if user_string is None:
|
||||
@@ -157,7 +94,7 @@ def read_dotnet_user_string(pe: dnfile.dnPE, token: StringToken) -> Optional[str
|
||||
return user_string.value
|
||||
|
||||
|
||||
def get_dotnet_managed_imports(pe: dnfile.dnPE) -> Iterator[DnMethod]:
|
||||
def get_dotnet_managed_imports(pe: dnfile.dnPE) -> Iterator[DnType]:
|
||||
"""get managed imports from MemberRef table
|
||||
|
||||
see https://www.ntcore.com/files/dotnetformat.htm
|
||||
@@ -171,15 +108,76 @@ def get_dotnet_managed_imports(pe: dnfile.dnPE) -> Iterator[DnMethod]:
|
||||
TypeName (index into String heap)
|
||||
TypeNamespace (index into String heap)
|
||||
"""
|
||||
for (rid, row) in enumerate(iter_dotnet_table(pe, "MemberRef")):
|
||||
if not isinstance(row.Class.row, dnfile.mdtable.TypeRefRow):
|
||||
for rid, member_ref in iter_dotnet_table(pe, dnfile.mdtable.MemberRef.number):
|
||||
assert isinstance(member_ref, dnfile.mdtable.MemberRefRow)
|
||||
|
||||
if not isinstance(member_ref.Class.row, dnfile.mdtable.TypeRefRow):
|
||||
# only process class imports from TypeRef table
|
||||
continue
|
||||
|
||||
token: int = calculate_dotnet_token_value(pe.net.mdtables.MemberRef.number, rid + 1)
|
||||
yield DnMethod(token, row.Class.row.TypeNamespace, row.Class.row.TypeName, row.Name)
|
||||
token: int = calculate_dotnet_token_value(dnfile.mdtable.MemberRef.number, rid)
|
||||
access: Optional[str]
|
||||
|
||||
# assume .NET imports starting with get_/set_ are used to access a property
|
||||
if member_ref.Name.startswith("get_"):
|
||||
access = FeatureAccess.READ
|
||||
elif member_ref.Name.startswith("set_"):
|
||||
access = FeatureAccess.WRITE
|
||||
else:
|
||||
access = None
|
||||
|
||||
member_ref_name: str = member_ref.Name
|
||||
if member_ref_name.startswith(("get_", "set_")):
|
||||
# remove get_/set_ from MemberRef name
|
||||
member_ref_name = member_ref_name[4:]
|
||||
|
||||
yield DnType(
|
||||
token,
|
||||
member_ref.Class.row.TypeName,
|
||||
namespace=member_ref.Class.row.TypeNamespace,
|
||||
member=member_ref_name,
|
||||
access=access,
|
||||
)
|
||||
|
||||
|
||||
def get_dotnet_managed_methods(pe: dnfile.dnPE) -> Iterator[DnMethod]:
|
||||
def get_dotnet_methoddef_property_accessors(pe: dnfile.dnPE) -> Iterator[Tuple[int, str]]:
|
||||
"""get MethodDef methods used to access properties
|
||||
|
||||
see https://www.ntcore.com/files/dotnetformat.htm
|
||||
|
||||
24 - MethodSemantics Table
|
||||
Links Events and Properties to specific methods. For example one Event can be associated to more methods. A property uses this table to associate get/set methods.
|
||||
Semantics (a 2-byte bitmask of type MethodSemanticsAttributes)
|
||||
Method (index into the MethodDef table)
|
||||
Association (index into the Event or Property table; more precisely, a HasSemantics coded index)
|
||||
"""
|
||||
for rid, method_semantics in iter_dotnet_table(pe, dnfile.mdtable.MethodSemantics.number):
|
||||
assert isinstance(method_semantics, dnfile.mdtable.MethodSemanticsRow)
|
||||
|
||||
if method_semantics.Association.row is None:
|
||||
logger.debug("MethodSemantics[0x%X] Association row is None", rid)
|
||||
continue
|
||||
|
||||
if isinstance(method_semantics.Association.row, dnfile.mdtable.EventRow):
|
||||
# ignore events
|
||||
logger.debug("MethodSemantics[0x%X] ignoring Event", rid)
|
||||
continue
|
||||
|
||||
if method_semantics.Method.table is None:
|
||||
logger.debug("MethodSemantics[0x%X] Method table is None", rid)
|
||||
continue
|
||||
|
||||
token: int = calculate_dotnet_token_value(
|
||||
method_semantics.Method.table.number, method_semantics.Method.row_index
|
||||
)
|
||||
|
||||
if method_semantics.Semantics.msSetter:
|
||||
yield token, FeatureAccess.WRITE
|
||||
elif method_semantics.Semantics.msGetter:
|
||||
yield token, FeatureAccess.READ
|
||||
|
||||
|
||||
def get_dotnet_managed_methods(pe: dnfile.dnPE) -> Iterator[DnType]:
|
||||
"""get managed method names from TypeDef table
|
||||
|
||||
see https://www.ntcore.com/files/dotnetformat.htm
|
||||
@@ -188,29 +186,74 @@ def get_dotnet_managed_methods(pe: dnfile.dnPE) -> Iterator[DnMethod]:
|
||||
Each row represents a class in the current assembly.
|
||||
TypeName (index into String heap)
|
||||
TypeNamespace (index into String heap)
|
||||
MethodList (index into MethodDef table; it marks the first of a continguous run of Methods owned by this Type)
|
||||
MethodList (index into MethodDef table; it marks the first of a contiguous run of Methods owned by this Type)
|
||||
"""
|
||||
for row in iter_dotnet_table(pe, "TypeDef"):
|
||||
for index in row.MethodList:
|
||||
token = calculate_dotnet_token_value(index.table.number, index.row_index)
|
||||
yield DnMethod(token, row.TypeNamespace, row.TypeName, index.row.Name)
|
||||
accessor_map: Dict[int, str] = {}
|
||||
for methoddef, methoddef_access in get_dotnet_methoddef_property_accessors(pe):
|
||||
accessor_map[methoddef] = methoddef_access
|
||||
|
||||
for rid, typedef in iter_dotnet_table(pe, dnfile.mdtable.TypeDef.number):
|
||||
assert isinstance(typedef, dnfile.mdtable.TypeDefRow)
|
||||
|
||||
for idx, method in enumerate(typedef.MethodList):
|
||||
if method.table is None:
|
||||
logger.debug("TypeDef[0x%X] MethodList[0x%X] table is None", rid, idx)
|
||||
continue
|
||||
if method.row is None:
|
||||
logger.debug("TypeDef[0x%X] MethodList[0x%X] row is None", rid, idx)
|
||||
continue
|
||||
|
||||
token: int = calculate_dotnet_token_value(method.table.number, method.row_index)
|
||||
access: Optional[str] = accessor_map.get(token, None)
|
||||
|
||||
method_name: str = method.row.Name
|
||||
if method_name.startswith(("get_", "set_")):
|
||||
# remove get_/set_
|
||||
method_name = method_name[4:]
|
||||
|
||||
yield DnType(token, typedef.TypeName, namespace=typedef.TypeNamespace, member=method_name, access=access)
|
||||
|
||||
|
||||
def get_dotnet_fields(pe: dnfile.dnPE) -> Iterator[DnType]:
|
||||
"""get fields from TypeDef table
|
||||
|
||||
see https://www.ntcore.com/files/dotnetformat.htm
|
||||
|
||||
02 - TypeDef Table
|
||||
Each row represents a class in the current assembly.
|
||||
TypeName (index into String heap)
|
||||
TypeNamespace (index into String heap)
|
||||
FieldList (index into Field table; it marks the first of a contiguous run of Fields owned by this Type)
|
||||
"""
|
||||
for rid, typedef in iter_dotnet_table(pe, dnfile.mdtable.TypeDef.number):
|
||||
assert isinstance(typedef, dnfile.mdtable.TypeDefRow)
|
||||
|
||||
for idx, field in enumerate(typedef.FieldList):
|
||||
if field.table is None:
|
||||
logger.debug("TypeDef[0x%X] FieldList[0x%X] table is None", rid, idx)
|
||||
continue
|
||||
if field.row is None:
|
||||
logger.debug("TypeDef[0x%X] FieldList[0x%X] row is None", rid, idx)
|
||||
continue
|
||||
token: int = calculate_dotnet_token_value(field.table.number, field.row_index)
|
||||
yield DnType(token, typedef.TypeName, namespace=typedef.TypeNamespace, member=field.row.Name)
|
||||
|
||||
|
||||
def get_dotnet_managed_method_bodies(pe: dnfile.dnPE) -> Iterator[Tuple[int, CilMethodBody]]:
|
||||
"""get managed methods from MethodDef table"""
|
||||
if not hasattr(pe.net.mdtables, "MethodDef"):
|
||||
return
|
||||
for rid, method_def in iter_dotnet_table(pe, dnfile.mdtable.MethodDef.number):
|
||||
assert isinstance(method_def, dnfile.mdtable.MethodDefRow)
|
||||
|
||||
for (rid, row) in enumerate(pe.net.mdtables.MethodDef):
|
||||
if not row.ImplFlags.miIL or any((row.Flags.mdAbstract, row.Flags.mdPinvokeImpl)):
|
||||
if not method_def.ImplFlags.miIL or any((method_def.Flags.mdAbstract, method_def.Flags.mdPinvokeImpl)):
|
||||
# skip methods that do not have a method body
|
||||
continue
|
||||
|
||||
body: Optional[CilMethodBody] = read_dotnet_method_body(pe, row)
|
||||
body: Optional[CilMethodBody] = read_dotnet_method_body(pe, method_def)
|
||||
if body is None:
|
||||
logger.debug("MethodDef[0x%X] method body is None", rid)
|
||||
continue
|
||||
|
||||
token: int = calculate_dotnet_token_value(dnfile.enums.MetadataTables.MethodDef.value, rid + 1)
|
||||
token: int = calculate_dotnet_token_value(dnfile.mdtable.MethodDef.number, rid)
|
||||
yield token, body
|
||||
|
||||
|
||||
@@ -225,37 +268,68 @@ def get_dotnet_unmanaged_imports(pe: dnfile.dnPE) -> Iterator[DnUnmanagedMethod]
|
||||
ImportName (index into the String heap)
|
||||
ImportScope (index into the ModuleRef table)
|
||||
"""
|
||||
for row in iter_dotnet_table(pe, "ImplMap"):
|
||||
modulename: str = row.ImportScope.row.Name
|
||||
methodname: str = row.ImportName
|
||||
for rid, impl_map in iter_dotnet_table(pe, dnfile.mdtable.ImplMap.number):
|
||||
assert isinstance(impl_map, dnfile.mdtable.ImplMapRow)
|
||||
|
||||
module: str
|
||||
if impl_map.ImportScope.row is None:
|
||||
logger.debug("ImplMap[0x%X] ImportScope row is None", rid)
|
||||
module = ""
|
||||
else:
|
||||
module = impl_map.ImportScope.row.Name
|
||||
method: str = impl_map.ImportName
|
||||
|
||||
member_forward_table: int
|
||||
if impl_map.MemberForwarded.table is None:
|
||||
logger.debug("ImplMap[0x%X] MemberForwarded table is None", rid)
|
||||
continue
|
||||
else:
|
||||
member_forward_table = impl_map.MemberForwarded.table.number
|
||||
member_forward_row: int = impl_map.MemberForwarded.row_index
|
||||
|
||||
# ECMA says "Each row of the ImplMap table associates a row in the MethodDef table (MemberForwarded) with the
|
||||
# name of a routine (ImportName) in some unmanaged DLL (ImportScope)"; so we calculate and map the MemberForwarded
|
||||
# MethodDef table token to help us later record native import method calls made from CIL
|
||||
token: int = calculate_dotnet_token_value(row.MemberForwarded.table.number, row.MemberForwarded.row_index)
|
||||
token: int = calculate_dotnet_token_value(member_forward_table, member_forward_row)
|
||||
|
||||
# like Kernel32.dll
|
||||
if modulename and "." in modulename:
|
||||
modulename = modulename.split(".")[0]
|
||||
if module and "." in module:
|
||||
module = module.split(".")[0]
|
||||
|
||||
# like kernel32.CreateFileA
|
||||
yield DnUnmanagedMethod(token, modulename, methodname)
|
||||
yield DnUnmanagedMethod(token, module, method)
|
||||
|
||||
|
||||
def get_dotnet_types(pe: dnfile.dnPE) -> Iterator[DnType]:
|
||||
"""get .NET types from TypeDef and TypeRef tables"""
|
||||
for rid, typedef in iter_dotnet_table(pe, dnfile.mdtable.TypeDef.number):
|
||||
assert isinstance(typedef, dnfile.mdtable.TypeDefRow)
|
||||
|
||||
typedef_token: int = calculate_dotnet_token_value(dnfile.mdtable.TypeDef.number, rid)
|
||||
yield DnType(typedef_token, typedef.TypeName, namespace=typedef.TypeNamespace)
|
||||
|
||||
for rid, typeref in iter_dotnet_table(pe, dnfile.mdtable.TypeRef.number):
|
||||
assert isinstance(typeref, dnfile.mdtable.TypeRefRow)
|
||||
|
||||
typeref_token: int = calculate_dotnet_token_value(dnfile.mdtable.TypeRef.number, rid)
|
||||
yield DnType(typeref_token, typeref.TypeName, namespace=typeref.TypeNamespace)
|
||||
|
||||
|
||||
def calculate_dotnet_token_value(table: int, rid: int) -> int:
|
||||
return ((table & 0xFF) << Token.TABLE_SHIFT) | (rid & Token.RID_MASK)
|
||||
|
||||
|
||||
def is_dotnet_table_valid(pe: dnfile.dnPE, table_name: str) -> bool:
|
||||
return bool(getattr(pe.net.mdtables, table_name, None))
|
||||
|
||||
|
||||
def is_dotnet_mixed_mode(pe: dnfile.dnPE) -> bool:
|
||||
assert pe.net is not None
|
||||
assert pe.net.Flags is not None
|
||||
|
||||
return not bool(pe.net.Flags.CLR_ILONLY)
|
||||
|
||||
|
||||
def iter_dotnet_table(pe: dnfile.dnPE, name: str) -> Iterator[Any]:
|
||||
if not is_dotnet_table_valid(pe, name):
|
||||
return
|
||||
for row in getattr(pe.net.mdtables, name):
|
||||
yield row
|
||||
def iter_dotnet_table(pe: dnfile.dnPE, table_index: int) -> Iterator[Tuple[int, dnfile.base.MDTableRow]]:
|
||||
assert pe.net is not None
|
||||
assert pe.net.mdtables is not None
|
||||
|
||||
for rid, row in enumerate(pe.net.mdtables.tables.get(table_index, [])):
|
||||
# .NET tables are 1-indexed
|
||||
yield rid + 1, row
|
||||
|
||||
@@ -8,175 +8,220 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, Tuple, Union, Iterator, Optional
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, Dict, Tuple, Union, Iterator, Optional
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from capa.features.extractors.dnfile.extractor import DnFileFeatureExtractorCache
|
||||
|
||||
import dnfile
|
||||
from dncil.cil.body import CilMethodBody
|
||||
from dncil.clr.token import Token, StringToken, InvalidToken
|
||||
from dncil.cil.opcode import OpCodes
|
||||
from dncil.cil.instruction import Instruction
|
||||
|
||||
import capa.features.extractors.helpers
|
||||
from capa.features.insn import API, Number
|
||||
from capa.features.common import Class, String, Feature, Namespace, Characteristic
|
||||
from capa.features.insn import API, Number, Property
|
||||
from capa.features.common import Class, String, Feature, Namespace, FeatureAccess, Characteristic
|
||||
from capa.features.address import Address
|
||||
from capa.features.extractors.dnfile.types import DnType, DnUnmanagedMethod
|
||||
from capa.features.extractors.base_extractor import BBHandle, InsnHandle, FunctionHandle
|
||||
from capa.features.extractors.dnfile.helpers import (
|
||||
DnClass,
|
||||
DnMethod,
|
||||
DnUnmanagedMethod,
|
||||
resolve_dotnet_token,
|
||||
read_dotnet_user_string,
|
||||
get_dotnet_managed_imports,
|
||||
get_dotnet_managed_methods,
|
||||
get_dotnet_unmanaged_imports,
|
||||
calculate_dotnet_token_value,
|
||||
)
|
||||
|
||||
|
||||
def get_managed_imports(ctx: Dict) -> Dict:
|
||||
if "managed_imports_cache" not in ctx:
|
||||
ctx["managed_imports_cache"] = {}
|
||||
for method in get_dotnet_managed_imports(ctx["pe"]):
|
||||
ctx["managed_imports_cache"][method.token] = method
|
||||
return ctx["managed_imports_cache"]
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_unmanaged_imports(ctx: Dict) -> Dict:
|
||||
if "unmanaged_imports_cache" not in ctx:
|
||||
ctx["unmanaged_imports_cache"] = {}
|
||||
for imp in get_dotnet_unmanaged_imports(ctx["pe"]):
|
||||
ctx["unmanaged_imports_cache"][imp.token] = imp
|
||||
return ctx["unmanaged_imports_cache"]
|
||||
def get_callee(
|
||||
pe: dnfile.dnPE, cache: DnFileFeatureExtractorCache, token: Token
|
||||
) -> Optional[Union[DnType, DnUnmanagedMethod]]:
|
||||
"""map .NET token to un/managed (generic) method"""
|
||||
token_: int
|
||||
if token.table == dnfile.mdtable.MethodSpec.number:
|
||||
# map MethodSpec to MethodDef or MemberRef
|
||||
row: Union[dnfile.base.MDTableRow, InvalidToken, str] = resolve_dotnet_token(pe, token)
|
||||
assert isinstance(row, dnfile.mdtable.MethodSpecRow)
|
||||
|
||||
if row.Method.table is None:
|
||||
logger.debug("MethodSpec[0x%X] Method table is None", token.rid)
|
||||
return None
|
||||
|
||||
def get_methods(ctx: Dict) -> Dict:
|
||||
if "methods_cache" not in ctx:
|
||||
ctx["methods_cache"] = {}
|
||||
for method in get_dotnet_managed_methods(ctx["pe"]):
|
||||
ctx["methods_cache"][method.token] = method
|
||||
return ctx["methods_cache"]
|
||||
token_ = calculate_dotnet_token_value(row.Method.table.number, row.Method.row_index)
|
||||
else:
|
||||
token_ = token.value
|
||||
|
||||
|
||||
def get_callee(ctx: Dict, token: int) -> Union[DnMethod, DnUnmanagedMethod, None]:
|
||||
"""map dotnet token to un/managed method"""
|
||||
callee: Union[DnMethod, DnUnmanagedMethod, None] = get_managed_imports(ctx).get(token, None)
|
||||
if not callee:
|
||||
callee: Optional[Union[DnType, DnUnmanagedMethod]] = cache.get_import(token_)
|
||||
if callee is None:
|
||||
# we must check unmanaged imports before managed methods because we map forwarded managed methods
|
||||
# to their unmanaged imports; we prefer a forwarded managed method be mapped to its unmanaged import for analysis
|
||||
callee = get_unmanaged_imports(ctx).get(token, None)
|
||||
if not callee:
|
||||
callee = get_methods(ctx).get(token, None)
|
||||
callee = cache.get_native_import(token_)
|
||||
if callee is None:
|
||||
callee = cache.get_method(token_)
|
||||
return callee
|
||||
|
||||
|
||||
def extract_insn_api_features(fh: FunctionHandle, bh, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""parse instruction API features"""
|
||||
insn: Instruction = ih.inner
|
||||
|
||||
if insn.opcode not in (OpCodes.Call, OpCodes.Callvirt, OpCodes.Jmp, OpCodes.Calli):
|
||||
if ih.inner.opcode not in (
|
||||
OpCodes.Call,
|
||||
OpCodes.Callvirt,
|
||||
OpCodes.Jmp,
|
||||
OpCodes.Newobj,
|
||||
):
|
||||
return
|
||||
|
||||
callee: Union[DnMethod, DnUnmanagedMethod, None] = get_callee(fh.ctx, insn.operand.value)
|
||||
if callee is None:
|
||||
return
|
||||
|
||||
if isinstance(callee, DnUnmanagedMethod):
|
||||
callee: Optional[Union[DnType, DnUnmanagedMethod]] = get_callee(fh.ctx["pe"], fh.ctx["cache"], ih.inner.operand)
|
||||
if isinstance(callee, DnType):
|
||||
# ignore methods used to access properties
|
||||
if callee.access is None:
|
||||
# like System.IO.File::Delete
|
||||
yield API(str(callee)), ih.address
|
||||
elif isinstance(callee, DnUnmanagedMethod):
|
||||
# like kernel32.CreateFileA
|
||||
for name in capa.features.extractors.helpers.generate_symbols(callee.modulename, callee.methodname):
|
||||
for name in capa.features.extractors.helpers.generate_symbols(callee.module, callee.method):
|
||||
yield API(name), ih.address
|
||||
else:
|
||||
# like System.IO.File::Delete
|
||||
yield API(str(callee)), ih.address
|
||||
|
||||
|
||||
def extract_insn_class_features(fh: FunctionHandle, bh, ih: InsnHandle) -> Iterator[Tuple[Class, Address]]:
|
||||
"""parse instruction class features"""
|
||||
if ih.inner.opcode not in (OpCodes.Call, OpCodes.Callvirt, OpCodes.Jmp, OpCodes.Calli):
|
||||
return
|
||||
def extract_insn_property_features(fh: FunctionHandle, bh, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""parse instruction property features"""
|
||||
name: Optional[str] = None
|
||||
access: Optional[str] = None
|
||||
|
||||
row: Any = resolve_dotnet_token(fh.ctx["pe"], Token(ih.inner.operand.value))
|
||||
if ih.inner.opcode in (OpCodes.Call, OpCodes.Callvirt, OpCodes.Jmp):
|
||||
# property access via MethodDef or MemberRef
|
||||
callee: Optional[Union[DnType, DnUnmanagedMethod]] = get_callee(fh.ctx["pe"], fh.ctx["cache"], ih.inner.operand)
|
||||
if isinstance(callee, DnType):
|
||||
if callee.access is not None:
|
||||
name = str(callee)
|
||||
access = callee.access
|
||||
|
||||
if not isinstance(row, dnfile.mdtable.MemberRefRow):
|
||||
return
|
||||
if not isinstance(row.Class.row, (dnfile.mdtable.TypeRefRow, dnfile.mdtable.TypeDefRow)):
|
||||
return
|
||||
elif ih.inner.opcode in (OpCodes.Ldfld, OpCodes.Ldflda, OpCodes.Ldsfld, OpCodes.Ldsflda):
|
||||
# property read via Field
|
||||
read_field: Optional[Union[DnType, DnUnmanagedMethod]] = fh.ctx["cache"].get_field(ih.inner.operand.value)
|
||||
if read_field is not None:
|
||||
name = str(read_field)
|
||||
access = FeatureAccess.READ
|
||||
|
||||
yield Class(DnClass.format_name(row.Class.row.TypeNamespace, row.Class.row.TypeName)), ih.address
|
||||
elif ih.inner.opcode in (OpCodes.Stfld, OpCodes.Stsfld):
|
||||
# property write via Field
|
||||
write_field: Optional[Union[DnType, DnUnmanagedMethod]] = fh.ctx["cache"].get_field(ih.inner.operand.value)
|
||||
if write_field is not None:
|
||||
name = str(write_field)
|
||||
access = FeatureAccess.WRITE
|
||||
|
||||
if name is not None:
|
||||
if access is not None:
|
||||
yield Property(name, access=access), ih.address
|
||||
yield Property(name), ih.address
|
||||
|
||||
|
||||
def extract_insn_namespace_features(fh: FunctionHandle, bh, ih: InsnHandle) -> Iterator[Tuple[Namespace, Address]]:
|
||||
"""parse instruction namespace features"""
|
||||
if ih.inner.opcode not in (OpCodes.Call, OpCodes.Callvirt, OpCodes.Jmp, OpCodes.Calli):
|
||||
return
|
||||
def extract_insn_namespace_class_features(
|
||||
fh: FunctionHandle, bh, ih: InsnHandle
|
||||
) -> Iterator[Tuple[Union[Namespace, Class], Address]]:
|
||||
"""parse instruction namespace and class features"""
|
||||
type_: Optional[Union[DnType, DnUnmanagedMethod]] = None
|
||||
|
||||
row: Any = resolve_dotnet_token(fh.ctx["pe"], Token(ih.inner.operand.value))
|
||||
if ih.inner.opcode in (
|
||||
OpCodes.Call,
|
||||
OpCodes.Callvirt,
|
||||
OpCodes.Jmp,
|
||||
OpCodes.Ldvirtftn,
|
||||
OpCodes.Ldftn,
|
||||
OpCodes.Newobj,
|
||||
):
|
||||
# method call - includes managed methods (MethodDef, TypeRef) and properties (MethodSemantics, TypeRef)
|
||||
type_ = get_callee(fh.ctx["pe"], fh.ctx["cache"], ih.inner.operand)
|
||||
|
||||
if not isinstance(row, dnfile.mdtable.MemberRefRow):
|
||||
return
|
||||
if not isinstance(row.Class.row, (dnfile.mdtable.TypeRefRow, dnfile.mdtable.TypeDefRow)):
|
||||
return
|
||||
if not row.Class.row.TypeNamespace:
|
||||
return
|
||||
elif ih.inner.opcode in (
|
||||
OpCodes.Ldfld,
|
||||
OpCodes.Ldflda,
|
||||
OpCodes.Ldsfld,
|
||||
OpCodes.Ldsflda,
|
||||
OpCodes.Stfld,
|
||||
OpCodes.Stsfld,
|
||||
):
|
||||
# field access
|
||||
type_ = fh.ctx["cache"].get_field(ih.inner.operand.value)
|
||||
|
||||
yield Namespace(row.Class.row.TypeNamespace), ih.address
|
||||
# ECMA 335 VI.C.4.10
|
||||
elif ih.inner.opcode in (
|
||||
OpCodes.Initobj,
|
||||
OpCodes.Box,
|
||||
OpCodes.Castclass,
|
||||
OpCodes.Cpobj,
|
||||
OpCodes.Isinst,
|
||||
OpCodes.Ldelem,
|
||||
OpCodes.Ldelema,
|
||||
OpCodes.Ldobj,
|
||||
OpCodes.Mkrefany,
|
||||
OpCodes.Newarr,
|
||||
OpCodes.Refanyval,
|
||||
OpCodes.Sizeof,
|
||||
OpCodes.Stobj,
|
||||
OpCodes.Unbox,
|
||||
OpCodes.Constrained,
|
||||
OpCodes.Stelem,
|
||||
OpCodes.Unbox_Any,
|
||||
):
|
||||
# type access
|
||||
type_ = fh.ctx["cache"].get_type(ih.inner.operand.value)
|
||||
|
||||
if isinstance(type_, DnType):
|
||||
yield Class(DnType.format_name(type_.class_, namespace=type_.namespace)), ih.address
|
||||
if type_.namespace:
|
||||
yield Namespace(type_.namespace), ih.address
|
||||
|
||||
|
||||
def extract_insn_number_features(fh, bh, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""parse instruction number features"""
|
||||
insn: Instruction = ih.inner
|
||||
|
||||
if insn.is_ldc():
|
||||
yield Number(insn.get_ldc()), ih.address
|
||||
if ih.inner.is_ldc():
|
||||
yield Number(ih.inner.get_ldc()), ih.address
|
||||
|
||||
|
||||
def extract_insn_string_features(fh: FunctionHandle, bh, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""parse instruction string features"""
|
||||
f: CilMethodBody = fh.inner
|
||||
insn: Instruction = ih.inner
|
||||
|
||||
if not insn.is_ldstr():
|
||||
if not ih.inner.is_ldstr():
|
||||
return
|
||||
|
||||
if not isinstance(insn.operand, StringToken):
|
||||
if not isinstance(ih.inner.operand, StringToken):
|
||||
return
|
||||
|
||||
user_string: Optional[str] = read_dotnet_user_string(fh.ctx["pe"], insn.operand)
|
||||
user_string: Optional[str] = read_dotnet_user_string(fh.ctx["pe"], ih.inner.operand)
|
||||
if user_string is None:
|
||||
return
|
||||
|
||||
yield String(user_string), ih.address
|
||||
if len(user_string) >= 4:
|
||||
yield String(user_string), ih.address
|
||||
|
||||
|
||||
def extract_unmanaged_call_characteristic_features(
|
||||
fh: FunctionHandle, bb: BBHandle, ih: InsnHandle
|
||||
) -> Iterator[Tuple[Characteristic, Address]]:
|
||||
insn: Instruction = ih.inner
|
||||
if insn.opcode not in (OpCodes.Call, OpCodes.Callvirt, OpCodes.Jmp, OpCodes.Calli):
|
||||
if ih.inner.opcode not in (OpCodes.Call, OpCodes.Callvirt, OpCodes.Jmp):
|
||||
return
|
||||
|
||||
token: Any = resolve_dotnet_token(fh.ctx["pe"], insn.operand)
|
||||
if isinstance(token, InvalidToken):
|
||||
return
|
||||
if not isinstance(token, dnfile.mdtable.MethodDefRow):
|
||||
row: Union[str, InvalidToken, dnfile.base.MDTableRow] = resolve_dotnet_token(fh.ctx["pe"], ih.inner.operand)
|
||||
if not isinstance(row, dnfile.mdtable.MethodDefRow):
|
||||
return
|
||||
|
||||
if any((token.Flags.mdPinvokeImpl, token.ImplFlags.miUnmanaged, token.ImplFlags.miNative)):
|
||||
if any((row.Flags.mdPinvokeImpl, row.ImplFlags.miUnmanaged, row.ImplFlags.miNative)):
|
||||
yield Characteristic("unmanaged call"), ih.address
|
||||
|
||||
|
||||
def extract_features(fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""extract instruction features"""
|
||||
for inst_handler in INSTRUCTION_HANDLERS:
|
||||
for (feature, addr) in inst_handler(fh, bbh, ih):
|
||||
for feature, addr in inst_handler(fh, bbh, ih):
|
||||
assert isinstance(addr, Address)
|
||||
yield feature, addr
|
||||
|
||||
|
||||
INSTRUCTION_HANDLERS = (
|
||||
extract_insn_api_features,
|
||||
extract_insn_property_features,
|
||||
extract_insn_number_features,
|
||||
extract_insn_string_features,
|
||||
extract_insn_namespace_features,
|
||||
extract_insn_class_features,
|
||||
extract_insn_namespace_class_features,
|
||||
extract_unmanaged_call_characteristic_features,
|
||||
)
|
||||
|
||||
75
capa/features/extractors/dnfile/types.py
Normal file
75
capa/features/extractors/dnfile/types.py
Normal file
@@ -0,0 +1,75 @@
|
||||
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
from enum import Enum
|
||||
from typing import Union, Optional
|
||||
|
||||
|
||||
class DnType(object):
|
||||
def __init__(self, token: int, class_: str, namespace: str = "", member: str = "", access: Optional[str] = None):
|
||||
self.token: int = token
|
||||
self.access: Optional[str] = access
|
||||
self.namespace: str = namespace
|
||||
self.class_: str = class_
|
||||
|
||||
if member == ".ctor":
|
||||
member = "ctor"
|
||||
if member == ".cctor":
|
||||
member = "cctor"
|
||||
|
||||
self.member: str = member
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.token, self.access, self.namespace, self.class_, self.member))
|
||||
|
||||
def __eq__(self, other):
|
||||
return (
|
||||
self.token == other.token
|
||||
and self.access == other.access
|
||||
and self.namespace == other.namespace
|
||||
and self.class_ == other.class_
|
||||
and self.member == other.member
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return DnType.format_name(self.class_, namespace=self.namespace, member=self.member)
|
||||
|
||||
def __repr__(self):
|
||||
return str(self)
|
||||
|
||||
@staticmethod
|
||||
def format_name(class_: str, namespace: str = "", member: str = ""):
|
||||
# like File::OpenRead
|
||||
name: str = f"{class_}::{member}" if member else class_
|
||||
if namespace:
|
||||
# like System.IO.File::OpenRead
|
||||
name = f"{namespace}.{name}"
|
||||
return name
|
||||
|
||||
|
||||
class DnUnmanagedMethod:
|
||||
def __init__(self, token: int, module: str, method: str):
|
||||
self.token: int = token
|
||||
self.module: str = module
|
||||
self.method: str = method
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.token, self.module, self.method))
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.token == other.token and self.module == other.module and self.method == other.method
|
||||
|
||||
def __str__(self):
|
||||
return DnUnmanagedMethod.format_name(self.module, self.method)
|
||||
|
||||
def __repr__(self):
|
||||
return str(self)
|
||||
|
||||
@staticmethod
|
||||
def format_name(module, method):
|
||||
return f"{module}.{method}"
|
||||
@@ -4,7 +4,18 @@ from typing import Tuple, Iterator
|
||||
import dnfile
|
||||
import pefile
|
||||
|
||||
from capa.features.common import OS, OS_ANY, ARCH_ANY, ARCH_I386, ARCH_AMD64, FORMAT_DOTNET, Arch, Format, Feature
|
||||
from capa.features.common import (
|
||||
OS,
|
||||
OS_ANY,
|
||||
ARCH_ANY,
|
||||
ARCH_I386,
|
||||
FORMAT_PE,
|
||||
ARCH_AMD64,
|
||||
FORMAT_DOTNET,
|
||||
Arch,
|
||||
Format,
|
||||
Feature,
|
||||
)
|
||||
from capa.features.address import NO_ADDRESS, Address, AbsoluteVirtualAddress
|
||||
from capa.features.extractors.base_extractor import FeatureExtractor
|
||||
|
||||
@@ -12,6 +23,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def extract_file_format(**kwargs) -> Iterator[Tuple[Feature, Address]]:
|
||||
yield Format(FORMAT_PE), NO_ADDRESS
|
||||
yield Format(FORMAT_DOTNET), NO_ADDRESS
|
||||
|
||||
|
||||
@@ -19,9 +31,12 @@ def extract_file_os(**kwargs) -> Iterator[Tuple[Feature, Address]]:
|
||||
yield OS(OS_ANY), NO_ADDRESS
|
||||
|
||||
|
||||
def extract_file_arch(pe, **kwargs) -> Iterator[Tuple[Feature, Address]]:
|
||||
def extract_file_arch(pe: dnfile.dnPE, **kwargs) -> Iterator[Tuple[Feature, Address]]:
|
||||
# to distinguish in more detail, see https://stackoverflow.com/a/23614024/10548020
|
||||
# .NET 4.5 added option: any CPU, 32-bit preferred
|
||||
assert pe.net is not None
|
||||
assert pe.net.Flags is not None
|
||||
|
||||
if pe.net.Flags.CLR_32BITREQUIRED and pe.PE_TYPE == pefile.OPTIONAL_HEADER_MAGIC_PE:
|
||||
yield Arch(ARCH_I386), NO_ADDRESS
|
||||
elif not pe.net.Flags.CLR_32BITREQUIRED and pe.PE_TYPE == pefile.OPTIONAL_HEADER_MAGIC_PE_PLUS:
|
||||
@@ -60,7 +75,7 @@ GLOBAL_HANDLERS = (
|
||||
|
||||
class DnfileFeatureExtractor(FeatureExtractor):
|
||||
def __init__(self, path: str):
|
||||
super(DnfileFeatureExtractor, self).__init__()
|
||||
super().__init__()
|
||||
self.path: str = path
|
||||
self.pe: dnfile.dnPE = dnfile.dnPE(path)
|
||||
|
||||
@@ -71,6 +86,9 @@ class DnfileFeatureExtractor(FeatureExtractor):
|
||||
# self.pe.net.Flags.CLT_NATIVE_ENTRYPOINT
|
||||
# True: native EP: Token
|
||||
# False: managed EP: RVA
|
||||
assert self.pe.net is not None
|
||||
assert self.pe.net.struct is not None
|
||||
|
||||
return self.pe.net.struct.EntryPointTokenOrRva
|
||||
|
||||
def extract_global_features(self):
|
||||
@@ -83,13 +101,29 @@ class DnfileFeatureExtractor(FeatureExtractor):
|
||||
return bool(self.pe.net)
|
||||
|
||||
def is_mixed_mode(self) -> bool:
|
||||
assert self.pe is not None
|
||||
assert self.pe.net is not None
|
||||
assert self.pe.net.Flags is not None
|
||||
|
||||
return not bool(self.pe.net.Flags.CLR_ILONLY)
|
||||
|
||||
def get_runtime_version(self) -> Tuple[int, int]:
|
||||
assert self.pe is not None
|
||||
assert self.pe.net is not None
|
||||
assert self.pe.net.struct is not None
|
||||
|
||||
return self.pe.net.struct.MajorRuntimeVersion, self.pe.net.struct.MinorRuntimeVersion
|
||||
|
||||
def get_meta_version_string(self) -> str:
|
||||
return self.pe.net.metadata.struct.Version.rstrip(b"\x00").decode("utf-8")
|
||||
assert self.pe.net is not None
|
||||
assert self.pe.net.metadata is not None
|
||||
assert self.pe.net.metadata.struct is not None
|
||||
assert self.pe.net.metadata.struct.Version is not None
|
||||
|
||||
vbuf = self.pe.net.metadata.struct.Version
|
||||
assert isinstance(vbuf, bytes)
|
||||
|
||||
return vbuf.rstrip(b"\x00").decode("utf-8")
|
||||
|
||||
def get_functions(self):
|
||||
raise NotImplementedError("DnfileFeatureExtractor can only be used to extract file features")
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import logging
|
||||
import itertools
|
||||
from typing import Tuple, Iterator
|
||||
from typing import Tuple, Iterator, cast
|
||||
|
||||
import dnfile
|
||||
import pefile
|
||||
from dncil.clr.token import Token
|
||||
|
||||
import capa.features.extractors.helpers
|
||||
from capa.features.file import Import, FunctionName
|
||||
@@ -13,6 +11,7 @@ from capa.features.common import (
|
||||
OS_ANY,
|
||||
ARCH_ANY,
|
||||
ARCH_I386,
|
||||
FORMAT_PE,
|
||||
ARCH_AMD64,
|
||||
FORMAT_DOTNET,
|
||||
Arch,
|
||||
@@ -23,11 +22,10 @@ from capa.features.common import (
|
||||
Namespace,
|
||||
Characteristic,
|
||||
)
|
||||
from capa.features.address import NO_ADDRESS, Address, DNTokenAddress, DNTokenOffsetAddress, AbsoluteVirtualAddress
|
||||
from capa.features.address import NO_ADDRESS, Address, DNTokenAddress
|
||||
from capa.features.extractors.base_extractor import FeatureExtractor
|
||||
from capa.features.extractors.dnfile.helpers import (
|
||||
DnClass,
|
||||
DnMethod,
|
||||
DnType,
|
||||
iter_dotnet_table,
|
||||
is_dotnet_mixed_mode,
|
||||
get_dotnet_managed_imports,
|
||||
@@ -40,23 +38,24 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def extract_file_format(**kwargs) -> Iterator[Tuple[Format, Address]]:
|
||||
yield Format(FORMAT_PE), NO_ADDRESS
|
||||
yield Format(FORMAT_DOTNET), NO_ADDRESS
|
||||
|
||||
|
||||
def extract_file_import_names(pe: dnfile.dnPE, **kwargs) -> Iterator[Tuple[Import, Address]]:
|
||||
for method in get_dotnet_managed_imports(pe):
|
||||
# like System.IO.File::OpenRead
|
||||
yield Import(str(method)), DNTokenAddress(Token(method.token))
|
||||
yield Import(str(method)), DNTokenAddress(method.token)
|
||||
|
||||
for imp in get_dotnet_unmanaged_imports(pe):
|
||||
# like kernel32.CreateFileA
|
||||
for name in capa.features.extractors.helpers.generate_symbols(imp.modulename, imp.methodname):
|
||||
yield Import(name), DNTokenAddress(Token(imp.token))
|
||||
for name in capa.features.extractors.helpers.generate_symbols(imp.module, imp.method):
|
||||
yield Import(name), DNTokenAddress(imp.token)
|
||||
|
||||
|
||||
def extract_file_function_names(pe: dnfile.dnPE, **kwargs) -> Iterator[Tuple[FunctionName, Address]]:
|
||||
for method in get_dotnet_managed_methods(pe):
|
||||
yield FunctionName(str(method)), DNTokenAddress(Token(method.token))
|
||||
yield FunctionName(str(method)), DNTokenAddress(method.token)
|
||||
|
||||
|
||||
def extract_file_namespace_features(pe: dnfile.dnPE, **kwargs) -> Iterator[Tuple[Namespace, Address]]:
|
||||
@@ -65,11 +64,15 @@ def extract_file_namespace_features(pe: dnfile.dnPE, **kwargs) -> Iterator[Tuple
|
||||
# namespaces may be referenced multiple times, so we need to filter
|
||||
namespaces = set()
|
||||
|
||||
for row in iter_dotnet_table(pe, "TypeDef"):
|
||||
namespaces.add(row.TypeNamespace)
|
||||
for _, typedef in iter_dotnet_table(pe, dnfile.mdtable.TypeDef.number):
|
||||
# emit internal .NET namespaces
|
||||
assert isinstance(typedef, dnfile.mdtable.TypeDefRow)
|
||||
namespaces.add(typedef.TypeNamespace)
|
||||
|
||||
for row in iter_dotnet_table(pe, "TypeRef"):
|
||||
namespaces.add(row.TypeNamespace)
|
||||
for _, typeref in iter_dotnet_table(pe, dnfile.mdtable.TypeRef.number):
|
||||
# emit external .NET namespaces
|
||||
assert isinstance(typeref, dnfile.mdtable.TypeRefRow)
|
||||
namespaces.add(typeref.TypeNamespace)
|
||||
|
||||
# namespaces may be empty, discard
|
||||
namespaces.discard("")
|
||||
@@ -81,13 +84,19 @@ def extract_file_namespace_features(pe: dnfile.dnPE, **kwargs) -> Iterator[Tuple
|
||||
|
||||
def extract_file_class_features(pe: dnfile.dnPE, **kwargs) -> Iterator[Tuple[Class, Address]]:
|
||||
"""emit class features from TypeRef and TypeDef tables"""
|
||||
for (rid, row) in enumerate(iter_dotnet_table(pe, "TypeDef")):
|
||||
token = calculate_dotnet_token_value(pe.net.mdtables.TypeDef.number, rid + 1)
|
||||
yield Class(DnClass.format_name(row.TypeNamespace, row.TypeName)), DNTokenAddress(Token(token))
|
||||
for rid, typedef in iter_dotnet_table(pe, dnfile.mdtable.TypeDef.number):
|
||||
# emit internal .NET classes
|
||||
assert isinstance(typedef, dnfile.mdtable.TypeDefRow)
|
||||
|
||||
for (rid, row) in enumerate(iter_dotnet_table(pe, "TypeRef")):
|
||||
token = calculate_dotnet_token_value(pe.net.mdtables.TypeRef.number, rid + 1)
|
||||
yield Class(DnClass.format_name(row.TypeNamespace, row.TypeName)), DNTokenAddress(Token(token))
|
||||
token = calculate_dotnet_token_value(dnfile.mdtable.TypeDef.number, rid)
|
||||
yield Class(DnType.format_name(typedef.TypeName, namespace=typedef.TypeNamespace)), DNTokenAddress(token)
|
||||
|
||||
for rid, typeref in iter_dotnet_table(pe, dnfile.mdtable.TypeRef.number):
|
||||
# emit external .NET classes
|
||||
assert isinstance(typeref, dnfile.mdtable.TypeRefRow)
|
||||
|
||||
token = calculate_dotnet_token_value(dnfile.mdtable.TypeRef.number, rid)
|
||||
yield Class(DnType.format_name(typeref.TypeName, namespace=typeref.TypeNamespace)), DNTokenAddress(token)
|
||||
|
||||
|
||||
def extract_file_os(**kwargs) -> Iterator[Tuple[OS, Address]]:
|
||||
@@ -97,6 +106,9 @@ def extract_file_os(**kwargs) -> Iterator[Tuple[OS, Address]]:
|
||||
def extract_file_arch(pe: dnfile.dnPE, **kwargs) -> Iterator[Tuple[Arch, Address]]:
|
||||
# to distinguish in more detail, see https://stackoverflow.com/a/23614024/10548020
|
||||
# .NET 4.5 added option: any CPU, 32-bit preferred
|
||||
assert pe.net is not None
|
||||
assert pe.net.Flags is not None
|
||||
|
||||
if pe.net.Flags.CLR_32BITREQUIRED and pe.PE_TYPE == pefile.OPTIONAL_HEADER_MAGIC_PE:
|
||||
yield Arch(ARCH_I386), NO_ADDRESS
|
||||
elif not pe.net.Flags.CLR_32BITREQUIRED and pe.PE_TYPE == pefile.OPTIONAL_HEADER_MAGIC_PE_PLUS:
|
||||
@@ -147,7 +159,7 @@ GLOBAL_HANDLERS = (
|
||||
|
||||
class DotnetFileFeatureExtractor(FeatureExtractor):
|
||||
def __init__(self, path: str):
|
||||
super(DotnetFileFeatureExtractor, self).__init__()
|
||||
super().__init__()
|
||||
self.path: str = path
|
||||
self.pe: dnfile.dnPE = dnfile.dnPE(path)
|
||||
|
||||
@@ -158,6 +170,9 @@ class DotnetFileFeatureExtractor(FeatureExtractor):
|
||||
# self.pe.net.Flags.CLT_NATIVE_ENTRYPOINT
|
||||
# True: native EP: Token
|
||||
# False: managed EP: RVA
|
||||
assert self.pe.net is not None
|
||||
assert self.pe.net.struct is not None
|
||||
|
||||
return self.pe.net.struct.EntryPointTokenOrRva
|
||||
|
||||
def extract_global_features(self):
|
||||
@@ -173,10 +188,23 @@ class DotnetFileFeatureExtractor(FeatureExtractor):
|
||||
return is_dotnet_mixed_mode(self.pe)
|
||||
|
||||
def get_runtime_version(self) -> Tuple[int, int]:
|
||||
assert self.pe.net is not None
|
||||
assert self.pe.net.struct is not None
|
||||
assert self.pe.net.struct.MajorRuntimeVersion is not None
|
||||
assert self.pe.net.struct.MinorRuntimeVersion is not None
|
||||
|
||||
return self.pe.net.struct.MajorRuntimeVersion, self.pe.net.struct.MinorRuntimeVersion
|
||||
|
||||
def get_meta_version_string(self) -> str:
|
||||
return self.pe.net.metadata.struct.Version.rstrip(b"\x00").decode("utf-8")
|
||||
assert self.pe.net is not None
|
||||
assert self.pe.net.metadata is not None
|
||||
assert self.pe.net.metadata.struct is not None
|
||||
assert self.pe.net.metadata.struct.Version is not None
|
||||
|
||||
vbuf = self.pe.net.metadata.struct.Version
|
||||
assert isinstance(vbuf, bytes)
|
||||
|
||||
return vbuf.rstrip(b"\x00").decode("utf-8")
|
||||
|
||||
def get_functions(self):
|
||||
raise NotImplementedError("DotnetFileFeatureExtractor can only be used to extract file features")
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,6 @@
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
import io
|
||||
import logging
|
||||
import contextlib
|
||||
from typing import Tuple, Iterator
|
||||
|
||||
from elftools.elf.elffile import ELFFile, SymbolTableSection
|
||||
@@ -16,7 +15,6 @@ import capa.features.extractors.common
|
||||
from capa.features.file import Import, Section
|
||||
from capa.features.common import OS, FORMAT_ELF, Arch, Format, Feature
|
||||
from capa.features.address import NO_ADDRESS, FileOffsetAddress, AbsoluteVirtualAddress
|
||||
from capa.features.extractors.elf import Arch as ElfArch
|
||||
from capa.features.extractors.base_extractor import FeatureExtractor
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -26,17 +24,17 @@ def extract_file_import_names(elf, **kwargs):
|
||||
# see https://github.com/eliben/pyelftools/blob/0664de05ed2db3d39041e2d51d19622a8ef4fb0f/scripts/readelf.py#L372
|
||||
symbol_tables = [(idx, s) for idx, s in enumerate(elf.iter_sections()) if isinstance(s, SymbolTableSection)]
|
||||
|
||||
for section_index, section in symbol_tables:
|
||||
for _, section in symbol_tables:
|
||||
if not isinstance(section, SymbolTableSection):
|
||||
continue
|
||||
|
||||
if section["sh_entsize"] == 0:
|
||||
logger.debug("Symbol table '%s' has a sh_entsize of zero!" % (section.name))
|
||||
logger.debug("Symbol table '%s' has a sh_entsize of zero!", section.name)
|
||||
continue
|
||||
|
||||
logger.debug("Symbol table '%s' contains %s entries:" % (section.name, section.num_symbols()))
|
||||
logger.debug("Symbol table '%s' contains %s entries:", section.name, section.num_symbols())
|
||||
|
||||
for nsym, symbol in enumerate(section.iter_symbols()):
|
||||
for _, symbol in enumerate(section.iter_symbols()):
|
||||
if symbol.name and symbol.entry.st_info.type == "STT_FUNC":
|
||||
# TODO symbol address
|
||||
# TODO symbol version info?
|
||||
@@ -73,9 +71,9 @@ def extract_file_arch(elf, **kwargs):
|
||||
# TODO merge with capa.features.extractors.elf.detect_elf_arch()
|
||||
arch = elf.get_machine_arch()
|
||||
if arch == "x86":
|
||||
yield Arch(ElfArch.I386), NO_ADDRESS
|
||||
yield Arch("i386"), NO_ADDRESS
|
||||
elif arch == "x64":
|
||||
yield Arch(ElfArch.AMD64), NO_ADDRESS
|
||||
yield Arch("amd64"), NO_ADDRESS
|
||||
else:
|
||||
logger.warning("unsupported architecture: %s", arch)
|
||||
|
||||
@@ -110,7 +108,7 @@ GLOBAL_HANDLERS = (
|
||||
|
||||
class ElfFeatureExtractor(FeatureExtractor):
|
||||
def __init__(self, path: str):
|
||||
super(ElfFeatureExtractor, self).__init__()
|
||||
super().__init__()
|
||||
self.path = path
|
||||
with open(self.path, "rb") as f:
|
||||
self.elf = ELFFile(io.BytesIO(f.read()))
|
||||
@@ -153,8 +151,8 @@ class ElfFeatureExtractor(FeatureExtractor):
|
||||
def extract_insn_features(self, f, bb, insn):
|
||||
raise NotImplementedError("ElfFeatureExtractor can only be used to extract file features")
|
||||
|
||||
def is_library_function(self, va):
|
||||
def is_library_function(self, addr):
|
||||
raise NotImplementedError("ElfFeatureExtractor can only be used to extract file features")
|
||||
|
||||
def get_function_name(self, va):
|
||||
def get_function_name(self, addr):
|
||||
raise NotImplementedError("ElfFeatureExtractor can only be used to extract file features")
|
||||
|
||||
@@ -55,7 +55,7 @@ def generate_symbols(dll: str, symbol: str) -> Iterator[str]:
|
||||
dll = dll.lower()
|
||||
|
||||
# kernel32.CreateFileA
|
||||
yield "%s.%s" % (dll, symbol)
|
||||
yield f"{dll}.{symbol}"
|
||||
|
||||
if not is_ordinal(symbol):
|
||||
# CreateFileA
|
||||
@@ -63,7 +63,7 @@ def generate_symbols(dll: str, symbol: str) -> Iterator[str]:
|
||||
|
||||
if is_aw_function(symbol):
|
||||
# kernel32.CreateFile
|
||||
yield "%s.%s" % (dll, symbol[:-1])
|
||||
yield f"{dll}.{symbol[:-1]}"
|
||||
|
||||
if not is_ordinal(symbol):
|
||||
# CreateFile
|
||||
@@ -112,7 +112,6 @@ def carve_pe(pbytes: bytes, offset: int = 0) -> Iterator[Tuple[int, int]]:
|
||||
todo = [(off, mzx, pex, key) for (off, mzx, pex, key) in todo if off != -1]
|
||||
|
||||
while len(todo):
|
||||
|
||||
off, mzx, pex, key = todo.pop()
|
||||
|
||||
# The MZ header has one field we will check
|
||||
|
||||
@@ -34,7 +34,7 @@ def get_printable_len(op: idaapi.op_t) -> int:
|
||||
elif op.dtype == idaapi.dt_qword:
|
||||
chars = struct.pack("<Q", op_val)
|
||||
else:
|
||||
raise ValueError("Unhandled operand data type 0x%x." % op.dtype)
|
||||
raise ValueError(f"Unhandled operand data type 0x{op.dtype:x}.")
|
||||
|
||||
def is_printable_ascii(chars_: bytes):
|
||||
return all(c < 127 and chr(c) in string.printable for c in chars_)
|
||||
@@ -95,7 +95,7 @@ def extract_bb_tight_loop(fh: FunctionHandle, bbh: BBHandle) -> Iterator[Tuple[F
|
||||
def extract_features(fh: FunctionHandle, bbh: BBHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""extract basic block features"""
|
||||
for bb_handler in BASIC_BLOCK_HANDLERS:
|
||||
for (feature, addr) in bb_handler(fh, bbh):
|
||||
for feature, addr in bb_handler(fh, bbh):
|
||||
yield feature, addr
|
||||
yield BasicBlock(), bbh.address
|
||||
|
||||
|
||||
@@ -23,8 +23,9 @@ from capa.features.extractors.base_extractor import BBHandle, InsnHandle, Functi
|
||||
|
||||
class IdaFeatureExtractor(FeatureExtractor):
|
||||
def __init__(self):
|
||||
super(IdaFeatureExtractor, self).__init__()
|
||||
super().__init__()
|
||||
self.global_features: List[Tuple[Feature, Address]] = []
|
||||
self.global_features.extend(capa.features.extractors.ida.file.extract_file_format())
|
||||
self.global_features.extend(capa.features.extractors.ida.global_.extract_os())
|
||||
self.global_features.extend(capa.features.extractors.ida.global_.extract_arch())
|
||||
|
||||
|
||||
@@ -21,12 +21,14 @@ from capa.features.file import Export, Import, Section, FunctionName
|
||||
from capa.features.common import FORMAT_PE, FORMAT_ELF, Format, String, Feature, Characteristic
|
||||
from capa.features.address import NO_ADDRESS, Address, FileOffsetAddress, AbsoluteVirtualAddress
|
||||
|
||||
MAX_OFFSET_PE_AFTER_MZ = 0x200
|
||||
|
||||
|
||||
def check_segment_for_pe(seg: idaapi.segment_t) -> Iterator[Tuple[int, int]]:
|
||||
"""check segment for embedded PE
|
||||
|
||||
adapted for IDA from:
|
||||
https://github.com/vivisect/vivisect/blob/7be4037b1cecc4551b397f840405a1fc606f9b53/PE/carve.py#L19
|
||||
https://github.com/vivisect/vivisect/blob/91e8419a861f49779f18316f155311967e696836/PE/carve.py#L25
|
||||
"""
|
||||
seg_max = seg.end_ea
|
||||
mz_xor = [
|
||||
@@ -39,14 +41,15 @@ def check_segment_for_pe(seg: idaapi.segment_t) -> Iterator[Tuple[int, int]]:
|
||||
]
|
||||
|
||||
todo = []
|
||||
for (mzx, pex, i) in mz_xor:
|
||||
for mzx, pex, i in mz_xor:
|
||||
# find all segment offsets containing XOR'd "MZ" bytes
|
||||
for off in capa.features.extractors.ida.helpers.find_byte_sequence(seg.start_ea, seg.end_ea, mzx):
|
||||
todo.append((off, mzx, pex, i))
|
||||
|
||||
while len(todo):
|
||||
off, mzx, pex, i = todo.pop()
|
||||
|
||||
# The MZ header has one field we will check e_lfanew is at 0x3c
|
||||
# MZ header has one field we will check e_lfanew is at 0x3c
|
||||
e_lfanew = off + 0x3C
|
||||
|
||||
if seg_max < (e_lfanew + 4):
|
||||
@@ -54,6 +57,10 @@ def check_segment_for_pe(seg: idaapi.segment_t) -> Iterator[Tuple[int, int]]:
|
||||
|
||||
newoff = struct.unpack("<I", capa.features.extractors.helpers.xor_static(idc.get_bytes(e_lfanew, 4), i))[0]
|
||||
|
||||
# assume XOR'd "PE" bytes exist within threshold
|
||||
if newoff > MAX_OFFSET_PE_AFTER_MZ:
|
||||
continue
|
||||
|
||||
peoff = off + newoff
|
||||
if seg_max < (peoff + 2):
|
||||
continue
|
||||
@@ -61,9 +68,6 @@ def check_segment_for_pe(seg: idaapi.segment_t) -> Iterator[Tuple[int, int]]:
|
||||
if idc.get_bytes(peoff, 2) == pex:
|
||||
yield off, i
|
||||
|
||||
for nextres in capa.features.extractors.ida.helpers.find_byte_sequence(off + 1, seg.end_ea, mzx):
|
||||
todo.append((nextres, mzx, pex, i))
|
||||
|
||||
|
||||
def extract_file_embedded_pe() -> Iterator[Tuple[Feature, Address]]:
|
||||
"""extract embedded PE features
|
||||
@@ -73,13 +77,13 @@ def extract_file_embedded_pe() -> Iterator[Tuple[Feature, Address]]:
|
||||
- Check 'Load resource sections' when opening binary in IDA manually
|
||||
"""
|
||||
for seg in capa.features.extractors.ida.helpers.get_segments(skip_header_segments=True):
|
||||
for (ea, _) in check_segment_for_pe(seg):
|
||||
for ea, _ in check_segment_for_pe(seg):
|
||||
yield Characteristic("embedded pe"), FileOffsetAddress(ea)
|
||||
|
||||
|
||||
def extract_file_export_names() -> Iterator[Tuple[Feature, Address]]:
|
||||
"""extract function exports"""
|
||||
for (_, _, ea, name) in idautils.Entries():
|
||||
for _, _, ea, name in idautils.Entries():
|
||||
yield Export(name), AbsoluteVirtualAddress(ea)
|
||||
|
||||
|
||||
@@ -94,7 +98,7 @@ def extract_file_import_names() -> Iterator[Tuple[Feature, Address]]:
|
||||
- modulename.importname
|
||||
- importname
|
||||
"""
|
||||
for (ea, info) in capa.features.extractors.ida.helpers.get_file_imports().items():
|
||||
for ea, info in capa.features.extractors.ida.helpers.get_file_imports().items():
|
||||
addr = AbsoluteVirtualAddress(ea)
|
||||
if info[1] and info[2]:
|
||||
# e.g. in mimikatz: ('cabinet', 'FCIAddFile', 11L)
|
||||
@@ -102,19 +106,22 @@ def extract_file_import_names() -> Iterator[Tuple[Feature, Address]]:
|
||||
for name in capa.features.extractors.helpers.generate_symbols(info[0], info[1]):
|
||||
yield Import(name), addr
|
||||
dll = info[0]
|
||||
symbol = "#%d" % (info[2])
|
||||
symbol = f"#{info[2]}"
|
||||
elif info[1]:
|
||||
dll = info[0]
|
||||
symbol = info[1]
|
||||
elif info[2]:
|
||||
dll = info[0]
|
||||
symbol = "#%d" % (info[2])
|
||||
symbol = f"#{info[2]}"
|
||||
else:
|
||||
continue
|
||||
|
||||
for name in capa.features.extractors.helpers.generate_symbols(dll, symbol):
|
||||
yield Import(name), addr
|
||||
|
||||
for ea, info in capa.features.extractors.ida.helpers.get_file_externs().items():
|
||||
yield Import(info[1]), AbsoluteVirtualAddress(ea)
|
||||
|
||||
|
||||
def extract_file_section_names() -> Iterator[Tuple[Feature, Address]]:
|
||||
"""extract section names
|
||||
@@ -165,7 +172,7 @@ def extract_file_function_names() -> Iterator[Tuple[Feature, Address]]:
|
||||
def extract_file_format() -> Iterator[Tuple[Feature, Address]]:
|
||||
file_info = idaapi.get_inf_structure()
|
||||
|
||||
if file_info.filetype == idaapi.f_PE:
|
||||
if file_info.filetype in (idaapi.f_PE, idaapi.f_COFF):
|
||||
yield Format(FORMAT_PE), NO_ADDRESS
|
||||
elif file_info.filetype == idaapi.f_ELF:
|
||||
yield Format(FORMAT_ELF), NO_ADDRESS
|
||||
@@ -173,7 +180,7 @@ def extract_file_format() -> Iterator[Tuple[Feature, Address]]:
|
||||
# no file type to return when processing a binary file, but we want to continue processing
|
||||
return
|
||||
else:
|
||||
raise NotImplementedError("file format: %d" % file_info.filetype)
|
||||
raise NotImplementedError(f"unexpected file format: {file_info.filetype}")
|
||||
|
||||
|
||||
def extract_features() -> Iterator[Tuple[Feature, Address]]:
|
||||
|
||||
@@ -45,7 +45,7 @@ def extract_recursive_call(fh: FunctionHandle):
|
||||
|
||||
def extract_features(fh: FunctionHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
for func_handler in FUNCTION_HANDLERS:
|
||||
for (feature, addr) in func_handler(fh):
|
||||
for feature, addr in func_handler(fh):
|
||||
yield feature, addr
|
||||
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ def extract_os() -> Iterator[Tuple[Feature, Address]]:
|
||||
# 2. handling a new file format (e.g. macho)
|
||||
#
|
||||
# for (1) we can't do much - its shellcode and all bets are off.
|
||||
# we could maybe accept a futher CLI argument to specify the OS,
|
||||
# we could maybe accept a further CLI argument to specify the OS,
|
||||
# but i think this would be rarely used.
|
||||
# rules that rely on OS conditions will fail to match on shellcode.
|
||||
#
|
||||
|
||||
@@ -5,12 +5,13 @@
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
from typing import Any, Dict, Tuple, Iterator
|
||||
from typing import Any, Dict, Tuple, Iterator, Optional
|
||||
|
||||
import idc
|
||||
import idaapi
|
||||
import idautils
|
||||
import ida_bytes
|
||||
import ida_segment
|
||||
|
||||
from capa.features.address import AbsoluteVirtualAddress
|
||||
from capa.features.extractors.base_extractor import FunctionHandle
|
||||
@@ -24,7 +25,7 @@ def find_byte_sequence(start: int, end: int, seq: bytes) -> Iterator[int]:
|
||||
end: max virtual address
|
||||
seq: bytes to search e.g. b"\x01\x03"
|
||||
"""
|
||||
seqstr = " ".join(["%02x" % b for b in seq])
|
||||
seqstr = " ".join([f"{b:02x}" for b in seq])
|
||||
while True:
|
||||
# TODO find_binary: Deprecated. Please use ida_bytes.bin_search() instead.
|
||||
ea = idaapi.find_binary(start, end, seqstr, 0, idaapi.SEARCH_DOWN)
|
||||
@@ -35,7 +36,7 @@ def find_byte_sequence(start: int, end: int, seq: bytes) -> Iterator[int]:
|
||||
|
||||
|
||||
def get_functions(
|
||||
start: int = None, end: int = None, skip_thunks: bool = False, skip_libs: bool = False
|
||||
start: Optional[int] = None, end: Optional[int] = None, skip_thunks: bool = False, skip_libs: bool = False
|
||||
) -> Iterator[FunctionHandle]:
|
||||
"""get functions, range optional
|
||||
|
||||
@@ -89,8 +90,11 @@ def get_file_imports() -> Dict[int, Tuple[str, str, int]]:
|
||||
if not library:
|
||||
continue
|
||||
|
||||
# IDA uses section names for the library of ELF imports, like ".dynsym"
|
||||
library = library.lstrip(".")
|
||||
# IDA uses section names for the library of ELF imports, like ".dynsym".
|
||||
# These are not useful to us, we may need to expand this list over time
|
||||
# TODO: exhaust this list, see #1419
|
||||
if library == ".dynsym":
|
||||
library = ""
|
||||
|
||||
def inspect_import(ea, function, ordinal):
|
||||
if function and function.startswith("__imp_"):
|
||||
@@ -109,6 +113,19 @@ def get_file_imports() -> Dict[int, Tuple[str, str, int]]:
|
||||
return imports
|
||||
|
||||
|
||||
def get_file_externs() -> Dict[int, Tuple[str, str, int]]:
|
||||
externs = {}
|
||||
|
||||
for seg in get_segments(skip_header_segments=True):
|
||||
if not (seg.type == ida_segment.SEG_XTRN):
|
||||
continue
|
||||
|
||||
for ea in idautils.Functions(seg.start_ea, seg.end_ea):
|
||||
externs[ea] = ("", idaapi.get_func_name(ea), -1)
|
||||
|
||||
return externs
|
||||
|
||||
|
||||
def get_instructions_in_range(start: int, end: int) -> Iterator[idaapi.insn_t]:
|
||||
"""yield instructions in range
|
||||
|
||||
@@ -183,7 +200,7 @@ def read_bytes_at(ea: int, count: int) -> bytes:
|
||||
def find_string_at(ea: int, min_: int = 4) -> str:
|
||||
"""check if ASCII string exists at a given virtual address"""
|
||||
found = idaapi.get_strlit_contents(ea, -1, idaapi.STRTYPE_C)
|
||||
if found and len(found) > min_:
|
||||
if found and len(found) >= min_:
|
||||
try:
|
||||
found = found.decode("ascii")
|
||||
# hacky check for IDA bug; get_strlit_contents also reads Unicode as
|
||||
@@ -207,7 +224,8 @@ def get_op_phrase_info(op: idaapi.op_t) -> Dict:
|
||||
return {}
|
||||
|
||||
scale = 1 << ((op.specflag2 & 0xC0) >> 6)
|
||||
offset = op.addr
|
||||
# IDA ea_t may be 32- or 64-bit; we assume displacement can only be 32-bit
|
||||
offset = op.addr & 0xFFFFFFFF
|
||||
|
||||
if op.specflag1 == 0:
|
||||
index = None
|
||||
@@ -273,7 +291,7 @@ def is_frame_register(reg: int) -> bool:
|
||||
return reg in (idautils.procregs.sp.reg, idautils.procregs.bp.reg)
|
||||
|
||||
|
||||
def get_insn_ops(insn: idaapi.insn_t, target_ops: Tuple[Any] = None) -> idaapi.op_t:
|
||||
def get_insn_ops(insn: idaapi.insn_t, target_ops: Optional[Tuple[Any]] = None) -> idaapi.op_t:
|
||||
"""yield op_t for instruction, filter on type if specified"""
|
||||
for op in insn.ops:
|
||||
if op.type == idaapi.o_void:
|
||||
|
||||
@@ -23,13 +23,19 @@ from capa.features.extractors.base_extractor import BBHandle, InsnHandle, Functi
|
||||
SECURITY_COOKIE_BYTES_DELTA = 0x40
|
||||
|
||||
|
||||
def get_imports(ctx: Dict[str, Any]) -> Dict[str, Any]:
|
||||
def get_imports(ctx: Dict[str, Any]) -> Dict[int, Any]:
|
||||
if "imports_cache" not in ctx:
|
||||
ctx["imports_cache"] = capa.features.extractors.ida.helpers.get_file_imports()
|
||||
return ctx["imports_cache"]
|
||||
|
||||
|
||||
def check_for_api_call(ctx: Dict[str, Any], insn: idaapi.insn_t) -> Iterator[str]:
|
||||
def get_externs(ctx: Dict[str, Any]) -> Dict[int, Any]:
|
||||
if "externs_cache" not in ctx:
|
||||
ctx["externs_cache"] = capa.features.extractors.ida.helpers.get_file_externs()
|
||||
return ctx["externs_cache"]
|
||||
|
||||
|
||||
def check_for_api_call(insn: idaapi.insn_t, funcs: Dict[int, Any]) -> Iterator[Any]:
|
||||
"""check instruction for API call"""
|
||||
info = ()
|
||||
ref = insn.ea
|
||||
@@ -46,7 +52,7 @@ def check_for_api_call(ctx: Dict[str, Any], insn: idaapi.insn_t) -> Iterator[str
|
||||
except IndexError:
|
||||
break
|
||||
|
||||
info = get_imports(ctx).get(ref, ())
|
||||
info = funcs.get(ref, ())
|
||||
if info:
|
||||
break
|
||||
|
||||
@@ -55,7 +61,7 @@ def check_for_api_call(ctx: Dict[str, Any], insn: idaapi.insn_t) -> Iterator[str
|
||||
break
|
||||
|
||||
if info:
|
||||
yield "%s.%s" % (info[0], info[1])
|
||||
yield info
|
||||
|
||||
|
||||
def extract_insn_api_features(fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
@@ -70,11 +76,17 @@ def extract_insn_api_features(fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle)
|
||||
if not insn.get_canon_mnem() in ("call", "jmp"):
|
||||
return
|
||||
|
||||
for api in check_for_api_call(fh.ctx, insn):
|
||||
dll, _, symbol = api.rpartition(".")
|
||||
for name in capa.features.extractors.helpers.generate_symbols(dll, symbol):
|
||||
# check calls to imported functions
|
||||
for api in check_for_api_call(insn, get_imports(fh.ctx)):
|
||||
# tuple (<module>, <function>, <ordinal>)
|
||||
for name in capa.features.extractors.helpers.generate_symbols(api[0], api[1]):
|
||||
yield API(name), ih.address
|
||||
|
||||
# check calls to extern functions
|
||||
for api in check_for_api_call(insn, get_externs(fh.ctx)):
|
||||
# tuple (<module>, <function>, <ordinal>)
|
||||
yield API(api[1]), ih.address
|
||||
|
||||
# extract IDA/FLIRT recognized API functions
|
||||
targets = tuple(idautils.CodeRefsFrom(insn.ea, False))
|
||||
if not targets:
|
||||
@@ -160,7 +172,9 @@ def extract_insn_bytes_features(fh: FunctionHandle, bbh: BBHandle, ih: InsnHandl
|
||||
if ref != insn.ea:
|
||||
extracted_bytes = capa.features.extractors.ida.helpers.read_bytes_at(ref, MAX_BYTES_FEATURE_SIZE)
|
||||
if extracted_bytes and not capa.features.extractors.helpers.all_zeros(extracted_bytes):
|
||||
yield Bytes(extracted_bytes), ih.address
|
||||
if not capa.features.extractors.ida.helpers.find_string_at(ref):
|
||||
# don't extract byte features for obvious strings
|
||||
yield Bytes(extracted_bytes), ih.address
|
||||
|
||||
|
||||
def extract_insn_string_features(
|
||||
@@ -201,7 +215,11 @@ def extract_insn_offset_features(
|
||||
continue
|
||||
|
||||
p_info = capa.features.extractors.ida.helpers.get_op_phrase_info(op)
|
||||
op_off = p_info.get("offset", 0)
|
||||
|
||||
op_off = p_info.get("offset", None)
|
||||
if op_off is None:
|
||||
continue
|
||||
|
||||
if idaapi.is_mapped(op_off):
|
||||
# Ignore:
|
||||
# mov esi, dword_1005B148[esi]
|
||||
@@ -464,7 +482,7 @@ def extract_function_indirect_call_characteristic_features(
|
||||
def extract_features(f: FunctionHandle, bbh: BBHandle, insn: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""extract instruction features"""
|
||||
for inst_handler in INSTRUCTION_HANDLERS:
|
||||
for (feature, ea) in inst_handler(f, bbh, insn):
|
||||
for feature, ea in inst_handler(f, bbh, insn):
|
||||
yield feature, ea
|
||||
|
||||
|
||||
|
||||
@@ -52,26 +52,21 @@ class NullFeatureExtractor(FeatureExtractor):
|
||||
yield FunctionHandle(address, None)
|
||||
|
||||
def extract_function_features(self, f):
|
||||
for address, feature in self.functions.get(f.address, {}).features:
|
||||
for address, feature in self.functions[f.address].features:
|
||||
yield feature, address
|
||||
|
||||
def get_basic_blocks(self, f):
|
||||
for address in sorted(self.functions.get(f.address, {}).basic_blocks.keys()):
|
||||
for address in sorted(self.functions[f.address].basic_blocks.keys()):
|
||||
yield BBHandle(address, None)
|
||||
|
||||
def extract_basic_block_features(self, f, bb):
|
||||
for address, feature in self.functions.get(f.address, {}).basic_blocks.get(bb.address, {}).features:
|
||||
for address, feature in self.functions[f.address].basic_blocks[bb.address].features:
|
||||
yield feature, address
|
||||
|
||||
def get_instructions(self, f, bb):
|
||||
for address in sorted(self.functions.get(f.address, {}).basic_blocks.get(bb.address, {}).instructions.keys()):
|
||||
for address in sorted(self.functions[f.address].basic_blocks[bb.address].instructions.keys()):
|
||||
yield InsnHandle(address, None)
|
||||
|
||||
def extract_insn_features(self, f, bb, insn):
|
||||
for address, feature in (
|
||||
self.functions.get(f.address, {})
|
||||
.basic_blocks.get(bb.address, {})
|
||||
.instructions.get(insn.address, {})
|
||||
.features
|
||||
):
|
||||
for address, feature in self.functions[f.address].basic_blocks[bb.address].instructions[insn.address].features:
|
||||
yield feature, address
|
||||
|
||||
@@ -64,7 +64,7 @@ def extract_file_import_names(pe, **kwargs):
|
||||
|
||||
for imp in dll.imports:
|
||||
if imp.import_by_ordinal:
|
||||
impname = "#%s" % imp.ordinal
|
||||
impname = f"#{imp.ordinal}"
|
||||
else:
|
||||
try:
|
||||
impname = imp.name.partition(b"\x00")[0].decode("ascii")
|
||||
@@ -133,7 +133,8 @@ def extract_file_features(pe, buf):
|
||||
"""
|
||||
|
||||
for file_handler in FILE_HANDLERS:
|
||||
for feature, va in file_handler(pe=pe, buf=buf):
|
||||
# file_handler: type: (pe, bytes) -> Iterable[Tuple[Feature, Address]]
|
||||
for feature, va in file_handler(pe=pe, buf=buf): # type: ignore
|
||||
yield feature, va
|
||||
|
||||
|
||||
@@ -160,7 +161,8 @@ def extract_global_features(pe, buf):
|
||||
Tuple[Feature, VA]: a feature and its location.
|
||||
"""
|
||||
for handler in GLOBAL_HANDLERS:
|
||||
for feature, va in handler(pe=pe, buf=buf):
|
||||
# file_handler: type: (pe, bytes) -> Iterable[Tuple[Feature, Address]]
|
||||
for feature, va in handler(pe=pe, buf=buf): # type: ignore
|
||||
yield feature, va
|
||||
|
||||
|
||||
@@ -172,7 +174,7 @@ GLOBAL_HANDLERS = (
|
||||
|
||||
class PefileFeatureExtractor(FeatureExtractor):
|
||||
def __init__(self, path: str):
|
||||
super(PefileFeatureExtractor, self).__init__()
|
||||
super().__init__()
|
||||
self.path = path
|
||||
self.pe = pefile.PE(path)
|
||||
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
import string
|
||||
import struct
|
||||
from typing import Tuple, Iterator
|
||||
|
||||
from capa.features.common import Feature, Characteristic
|
||||
from capa.features.address import Address
|
||||
from capa.features.basicblock import BasicBlock
|
||||
from capa.features.extractors.helpers import MIN_STACKSTRING_LEN
|
||||
from capa.features.extractors.base_extractor import BBHandle, FunctionHandle
|
||||
|
||||
|
||||
def _bb_has_tight_loop(f, bb):
|
||||
"""
|
||||
parse tight loops, true if last instruction in basic block branches to bb start
|
||||
"""
|
||||
return bb.offset in f.blockrefs[bb.offset] if bb.offset in f.blockrefs else False
|
||||
|
||||
|
||||
def extract_bb_tight_loop(f: FunctionHandle, bb: BBHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""check basic block for tight loop indicators"""
|
||||
if _bb_has_tight_loop(f.inner, bb.inner):
|
||||
yield Characteristic("tight loop"), bb.address
|
||||
|
||||
|
||||
def _bb_has_stackstring(f, bb):
|
||||
"""
|
||||
extract potential stackstring creation, using the following heuristics:
|
||||
- basic block contains enough moves of constant bytes to the stack
|
||||
"""
|
||||
count = 0
|
||||
for instr in bb.getInstructions():
|
||||
if is_mov_imm_to_stack(instr):
|
||||
count += get_printable_len(instr.getDetailed())
|
||||
if count > MIN_STACKSTRING_LEN:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def get_operands(smda_ins):
|
||||
return [o.strip() for o in smda_ins.operands.split(",")]
|
||||
|
||||
|
||||
def extract_stackstring(f: FunctionHandle, bb: BBHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""check basic block for stackstring indicators"""
|
||||
if _bb_has_stackstring(f.inner, bb.inner):
|
||||
yield Characteristic("stack string"), bb.address
|
||||
|
||||
|
||||
def is_mov_imm_to_stack(smda_ins):
|
||||
"""
|
||||
Return if instruction moves immediate onto stack
|
||||
"""
|
||||
if not smda_ins.mnemonic.startswith("mov"):
|
||||
return False
|
||||
|
||||
try:
|
||||
dst, src = get_operands(smda_ins)
|
||||
except ValueError:
|
||||
# not two operands
|
||||
return False
|
||||
|
||||
try:
|
||||
int(src, 16)
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
if not any(regname in dst for regname in ["ebp", "rbp", "esp", "rsp"]):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def is_printable_ascii(chars):
|
||||
return all(c < 127 and chr(c) in string.printable for c in chars)
|
||||
|
||||
|
||||
def is_printable_utf16le(chars):
|
||||
if all(c == 0x00 for c in chars[1::2]):
|
||||
return is_printable_ascii(chars[::2])
|
||||
|
||||
|
||||
def get_printable_len(instr):
|
||||
"""
|
||||
Return string length if all operand bytes are ascii or utf16-le printable
|
||||
|
||||
Works on a capstone instruction
|
||||
"""
|
||||
# should have exactly two operands for mov immediate
|
||||
if len(instr.operands) != 2:
|
||||
return 0
|
||||
|
||||
op_value = instr.operands[1].value.imm
|
||||
|
||||
if instr.imm_size == 1:
|
||||
chars = struct.pack("<B", op_value & 0xFF)
|
||||
elif instr.imm_size == 2:
|
||||
chars = struct.pack("<H", op_value & 0xFFFF)
|
||||
elif instr.imm_size == 4:
|
||||
chars = struct.pack("<I", op_value & 0xFFFFFFFF)
|
||||
elif instr.imm_size == 8:
|
||||
chars = struct.pack("<Q", op_value & 0xFFFFFFFFFFFFFFFF)
|
||||
else:
|
||||
raise ValueError("Unhandled operand data type 0x%x." % instr.imm_size)
|
||||
|
||||
if is_printable_ascii(chars):
|
||||
return instr.imm_size
|
||||
if is_printable_utf16le(chars):
|
||||
return instr.imm_size // 2
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def extract_features(f: FunctionHandle, bb: BBHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
extract features from the given basic block.
|
||||
|
||||
args:
|
||||
f: the function from which to extract features
|
||||
bb: the basic block to process.
|
||||
|
||||
yields:
|
||||
Tuple[Feature, Address]: the features and their location found in this basic block.
|
||||
"""
|
||||
yield BasicBlock(), bb.address
|
||||
for bb_handler in BASIC_BLOCK_HANDLERS:
|
||||
for feature, addr in bb_handler(f, bb):
|
||||
yield feature, addr
|
||||
|
||||
|
||||
BASIC_BLOCK_HANDLERS = (
|
||||
extract_bb_tight_loop,
|
||||
extract_stackstring,
|
||||
)
|
||||
@@ -1,57 +0,0 @@
|
||||
from typing import List, Tuple
|
||||
|
||||
from smda.common.SmdaReport import SmdaReport
|
||||
|
||||
import capa.features.extractors.common
|
||||
import capa.features.extractors.smda.file
|
||||
import capa.features.extractors.smda.insn
|
||||
import capa.features.extractors.smda.global_
|
||||
import capa.features.extractors.smda.function
|
||||
import capa.features.extractors.smda.basicblock
|
||||
from capa.features.common import Feature
|
||||
from capa.features.address import Address, AbsoluteVirtualAddress
|
||||
from capa.features.extractors.base_extractor import BBHandle, InsnHandle, FunctionHandle, FeatureExtractor
|
||||
|
||||
|
||||
class SmdaFeatureExtractor(FeatureExtractor):
|
||||
def __init__(self, smda_report: SmdaReport, path):
|
||||
super(SmdaFeatureExtractor, self).__init__()
|
||||
self.smda_report = smda_report
|
||||
self.path = path
|
||||
with open(self.path, "rb") as f:
|
||||
self.buf = f.read()
|
||||
|
||||
# pre-compute these because we'll yield them at *every* scope.
|
||||
self.global_features: List[Tuple[Feature, Address]] = []
|
||||
self.global_features.extend(capa.features.extractors.common.extract_os(self.buf))
|
||||
self.global_features.extend(capa.features.extractors.smda.global_.extract_arch(self.smda_report))
|
||||
|
||||
def get_base_address(self):
|
||||
return AbsoluteVirtualAddress(self.smda_report.base_addr)
|
||||
|
||||
def extract_global_features(self):
|
||||
yield from self.global_features
|
||||
|
||||
def extract_file_features(self):
|
||||
yield from capa.features.extractors.smda.file.extract_features(self.smda_report, self.buf)
|
||||
|
||||
def get_functions(self):
|
||||
for function in self.smda_report.getFunctions():
|
||||
yield FunctionHandle(address=AbsoluteVirtualAddress(function.offset), inner=function)
|
||||
|
||||
def extract_function_features(self, fh):
|
||||
yield from capa.features.extractors.smda.function.extract_features(fh)
|
||||
|
||||
def get_basic_blocks(self, fh):
|
||||
for bb in fh.inner.getBlocks():
|
||||
yield BBHandle(address=AbsoluteVirtualAddress(bb.offset), inner=bb)
|
||||
|
||||
def extract_basic_block_features(self, fh, bbh):
|
||||
yield from capa.features.extractors.smda.basicblock.extract_features(fh, bbh)
|
||||
|
||||
def get_instructions(self, fh, bbh):
|
||||
for smda_ins in bbh.inner.getInstructions():
|
||||
yield InsnHandle(address=AbsoluteVirtualAddress(smda_ins.offset), inner=smda_ins)
|
||||
|
||||
def extract_insn_features(self, fh, bbh, ih):
|
||||
yield from capa.features.extractors.smda.insn.extract_features(fh, bbh, ih)
|
||||
@@ -1,103 +0,0 @@
|
||||
# if we have SMDA we definitely have lief
|
||||
import lief
|
||||
|
||||
import capa.features.extractors.common
|
||||
import capa.features.extractors.helpers
|
||||
import capa.features.extractors.strings
|
||||
from capa.features.file import Export, Import, Section
|
||||
from capa.features.common import String, Characteristic
|
||||
from capa.features.address import FileOffsetAddress, AbsoluteVirtualAddress
|
||||
|
||||
|
||||
def extract_file_embedded_pe(buf, **kwargs):
|
||||
for offset, _ in capa.features.extractors.helpers.carve_pe(buf, 1):
|
||||
yield Characteristic("embedded pe"), FileOffsetAddress(offset)
|
||||
|
||||
|
||||
def extract_file_export_names(buf, **kwargs):
|
||||
lief_binary = lief.parse(buf)
|
||||
|
||||
if lief_binary is not None:
|
||||
for function in lief_binary.exported_functions:
|
||||
yield Export(function.name), AbsoluteVirtualAddress(function.address)
|
||||
|
||||
|
||||
def extract_file_import_names(smda_report, buf):
|
||||
# extract import table info via LIEF
|
||||
lief_binary = lief.parse(buf)
|
||||
if not isinstance(lief_binary, lief.PE.Binary):
|
||||
return
|
||||
for imported_library in lief_binary.imports:
|
||||
library_name = imported_library.name.lower()
|
||||
library_name = library_name[:-4] if library_name.endswith(".dll") else library_name
|
||||
for func in imported_library.entries:
|
||||
va = func.iat_address + smda_report.base_addr
|
||||
if func.name:
|
||||
for name in capa.features.extractors.helpers.generate_symbols(library_name, func.name):
|
||||
yield Import(name), AbsoluteVirtualAddress(va)
|
||||
elif func.is_ordinal:
|
||||
for name in capa.features.extractors.helpers.generate_symbols(library_name, "#%s" % func.ordinal):
|
||||
yield Import(name), AbsoluteVirtualAddress(va)
|
||||
|
||||
|
||||
def extract_file_section_names(buf, **kwargs):
|
||||
lief_binary = lief.parse(buf)
|
||||
if not isinstance(lief_binary, lief.PE.Binary):
|
||||
return
|
||||
if lief_binary and lief_binary.sections:
|
||||
base_address = lief_binary.optional_header.imagebase
|
||||
for section in lief_binary.sections:
|
||||
yield Section(section.name), AbsoluteVirtualAddress(base_address + section.virtual_address)
|
||||
|
||||
|
||||
def extract_file_strings(buf, **kwargs):
|
||||
"""
|
||||
extract ASCII and UTF-16 LE strings from file
|
||||
"""
|
||||
for s in capa.features.extractors.strings.extract_ascii_strings(buf):
|
||||
yield String(s.s), FileOffsetAddress(s.offset)
|
||||
|
||||
for s in capa.features.extractors.strings.extract_unicode_strings(buf):
|
||||
yield String(s.s), FileOffsetAddress(s.offset)
|
||||
|
||||
|
||||
def extract_file_function_names(smda_report, **kwargs):
|
||||
"""
|
||||
extract the names of statically-linked library functions.
|
||||
"""
|
||||
if False:
|
||||
# using a `yield` here to force this to be a generator, not function.
|
||||
yield NotImplementedError("SMDA doesn't have library matching")
|
||||
return
|
||||
|
||||
|
||||
def extract_file_format(buf, **kwargs):
|
||||
yield from capa.features.extractors.common.extract_format(buf)
|
||||
|
||||
|
||||
def extract_features(smda_report, buf):
|
||||
"""
|
||||
extract file features from given workspace
|
||||
|
||||
args:
|
||||
smda_report (smda.common.SmdaReport): a SmdaReport
|
||||
buf: the raw bytes of the sample
|
||||
|
||||
yields:
|
||||
Tuple[Feature, VA]: a feature and its location.
|
||||
"""
|
||||
|
||||
for file_handler in FILE_HANDLERS:
|
||||
for feature, addr in file_handler(smda_report=smda_report, buf=buf):
|
||||
yield feature, addr
|
||||
|
||||
|
||||
FILE_HANDLERS = (
|
||||
extract_file_embedded_pe,
|
||||
extract_file_export_names,
|
||||
extract_file_import_names,
|
||||
extract_file_section_names,
|
||||
extract_file_strings,
|
||||
extract_file_function_names,
|
||||
extract_file_format,
|
||||
)
|
||||
@@ -1,42 +0,0 @@
|
||||
from typing import Tuple, Iterator
|
||||
|
||||
from capa.features.common import Feature, Characteristic
|
||||
from capa.features.address import Address, AbsoluteVirtualAddress
|
||||
from capa.features.extractors import loops
|
||||
from capa.features.extractors.base_extractor import FunctionHandle
|
||||
|
||||
|
||||
def extract_function_calls_to(f: FunctionHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
for inref in f.inner.inrefs:
|
||||
yield Characteristic("calls to"), AbsoluteVirtualAddress(inref)
|
||||
|
||||
|
||||
def extract_function_loop(f: FunctionHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
parse if a function has a loop
|
||||
"""
|
||||
edges = []
|
||||
for bb_from, bb_tos in f.inner.blockrefs.items():
|
||||
for bb_to in bb_tos:
|
||||
edges.append((bb_from, bb_to))
|
||||
|
||||
if edges and loops.has_loop(edges):
|
||||
yield Characteristic("loop"), f.address
|
||||
|
||||
|
||||
def extract_features(f: FunctionHandle):
|
||||
"""
|
||||
extract features from the given function.
|
||||
|
||||
args:
|
||||
f: the function from which to extract features
|
||||
|
||||
yields:
|
||||
Tuple[Feature, Address]: the features and their location found in this function.
|
||||
"""
|
||||
for func_handler in FUNCTION_HANDLERS:
|
||||
for feature, addr in func_handler(f):
|
||||
yield feature, addr
|
||||
|
||||
|
||||
FUNCTION_HANDLERS = (extract_function_calls_to, extract_function_loop)
|
||||
@@ -1,21 +0,0 @@
|
||||
import logging
|
||||
|
||||
from capa.features.common import ARCH_I386, ARCH_AMD64, Arch
|
||||
from capa.features.address import NO_ADDRESS
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def extract_arch(smda_report):
|
||||
if smda_report.architecture == "intel":
|
||||
if smda_report.bitness == 32:
|
||||
yield Arch(ARCH_I386), NO_ADDRESS
|
||||
elif smda_report.bitness == 64:
|
||||
yield Arch(ARCH_AMD64), NO_ADDRESS
|
||||
else:
|
||||
# we likely end up here:
|
||||
# 1. handling a new architecture (e.g. aarch64)
|
||||
#
|
||||
# for (1), this logic will need to be updated as the format is implemented.
|
||||
logger.debug("unsupported architecture: %s", smda_report.architecture)
|
||||
return
|
||||
@@ -1,455 +0,0 @@
|
||||
import re
|
||||
import string
|
||||
import struct
|
||||
from typing import Tuple, Iterator
|
||||
|
||||
import smda
|
||||
|
||||
import capa.features.extractors.helpers
|
||||
from capa.features.insn import API, MAX_STRUCTURE_SIZE, Number, Offset, Mnemonic, OperandNumber, OperandOffset
|
||||
from capa.features.common import MAX_BYTES_FEATURE_SIZE, THUNK_CHAIN_DEPTH_DELTA, Bytes, String, Feature, Characteristic
|
||||
from capa.features.address import Address, AbsoluteVirtualAddress
|
||||
from capa.features.extractors.base_extractor import BBHandle, InsnHandle, FunctionHandle
|
||||
|
||||
# security cookie checks may perform non-zeroing XORs, these are expected within a certain
|
||||
# byte range within the first and returning basic blocks, this helps to reduce FP features
|
||||
SECURITY_COOKIE_BYTES_DELTA = 0x40
|
||||
PATTERN_HEXNUM = re.compile(r"[+\-] (?P<num>0x[a-fA-F0-9]+)")
|
||||
PATTERN_SINGLENUM = re.compile(r"[+\-] (?P<num>[0-9])")
|
||||
|
||||
|
||||
def extract_insn_api_features(fh: FunctionHandle, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""parse API features from the given instruction."""
|
||||
f: smda.Function = fh.inner
|
||||
insn: smda.Insn = ih.inner
|
||||
|
||||
if ih.address in f.apirefs:
|
||||
api_entry = f.apirefs[ih.address]
|
||||
# reformat
|
||||
dll_name, api_name = api_entry.split("!")
|
||||
dll_name = dll_name.split(".")[0]
|
||||
dll_name = dll_name.lower()
|
||||
for name in capa.features.extractors.helpers.generate_symbols(dll_name, api_name):
|
||||
yield API(name), ih.address
|
||||
elif ih.address in f.outrefs:
|
||||
current_function = f
|
||||
current_instruction = insn
|
||||
for index in range(THUNK_CHAIN_DEPTH_DELTA):
|
||||
if current_function and len(current_function.outrefs[current_instruction.offset]) == 1:
|
||||
target = current_function.outrefs[current_instruction.offset][0]
|
||||
referenced_function = current_function.smda_report.getFunction(target)
|
||||
if referenced_function:
|
||||
# TODO SMDA: implement this function for both jmp and call, checking if function has 1 instruction which refs an API
|
||||
if referenced_function.isApiThunk():
|
||||
api_entry = (
|
||||
referenced_function.apirefs[target] if target in referenced_function.apirefs else None
|
||||
)
|
||||
if api_entry:
|
||||
# reformat
|
||||
dll_name, api_name = api_entry.split("!")
|
||||
dll_name = dll_name.split(".")[0]
|
||||
dll_name = dll_name.lower()
|
||||
for name in capa.features.extractors.helpers.generate_symbols(dll_name, api_name):
|
||||
yield API(name), ih.address
|
||||
elif referenced_function.num_instructions == 1 and referenced_function.num_outrefs == 1:
|
||||
current_function = referenced_function
|
||||
current_instruction = [i for i in referenced_function.getInstructions()][0]
|
||||
else:
|
||||
return
|
||||
|
||||
|
||||
def extract_insn_number_features(fh: FunctionHandle, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""parse number features from the given instruction."""
|
||||
# example:
|
||||
#
|
||||
# push 3136B0h ; dwControlCode
|
||||
f: smda.Function = fh.inner
|
||||
insn: smda.Insn = ih.inner
|
||||
|
||||
operands = [o.strip() for o in insn.operands.split(",")]
|
||||
if insn.mnemonic == "add" and operands[0] in ["esp", "rsp"]:
|
||||
# skip things like:
|
||||
#
|
||||
# .text:00401140 call sub_407E2B
|
||||
# .text:00401145 add esp, 0Ch
|
||||
return
|
||||
for i, operand in enumerate(operands):
|
||||
try:
|
||||
# The result of bitwise operations is calculated as though carried out
|
||||
# in two’s complement with an infinite number of sign bits
|
||||
value = int(operand, 16) & ((1 << f.smda_report.bitness) - 1)
|
||||
except ValueError:
|
||||
continue
|
||||
else:
|
||||
yield Number(value), ih.address
|
||||
yield OperandNumber(i, value), ih.address
|
||||
|
||||
if insn.mnemonic == "add" and 0 < value < MAX_STRUCTURE_SIZE:
|
||||
# for pattern like:
|
||||
#
|
||||
# add eax, 0x10
|
||||
#
|
||||
# assume 0x10 is also an offset (imagine eax is a pointer).
|
||||
yield Offset(value), ih.address
|
||||
yield OperandOffset(i, value), ih.address
|
||||
|
||||
|
||||
def read_bytes(smda_report, va, num_bytes=None):
|
||||
"""
|
||||
read up to MAX_BYTES_FEATURE_SIZE from the given address.
|
||||
"""
|
||||
|
||||
rva = va - smda_report.base_addr
|
||||
if smda_report.buffer is None:
|
||||
raise ValueError("buffer is empty")
|
||||
buffer_end = len(smda_report.buffer)
|
||||
max_bytes = num_bytes if num_bytes is not None else MAX_BYTES_FEATURE_SIZE
|
||||
if rva + max_bytes > buffer_end:
|
||||
return smda_report.buffer[rva:]
|
||||
else:
|
||||
return smda_report.buffer[rva : rva + max_bytes]
|
||||
|
||||
|
||||
def derefs(smda_report, p):
|
||||
"""
|
||||
recursively follow the given pointer, yielding the valid memory addresses along the way.
|
||||
useful when you may have a pointer to string, or pointer to pointer to string, etc.
|
||||
|
||||
this is a "do what i mean" type of helper function.
|
||||
|
||||
based on the implementation in viv/insn.py
|
||||
"""
|
||||
depth = 0
|
||||
while True:
|
||||
if not smda_report.isAddrWithinMemoryImage(p):
|
||||
return
|
||||
yield p
|
||||
|
||||
bytes_ = read_bytes(smda_report, p, num_bytes=4)
|
||||
val = struct.unpack("I", bytes_)[0]
|
||||
|
||||
# sanity: pointer points to self
|
||||
if val == p:
|
||||
return
|
||||
|
||||
# sanity: avoid chains of pointers that are unreasonably deep
|
||||
depth += 1
|
||||
if depth > 10:
|
||||
return
|
||||
|
||||
p = val
|
||||
|
||||
|
||||
def extract_insn_bytes_features(fh: FunctionHandle, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
parse byte sequence features from the given instruction.
|
||||
example:
|
||||
# push offset iid_004118d4_IShellLinkA ; riid
|
||||
"""
|
||||
f: smda.Function = fh.inner
|
||||
insn: smda.Insn = ih.inner
|
||||
|
||||
for data_ref in insn.getDataRefs():
|
||||
for v in derefs(f.smda_report, data_ref):
|
||||
bytes_read = read_bytes(f.smda_report, v)
|
||||
if bytes_read is None:
|
||||
continue
|
||||
if capa.features.extractors.helpers.all_zeros(bytes_read):
|
||||
continue
|
||||
|
||||
yield Bytes(bytes_read), ih.address
|
||||
|
||||
|
||||
def detect_ascii_len(smda_report, offset):
|
||||
if smda_report.buffer is None:
|
||||
return 0
|
||||
ascii_len = 0
|
||||
rva = offset - smda_report.base_addr
|
||||
char = smda_report.buffer[rva]
|
||||
while char < 127 and chr(char) in string.printable:
|
||||
ascii_len += 1
|
||||
rva += 1
|
||||
char = smda_report.buffer[rva]
|
||||
if char == 0:
|
||||
return ascii_len
|
||||
return 0
|
||||
|
||||
|
||||
def detect_unicode_len(smda_report, offset):
|
||||
if smda_report.buffer is None:
|
||||
return 0
|
||||
unicode_len = 0
|
||||
rva = offset - smda_report.base_addr
|
||||
char = smda_report.buffer[rva]
|
||||
second_char = smda_report.buffer[rva + 1]
|
||||
while char < 127 and chr(char) in string.printable and second_char == 0:
|
||||
unicode_len += 2
|
||||
rva += 2
|
||||
char = smda_report.buffer[rva]
|
||||
second_char = smda_report.buffer[rva + 1]
|
||||
if char == 0 and second_char == 0:
|
||||
return unicode_len
|
||||
return 0
|
||||
|
||||
|
||||
def read_string(smda_report, offset):
|
||||
alen = detect_ascii_len(smda_report, offset)
|
||||
if alen > 1:
|
||||
return read_bytes(smda_report, offset, alen).decode("utf-8")
|
||||
ulen = detect_unicode_len(smda_report, offset)
|
||||
if ulen > 2:
|
||||
return read_bytes(smda_report, offset, ulen).decode("utf-16")
|
||||
|
||||
|
||||
def extract_insn_string_features(fh: FunctionHandle, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""parse string features from the given instruction."""
|
||||
# example:
|
||||
#
|
||||
# push offset aAcr ; "ACR > "
|
||||
f: smda.Function = fh.inner
|
||||
insn: smda.Insn = ih.inner
|
||||
|
||||
for data_ref in insn.getDataRefs():
|
||||
for v in derefs(f.smda_report, data_ref):
|
||||
string_read = read_string(f.smda_report, v)
|
||||
if string_read:
|
||||
yield String(string_read.rstrip("\x00")), ih.address
|
||||
|
||||
|
||||
def extract_insn_offset_features(f, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""parse structure offset features from the given instruction."""
|
||||
# examples:
|
||||
#
|
||||
# mov eax, [esi + 4]
|
||||
# mov eax, [esi + ecx + 16384]
|
||||
insn: smda.Insn = ih.inner
|
||||
|
||||
operands = [o.strip() for o in insn.operands.split(",")]
|
||||
for i, operand in enumerate(operands):
|
||||
if "esp" in operand or "ebp" in operand or "rbp" in operand:
|
||||
continue
|
||||
|
||||
number = 0
|
||||
number_hex = re.search(PATTERN_HEXNUM, operand)
|
||||
number_int = re.search(PATTERN_SINGLENUM, operand)
|
||||
if number_hex:
|
||||
number = int(number_hex.group("num"), 16)
|
||||
number = -1 * number if number_hex.group().startswith("-") else number
|
||||
elif number_int:
|
||||
number = int(number_int.group("num"))
|
||||
number = -1 * number if number_int.group().startswith("-") else number
|
||||
|
||||
if "ptr" not in operand:
|
||||
if (
|
||||
insn.mnemonic == "lea"
|
||||
and i == 1
|
||||
and (operand.count("+") + operand.count("-")) == 1
|
||||
and operand.count("*") == 0
|
||||
):
|
||||
# for pattern like:
|
||||
#
|
||||
# lea eax, [ebx + 1]
|
||||
#
|
||||
# assume 1 is also an offset (imagine ebx is a zero register).
|
||||
yield Number(number), ih.address
|
||||
yield OperandNumber(i, number), ih.address
|
||||
|
||||
continue
|
||||
|
||||
yield Offset(number), ih.address
|
||||
yield OperandOffset(i, number), ih.address
|
||||
|
||||
|
||||
def is_security_cookie(f, bb, insn):
|
||||
"""
|
||||
check if an instruction is related to security cookie checks
|
||||
"""
|
||||
# security cookie check should use SP or BP
|
||||
operands = [o.strip() for o in insn.operands.split(",")]
|
||||
if operands[1] not in ["esp", "ebp", "rsp", "rbp"]:
|
||||
return False
|
||||
for index, block in enumerate(f.getBlocks()):
|
||||
# expect security cookie init in first basic block within first bytes (instructions)
|
||||
block_instructions = [i for i in block.getInstructions()]
|
||||
if index == 0 and insn.offset < (block_instructions[0].offset + SECURITY_COOKIE_BYTES_DELTA):
|
||||
return True
|
||||
# ... or within last bytes (instructions) before a return
|
||||
if block_instructions[-1].mnemonic.startswith("ret") and insn.offset > (
|
||||
block_instructions[-1].offset - SECURITY_COOKIE_BYTES_DELTA
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def extract_insn_nzxor_characteristic_features(
|
||||
fh: FunctionHandle, bh: BBHandle, ih: InsnHandle
|
||||
) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
parse non-zeroing XOR instruction from the given instruction.
|
||||
ignore expected non-zeroing XORs, e.g. security cookies.
|
||||
"""
|
||||
f: smda.Function = fh.inner
|
||||
bb: smda.BasicBlock = bh.inner
|
||||
insn: smda.Insn = ih.inner
|
||||
|
||||
if insn.mnemonic not in ("xor", "xorpd", "xorps", "pxor"):
|
||||
return
|
||||
|
||||
operands = [o.strip() for o in insn.operands.split(",")]
|
||||
if operands[0] == operands[1]:
|
||||
return
|
||||
|
||||
if is_security_cookie(f, bb, insn):
|
||||
return
|
||||
|
||||
yield Characteristic("nzxor"), ih.address
|
||||
|
||||
|
||||
def extract_insn_mnemonic_features(f, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""parse mnemonic features from the given instruction."""
|
||||
yield Mnemonic(ih.inner.mnemonic), ih.address
|
||||
|
||||
|
||||
def extract_insn_obfs_call_plus_5_characteristic_features(f, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
parse call $+5 instruction from the given instruction.
|
||||
"""
|
||||
insn: smda.Insn = ih.inner
|
||||
|
||||
if insn.mnemonic != "call":
|
||||
return
|
||||
|
||||
if not insn.operands.startswith("0x"):
|
||||
return
|
||||
|
||||
if int(insn.operands, 16) == insn.offset + 5:
|
||||
yield Characteristic("call $+5"), ih.address
|
||||
|
||||
|
||||
def extract_insn_peb_access_characteristic_features(f, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
parse peb access from the given function. fs:[0x30] on x86, gs:[0x60] on x64
|
||||
"""
|
||||
insn: smda.Insn = ih.inner
|
||||
|
||||
if insn.mnemonic not in ["push", "mov"]:
|
||||
return
|
||||
|
||||
operands = [o.strip() for o in insn.operands.split(",")]
|
||||
for operand in operands:
|
||||
if "fs:" in operand and "0x30" in operand:
|
||||
yield Characteristic("peb access"), ih.address
|
||||
elif "gs:" in operand and "0x60" in operand:
|
||||
yield Characteristic("peb access"), ih.address
|
||||
|
||||
|
||||
def extract_insn_segment_access_features(f, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""parse the instruction for access to fs or gs"""
|
||||
insn: smda.Insn = ih.inner
|
||||
|
||||
operands = [o.strip() for o in insn.operands.split(",")]
|
||||
for operand in operands:
|
||||
if "fs:" in operand:
|
||||
yield Characteristic("fs access"), ih.address
|
||||
elif "gs:" in operand:
|
||||
yield Characteristic("gs access"), ih.address
|
||||
|
||||
|
||||
def extract_insn_cross_section_cflow(fh: FunctionHandle, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
inspect the instruction for a CALL or JMP that crosses section boundaries.
|
||||
"""
|
||||
f: smda.Function = fh.inner
|
||||
insn: smda.Insn = ih.inner
|
||||
|
||||
if insn.mnemonic in ["call", "jmp"]:
|
||||
if ih.address in f.apirefs:
|
||||
return
|
||||
|
||||
smda_report = insn.smda_function.smda_report
|
||||
if ih.address in f.outrefs:
|
||||
for target in f.outrefs[ih.address]:
|
||||
if smda_report.getSection(ih.address) != smda_report.getSection(target):
|
||||
yield Characteristic("cross section flow"), ih.address
|
||||
elif insn.operands.startswith("0x"):
|
||||
target = int(insn.operands, 16)
|
||||
if smda_report.getSection(ih.address) != smda_report.getSection(target):
|
||||
yield Characteristic("cross section flow"), ih.address
|
||||
|
||||
|
||||
# this is a feature that's most relevant at the function scope,
|
||||
# however, its most efficient to extract at the instruction scope.
|
||||
def extract_function_calls_from(fh: FunctionHandle, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
f: smda.Function = fh.inner
|
||||
insn: smda.Insn = ih.inner
|
||||
|
||||
if insn.mnemonic != "call":
|
||||
return
|
||||
|
||||
if ih.address in f.outrefs:
|
||||
for outref in f.outrefs[ih.address]:
|
||||
yield Characteristic("calls from"), AbsoluteVirtualAddress(outref)
|
||||
|
||||
if outref == f.offset:
|
||||
# if we found a jump target and it's the function address
|
||||
# mark as recursive
|
||||
yield Characteristic("recursive call"), AbsoluteVirtualAddress(outref)
|
||||
if ih.address in f.apirefs:
|
||||
yield Characteristic("calls from"), ih.address
|
||||
|
||||
|
||||
# this is a feature that's most relevant at the function or basic block scope,
|
||||
# however, its most efficient to extract at the instruction scope.
|
||||
def extract_function_indirect_call_characteristic_features(f, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
extract indirect function call characteristic (e.g., call eax or call dword ptr [edx+4])
|
||||
does not include calls like => call ds:dword_ABD4974
|
||||
"""
|
||||
insn: smda.Insn = ih.inner
|
||||
|
||||
if insn.mnemonic != "call":
|
||||
return
|
||||
if insn.operands.startswith("0x"):
|
||||
return False
|
||||
if "qword ptr" in insn.operands and "rip" in insn.operands:
|
||||
return False
|
||||
if insn.operands.startswith("dword ptr [0x"):
|
||||
return False
|
||||
# call edx
|
||||
# call dword ptr [eax+50h]
|
||||
# call qword ptr [rsp+78h]
|
||||
yield Characteristic("indirect call"), ih.address
|
||||
|
||||
|
||||
def extract_features(f, bb, insn):
|
||||
"""
|
||||
extract features from the given insn.
|
||||
|
||||
args:
|
||||
f: the function to process.
|
||||
bb: the basic block to process.
|
||||
insn: the instruction to process.
|
||||
|
||||
yields:
|
||||
Tuple[Feature, Address]: the features and their location found in this insn.
|
||||
"""
|
||||
for insn_handler in INSTRUCTION_HANDLERS:
|
||||
for feature, addr in insn_handler(f, bb, insn):
|
||||
yield feature, addr
|
||||
|
||||
|
||||
INSTRUCTION_HANDLERS = (
|
||||
extract_insn_api_features,
|
||||
extract_insn_number_features,
|
||||
extract_insn_string_features,
|
||||
extract_insn_bytes_features,
|
||||
extract_insn_offset_features,
|
||||
extract_insn_nzxor_characteristic_features,
|
||||
extract_insn_mnemonic_features,
|
||||
extract_insn_obfs_call_plus_5_characteristic_features,
|
||||
extract_insn_peb_access_characteristic_features,
|
||||
extract_insn_cross_section_cflow,
|
||||
extract_insn_segment_access_features,
|
||||
extract_function_calls_from,
|
||||
extract_function_indirect_call_characteristic_features,
|
||||
)
|
||||
@@ -31,7 +31,7 @@ def interface_extract_basic_block_XXX(f: FunctionHandle, bb: BBHandle) -> Iterat
|
||||
yields:
|
||||
(Feature, Address): the feature and the address at which its found.
|
||||
"""
|
||||
...
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def _bb_has_tight_loop(f, bb):
|
||||
@@ -121,7 +121,7 @@ def get_printable_len(oper: envi.archs.i386.disasm.i386ImmOper) -> int:
|
||||
elif oper.tsize == 8:
|
||||
chars = struct.pack("<Q", oper.imm)
|
||||
else:
|
||||
raise ValueError("unexpected oper.tsize: %d" % (oper.tsize))
|
||||
raise ValueError(f"unexpected oper.tsize: {oper.tsize}")
|
||||
|
||||
if is_printable_ascii(chars):
|
||||
return oper.tsize
|
||||
|
||||
@@ -25,8 +25,8 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class VivisectFeatureExtractor(FeatureExtractor):
|
||||
def __init__(self, vw, path):
|
||||
super(VivisectFeatureExtractor, self).__init__()
|
||||
def __init__(self, vw, path, os):
|
||||
super().__init__()
|
||||
self.vw = vw
|
||||
self.path = path
|
||||
with open(self.path, "rb") as f:
|
||||
@@ -34,7 +34,8 @@ class VivisectFeatureExtractor(FeatureExtractor):
|
||||
|
||||
# pre-compute these because we'll yield them at *every* scope.
|
||||
self.global_features: List[Tuple[Feature, Address]] = []
|
||||
self.global_features.extend(capa.features.extractors.common.extract_os(self.buf))
|
||||
self.global_features.extend(capa.features.extractors.viv.file.extract_file_format(self.buf))
|
||||
self.global_features.extend(capa.features.extractors.common.extract_os(self.buf, os))
|
||||
self.global_features.extend(capa.features.extractors.viv.global_.extract_arch(self.vw))
|
||||
|
||||
def get_base_address(self):
|
||||
|
||||
@@ -44,7 +44,7 @@ def extract_file_import_names(vw, **kwargs) -> Iterator[Tuple[Feature, Address]]
|
||||
modname, impname = tinfo.split(".", 1)
|
||||
if is_viv_ord_impname(impname):
|
||||
# replace ord prefix with #
|
||||
impname = "#%s" % impname[len("ord") :]
|
||||
impname = "#" + impname[len("ord") :]
|
||||
|
||||
addr = AbsoluteVirtualAddress(va)
|
||||
for name in capa.features.extractors.helpers.generate_symbols(modname, impname):
|
||||
|
||||
@@ -27,7 +27,7 @@ def interface_extract_function_XXX(fh: FunctionHandle) -> Iterator[Tuple[Feature
|
||||
yields:
|
||||
(Feature, Address): the feature and the address at which its found.
|
||||
"""
|
||||
...
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def extract_function_calls_to(fhandle: FunctionHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
@@ -47,6 +47,10 @@ def extract_function_loop(fhandle: FunctionHandle) -> Iterator[Tuple[Feature, Ad
|
||||
for bb in f.basic_blocks:
|
||||
if len(bb.instructions) > 0:
|
||||
for bva, bflags in bb.instructions[-1].getBranches():
|
||||
if bva is None:
|
||||
# vivisect may be unable to recover the call target, e.g. on dynamic calls like `call esi`
|
||||
# for this bva is None, and we don't want to add it for loop detection, ref: vivisect#574
|
||||
continue
|
||||
# vivisect does not set branch flags for non-conditional jmp so add explicit check
|
||||
if (
|
||||
bflags & envi.BR_COND
|
||||
|
||||
@@ -42,7 +42,7 @@ def get_previous_instructions(vw: VivWorkspace, va: int) -> List[int]:
|
||||
ret = []
|
||||
|
||||
# find the immediate prior instruction.
|
||||
# ensure that it fallsthrough to this one.
|
||||
# ensure that it falls through to this one.
|
||||
loc = vw.getPrevLocation(va, adjacent=True)
|
||||
if loc is not None:
|
||||
ploc = vw.getPrevLocation(va, adjacent=True)
|
||||
@@ -59,7 +59,7 @@ def get_previous_instructions(vw: VivWorkspace, va: int) -> List[int]:
|
||||
#
|
||||
# from vivisect.const:
|
||||
# xref: (XR_FROM, XR_TO, XR_RTYPE, XR_RFLAG)
|
||||
for (xfrom, _, _, xflag) in vw.getXrefsTo(va, REF_CODE):
|
||||
for xfrom, _, _, xflag in vw.getXrefsTo(va, REF_CODE):
|
||||
if (xflag & FAR_BRANCH_MASK) != 0:
|
||||
continue
|
||||
ret.append(xfrom)
|
||||
|
||||
@@ -44,7 +44,7 @@ def interface_extract_instruction_XXX(
|
||||
yields:
|
||||
(Feature, Address): the feature and the address at which its found.
|
||||
"""
|
||||
...
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def get_imports(vw):
|
||||
@@ -175,8 +175,13 @@ def derefs(vw, p):
|
||||
while True:
|
||||
if not vw.isValidPointer(p):
|
||||
return
|
||||
|
||||
yield p
|
||||
|
||||
if vw.isProbablyString(p) or vw.isProbablyUnicode(p):
|
||||
# don't deref strings that coincidentally are pointers
|
||||
return
|
||||
|
||||
try:
|
||||
next = vw.readMemoryPtr(p)
|
||||
except Exception:
|
||||
@@ -271,6 +276,10 @@ def extract_insn_bytes_features(fh: FunctionHandle, bb, ih: InsnHandle) -> Itera
|
||||
if capa.features.extractors.helpers.all_zeros(buf):
|
||||
continue
|
||||
|
||||
if f.vw.isProbablyString(v) or f.vw.isProbablyUnicode(v):
|
||||
# don't extract byte features for obvious strings
|
||||
continue
|
||||
|
||||
yield Bytes(buf), ih.address
|
||||
|
||||
|
||||
@@ -281,7 +290,12 @@ def read_string(vw, offset: int) -> str:
|
||||
pass
|
||||
else:
|
||||
if alen > 0:
|
||||
return read_memory(vw, offset, alen).decode("utf-8")
|
||||
buf = read_memory(vw, offset, alen)
|
||||
if b"\x00" in buf:
|
||||
# account for bug #1271.
|
||||
# remove when vivisect is fixed.
|
||||
buf = buf.partition(b"\x00")[0]
|
||||
return buf.decode("utf-8")
|
||||
|
||||
try:
|
||||
ulen = vw.detectUnicode(offset)
|
||||
@@ -300,7 +314,9 @@ def read_string(vw, offset: int) -> str:
|
||||
# vivisect seems to mis-detect the end unicode strings
|
||||
# off by two, too short
|
||||
ulen += 2
|
||||
return read_memory(vw, offset, ulen).decode("utf-16")
|
||||
# partition to account for bug #1271.
|
||||
# remove when vivisect is fixed.
|
||||
return read_memory(vw, offset, ulen).decode("utf-16").partition("\x00")[0]
|
||||
|
||||
raise ValueError("not a string", offset)
|
||||
|
||||
@@ -493,7 +509,8 @@ def extract_function_calls_from(fh: FunctionHandle, bb, ih: InsnHandle) -> Itera
|
||||
if isinstance(insn.opers[0], envi.archs.i386.disasm.i386ImmMemOper):
|
||||
oper = insn.opers[0]
|
||||
target = oper.getOperAddr(insn)
|
||||
yield Characteristic("calls from"), AbsoluteVirtualAddress(target)
|
||||
if target >= 0:
|
||||
yield Characteristic("calls from"), AbsoluteVirtualAddress(target)
|
||||
|
||||
# call via thunk on x86,
|
||||
# see 9324d1a8ae37a36ae560c37448c9705a at 0x407985
|
||||
@@ -509,7 +526,8 @@ def extract_function_calls_from(fh: FunctionHandle, bb, ih: InsnHandle) -> Itera
|
||||
elif isinstance(insn.opers[0], envi.archs.amd64.disasm.Amd64RipRelOper):
|
||||
op = insn.opers[0]
|
||||
target = op.getOperAddr(insn)
|
||||
yield Characteristic("calls from"), AbsoluteVirtualAddress(target)
|
||||
if target >= 0:
|
||||
yield Characteristic("calls from"), AbsoluteVirtualAddress(target)
|
||||
|
||||
if target and target == f.va:
|
||||
# if we found a jump target and it's the function address
|
||||
@@ -663,11 +681,12 @@ def extract_op_string_features(
|
||||
|
||||
for v in derefs(f.vw, v):
|
||||
try:
|
||||
s = read_string(f.vw, v)
|
||||
s = read_string(f.vw, v).rstrip("\x00")
|
||||
except ValueError:
|
||||
continue
|
||||
else:
|
||||
yield String(s.rstrip("\x00")), ih.address
|
||||
if len(s) >= 4:
|
||||
yield String(s), ih.address
|
||||
|
||||
|
||||
def extract_operand_features(f: FunctionHandle, bb, insn: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
|
||||
@@ -12,19 +12,19 @@ from capa.features.common import Feature
|
||||
class Export(Feature):
|
||||
def __init__(self, value: str, description=None):
|
||||
# value is export name
|
||||
super(Export, self).__init__(value, description=description)
|
||||
super().__init__(value, description=description)
|
||||
|
||||
|
||||
class Import(Feature):
|
||||
def __init__(self, value: str, description=None):
|
||||
# value is import name
|
||||
super(Import, self).__init__(value, description=description)
|
||||
super().__init__(value, description=description)
|
||||
|
||||
|
||||
class Section(Feature):
|
||||
def __init__(self, value: str, description=None):
|
||||
# value is section name
|
||||
super(Section, self).__init__(value, description=description)
|
||||
super().__init__(value, description=description)
|
||||
|
||||
|
||||
class FunctionName(Feature):
|
||||
@@ -32,7 +32,7 @@ class FunctionName(Feature):
|
||||
|
||||
def __init__(self, name: str, description=None):
|
||||
# value is function name
|
||||
super(FunctionName, self).__init__(name, description=description)
|
||||
super().__init__(name, description=description)
|
||||
# override the name property set by `capa.features.Feature`
|
||||
# that would be `functionname` (note missing dash)
|
||||
self.name = "function-name"
|
||||
|
||||
@@ -12,9 +12,8 @@ See the License for the specific language governing permissions and limitations
|
||||
import zlib
|
||||
import logging
|
||||
from enum import Enum
|
||||
from typing import Any, List, Tuple
|
||||
from typing import Any, List, Tuple, Union
|
||||
|
||||
import dncil.clr.token
|
||||
from pydantic import Field, BaseModel
|
||||
|
||||
import capa.helpers
|
||||
@@ -47,7 +46,7 @@ class AddressType(str, Enum):
|
||||
|
||||
class Address(HashableModel):
|
||||
type: AddressType
|
||||
value: Any
|
||||
value: Union[int, Tuple[int, int], None]
|
||||
|
||||
@classmethod
|
||||
def from_capa(cls, a: capa.features.address.Address) -> "Address":
|
||||
@@ -61,10 +60,10 @@ class Address(HashableModel):
|
||||
return cls(type=AddressType.FILE, value=int(a))
|
||||
|
||||
elif isinstance(a, capa.features.address.DNTokenAddress):
|
||||
return cls(type=AddressType.DN_TOKEN, value=a.token.value)
|
||||
return cls(type=AddressType.DN_TOKEN, value=int(a))
|
||||
|
||||
elif isinstance(a, capa.features.address.DNTokenOffsetAddress):
|
||||
return cls(type=AddressType.DN_TOKEN_OFFSET, value=(a.token.value, a.offset))
|
||||
return cls(type=AddressType.DN_TOKEN_OFFSET, value=(a.token, a.offset))
|
||||
|
||||
elif a == capa.features.address.NO_ADDRESS or isinstance(a, capa.features.address._NoAddress):
|
||||
return cls(type=AddressType.NO_ADDRESS, value=None)
|
||||
@@ -80,20 +79,27 @@ class Address(HashableModel):
|
||||
|
||||
def to_capa(self) -> capa.features.address.Address:
|
||||
if self.type is AddressType.ABSOLUTE:
|
||||
assert isinstance(self.value, int)
|
||||
return capa.features.address.AbsoluteVirtualAddress(self.value)
|
||||
|
||||
elif self.type is AddressType.RELATIVE:
|
||||
assert isinstance(self.value, int)
|
||||
return capa.features.address.RelativeVirtualAddress(self.value)
|
||||
|
||||
elif self.type is AddressType.FILE:
|
||||
assert isinstance(self.value, int)
|
||||
return capa.features.address.FileOffsetAddress(self.value)
|
||||
|
||||
elif self.type is AddressType.DN_TOKEN:
|
||||
return capa.features.address.DNTokenAddress(dncil.clr.token.Token(self.value))
|
||||
assert isinstance(self.value, int)
|
||||
return capa.features.address.DNTokenAddress(self.value)
|
||||
|
||||
elif self.type is AddressType.DN_TOKEN_OFFSET:
|
||||
assert isinstance(self.value, tuple)
|
||||
token, offset = self.value
|
||||
return capa.features.address.DNTokenOffsetAddress(dncil.clr.token.Token(token), offset)
|
||||
assert isinstance(token, int)
|
||||
assert isinstance(offset, int)
|
||||
return capa.features.address.DNTokenOffsetAddress(token, offset)
|
||||
|
||||
elif self.type is AddressType.NO_ADDRESS:
|
||||
return capa.features.address.NO_ADDRESS
|
||||
@@ -109,7 +115,11 @@ class Address(HashableModel):
|
||||
return True
|
||||
|
||||
else:
|
||||
return self.value < other.value
|
||||
assert self.type == other.type
|
||||
# mypy doesn't realize we've proven that either
|
||||
# both are ints, or both are tuples of ints.
|
||||
# and both of these are comparable.
|
||||
return self.value < other.value # type: ignore
|
||||
|
||||
|
||||
class GlobalFeature(HashableModel):
|
||||
@@ -146,10 +156,13 @@ class BasicBlockFeature(HashableModel):
|
||||
versus right at its starting address.
|
||||
"""
|
||||
|
||||
basic_block: Address
|
||||
basic_block: Address = Field(alias="basic block")
|
||||
address: Address
|
||||
feature: Feature
|
||||
|
||||
class Config:
|
||||
allow_population_by_field_name = True
|
||||
|
||||
|
||||
class InstructionFeature(HashableModel):
|
||||
"""
|
||||
@@ -180,7 +193,7 @@ class BasicBlockFeatures(BaseModel):
|
||||
class FunctionFeatures(BaseModel):
|
||||
address: Address
|
||||
features: Tuple[FunctionFeature, ...]
|
||||
basic_blocks: Tuple[BasicBlockFeatures, ...] = Field(alias="basic block")
|
||||
basic_blocks: Tuple[BasicBlockFeatures, ...] = Field(alias="basic blocks")
|
||||
|
||||
class Config:
|
||||
allow_population_by_field_name = True
|
||||
@@ -255,7 +268,8 @@ def dumps(extractor: capa.features.extractors.base_extractor.FeatureExtractor) -
|
||||
basic_block=bbaddr,
|
||||
address=Address.from_capa(addr),
|
||||
feature=feature_from_capa(feature),
|
||||
)
|
||||
) # type: ignore
|
||||
# Mypy is unable to recognise `basic_block` as a argument due to alias
|
||||
for feature, addr in extractor.extract_basic_block_features(f, bb)
|
||||
]
|
||||
|
||||
@@ -274,38 +288,41 @@ def dumps(extractor: capa.features.extractors.base_extractor.FeatureExtractor) -
|
||||
instructions.append(
|
||||
InstructionFeatures(
|
||||
address=iaddr,
|
||||
features=ifeatures,
|
||||
features=tuple(ifeatures),
|
||||
)
|
||||
)
|
||||
|
||||
basic_blocks.append(
|
||||
BasicBlockFeatures(
|
||||
address=bbaddr,
|
||||
features=bbfeatures,
|
||||
instructions=instructions,
|
||||
features=tuple(bbfeatures),
|
||||
instructions=tuple(instructions),
|
||||
)
|
||||
)
|
||||
|
||||
function_features.append(
|
||||
FunctionFeatures(
|
||||
address=faddr,
|
||||
features=ffeatures,
|
||||
features=tuple(ffeatures),
|
||||
basic_blocks=basic_blocks,
|
||||
)
|
||||
) # type: ignore
|
||||
# Mypy is unable to recognise `basic_blocks` as a argument due to alias
|
||||
)
|
||||
|
||||
features = Features(
|
||||
global_=global_features,
|
||||
file=file_features,
|
||||
functions=function_features,
|
||||
)
|
||||
file=tuple(file_features),
|
||||
functions=tuple(function_features),
|
||||
) # type: ignore
|
||||
# Mypy is unable to recognise `global_` as a argument due to alias
|
||||
|
||||
freeze = Freeze(
|
||||
version=2,
|
||||
base_address=Address.from_capa(extractor.get_base_address()),
|
||||
extractor=Extractor(name=extractor.__class__.__name__),
|
||||
features=features,
|
||||
)
|
||||
) # type: ignore
|
||||
# Mypy is unable to recognise `base_address` as a argument due to alias
|
||||
|
||||
return freeze.json()
|
||||
|
||||
@@ -316,7 +333,7 @@ def loads(s: str) -> capa.features.extractors.base_extractor.FeatureExtractor:
|
||||
|
||||
freeze = Freeze.parse_raw(s)
|
||||
if freeze.version != 2:
|
||||
raise ValueError("unsupported freeze format version: %d", freeze.version)
|
||||
raise ValueError(f"unsupported freeze format version: {freeze.version}")
|
||||
|
||||
return null.NullFeatureExtractor(
|
||||
base_address=freeze.base_address.to_capa(),
|
||||
@@ -372,14 +389,14 @@ def main(argv=None):
|
||||
argv = sys.argv[1:]
|
||||
|
||||
parser = argparse.ArgumentParser(description="save capa features to a file")
|
||||
capa.main.install_common_args(parser, {"sample", "format", "backend", "signatures"})
|
||||
capa.main.install_common_args(parser, {"sample", "format", "backend", "os", "signatures"})
|
||||
parser.add_argument("output", type=str, help="Path to output file")
|
||||
args = parser.parse_args(args=argv)
|
||||
capa.main.handle_common_args(args)
|
||||
|
||||
sigpaths = capa.main.get_signatures(args.signatures)
|
||||
|
||||
extractor = capa.main.get_extractor(args.sample, args.format, args.backend, sigpaths, False)
|
||||
extractor = capa.main.get_extractor(args.sample, args.format, args.os, args.backend, sigpaths, False)
|
||||
|
||||
with open(args.output, "wb") as f:
|
||||
f.write(dump(extractor))
|
||||
|
||||
@@ -66,6 +66,9 @@ class FeatureModel(BaseModel):
|
||||
elif isinstance(self, APIFeature):
|
||||
return capa.features.insn.API(self.api, description=self.description)
|
||||
|
||||
elif isinstance(self, PropertyFeature):
|
||||
return capa.features.insn.Property(self.property, access=self.access, description=self.description)
|
||||
|
||||
elif isinstance(self, NumberFeature):
|
||||
return capa.features.insn.Number(self.number, description=self.description)
|
||||
|
||||
@@ -98,56 +101,79 @@ class FeatureModel(BaseModel):
|
||||
|
||||
def feature_from_capa(f: capa.features.common.Feature) -> "Feature":
|
||||
if isinstance(f, capa.features.common.OS):
|
||||
assert isinstance(f.value, str)
|
||||
return OSFeature(os=f.value, description=f.description)
|
||||
|
||||
elif isinstance(f, capa.features.common.Arch):
|
||||
assert isinstance(f.value, str)
|
||||
return ArchFeature(arch=f.value, description=f.description)
|
||||
|
||||
elif isinstance(f, capa.features.common.Format):
|
||||
assert isinstance(f.value, str)
|
||||
return FormatFeature(format=f.value, description=f.description)
|
||||
|
||||
elif isinstance(f, capa.features.common.MatchedRule):
|
||||
assert isinstance(f.value, str)
|
||||
return MatchFeature(match=f.value, description=f.description)
|
||||
|
||||
elif isinstance(f, capa.features.common.Characteristic):
|
||||
assert isinstance(f.value, str)
|
||||
return CharacteristicFeature(characteristic=f.value, description=f.description)
|
||||
|
||||
elif isinstance(f, capa.features.file.Export):
|
||||
assert isinstance(f.value, str)
|
||||
return ExportFeature(export=f.value, description=f.description)
|
||||
|
||||
elif isinstance(f, capa.features.file.Import):
|
||||
return ImportFeature(import_=f.value, description=f.description)
|
||||
assert isinstance(f.value, str)
|
||||
return ImportFeature(import_=f.value, description=f.description) # type: ignore
|
||||
# Mypy is unable to recognise `import_` as a argument due to alias
|
||||
|
||||
elif isinstance(f, capa.features.file.Section):
|
||||
assert isinstance(f.value, str)
|
||||
return SectionFeature(section=f.value, description=f.description)
|
||||
|
||||
elif isinstance(f, capa.features.file.FunctionName):
|
||||
return FunctionNameFeature(function_name=f.value, description=f.description)
|
||||
assert isinstance(f.value, str)
|
||||
return FunctionNameFeature(function_name=f.value, description=f.description) # type: ignore
|
||||
# Mypy is unable to recognise `function_name` as a argument due to alias
|
||||
|
||||
# must come before check for String due to inheritance
|
||||
elif isinstance(f, capa.features.common.Substring):
|
||||
assert isinstance(f.value, str)
|
||||
return SubstringFeature(substring=f.value, description=f.description)
|
||||
|
||||
# must come before check for String due to inheritance
|
||||
elif isinstance(f, capa.features.common.Regex):
|
||||
assert isinstance(f.value, str)
|
||||
return RegexFeature(regex=f.value, description=f.description)
|
||||
|
||||
elif isinstance(f, capa.features.common.String):
|
||||
assert isinstance(f.value, str)
|
||||
return StringFeature(string=f.value, description=f.description)
|
||||
|
||||
elif isinstance(f, capa.features.common.Class):
|
||||
return ClassFeature(class_=f.value, description=f.description)
|
||||
assert isinstance(f.value, str)
|
||||
return ClassFeature(class_=f.value, description=f.description) # type: ignore
|
||||
# Mypy is unable to recognise `class_` as a argument due to alias
|
||||
|
||||
elif isinstance(f, capa.features.common.Namespace):
|
||||
assert isinstance(f.value, str)
|
||||
return NamespaceFeature(namespace=f.value, description=f.description)
|
||||
|
||||
elif isinstance(f, capa.features.basicblock.BasicBlock):
|
||||
return BasicBlockFeature(description=f.description)
|
||||
|
||||
elif isinstance(f, capa.features.insn.API):
|
||||
assert isinstance(f.value, str)
|
||||
return APIFeature(api=f.value, description=f.description)
|
||||
|
||||
elif isinstance(f, capa.features.insn.Property):
|
||||
assert isinstance(f.value, str)
|
||||
return PropertyFeature(property=f.value, access=f.access, description=f.description)
|
||||
|
||||
elif isinstance(f, capa.features.insn.Number):
|
||||
assert isinstance(f.value, (int, float))
|
||||
return NumberFeature(number=f.value, description=f.description)
|
||||
|
||||
elif isinstance(f, capa.features.common.Bytes):
|
||||
@@ -156,16 +182,22 @@ def feature_from_capa(f: capa.features.common.Feature) -> "Feature":
|
||||
return BytesFeature(bytes=binascii.hexlify(buf).decode("ascii"), description=f.description)
|
||||
|
||||
elif isinstance(f, capa.features.insn.Offset):
|
||||
assert isinstance(f.value, int)
|
||||
return OffsetFeature(offset=f.value, description=f.description)
|
||||
|
||||
elif isinstance(f, capa.features.insn.Mnemonic):
|
||||
assert isinstance(f.value, str)
|
||||
return MnemonicFeature(mnemonic=f.value, description=f.description)
|
||||
|
||||
elif isinstance(f, capa.features.insn.OperandNumber):
|
||||
return OperandNumberFeature(index=f.index, operand_number=f.value, description=f.description)
|
||||
assert isinstance(f.value, int)
|
||||
return OperandNumberFeature(index=f.index, operand_number=f.value, description=f.description) # type: ignore
|
||||
# Mypy is unable to recognise `operand_number` as a argument due to alias
|
||||
|
||||
elif isinstance(f, capa.features.insn.OperandOffset):
|
||||
return OperandOffsetFeature(index=f.index, operand_offset=f.value, description=f.description)
|
||||
assert isinstance(f.value, int)
|
||||
return OperandOffsetFeature(index=f.index, operand_offset=f.value, description=f.description) # type: ignore
|
||||
# Mypy is unable to recognise `operand_offset` as a argument due to alias
|
||||
|
||||
else:
|
||||
raise NotImplementedError(f"feature_from_capa({type(f)}) not implemented")
|
||||
@@ -266,6 +298,13 @@ class APIFeature(FeatureModel):
|
||||
description: Optional[str]
|
||||
|
||||
|
||||
class PropertyFeature(FeatureModel):
|
||||
type: str = "property"
|
||||
access: Optional[str]
|
||||
property: str
|
||||
description: Optional[str]
|
||||
|
||||
|
||||
class NumberFeature(FeatureModel):
|
||||
type: str = "number"
|
||||
number: Union[int, float]
|
||||
@@ -320,13 +359,13 @@ Feature = Union[
|
||||
ClassFeature,
|
||||
NamespaceFeature,
|
||||
APIFeature,
|
||||
PropertyFeature,
|
||||
NumberFeature,
|
||||
BytesFeature,
|
||||
OffsetFeature,
|
||||
MnemonicFeature,
|
||||
OperandNumberFeature,
|
||||
OperandOffsetFeature,
|
||||
# this has to go last because...? pydantic fails to serialize correctly otherwise.
|
||||
# possibly because this feature has no associated value?
|
||||
# Note! this must be last, see #1161
|
||||
BasicBlockFeature,
|
||||
]
|
||||
|
||||
@@ -6,35 +6,71 @@
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
import abc
|
||||
from typing import Union
|
||||
from typing import Union, Optional
|
||||
|
||||
from capa.features.common import Feature
|
||||
import capa.helpers
|
||||
from capa.features.common import VALID_FEATURE_ACCESS, Feature
|
||||
|
||||
|
||||
def hex(n: int) -> str:
|
||||
"""render the given number using upper case hex, like: 0x123ABC"""
|
||||
if n < 0:
|
||||
return "-0x%X" % (-n)
|
||||
return f"-0x{(-n):X}"
|
||||
else:
|
||||
return "0x%X" % n
|
||||
return f"0x{(n):X}"
|
||||
|
||||
|
||||
class API(Feature):
|
||||
def __init__(self, name: str, description=None):
|
||||
super(API, self).__init__(name, description=description)
|
||||
super().__init__(name, description=description)
|
||||
|
||||
|
||||
class _AccessFeature(Feature, abc.ABC):
|
||||
# superclass: don't use directly
|
||||
def __init__(self, value: str, access: Optional[str] = None, description: Optional[str] = None):
|
||||
super().__init__(value, description=description)
|
||||
if access is not None:
|
||||
if access not in VALID_FEATURE_ACCESS:
|
||||
raise ValueError(f"{self.name} access type {access} not valid")
|
||||
self.access = access
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.name, self.value, self.access))
|
||||
|
||||
def __eq__(self, other):
|
||||
return super().__eq__(other) and self.access == other.access
|
||||
|
||||
def get_name_str(self) -> str:
|
||||
if self.access is not None:
|
||||
return f"{self.name}/{self.access}"
|
||||
return self.name
|
||||
|
||||
|
||||
class Property(_AccessFeature):
|
||||
def __init__(self, value: str, access: Optional[str] = None, description=None):
|
||||
super().__init__(value, access=access, description=description)
|
||||
|
||||
|
||||
class Number(Feature):
|
||||
def __init__(self, value: Union[int, float], description=None):
|
||||
super(Number, self).__init__(value, description=description)
|
||||
"""
|
||||
args:
|
||||
value (int or float): positive or negative integer, or floating point number.
|
||||
|
||||
the range of the value is:
|
||||
- if positive, the range of u64
|
||||
- if negative, the range of i64
|
||||
- if floating, the range and precision of double
|
||||
"""
|
||||
super().__init__(value, description=description)
|
||||
|
||||
def get_value_str(self):
|
||||
if isinstance(self.value, int):
|
||||
return hex(self.value)
|
||||
return capa.helpers.hex(self.value)
|
||||
elif isinstance(self.value, float):
|
||||
return str(self.value)
|
||||
else:
|
||||
raise ValueError("invalid value type")
|
||||
raise ValueError(f"invalid value type {type(self.value)}")
|
||||
|
||||
|
||||
# max recognized structure size (and therefore, offset size)
|
||||
@@ -43,18 +79,27 @@ MAX_STRUCTURE_SIZE = 0x10000
|
||||
|
||||
class Offset(Feature):
|
||||
def __init__(self, value: int, description=None):
|
||||
super(Offset, self).__init__(value, description=description)
|
||||
"""
|
||||
args:
|
||||
value (int): the offset, which can be positive or negative.
|
||||
|
||||
the range of the value is:
|
||||
- if positive, the range of u64
|
||||
- if negative, the range of i64
|
||||
"""
|
||||
super().__init__(value, description=description)
|
||||
|
||||
def get_value_str(self):
|
||||
assert isinstance(self.value, int)
|
||||
return hex(self.value)
|
||||
|
||||
|
||||
class Mnemonic(Feature):
|
||||
def __init__(self, value: str, description=None):
|
||||
super(Mnemonic, self).__init__(value, description=description)
|
||||
super().__init__(value, description=description)
|
||||
|
||||
|
||||
# max number of operands to consider for a given instrucion.
|
||||
# max number of operands to consider for a given instruction.
|
||||
# since we only support Intel and .NET, we can assume this is 3
|
||||
# which covers cases up to e.g. "vinserti128 ymm0,ymm0,ymm5,1"
|
||||
MAX_OPERAND_COUNT = 4
|
||||
@@ -64,8 +109,8 @@ MAX_OPERAND_INDEX = MAX_OPERAND_COUNT - 1
|
||||
class _Operand(Feature, abc.ABC):
|
||||
# superclass: don't use directly
|
||||
# subclasses should set self.name and provide the value string formatter
|
||||
def __init__(self, index: int, value: int, description=None):
|
||||
super(_Operand, self).__init__(value, description=description)
|
||||
def __init__(self, index: int, value: Union[int, float], description=None):
|
||||
super().__init__(value, description=description)
|
||||
self.index = index
|
||||
|
||||
def __hash__(self):
|
||||
@@ -77,25 +122,46 @@ class _Operand(Feature, abc.ABC):
|
||||
|
||||
class OperandNumber(_Operand):
|
||||
# cached names so we don't do extra string formatting every ctor
|
||||
NAMES = ["operand[%d].number" % i for i in range(MAX_OPERAND_COUNT)]
|
||||
NAMES = [f"operand[{i}].number" for i in range(MAX_OPERAND_COUNT)]
|
||||
|
||||
# operand[i].number: 0x12
|
||||
def __init__(self, index: int, value: int, description=None):
|
||||
super(OperandNumber, self).__init__(index, value, description=description)
|
||||
def __init__(self, index: int, value: Union[int, float], description=None):
|
||||
"""
|
||||
args:
|
||||
value (int or float): positive or negative integer, or floating point number.
|
||||
|
||||
the range of the value is:
|
||||
- if positive, the range of u64
|
||||
- if negative, the range of i64
|
||||
- if floating, the range and precision of double
|
||||
"""
|
||||
super().__init__(index, value, description=description)
|
||||
self.name = self.NAMES[index]
|
||||
|
||||
def get_value_str(self) -> str:
|
||||
assert isinstance(self.value, int)
|
||||
return hex(self.value)
|
||||
if isinstance(self.value, int):
|
||||
return capa.helpers.hex(self.value)
|
||||
elif isinstance(self.value, float):
|
||||
return str(self.value)
|
||||
else:
|
||||
raise ValueError("invalid value type")
|
||||
|
||||
|
||||
class OperandOffset(_Operand):
|
||||
# cached names so we don't do extra string formatting every ctor
|
||||
NAMES = ["operand[%d].offset" % i for i in range(MAX_OPERAND_COUNT)]
|
||||
NAMES = [f"operand[{i}].offset" for i in range(MAX_OPERAND_COUNT)]
|
||||
|
||||
# operand[i].offset: 0x12
|
||||
def __init__(self, index: int, value: int, description=None):
|
||||
super(OperandOffset, self).__init__(index, value, description=description)
|
||||
"""
|
||||
args:
|
||||
value (int): the offset, which can be positive or negative.
|
||||
|
||||
the range of the value is:
|
||||
- if positive, the range of u64
|
||||
- if negative, the range of i64
|
||||
"""
|
||||
super().__init__(index, value, description=description)
|
||||
self.name = self.NAMES[index]
|
||||
|
||||
def get_value_str(self) -> str:
|
||||
|
||||
@@ -10,7 +10,7 @@ import logging
|
||||
from typing import NoReturn
|
||||
|
||||
from capa.exceptions import UnsupportedFormatError
|
||||
from capa.features.common import FORMAT_SC32, FORMAT_SC64, FORMAT_UNKNOWN
|
||||
from capa.features.common import FORMAT_PE, FORMAT_SC32, FORMAT_SC64, FORMAT_DOTNET, FORMAT_UNKNOWN, Format
|
||||
|
||||
EXTENSIONS_SHELLCODE_32 = ("sc32", "raw32")
|
||||
EXTENSIONS_SHELLCODE_64 = ("sc64", "raw64")
|
||||
@@ -18,16 +18,18 @@ EXTENSIONS_ELF = "elf_"
|
||||
|
||||
logger = logging.getLogger("capa")
|
||||
|
||||
_hex = hex
|
||||
|
||||
|
||||
def hex(i):
|
||||
return _hex(int(i))
|
||||
def hex(n: int) -> str:
|
||||
"""render the given number using upper case hex, like: 0x123ABC"""
|
||||
if n < 0:
|
||||
return f"-0x{(-n):X}"
|
||||
else:
|
||||
return f"0x{(n):X}"
|
||||
|
||||
|
||||
def get_file_taste(sample_path: str) -> bytes:
|
||||
if not os.path.exists(sample_path):
|
||||
raise IOError("sample path %s does not exist or cannot be accessed" % sample_path)
|
||||
raise IOError(f"sample path {sample_path} does not exist or cannot be accessed")
|
||||
with open(sample_path, "rb") as f:
|
||||
taste = f.read(8)
|
||||
return taste
|
||||
@@ -42,7 +44,7 @@ def is_runtime_ida():
|
||||
return True
|
||||
|
||||
|
||||
def assert_never(value: NoReturn) -> NoReturn:
|
||||
def assert_never(value) -> NoReturn:
|
||||
assert False, f"Unhandled value: {value} ({type(value).__name__})"
|
||||
|
||||
|
||||
@@ -66,11 +68,17 @@ def get_auto_format(path: str) -> str:
|
||||
def get_format(sample: str) -> str:
|
||||
# imported locally to avoid import cycle
|
||||
from capa.features.extractors.common import extract_format
|
||||
from capa.features.extractors.dnfile_ import DnfileFeatureExtractor
|
||||
|
||||
with open(sample, "rb") as f:
|
||||
buf = f.read()
|
||||
|
||||
for feature, _ in extract_format(buf):
|
||||
if feature == Format(FORMAT_PE):
|
||||
dnfile_extractor = DnfileFeatureExtractor(sample)
|
||||
if dnfile_extractor.is_dotnet_file():
|
||||
feature = Format(FORMAT_DOTNET)
|
||||
|
||||
assert isinstance(feature.value, str)
|
||||
return feature.value
|
||||
|
||||
|
||||
@@ -5,20 +5,25 @@
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import json
|
||||
import logging
|
||||
import datetime
|
||||
import contextlib
|
||||
from typing import Optional
|
||||
|
||||
import idc
|
||||
import idaapi
|
||||
import idautils
|
||||
import ida_bytes
|
||||
import ida_loader
|
||||
from netnode import netnode
|
||||
|
||||
import capa
|
||||
import capa.version
|
||||
import capa.render.utils as rutils
|
||||
import capa.features.common
|
||||
import capa.render.result_document
|
||||
from capa.features.address import AbsoluteVirtualAddress
|
||||
|
||||
logger = logging.getLogger("capa")
|
||||
|
||||
@@ -27,15 +32,20 @@ SUPPORTED_FILE_TYPES = (
|
||||
idaapi.f_PE,
|
||||
idaapi.f_ELF,
|
||||
idaapi.f_BIN,
|
||||
idaapi.f_COFF,
|
||||
# idaapi.f_MACHO,
|
||||
)
|
||||
|
||||
# arch type as returned by idainfo.procname
|
||||
SUPPORTED_ARCH_TYPES = ("metapc",)
|
||||
|
||||
CAPA_NETNODE = f"$ com.mandiant.capa.v{capa.version.__version__}"
|
||||
NETNODE_RESULTS = "results"
|
||||
NETNODE_RULES_CACHE_ID = "rules-cache-id"
|
||||
|
||||
|
||||
def inform_user_ida_ui(message):
|
||||
idaapi.info("%s. Please refer to IDA Output window for more information." % message)
|
||||
idaapi.info(f"{message}. Please refer to IDA Output window for more information.")
|
||||
|
||||
|
||||
def is_supported_ida_version():
|
||||
@@ -170,7 +180,7 @@ class IDAIO:
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super(IDAIO, self).__init__()
|
||||
super().__init__()
|
||||
self.offset = 0
|
||||
|
||||
def seek(self, offset, whence=0):
|
||||
@@ -180,11 +190,63 @@ class IDAIO:
|
||||
def read(self, size):
|
||||
ea = ida_loader.get_fileregion_ea(self.offset)
|
||||
if ea == idc.BADADDR:
|
||||
# best guess, such as if file is mapped at address 0x0.
|
||||
ea = self.offset
|
||||
logger.debug("cannot read 0x%x bytes at 0x%x (ea: BADADDR)", size, self.offset)
|
||||
return b""
|
||||
|
||||
logger.debug("reading 0x%x bytes at 0x%x (ea: 0x%x)", size, self.offset, ea)
|
||||
return ida_bytes.get_bytes(ea, size)
|
||||
|
||||
# get_bytes returns None on error, for consistency with read always return bytes
|
||||
return ida_bytes.get_bytes(ea, size) or b""
|
||||
|
||||
def close(self):
|
||||
return
|
||||
|
||||
|
||||
def save_cached_results(resdoc):
|
||||
logger.debug("saving cached capa results to netnode '%s'", CAPA_NETNODE)
|
||||
n = netnode.Netnode(CAPA_NETNODE)
|
||||
n[NETNODE_RESULTS] = resdoc.json()
|
||||
|
||||
|
||||
def idb_contains_cached_results() -> bool:
|
||||
try:
|
||||
n = netnode.Netnode(CAPA_NETNODE)
|
||||
return bool(n.get(NETNODE_RESULTS))
|
||||
except netnode.NetnodeCorruptError as e:
|
||||
logger.error("%s", e, exc_info=True)
|
||||
return False
|
||||
|
||||
|
||||
def load_and_verify_cached_results() -> Optional[capa.render.result_document.ResultDocument]:
|
||||
"""verifies that cached results have valid (mapped) addresses for the current database"""
|
||||
logger.debug("loading cached capa results from netnode '%s'", CAPA_NETNODE)
|
||||
|
||||
n = netnode.Netnode(CAPA_NETNODE)
|
||||
doc = capa.render.result_document.ResultDocument.parse_obj(json.loads(n[NETNODE_RESULTS]))
|
||||
|
||||
for rule in rutils.capability_rules(doc):
|
||||
for location_, _ in rule.matches:
|
||||
location = location_.to_capa()
|
||||
if isinstance(location, AbsoluteVirtualAddress):
|
||||
ea = int(location)
|
||||
if not idaapi.is_mapped(ea):
|
||||
logger.error("cached address %s is not a valid location in this database", hex(ea))
|
||||
return None
|
||||
return doc
|
||||
|
||||
|
||||
def save_rules_cache_id(ruleset_id):
|
||||
logger.debug("saving ruleset ID to netnode '%s'", CAPA_NETNODE)
|
||||
n = netnode.Netnode(CAPA_NETNODE)
|
||||
n[NETNODE_RULES_CACHE_ID] = ruleset_id
|
||||
|
||||
|
||||
def load_rules_cache_id():
|
||||
n = netnode.Netnode(CAPA_NETNODE)
|
||||
return n[NETNODE_RULES_CACHE_ID]
|
||||
|
||||
|
||||
def delete_cached_results():
|
||||
logger.debug("deleting cached capa data")
|
||||
n = netnode.Netnode(CAPA_NETNODE)
|
||||
del n[NETNODE_RESULTS]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||

|
||||
|
||||
capa explorer is an IDAPython plugin that integrates the FLARE team's open-source framework, capa, with IDA Pro. capa is a framework that uses a well-defined collection of rules to
|
||||
identify capabilities in a program. You can run capa against a PE file or shellcode and it tells you what it thinks the program can do. For example, it might suggest that
|
||||
the program is a backdoor, can install services, or relies on HTTP to communicate. capa explorer runs capa directly against your IDA Pro database (IDB) without requiring access
|
||||
identify capabilities in a program. You can run capa against a PE file, ELF file, or shellcode and it tells you what it thinks the program can do. For example, it might suggest that
|
||||
the program is a backdoor, can install services, or relies on HTTP to communicate. capa explorer runs capa analysis on your IDA Pro database (IDB) without needing access
|
||||
to the original binary file. Once a database has been analyzed, capa explorer helps you identify interesting areas of a program and build new capa rules using features extracted from your IDB.
|
||||
|
||||
We love using capa explorer during malware analysis because it teaches us what parts of a program suggest a behavior. As we click on rows, capa explorer jumps directly
|
||||
@@ -21,10 +21,10 @@ We can use capa explorer to navigate our Disassembly view directly to the suspec
|
||||
Using the `Rule Information` and `Details` columns capa explorer shows us that the suspect function matched `self delete via COMSPEC environment variable` because it contains capa rule matches for `create process`, `get COMSPEC environment variable`,
|
||||
and `query environment variable`, references to the strings `COMSPEC`, ` > nul`, and `/c del `, and calls to the Windows API functions `GetEnvironmentVariableA` and `ShellExecuteEx`.
|
||||
|
||||
capa explorer also helps you build new capa rules. To start select the `Rule Generator` tab, navigate to a function in your Disassembly view,
|
||||
capa explorer also helps you build and test new capa rules. To start, select the `Rule Generator` tab, navigate to a function in your Disassembly view,
|
||||
and click `Analyze`. capa explorer will extract features from the function and display them in the `Features` pane. You can add features listed in this pane to the `Editor` pane
|
||||
by either double-clicking a feature or using multi-select + right-click to add multiple features at once. The `Preview` and `Editor` panes help edit your rule. Use the `Preview` pane
|
||||
to modify the rule text directly and the `Editor` pane to construct and rearrange your hierarchy of statements and features. When you finish a rule you can save it directly to a file by clicking `Save`.
|
||||
to modify rule text directly and the `Editor` pane to construct and rearrange your hierarchy of statements and features. When you finish a rule you can save it directly to a file by clicking `Save`.
|
||||
|
||||

|
||||
|
||||
@@ -32,62 +32,32 @@ For more information on the FLARE team's open-source framework, capa, check out
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Requirements
|
||||
### Installation
|
||||
|
||||
capa explorer supports Python versions >= 3.7.x and the following IDA Pro versions:
|
||||
|
||||
* IDA 7.4
|
||||
* IDA 7.5
|
||||
* IDA 7.6 (caveat below)
|
||||
* IDA 7.7
|
||||
|
||||
capa explorer is however limited to the Python versions supported by your IDA installation (which may not include all Python versions >= 3.7.x). Based on our testing the following matrix shows the Python versions supported
|
||||
by each supported IDA version:
|
||||
|
||||
| | IDA 7.4 | IDA 7.5 | IDA 7.6 |
|
||||
| --- | --- | --- | --- |
|
||||
| Python 3.7.x | Yes | Yes | Yes |
|
||||
| Python 3.8.x | Partial (see below) | Yes | Yes |
|
||||
| Python 3.9.x | No | Partial (see below) | Yes |
|
||||
|
||||
To use capa explorer with IDA 7.4 and Python 3.8.x you must follow the instructions provided by hex-rays [here](https://hex-rays.com/blog/ida-7-4-and-python-3-8/).
|
||||
|
||||
To use capa explorer with IDA 7.5 and Python 3.9.x you must follow the instructions provided by hex-rays [here](https://hex-rays.com/blog/python-3-9-support-for-ida-7-5/).
|
||||
|
||||
If you encounter issues with your specific setup, please open a new [Issue](https://github.com/mandiant/capa/issues).
|
||||
|
||||
#### IDA 7.6 caveat: IDA 7.6sp1 or patch required
|
||||
|
||||
As described [here](https://www.hex-rays.com/blog/ida-7-6-empty-qtreeview-qtreewidget/):
|
||||
|
||||
> A rather nasty issue evaded our testing and found its way into IDA 7.6: using the PyQt5 modules that are shipped with IDA, QTreeView (or QTreeWidget) instances will always fail to display contents.
|
||||
|
||||
Therefore, in order to use capa under IDA 7.6 you need the [Service Pack 1 for IDA 7.6](https://www.hex-rays.com/products/ida/news/7_6sp1). Alternatively, you can download and install the fix corresponding to your IDA installation, replacing the original QtWidgets DLL with the one contained in the .zip file (links to Hex-Rays):
|
||||
|
||||
|
||||
- Windows: [pyqt5_qtwidgets_win](https://www.hex-rays.com/wp-content/uploads/2021/04/pyqt5_qtwidgets_win.zip)
|
||||
- Linux: [pyqt5_qtwidgets_linux](https://www.hex-rays.com/wp-content/uploads/2021/04/pyqt5_qtwidgets_linux.zip)
|
||||
- MacOS (Intel): [pyqt5_qtwidgets_mac_x64](https://www.hex-rays.com/wp-content/uploads/2021/04/pyqt5_qtwidgets_mac_x64.zip)
|
||||
- MacOS (AppleSilicon): [pyqt5_qtwidgets_mac_arm](https://www.hex-rays.com/wp-content/uploads/2021/04/pyqt5_qtwidgets_mac_arm.zip)
|
||||
You can install capa explorer using the following steps:
|
||||
|
||||
1. Install capa and its dependencies from PyPI using the Python interpreter configured for your IDA installation:
|
||||
```
|
||||
$ pip install flare-capa
|
||||
```
|
||||
2. Download and extract the [official capa rules](https://github.com/mandiant/capa-rules/releases) that match the version of capa you have installed
|
||||
1. Use the following command to view the version of capa you have installed:
|
||||
```commandline
|
||||
$ pip show flare-capa
|
||||
OR
|
||||
$ capa --version
|
||||
```
|
||||
3. Copy [capa_explorer.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ida/plugin/capa_explorer.py) to your IDA plugins directory
|
||||
- find your plugin directories via `idaapi.get_ida_subdirs("plugins")` or see this [Hex-Rays blog](https://hex-rays.com/blog/igors-tip-of-the-week-103-sharing-plugins-between-ida-installs/)
|
||||
- common paths are `%APPDATA%\Hex-Rays\IDA Pro\plugins` (Windows) or `$HOME/.idapro/plugins` on Linux/Mac
|
||||
|
||||
### Supported File Types
|
||||
|
||||
capa explorer is limited to the file types supported by capa, which include:
|
||||
|
||||
* Windows x86 (32- and 64-bit) PE and ELF files
|
||||
* Windows x86 (32- and 64-bit) PE files
|
||||
* Windows x86 (32- and 64-bit) shellcode
|
||||
|
||||
### Installation
|
||||
|
||||
You can install capa explorer using the following steps:
|
||||
|
||||
1. Install capa and its dependencies from PyPI for the Python interpreter used by your IDA installation:
|
||||
```
|
||||
$ pip install flare-capa
|
||||
```
|
||||
3. Download the [standard collection of capa rules](https://github.com/mandiant/capa-rules) (capa explorer needs capa rules to analyze a database)
|
||||
4. Copy [capa_explorer.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ida/plugin/capa_explorer.py) to your IDA plugins directory
|
||||
* ELF files on various operating systems
|
||||
|
||||
### Usage
|
||||
|
||||
@@ -97,19 +67,20 @@ You can install capa explorer using the following steps:
|
||||
3. Select the `Program Analysis` tab
|
||||
4. Click the `Analyze` button
|
||||
|
||||
When running capa explorer for the first time you are prompted to select a file directory containing capa rules. The plugin conveniently
|
||||
remembers your selection for future runs; you can change this selection and other default settings by clicking `Settings`. We recommend
|
||||
downloading and using the [standard collection of capa rules](https://github.com/mandiant/capa-rules) when getting started with the plugin.
|
||||
The first time you run capa explorer you will be asked to specify a local directory containing capa rules to use for analysis. We recommend downloading and extracting the [official capa rules](https://github.com/mandiant/capa-rules/releases) that match
|
||||
the version of capa you have installed (see installation instructions above for more details). capa explorer remembers your selection for future analysis which you
|
||||
can update using the `Settings` button.
|
||||
|
||||
#### Tips for Program Analysis
|
||||
|
||||
* Start analysis by clicking the `Analyze` button
|
||||
* capa explorer caches results to the database and reuses them across IDA sessions
|
||||
* Reset the plugin user interface and remove highlighting from your Disassembly view by clicking the `Reset` button
|
||||
* Change your capa rules directory and other default settings by clicking `Settings`
|
||||
* Change your local capa rules directory, auto analysis settings, and other default settings by clicking the `Settings` button
|
||||
* Hover your cursor over a rule match to view the source content of the rule
|
||||
* Double-click the `Address` column to navigate your Disassembly view to the address of the associated feature
|
||||
* Double-click a result in the `Rule Information` column to expand its children
|
||||
* Select a checkbox in the `Rule Information` column to highlight the address of the associated feature in your Dissasembly view
|
||||
* Select a checkbox in the `Rule Information` column to highlight the address of the associated feature in your Disassembly view
|
||||
|
||||
#### Tips for Rule Generator
|
||||
|
||||
@@ -122,6 +93,22 @@ downloading and using the [standard collection of capa rules](https://github.com
|
||||
* Directly edit rule text and metadata fields using the `Preview` pane
|
||||
* Change the default rule author and default rule scope displayed in the `Preview` pane by clicking `Settings`
|
||||
|
||||
### Requirements
|
||||
|
||||
capa explorer supports Python versions >= 3.7.x and IDA Pro versions >= 7.4. The following IDA Pro versions have been tested:
|
||||
|
||||
* IDA 7.4
|
||||
* IDA 7.5
|
||||
* IDA 7.6 Service Pack 1
|
||||
* IDA 7.7
|
||||
* IDA 8.0
|
||||
* IDA 8.1
|
||||
* IDA 8.2
|
||||
|
||||
capa explorer is however limited to the Python versions supported by your IDA installation (which may not include all Python versions >= 3.7.x).
|
||||
|
||||
If you encounter issues with your specific setup, please open a new [Issue](https://github.com/mandiant/capa/issues).
|
||||
|
||||
## Development
|
||||
|
||||
capa explorer is packaged with capa so you will need to install capa locally for development. You can install capa locally by following the steps outlined in `Method 3: Inspecting the capa source code` of the [capa
|
||||
|
||||
@@ -17,7 +17,6 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CapaExplorerPlugin(idaapi.plugin_t):
|
||||
|
||||
# Mandatory definitions
|
||||
PLUGIN_NAME = "FLARE capa explorer"
|
||||
PLUGIN_VERSION = "1.0.0"
|
||||
@@ -39,6 +38,12 @@ class CapaExplorerPlugin(idaapi.plugin_t):
|
||||
"""called when IDA is loading the plugin"""
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
# do not load plugin unless hosted in idaq (IDA Qt)
|
||||
if not idaapi.is_idaq():
|
||||
# note: it does not appear that IDA calls "init" by default when hosted in idat; we keep this
|
||||
# check here for good measure
|
||||
return idaapi.PLUGIN_SKIP
|
||||
|
||||
import capa.ida.helpers
|
||||
|
||||
# do not load plugin if IDA version/file type not supported
|
||||
@@ -62,7 +67,16 @@ class CapaExplorerPlugin(idaapi.plugin_t):
|
||||
arg (int): bitflag. Setting LSB enables automatic analysis upon
|
||||
loading. The other bits are currently undefined. See `form.Options`.
|
||||
"""
|
||||
self.form = CapaExplorerForm(self.PLUGIN_NAME, arg)
|
||||
if not self.form:
|
||||
self.form = CapaExplorerForm(self.PLUGIN_NAME, arg)
|
||||
else:
|
||||
widget = idaapi.find_widget(self.form.form_title)
|
||||
if widget:
|
||||
idaapi.activate_widget(widget, True)
|
||||
else:
|
||||
self.form.Show()
|
||||
self.form.load_capa_results(False, True)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -85,7 +99,7 @@ class CapaExplorerPlugin(idaapi.plugin_t):
|
||||
# so we need to register a callback that's invoked from the main thread after the plugin is registered.
|
||||
#
|
||||
# after a lot of guess-and-check, we can use `UI_Hooks.updated_actions` to
|
||||
# receive notications after IDA has created an action for each plugin.
|
||||
# receive notifications after IDA has created an action for each plugin.
|
||||
# so, create this hook, wait for capa plugin to load, set the icon, and unhook.
|
||||
|
||||
|
||||
@@ -93,7 +107,7 @@ class OnUpdatedActionsHook(ida_kernwin.UI_Hooks):
|
||||
"""register a callback to be invoked each time the UI actions are updated"""
|
||||
|
||||
def __init__(self, cb):
|
||||
super(OnUpdatedActionsHook, self).__init__()
|
||||
super().__init__()
|
||||
self.cb = cb
|
||||
|
||||
def updated_actions(self):
|
||||
|
||||
220
capa/ida/plugin/cache.py
Normal file
220
capa/ida/plugin/cache.py
Normal file
@@ -0,0 +1,220 @@
|
||||
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import collections
|
||||
from typing import Set, Dict, List, Tuple, Union, Optional
|
||||
|
||||
import capa.engine
|
||||
from capa.rules import Scope, RuleSet
|
||||
from capa.engine import FeatureSet, MatchResults
|
||||
from capa.features.address import NO_ADDRESS, Address
|
||||
from capa.ida.plugin.extractor import CapaExplorerFeatureExtractor
|
||||
from capa.features.extractors.base_extractor import BBHandle, InsnHandle, FunctionHandle
|
||||
|
||||
|
||||
class CapaRuleGenFeatureCacheNode:
|
||||
def __init__(
|
||||
self,
|
||||
inner: Optional[Union[FunctionHandle, BBHandle, InsnHandle]],
|
||||
parent: Optional[CapaRuleGenFeatureCacheNode],
|
||||
):
|
||||
self.inner: Optional[Union[FunctionHandle, BBHandle, InsnHandle]] = inner
|
||||
self.address = NO_ADDRESS if self.inner is None else self.inner.address
|
||||
self.parent: Optional[CapaRuleGenFeatureCacheNode] = parent
|
||||
|
||||
if self.parent is not None:
|
||||
self.parent.children.add(self)
|
||||
|
||||
self.features: FeatureSet = collections.defaultdict(set)
|
||||
self.children: Set[CapaRuleGenFeatureCacheNode] = set()
|
||||
|
||||
def __hash__(self):
|
||||
# TODO: unique enough?
|
||||
return hash((self.address,))
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, type(self)):
|
||||
return NotImplemented
|
||||
# TODO: unique enough?
|
||||
return self.address == other.address
|
||||
|
||||
|
||||
class CapaRuleGenFeatureCache:
|
||||
def __init__(self, fh_list: List[FunctionHandle], extractor: CapaExplorerFeatureExtractor):
|
||||
self.global_features: FeatureSet = collections.defaultdict(set)
|
||||
|
||||
self.file_node: CapaRuleGenFeatureCacheNode = CapaRuleGenFeatureCacheNode(None, None)
|
||||
self.func_nodes: Dict[Address, CapaRuleGenFeatureCacheNode] = {}
|
||||
self.bb_nodes: Dict[Address, CapaRuleGenFeatureCacheNode] = {}
|
||||
self.insn_nodes: Dict[Address, CapaRuleGenFeatureCacheNode] = {}
|
||||
|
||||
self._find_global_features(extractor)
|
||||
self._find_file_features(extractor)
|
||||
self._find_function_and_below_features(fh_list, extractor)
|
||||
|
||||
def _find_global_features(self, extractor: CapaExplorerFeatureExtractor):
|
||||
for feature, addr in extractor.extract_global_features():
|
||||
# not all global features may have virtual addresses.
|
||||
# if not, then at least ensure the feature shows up in the index.
|
||||
# the set of addresses will still be empty.
|
||||
if addr is not None:
|
||||
self.global_features[feature].add(addr)
|
||||
else:
|
||||
if feature not in self.global_features:
|
||||
self.global_features[feature] = set()
|
||||
|
||||
def _find_file_features(self, extractor: CapaExplorerFeatureExtractor):
|
||||
# not all file features may have virtual addresses.
|
||||
# if not, then at least ensure the feature shows up in the index.
|
||||
# the set of addresses will still be empty.
|
||||
for feature, addr in extractor.extract_file_features():
|
||||
if addr is not None:
|
||||
self.file_node.features[feature].add(addr)
|
||||
else:
|
||||
if feature not in self.file_node.features:
|
||||
self.file_node.features[feature] = set()
|
||||
|
||||
def _find_function_and_below_features(self, fh_list: List[FunctionHandle], extractor: CapaExplorerFeatureExtractor):
|
||||
for fh in fh_list:
|
||||
f_node: CapaRuleGenFeatureCacheNode = CapaRuleGenFeatureCacheNode(fh, self.file_node)
|
||||
|
||||
# extract basic block and below features
|
||||
for bbh in extractor.get_basic_blocks(fh):
|
||||
bb_node: CapaRuleGenFeatureCacheNode = CapaRuleGenFeatureCacheNode(bbh, f_node)
|
||||
|
||||
# extract instruction features
|
||||
for ih in extractor.get_instructions(fh, bbh):
|
||||
inode: CapaRuleGenFeatureCacheNode = CapaRuleGenFeatureCacheNode(ih, bb_node)
|
||||
|
||||
for feature, addr in extractor.extract_insn_features(fh, bbh, ih):
|
||||
inode.features[feature].add(addr)
|
||||
|
||||
self.insn_nodes[inode.address] = inode
|
||||
|
||||
# extract basic block features
|
||||
for feature, addr in extractor.extract_basic_block_features(fh, bbh):
|
||||
bb_node.features[feature].add(addr)
|
||||
|
||||
# store basic block features in cache and function parent
|
||||
self.bb_nodes[bb_node.address] = bb_node
|
||||
|
||||
# extract function features
|
||||
for feature, addr in extractor.extract_function_features(fh):
|
||||
f_node.features[feature].add(addr)
|
||||
|
||||
self.func_nodes[f_node.address] = f_node
|
||||
|
||||
def _find_instruction_capabilities(
|
||||
self, ruleset: RuleSet, insn: CapaRuleGenFeatureCacheNode
|
||||
) -> Tuple[FeatureSet, MatchResults]:
|
||||
features: FeatureSet = collections.defaultdict(set)
|
||||
|
||||
for feature, locs in itertools.chain(insn.features.items(), self.global_features.items()):
|
||||
features[feature].update(locs)
|
||||
|
||||
_, matches = ruleset.match(Scope.INSTRUCTION, features, insn.address)
|
||||
for name, result in matches.items():
|
||||
rule = ruleset[name]
|
||||
for addr, _ in result:
|
||||
capa.engine.index_rule_matches(features, rule, [addr])
|
||||
|
||||
return features, matches
|
||||
|
||||
def _find_basic_block_capabilities(
|
||||
self, ruleset: RuleSet, bb: CapaRuleGenFeatureCacheNode
|
||||
) -> Tuple[FeatureSet, MatchResults, MatchResults]:
|
||||
features: FeatureSet = collections.defaultdict(set)
|
||||
insn_matches: MatchResults = collections.defaultdict(list)
|
||||
|
||||
for insn in bb.children:
|
||||
ifeatures, imatches = self._find_instruction_capabilities(ruleset, insn)
|
||||
for feature, locs in ifeatures.items():
|
||||
features[feature].update(locs)
|
||||
for name, result in imatches.items():
|
||||
insn_matches[name].extend(result)
|
||||
|
||||
for feature, locs in itertools.chain(bb.features.items(), self.global_features.items()):
|
||||
features[feature].update(locs)
|
||||
|
||||
_, matches = ruleset.match(Scope.BASIC_BLOCK, features, bb.address)
|
||||
for name, result in matches.items():
|
||||
rule = ruleset[name]
|
||||
for loc, _ in result:
|
||||
capa.engine.index_rule_matches(features, rule, [loc])
|
||||
|
||||
return features, matches, insn_matches
|
||||
|
||||
def find_code_capabilities(
|
||||
self, ruleset: RuleSet, fh: FunctionHandle
|
||||
) -> Tuple[FeatureSet, MatchResults, MatchResults, MatchResults]:
|
||||
f_node: Optional[CapaRuleGenFeatureCacheNode] = self.func_nodes.get(fh.address, None)
|
||||
if f_node is None:
|
||||
return {}, {}, {}, {}
|
||||
|
||||
insn_matches: MatchResults = collections.defaultdict(list)
|
||||
bb_matches: MatchResults = collections.defaultdict(list)
|
||||
function_features: FeatureSet = collections.defaultdict(set)
|
||||
|
||||
for bb in f_node.children:
|
||||
features, bmatches, imatches = self._find_basic_block_capabilities(ruleset, bb)
|
||||
for feature, locs in features.items():
|
||||
function_features[feature].update(locs)
|
||||
for name, result in bmatches.items():
|
||||
bb_matches[name].extend(result)
|
||||
for name, result in imatches.items():
|
||||
insn_matches[name].extend(result)
|
||||
|
||||
for feature, locs in itertools.chain(f_node.features.items(), self.global_features.items()):
|
||||
function_features[feature].update(locs)
|
||||
|
||||
_, function_matches = ruleset.match(Scope.FUNCTION, function_features, f_node.address)
|
||||
return function_features, function_matches, bb_matches, insn_matches
|
||||
|
||||
def find_file_capabilities(self, ruleset: RuleSet) -> Tuple[FeatureSet, MatchResults]:
|
||||
features: FeatureSet = collections.defaultdict(set)
|
||||
|
||||
for func_node in self.file_node.children:
|
||||
assert func_node.inner is not None
|
||||
assert isinstance(func_node.inner, FunctionHandle)
|
||||
|
||||
func_features, _, _, _ = self.find_code_capabilities(ruleset, func_node.inner)
|
||||
for feature, locs in func_features.items():
|
||||
features[feature].update(locs)
|
||||
|
||||
for feature, locs in itertools.chain(self.file_node.features.items(), self.global_features.items()):
|
||||
features[feature].update(locs)
|
||||
|
||||
_, matches = ruleset.match(Scope.FILE, features, NO_ADDRESS)
|
||||
return features, matches
|
||||
|
||||
def get_all_function_features(self, fh: FunctionHandle) -> FeatureSet:
|
||||
f_node: Optional[CapaRuleGenFeatureCacheNode] = self.func_nodes.get(fh.address, None)
|
||||
if f_node is None:
|
||||
return {}
|
||||
|
||||
all_function_features: FeatureSet = collections.defaultdict(set)
|
||||
all_function_features.update(f_node.features)
|
||||
|
||||
for bb_node in f_node.children:
|
||||
for i_node in bb_node.children:
|
||||
for feature, locs in i_node.features.items():
|
||||
all_function_features[feature].update(locs)
|
||||
for feature, locs in bb_node.features.items():
|
||||
all_function_features[feature].update(locs)
|
||||
|
||||
# include global features just once
|
||||
for feature, locs in self.global_features.items():
|
||||
all_function_features[feature].update(locs)
|
||||
|
||||
return all_function_features
|
||||
|
||||
def get_all_file_features(self):
|
||||
yield from itertools.chain(self.file_node.features.items(), self.global_features.items())
|
||||
13
capa/ida/plugin/error.py
Normal file
13
capa/ida/plugin/error.py
Normal file
@@ -0,0 +1,13 @@
|
||||
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
|
||||
class UserCancelledError(Exception):
|
||||
"""throw exception when user cancels action"""
|
||||
|
||||
pass
|
||||
44
capa/ida/plugin/extractor.py
Normal file
44
capa/ida/plugin/extractor.py
Normal file
@@ -0,0 +1,44 @@
|
||||
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import ida_kernwin
|
||||
from PyQt5 import QtCore
|
||||
|
||||
from capa.ida.plugin.error import UserCancelledError
|
||||
from capa.features.extractors.ida.extractor import IdaFeatureExtractor
|
||||
from capa.features.extractors.base_extractor import FunctionHandle
|
||||
|
||||
|
||||
class CapaExplorerProgressIndicator(QtCore.QObject):
|
||||
"""implement progress signal, used during feature extraction"""
|
||||
|
||||
progress = QtCore.pyqtSignal(str)
|
||||
|
||||
def update(self, text):
|
||||
"""emit progress update
|
||||
|
||||
check if user cancelled action, raise exception for parent function to catch
|
||||
"""
|
||||
if ida_kernwin.user_cancelled():
|
||||
raise UserCancelledError("user cancelled")
|
||||
self.progress.emit(f"extracting features from {text}")
|
||||
|
||||
|
||||
class CapaExplorerFeatureExtractor(IdaFeatureExtractor):
|
||||
"""subclass the IdaFeatureExtractor
|
||||
|
||||
track progress during feature extraction, also allow user to cancel feature extraction
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.indicator = CapaExplorerProgressIndicator()
|
||||
|
||||
def extract_function_features(self, fh: FunctionHandle):
|
||||
self.indicator.update(f"function at {hex(fh.inner.start_ea)}")
|
||||
return super().extract_function_features(fh)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -16,7 +16,7 @@ class CapaExplorerIdaHooks(idaapi.UI_Hooks):
|
||||
@param screen_ea_changed_hook: function hook for IDA screen ea changed
|
||||
@param action_hooks: dict of IDA action handles
|
||||
"""
|
||||
super(CapaExplorerIdaHooks, self).__init__()
|
||||
super().__init__()
|
||||
|
||||
self.screen_ea_changed_hook = screen_ea_changed_hook
|
||||
self.process_action_hooks = action_hooks
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -30,13 +30,13 @@ def info_to_name(display):
|
||||
|
||||
def ea_to_hex(ea):
|
||||
"""convert effective address (ea) to hex for display"""
|
||||
return "%08X" % ea
|
||||
return f"{hex(ea)}"
|
||||
|
||||
|
||||
class CapaExplorerDataItem:
|
||||
"""store data for CapaExplorerDataModel"""
|
||||
|
||||
def __init__(self, parent: "CapaExplorerDataItem", data: List[str], can_check=True):
|
||||
def __init__(self, parent: Optional["CapaExplorerDataItem"], data: List[str], can_check=True):
|
||||
"""initialize item"""
|
||||
self.pred = parent
|
||||
self._data = data
|
||||
@@ -110,7 +110,7 @@ class CapaExplorerDataItem:
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
def parent(self) -> "CapaExplorerDataItem":
|
||||
def parent(self) -> Optional["CapaExplorerDataItem"]:
|
||||
"""get parent"""
|
||||
return self.pred
|
||||
|
||||
@@ -181,7 +181,7 @@ class CapaExplorerRuleItem(CapaExplorerDataItem):
|
||||
@param source: rule source (tooltip)
|
||||
"""
|
||||
display = self.fmt % (name, count) if count > 1 else name
|
||||
super(CapaExplorerRuleItem, self).__init__(parent, [display, "", namespace], can_check)
|
||||
super().__init__(parent, [display, "", namespace], can_check)
|
||||
self._source = source
|
||||
|
||||
@property
|
||||
@@ -200,7 +200,7 @@ class CapaExplorerRuleMatchItem(CapaExplorerDataItem):
|
||||
@param display: text to display in UI
|
||||
@param source: rule match source to display (tooltip)
|
||||
"""
|
||||
super(CapaExplorerRuleMatchItem, self).__init__(parent, [display, "", ""])
|
||||
super().__init__(parent, [display, "", ""])
|
||||
self._source = source
|
||||
|
||||
@property
|
||||
@@ -222,14 +222,12 @@ class CapaExplorerFunctionItem(CapaExplorerDataItem):
|
||||
"""
|
||||
assert isinstance(location, AbsoluteVirtualAddress)
|
||||
ea = int(location)
|
||||
super(CapaExplorerFunctionItem, self).__init__(
|
||||
parent, [self.fmt % idaapi.get_name(ea), ea_to_hex(ea), ""], can_check
|
||||
)
|
||||
super().__init__(parent, [self.fmt % idaapi.get_name(ea), ea_to_hex(ea), ""], can_check)
|
||||
|
||||
@property
|
||||
def info(self):
|
||||
"""return function name"""
|
||||
info = super(CapaExplorerFunctionItem, self).info
|
||||
info = super().info
|
||||
display = info_to_name(info)
|
||||
return display if display else info
|
||||
|
||||
@@ -255,7 +253,7 @@ class CapaExplorerSubscopeItem(CapaExplorerDataItem):
|
||||
@param parent: parent node
|
||||
@param scope: subscope name
|
||||
"""
|
||||
super(CapaExplorerSubscopeItem, self).__init__(parent, [self.fmt % scope, "", ""])
|
||||
super().__init__(parent, [self.fmt % scope, "", ""])
|
||||
|
||||
|
||||
class CapaExplorerBlockItem(CapaExplorerDataItem):
|
||||
@@ -271,7 +269,13 @@ class CapaExplorerBlockItem(CapaExplorerDataItem):
|
||||
"""
|
||||
assert isinstance(location, AbsoluteVirtualAddress)
|
||||
ea = int(location)
|
||||
super(CapaExplorerBlockItem, self).__init__(parent, [self.fmt % ea, ea_to_hex(ea), ""])
|
||||
super().__init__(parent, [self.fmt % ea, ea_to_hex(ea), ""])
|
||||
|
||||
|
||||
class CapaExplorerInstructionItem(CapaExplorerBlockItem):
|
||||
"""store data for instruction match"""
|
||||
|
||||
fmt = "instruction(loc_%08X)"
|
||||
|
||||
|
||||
class CapaExplorerDefaultItem(CapaExplorerDataItem):
|
||||
@@ -292,9 +296,7 @@ class CapaExplorerDefaultItem(CapaExplorerDataItem):
|
||||
assert isinstance(location, AbsoluteVirtualAddress)
|
||||
ea = int(location)
|
||||
|
||||
super(CapaExplorerDefaultItem, self).__init__(
|
||||
parent, [display, ea_to_hex(ea) if ea is not None else "", details]
|
||||
)
|
||||
super().__init__(parent, [display, ea_to_hex(ea) if ea is not None else "", details])
|
||||
|
||||
|
||||
class CapaExplorerFeatureItem(CapaExplorerDataItem):
|
||||
@@ -313,9 +315,9 @@ class CapaExplorerFeatureItem(CapaExplorerDataItem):
|
||||
if location:
|
||||
assert isinstance(location, (AbsoluteVirtualAddress, FileOffsetAddress))
|
||||
ea = int(location)
|
||||
super(CapaExplorerFeatureItem, self).__init__(parent, [display, ea_to_hex(ea), details])
|
||||
super().__init__(parent, [display, ea_to_hex(ea), details])
|
||||
else:
|
||||
super(CapaExplorerFeatureItem, self).__init__(parent, [display, "", details])
|
||||
super().__init__(parent, [display, "", details])
|
||||
|
||||
|
||||
class CapaExplorerInstructionViewItem(CapaExplorerFeatureItem):
|
||||
@@ -333,7 +335,7 @@ class CapaExplorerInstructionViewItem(CapaExplorerFeatureItem):
|
||||
assert isinstance(location, AbsoluteVirtualAddress)
|
||||
ea = int(location)
|
||||
details = capa.ida.helpers.get_disasm_line(ea)
|
||||
super(CapaExplorerInstructionViewItem, self).__init__(parent, display, location=location, details=details)
|
||||
super().__init__(parent, display, location=location, details=details)
|
||||
self.ida_highlight = idc.get_color(ea, idc.CIC_ITEM)
|
||||
|
||||
|
||||
@@ -359,7 +361,7 @@ class CapaExplorerByteViewItem(CapaExplorerFeatureItem):
|
||||
byte_snap = codecs.encode(byte_snap, "hex").upper()
|
||||
details = " ".join([byte_snap[i : i + 2].decode() for i in range(0, len(byte_snap), 2)])
|
||||
|
||||
super(CapaExplorerByteViewItem, self).__init__(parent, display, location=location, details=details)
|
||||
super().__init__(parent, display, location=location, details=details)
|
||||
self.ida_highlight = idc.get_color(ea, idc.CIC_ITEM)
|
||||
|
||||
|
||||
@@ -376,5 +378,5 @@ class CapaExplorerStringViewItem(CapaExplorerFeatureItem):
|
||||
assert isinstance(location, (AbsoluteVirtualAddress, FileOffsetAddress))
|
||||
ea = int(location)
|
||||
|
||||
super(CapaExplorerStringViewItem, self).__init__(parent, display, location=location, details=value)
|
||||
super().__init__(parent, display, location=location, details=value)
|
||||
self.ida_highlight = idc.get_color(ea, idc.CIC_ITEM)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
from typing import Set, Dict, List, Tuple
|
||||
from typing import Set, Dict, List, Tuple, Optional
|
||||
from collections import deque
|
||||
|
||||
import idc
|
||||
@@ -31,6 +31,7 @@ from capa.ida.plugin.item import (
|
||||
CapaExplorerSubscopeItem,
|
||||
CapaExplorerRuleMatchItem,
|
||||
CapaExplorerStringViewItem,
|
||||
CapaExplorerInstructionItem,
|
||||
CapaExplorerInstructionViewItem,
|
||||
)
|
||||
from capa.features.address import Address, AbsoluteVirtualAddress
|
||||
@@ -50,7 +51,7 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
"""initialize model"""
|
||||
super(CapaExplorerDataModel, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
# root node does not have parent, contains header columns
|
||||
self.root_node = CapaExplorerDataItem(None, ["Rule Information", "Address", "Details"])
|
||||
|
||||
@@ -142,6 +143,7 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
|
||||
CapaExplorerFunctionItem,
|
||||
CapaExplorerFeatureItem,
|
||||
CapaExplorerSubscopeItem,
|
||||
CapaExplorerInstructionItem,
|
||||
),
|
||||
)
|
||||
and column == CapaExplorerDataModel.COLUMN_INDEX_RULE_INFORMATION
|
||||
@@ -363,37 +365,38 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
|
||||
@param doc: result doc
|
||||
"""
|
||||
|
||||
if isinstance(statement, (rd.AndStatement, rd.OrStatement, rd.OptionalStatement)):
|
||||
display = statement.type
|
||||
if statement.description:
|
||||
display += " (%s)" % statement.description
|
||||
return CapaExplorerDefaultItem(parent, display)
|
||||
elif isinstance(statement, rd.NotStatement):
|
||||
if isinstance(statement, rd.CompoundStatement):
|
||||
if statement.type != rd.CompoundStatementType.NOT:
|
||||
display = statement.type
|
||||
if statement.description:
|
||||
display += f" ({statement.description})"
|
||||
return CapaExplorerDefaultItem(parent, display)
|
||||
elif isinstance(statement, rd.CompoundStatement) and statement.type == rd.CompoundStatementType.NOT:
|
||||
# TODO: do we display 'not'
|
||||
pass
|
||||
elif isinstance(statement, rd.SomeStatement):
|
||||
display = "%d or more" % statement.count
|
||||
display = f"{statement.count} or more"
|
||||
if statement.description:
|
||||
display += " (%s)" % statement.description
|
||||
display += f" ({statement.description})"
|
||||
return CapaExplorerDefaultItem(parent, display)
|
||||
elif isinstance(statement, rd.RangeStatement):
|
||||
# `range` is a weird node, its almost a hybrid of statement + feature.
|
||||
# it is a specific feature repeated multiple times.
|
||||
# there's no additional logic in the feature part, just the existence of a feature.
|
||||
# so, we have to inline some of the feature rendering here.
|
||||
display = "count(%s): " % self.capa_doc_feature_to_display(statement.child)
|
||||
display = f"count({self.capa_doc_feature_to_display(statement.child)}): "
|
||||
|
||||
if statement.max == statement.min:
|
||||
display += "%d" % (statement.min)
|
||||
display += f"{statement.min}"
|
||||
elif statement.min == 0:
|
||||
display += "%d or fewer" % (statement.max)
|
||||
display += f"{statement.max} or fewer"
|
||||
elif statement.max == (1 << 64 - 1):
|
||||
display += "%d or more" % (statement.min)
|
||||
display += f"{statement.min} or more"
|
||||
else:
|
||||
display += "between %d and %d" % (statement.min, statement.max)
|
||||
display += f"between {statement.min} and {statement.max}"
|
||||
|
||||
if statement.description:
|
||||
display += " (%s)" % statement.description
|
||||
display += f" ({statement.description})"
|
||||
|
||||
parent2 = CapaExplorerFeatureItem(parent, display=display)
|
||||
|
||||
@@ -405,7 +408,7 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
|
||||
elif isinstance(statement, rd.SubscopeStatement):
|
||||
display = str(statement.scope)
|
||||
if statement.description:
|
||||
display += " (%s)" % statement.description
|
||||
display += f" ({statement.description})"
|
||||
return CapaExplorerSubscopeItem(parent, display)
|
||||
else:
|
||||
raise RuntimeError("unexpected match statement type: " + str(statement))
|
||||
@@ -422,7 +425,7 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
|
||||
return
|
||||
|
||||
# optional statement with no successful children is empty
|
||||
if isinstance(match.node, rd.StatementNode) and isinstance(match.node.statement, rd.OptionalStatement):
|
||||
if isinstance(match.node, rd.StatementNode) and match.node.statement.type == rd.CompoundStatementType.OPTIONAL:
|
||||
if not any(map(lambda m: m.success, match.children)):
|
||||
return
|
||||
|
||||
@@ -441,37 +444,45 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
|
||||
self.render_capa_doc_match(parent2, child, doc)
|
||||
|
||||
def render_capa_doc_by_function(self, doc: rd.ResultDocument):
|
||||
""" """
|
||||
matches_by_function: Dict[int, Tuple[CapaExplorerFunctionItem, Set[str]]] = {}
|
||||
"""render rule matches by function meaning each rule match is nested under function where it was found"""
|
||||
matches_by_function: Dict[AbsoluteVirtualAddress, Tuple[CapaExplorerFunctionItem, Set[str]]] = {}
|
||||
for rule in rutils.capability_rules(doc):
|
||||
for location_, _ in rule.matches:
|
||||
location = location_.to_capa()
|
||||
match_eas: List[int] = []
|
||||
|
||||
if not isinstance(location, AbsoluteVirtualAddress):
|
||||
# only handle matches with a VA
|
||||
continue
|
||||
ea = int(location)
|
||||
# initial pass of rule matches
|
||||
for addr_, _ in rule.matches:
|
||||
addr: Address = addr_.to_capa()
|
||||
if isinstance(addr, AbsoluteVirtualAddress):
|
||||
match_eas.append(int(addr))
|
||||
|
||||
ea = capa.ida.helpers.get_func_start_ea(ea)
|
||||
if ea is None:
|
||||
# file scope, skip rendering in this mode
|
||||
for ea in match_eas:
|
||||
func_ea: Optional[int] = capa.ida.helpers.get_func_start_ea(ea)
|
||||
if func_ea is None:
|
||||
# rule match address is not located in a defined function
|
||||
continue
|
||||
if not matches_by_function.get(ea, ()):
|
||||
# new function root
|
||||
matches_by_function[ea] = (
|
||||
CapaExplorerFunctionItem(self.root_node, location, can_check=False),
|
||||
|
||||
func_address: AbsoluteVirtualAddress = AbsoluteVirtualAddress(func_ea)
|
||||
if not matches_by_function.get(func_address, ()):
|
||||
# create a new function root to nest its rule matches; Note: we must use the address of the
|
||||
# function here so everything is displayed properly
|
||||
matches_by_function[func_address] = (
|
||||
CapaExplorerFunctionItem(self.root_node, func_address, can_check=False),
|
||||
set(),
|
||||
)
|
||||
function_root, match_cache = matches_by_function[ea]
|
||||
if rule.meta.name in match_cache:
|
||||
# rule match already rendered for this function root, skip it
|
||||
|
||||
func_root, func_match_cache = matches_by_function[func_address]
|
||||
if rule.meta.name in func_match_cache:
|
||||
# only nest each rule once, so if found, skip
|
||||
continue
|
||||
match_cache.add(rule.meta.name)
|
||||
|
||||
# add matched rule to its function cache; create a new rule node whose parent is the matched
|
||||
# function node
|
||||
func_match_cache.add(rule.meta.name)
|
||||
CapaExplorerRuleItem(
|
||||
function_root,
|
||||
func_root,
|
||||
rule.meta.name,
|
||||
rule.meta.namespace or "",
|
||||
len(rule.matches),
|
||||
len([ea for ea in match_eas if capa.ida.helpers.get_func_start_ea(ea) == func_ea]),
|
||||
rule.source,
|
||||
can_check=False,
|
||||
)
|
||||
@@ -483,7 +494,7 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
|
||||
rule_namespace = rule.meta.namespace or ""
|
||||
parent = CapaExplorerRuleItem(self.root_node, rule_name, rule_namespace, len(rule.matches), rule.source)
|
||||
|
||||
for (location_, match) in rule.matches:
|
||||
for location_, match in rule.matches:
|
||||
location = location_.to_capa()
|
||||
|
||||
parent2: CapaExplorerDataItem
|
||||
@@ -493,6 +504,8 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
|
||||
parent2 = CapaExplorerFunctionItem(parent, location)
|
||||
elif rule.meta.scope == capa.rules.BASIC_BLOCK_SCOPE:
|
||||
parent2 = CapaExplorerBlockItem(parent, location)
|
||||
elif rule.meta.scope == capa.rules.INSTRUCTION_SCOPE:
|
||||
parent2 = CapaExplorerInstructionItem(parent, location)
|
||||
else:
|
||||
raise RuntimeError("unexpected rule scope: " + str(rule.meta.scope))
|
||||
|
||||
@@ -520,17 +533,25 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
|
||||
@param feature: capa feature read from doc
|
||||
"""
|
||||
key = feature.type
|
||||
value = getattr(feature, feature.type)
|
||||
value = feature.dict(by_alias=True).get(feature.type)
|
||||
|
||||
if value:
|
||||
if isinstance(feature, frzf.StringFeature):
|
||||
value = '"%s"' % capa.features.common.escape_string(value)
|
||||
value = f'"{capa.features.common.escape_string(value)}"'
|
||||
|
||||
if isinstance(feature, frzf.PropertyFeature) and feature.access is not None:
|
||||
key = f"property/{feature.access}"
|
||||
elif isinstance(feature, frzf.OperandNumberFeature):
|
||||
key = f"operand[{feature.index}].number"
|
||||
elif isinstance(feature, frzf.OperandOffsetFeature):
|
||||
key = f"operand[{feature.index}].offset"
|
||||
|
||||
if feature.description:
|
||||
return "%s(%s = %s)" % (key, value, feature.description)
|
||||
return f"{key}({value} = {feature.description})"
|
||||
else:
|
||||
return "%s(%s)" % (key, value)
|
||||
return f"{key}({value})"
|
||||
else:
|
||||
return "%s" % key
|
||||
return f"{key}"
|
||||
|
||||
def render_capa_doc_feature_node(
|
||||
self,
|
||||
@@ -634,6 +655,8 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
|
||||
frzf.MnemonicFeature,
|
||||
frzf.NumberFeature,
|
||||
frzf.OffsetFeature,
|
||||
frzf.OperandNumberFeature,
|
||||
frzf.OperandOffsetFeature,
|
||||
),
|
||||
):
|
||||
# display instruction preview
|
||||
@@ -646,7 +669,7 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
|
||||
elif isinstance(feature, frzf.StringFeature):
|
||||
# display string preview
|
||||
return CapaExplorerStringViewItem(
|
||||
parent, display, location, '"%s"' % capa.features.common.escape_string(feature.string)
|
||||
parent, display, location, f'"{capa.features.common.escape_string(feature.string)}"'
|
||||
)
|
||||
|
||||
elif isinstance(
|
||||
|
||||
@@ -22,7 +22,7 @@ class CapaExplorerRangeProxyModel(QtCore.QSortFilterProxyModel):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
"""initialize proxy filter"""
|
||||
super(CapaExplorerRangeProxyModel, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
self.min_ea = None
|
||||
self.max_ea = None
|
||||
|
||||
@@ -92,7 +92,7 @@ class CapaExplorerRangeProxyModel(QtCore.QSortFilterProxyModel):
|
||||
@param parent: QModelIndex of parent
|
||||
"""
|
||||
# filter not set
|
||||
if self.min_ea is None and self.max_ea is None:
|
||||
if self.min_ea is None or self.max_ea is None:
|
||||
return True
|
||||
|
||||
index = self.sourceModel().index(row, 0, parent)
|
||||
@@ -145,7 +145,7 @@ class CapaExplorerSearchProxyModel(QtCore.QSortFilterProxyModel):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
""" """
|
||||
super(CapaExplorerSearchProxyModel, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
self.query = ""
|
||||
self.setFilterKeyColumn(-1) # all columns
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import capa.ida.helpers
|
||||
import capa.features.common
|
||||
import capa.features.basicblock
|
||||
from capa.ida.plugin.item import CapaExplorerFunctionItem
|
||||
from capa.features.address import Address, _NoAddress
|
||||
from capa.features.address import AbsoluteVirtualAddress, _NoAddress
|
||||
from capa.ida.plugin.model import CapaExplorerDataModel
|
||||
|
||||
MAX_SECTION_SIZE = 750
|
||||
@@ -58,7 +58,7 @@ def parse_yaml_line(feature):
|
||||
if m:
|
||||
# reconstruct count without description
|
||||
feature, value, description, count = m.groups()
|
||||
feature = "- count(%s(%s)): %s" % (feature, value, count)
|
||||
feature = f"- count({feature}({value})): {count}"
|
||||
elif not feature.startswith("#"):
|
||||
feature, _, comment = feature.partition("#")
|
||||
feature, _, description = feature.partition("=")
|
||||
@@ -72,18 +72,18 @@ def parse_node_for_feature(feature, description, comment, depth):
|
||||
display = ""
|
||||
|
||||
if feature.startswith("#"):
|
||||
display += "%s%s\n" % (" " * depth, feature)
|
||||
display += f"{' '*depth}{feature}\n"
|
||||
elif description:
|
||||
if feature.startswith(("- and", "- or", "- optional", "- basic block", "- not")):
|
||||
display += "%s%s" % (" " * depth, feature)
|
||||
if feature.startswith(("- and", "- or", "- optional", "- basic block", "- not", "- instruction:")):
|
||||
display += f"{' '*depth}{feature}\n"
|
||||
if comment:
|
||||
display += " # %s" % comment
|
||||
display += "\n%s- description: %s\n" % (" " * (depth + 2), description)
|
||||
display += f" # {comment}"
|
||||
display += f"\n{' '*(depth+2)}- description: {description}\n"
|
||||
elif feature.startswith("- string"):
|
||||
display += "%s%s" % (" " * depth, feature)
|
||||
display += f"{' '*depth}{feature}\n"
|
||||
if comment:
|
||||
display += " # %s" % comment
|
||||
display += "\n%sdescription: %s\n" % (" " * (depth + 2), description)
|
||||
display += f" # {comment}"
|
||||
display += f"\n{' '*(depth+2)}description: {description}\n"
|
||||
elif feature.startswith("- count"):
|
||||
# count is weird, we need to format description based on feature type, so we parse with regex
|
||||
# assume format - count(<feature_name>(<feature_value>)): <count>
|
||||
@@ -91,28 +91,22 @@ def parse_node_for_feature(feature, description, comment, depth):
|
||||
if m:
|
||||
name, value, count = m.groups()
|
||||
if name in ("string",):
|
||||
display += "%s%s" % (" " * depth, feature)
|
||||
display += f"{' '*depth}{feature}"
|
||||
if comment:
|
||||
display += " # %s" % comment
|
||||
display += "\n%sdescription: %s\n" % (" " * (depth + 2), description)
|
||||
display += f" # {comment}"
|
||||
display += f"\n{' '*(depth+2)}description: {description}\n"
|
||||
else:
|
||||
display += "%s- count(%s(%s = %s)): %s" % (
|
||||
" " * depth,
|
||||
name,
|
||||
value,
|
||||
description,
|
||||
count,
|
||||
)
|
||||
display += f"{' '*depth}- count({name}({value} = {description})): {count}"
|
||||
if comment:
|
||||
display += " # %s\n" % comment
|
||||
display += f" # {comment}\n"
|
||||
else:
|
||||
display += "%s%s = %s" % (" " * depth, feature, description)
|
||||
display += f"{' '*depth}{feature} = {description}"
|
||||
if comment:
|
||||
display += " # %s\n" % comment
|
||||
display += f" # {comment}\n"
|
||||
else:
|
||||
display += "%s%s" % (" " * depth, feature)
|
||||
display += f"{' '*depth}{feature}"
|
||||
if comment:
|
||||
display += " # %s\n" % comment
|
||||
display += f" # {comment}\n"
|
||||
|
||||
return display if display.endswith("\n") else display + "\n"
|
||||
|
||||
@@ -174,16 +168,16 @@ def resize_columns_to_content(header):
|
||||
|
||||
|
||||
class CapaExplorerRulegenPreview(QtWidgets.QTextEdit):
|
||||
|
||||
INDENT = " " * 2
|
||||
|
||||
def __init__(self, parent=None):
|
||||
""" """
|
||||
super(CapaExplorerRulegenPreview, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
|
||||
self.setFont(QtGui.QFont("Courier", weight=QtGui.QFont.Bold))
|
||||
self.setLineWrapMode(QtWidgets.QTextEdit.NoWrap)
|
||||
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
|
||||
self.setAcceptRichText(False)
|
||||
|
||||
def reset_view(self):
|
||||
""" """
|
||||
@@ -198,13 +192,14 @@ class CapaExplorerRulegenPreview(QtWidgets.QTextEdit):
|
||||
" name: <insert_name>",
|
||||
" namespace: <insert_namespace>",
|
||||
" authors:",
|
||||
" - %s" % author,
|
||||
" scope: %s" % scope,
|
||||
" references: <insert_references>",
|
||||
f" - {author}",
|
||||
f" scope: {scope}",
|
||||
" references:",
|
||||
" - <insert_references>",
|
||||
" examples:",
|
||||
" - %s:0x%X" % (capa.ida.helpers.get_file_md5().upper(), ea)
|
||||
f" - {capa.ida.helpers.get_file_md5().upper()}:{hex(ea)}"
|
||||
if ea
|
||||
else " - %s" % (capa.ida.helpers.get_file_md5().upper()),
|
||||
else f" - {capa.ida.helpers.get_file_md5().upper()}",
|
||||
" features:",
|
||||
]
|
||||
self.setText("\n".join(metadata_default))
|
||||
@@ -253,7 +248,7 @@ class CapaExplorerRulegenPreview(QtWidgets.QTextEdit):
|
||||
lines_modified = 0
|
||||
first_modified = False
|
||||
change = []
|
||||
for (lineno, line) in enumerate(plain[start_lineno : end_lineno + 1]):
|
||||
for lineno, line in enumerate(plain[start_lineno : end_lineno + 1]):
|
||||
if line.startswith(self.INDENT):
|
||||
if lineno == 0:
|
||||
# keep track if first line is modified, so we can properly display
|
||||
@@ -284,7 +279,7 @@ class CapaExplorerRulegenPreview(QtWidgets.QTextEdit):
|
||||
self.set_selection(select_start_ppos, select_end_ppos, len(self.toPlainText()))
|
||||
self.verticalScrollBar().setSliderPosition(scroll_ppos)
|
||||
else:
|
||||
super(CapaExplorerRulegenPreview, self).keyPressEvent(e)
|
||||
super().keyPressEvent(e)
|
||||
|
||||
def count_previous_lines_from_block(self, block):
|
||||
"""calculate number of lines preceding block"""
|
||||
@@ -305,12 +300,11 @@ class CapaExplorerRulegenPreview(QtWidgets.QTextEdit):
|
||||
|
||||
|
||||
class CapaExplorerRulegenEditor(QtWidgets.QTreeWidget):
|
||||
|
||||
updated = QtCore.pyqtSignal()
|
||||
|
||||
def __init__(self, preview, parent=None):
|
||||
""" """
|
||||
super(CapaExplorerRulegenEditor, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
|
||||
self.preview = preview
|
||||
|
||||
@@ -374,18 +368,18 @@ class CapaExplorerRulegenEditor(QtWidgets.QTreeWidget):
|
||||
|
||||
def dragMoveEvent(self, e):
|
||||
""" """
|
||||
super(CapaExplorerRulegenEditor, self).dragMoveEvent(e)
|
||||
super().dragMoveEvent(e)
|
||||
|
||||
def dragEventEnter(self, e):
|
||||
""" """
|
||||
super(CapaExplorerRulegenEditor, self).dragEventEnter(e)
|
||||
super().dragEventEnter(e)
|
||||
|
||||
def dropEvent(self, e):
|
||||
""" """
|
||||
if not self.indexAt(e.pos()).isValid():
|
||||
return
|
||||
|
||||
super(CapaExplorerRulegenEditor, self).dropEvent(e)
|
||||
super().dropEvent(e)
|
||||
|
||||
self.update_preview()
|
||||
expand_tree(self.invisibleRootItem())
|
||||
@@ -427,6 +421,10 @@ class CapaExplorerRulegenEditor(QtWidgets.QTreeWidget):
|
||||
# add default child expression when nesting under basic block
|
||||
new_parent.setExpanded(True)
|
||||
new_parent = self.new_expression_node(new_parent, ("- or:", ""))
|
||||
elif "instruction" in action.data()[0]:
|
||||
# add default child expression when nesting under instruction
|
||||
new_parent.setExpanded(True)
|
||||
new_parent = self.new_expression_node(new_parent, ("- or:", ""))
|
||||
|
||||
for o in self.get_features(selected=True):
|
||||
# take child from its parent by index, add to new parent
|
||||
@@ -447,6 +445,16 @@ class CapaExplorerRulegenEditor(QtWidgets.QTreeWidget):
|
||||
for child in children:
|
||||
new_parent.addChild(child)
|
||||
new_parent.setExpanded(True)
|
||||
elif "instruction" in expression and "instruction" not in o.text(
|
||||
CapaExplorerRulegenEditor.get_column_feature_index()
|
||||
):
|
||||
# current expression is "instruction", and not changing to "instruction" expression
|
||||
children = o.takeChildren()
|
||||
new_parent = self.new_expression_node(o, ("- or:", ""))
|
||||
for child in children:
|
||||
new_parent.addChild(child)
|
||||
new_parent.setExpanded(True)
|
||||
|
||||
o.setText(CapaExplorerRulegenEditor.get_column_feature_index(), expression)
|
||||
|
||||
def slot_clear_all(self, action):
|
||||
@@ -520,11 +528,12 @@ class CapaExplorerRulegenEditor(QtWidgets.QTreeWidget):
|
||||
("not", ("- not:",), self.slot_nest_features),
|
||||
("optional", ("- optional:",), self.slot_nest_features),
|
||||
("basic block", ("- basic block:",), self.slot_nest_features),
|
||||
("instruction", ("- instruction:",), self.slot_nest_features),
|
||||
)
|
||||
|
||||
# build submenu with modify actions
|
||||
sub_menu = build_context_menu(self.parent(), sub_actions)
|
||||
sub_menu.setTitle("Nest feature%s" % ("" if len(tuple(self.get_features(selected=True))) == 1 else "s"))
|
||||
sub_menu.setTitle(f"Nest feature{'' if len(tuple(self.get_features(selected=True))) == 1 else 's'}")
|
||||
|
||||
# build main menu with submenu + main actions
|
||||
menu = build_context_menu(self.parent(), (sub_menu,) + actions)
|
||||
@@ -541,6 +550,7 @@ class CapaExplorerRulegenEditor(QtWidgets.QTreeWidget):
|
||||
("not", ("- not:", self.itemAt(pos)), self.slot_edit_expression),
|
||||
("optional", ("- optional:", self.itemAt(pos)), self.slot_edit_expression),
|
||||
("basic block", ("- basic block:", self.itemAt(pos)), self.slot_edit_expression),
|
||||
("instruction", ("- instruction:", self.itemAt(pos)), self.slot_edit_expression),
|
||||
)
|
||||
|
||||
# build submenu with modify actions
|
||||
@@ -601,7 +611,7 @@ class CapaExplorerRulegenEditor(QtWidgets.QTreeWidget):
|
||||
""" """
|
||||
o = QtWidgets.QTreeWidgetItem(parent)
|
||||
self.set_expression_node(o)
|
||||
for (i, v) in enumerate(values):
|
||||
for i, v in enumerate(values):
|
||||
o.setText(i, v)
|
||||
return o
|
||||
|
||||
@@ -609,7 +619,7 @@ class CapaExplorerRulegenEditor(QtWidgets.QTreeWidget):
|
||||
""" """
|
||||
o = QtWidgets.QTreeWidgetItem(parent)
|
||||
self.set_feature_node(o)
|
||||
for (i, v) in enumerate(values):
|
||||
for i, v in enumerate(values):
|
||||
o.setText(i, v)
|
||||
return o
|
||||
|
||||
@@ -617,7 +627,7 @@ class CapaExplorerRulegenEditor(QtWidgets.QTreeWidget):
|
||||
""" """
|
||||
o = QtWidgets.QTreeWidgetItem(parent)
|
||||
self.set_comment_node(o)
|
||||
for (i, v) in enumerate(values):
|
||||
for i, v in enumerate(values):
|
||||
o.setText(i, v)
|
||||
return o
|
||||
|
||||
@@ -636,23 +646,23 @@ class CapaExplorerRulegenEditor(QtWidgets.QTreeWidget):
|
||||
counted = list(zip(Counter(features).keys(), Counter(features).values()))
|
||||
|
||||
# single features
|
||||
for (k, v) in filter(lambda t: t[1] == 1, counted):
|
||||
for k, v in filter(lambda t: t[1] == 1, counted):
|
||||
if isinstance(k, (capa.features.common.String,)):
|
||||
value = '"%s"' % capa.features.common.escape_string(k.get_value_str())
|
||||
value = f'"{capa.features.common.escape_string(k.get_value_str())}"'
|
||||
else:
|
||||
value = k.get_value_str()
|
||||
self.new_feature_node(top_node, ("- %s: %s" % (k.name.lower(), value), ""))
|
||||
self.new_feature_node(top_node, (f"- {k.name.lower()}: {value}", ""))
|
||||
|
||||
# n > 1 features
|
||||
for (k, v) in filter(lambda t: t[1] > 1, counted):
|
||||
for k, v in filter(lambda t: t[1] > 1, counted):
|
||||
if k.value:
|
||||
if isinstance(k, (capa.features.common.String,)):
|
||||
value = '"%s"' % capa.features.common.escape_string(k.get_value_str())
|
||||
value = f'"{capa.features.common.escape_string(k.get_value_str())}"'
|
||||
else:
|
||||
value = k.get_value_str()
|
||||
display = "- count(%s(%s)): %d" % (k.name.lower(), value, v)
|
||||
display = f"- count({k.name.lower()}({value})): {v}"
|
||||
else:
|
||||
display = "- count(%s): %d" % (k.name.lower(), v)
|
||||
display = f"- count({k.name.lower()}): {v}"
|
||||
self.new_feature_node(top_node, (display, ""))
|
||||
|
||||
self.update_preview()
|
||||
@@ -689,11 +699,11 @@ class CapaExplorerRulegenEditor(QtWidgets.QTreeWidget):
|
||||
node = QtWidgets.QTreeWidgetItem(parent)
|
||||
|
||||
# set node text to data parsed from feature
|
||||
for (idx, text) in enumerate((feature, comment, description)):
|
||||
for idx, text in enumerate((feature, comment, description)):
|
||||
node.setText(idx, text)
|
||||
|
||||
# we need to set our own type so we can control the GUI accordingly
|
||||
if feature.startswith(("- and:", "- or:", "- not:", "- basic block:", "- optional:")):
|
||||
if feature.startswith(("- and:", "- or:", "- not:", "- basic block:", "- instruction:", "- optional:")):
|
||||
setattr(node, "capa_type", CapaExplorerRulegenEditor.get_node_type_expression())
|
||||
elif feature.startswith("#"):
|
||||
setattr(node, "capa_type", CapaExplorerRulegenEditor.get_node_type_comment())
|
||||
@@ -784,7 +794,7 @@ class CapaExplorerRulegenEditor(QtWidgets.QTreeWidget):
|
||||
class CapaExplorerRulegenFeatures(QtWidgets.QTreeWidget):
|
||||
def __init__(self, editor, parent=None):
|
||||
""" """
|
||||
super(CapaExplorerRulegenFeatures, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
|
||||
self.parent_items = {}
|
||||
self.editor = editor
|
||||
@@ -864,7 +874,7 @@ class CapaExplorerRulegenFeatures(QtWidgets.QTreeWidget):
|
||||
if isinstance(self.selectedItems()[0].data(0, 0x100), capa.features.common.Bytes):
|
||||
actions.append(("Add n bytes...", (), self.slot_add_n_bytes_feature))
|
||||
else:
|
||||
action_add_features_fmt = "Add %d features" % selected_items_count
|
||||
action_add_features_fmt = f"Add {selected_items_count} features"
|
||||
|
||||
actions.append((action_add_features_fmt, (), self.slot_add_selected_features))
|
||||
|
||||
@@ -981,7 +991,7 @@ class CapaExplorerRulegenFeatures(QtWidgets.QTreeWidget):
|
||||
o = QtWidgets.QTreeWidgetItem(parent)
|
||||
|
||||
self.set_parent_node(o)
|
||||
for (i, v) in enumerate(data):
|
||||
for i, v in enumerate(data):
|
||||
o.setText(i, v)
|
||||
if feature:
|
||||
o.setData(0, 0x100, feature)
|
||||
@@ -993,7 +1003,7 @@ class CapaExplorerRulegenFeatures(QtWidgets.QTreeWidget):
|
||||
o = QtWidgets.QTreeWidgetItem(parent)
|
||||
|
||||
self.set_leaf_node(o)
|
||||
for (i, v) in enumerate(data):
|
||||
for i, v in enumerate(data):
|
||||
o.setText(i, v)
|
||||
if feature:
|
||||
o.setData(0, 0x100, feature)
|
||||
@@ -1012,18 +1022,20 @@ class CapaExplorerRulegenFeatures(QtWidgets.QTreeWidget):
|
||||
self.parent_items = {}
|
||||
|
||||
def format_address(e):
|
||||
assert isinstance(e, Address)
|
||||
return "%X" % e if not isinstance(e, _NoAddress) else ""
|
||||
if isinstance(e, AbsoluteVirtualAddress):
|
||||
return f"{hex(int(e))}"
|
||||
else:
|
||||
return ""
|
||||
|
||||
def format_feature(feature):
|
||||
""" """
|
||||
name = feature.name.lower()
|
||||
value = feature.get_value_str()
|
||||
if isinstance(feature, (capa.features.common.String,)):
|
||||
value = '"%s"' % capa.features.common.escape_string(value)
|
||||
return "%s(%s)" % (name, value)
|
||||
value = f'"{capa.features.common.escape_string(value)}"'
|
||||
return f"{name}({value})"
|
||||
|
||||
for (feature, addrs) in sorted(features.items(), key=lambda k: sorted(k[1])):
|
||||
for feature, addrs in sorted(features.items(), key=lambda k: sorted(k[1])):
|
||||
if isinstance(feature, capa.features.basicblock.BasicBlock):
|
||||
# filter basic blocks for now, we may want to add these back in some time
|
||||
# in the future
|
||||
@@ -1056,7 +1068,7 @@ class CapaExplorerRulegenFeatures(QtWidgets.QTreeWidget):
|
||||
else:
|
||||
# some features may not have an address e.g. "format"
|
||||
addr = _NoAddress()
|
||||
for (i, v) in enumerate((format_feature(feature), format_address(addr))):
|
||||
for i, v in enumerate((format_feature(feature), format_address(addr))):
|
||||
self.parent_items[feature].setText(i, v)
|
||||
self.parent_items[feature].setData(0, 0x100, feature)
|
||||
|
||||
@@ -1072,7 +1084,7 @@ class CapaExplorerQtreeView(QtWidgets.QTreeView):
|
||||
|
||||
def __init__(self, model, parent=None):
|
||||
"""initialize view"""
|
||||
super(CapaExplorerQtreeView, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
|
||||
self.setModel(model)
|
||||
|
||||
|
||||
364
capa/main.py
364
capa/main.py
@@ -17,11 +17,10 @@ import os.path
|
||||
import argparse
|
||||
import datetime
|
||||
import textwrap
|
||||
import warnings
|
||||
import itertools
|
||||
import contextlib
|
||||
import collections
|
||||
from typing import Any, Dict, List, Tuple
|
||||
from typing import Any, Dict, List, Tuple, Callable
|
||||
|
||||
import halo
|
||||
import tqdm
|
||||
@@ -34,6 +33,7 @@ import capa.rules
|
||||
import capa.engine
|
||||
import capa.version
|
||||
import capa.render.json
|
||||
import capa.rules.cache
|
||||
import capa.render.default
|
||||
import capa.render.verbose
|
||||
import capa.features.common
|
||||
@@ -58,33 +58,38 @@ from capa.helpers import (
|
||||
)
|
||||
from capa.exceptions import UnsupportedOSError, UnsupportedArchError, UnsupportedFormatError, UnsupportedRuntimeError
|
||||
from capa.features.common import (
|
||||
OS_AUTO,
|
||||
OS_LINUX,
|
||||
OS_MACOS,
|
||||
FORMAT_PE,
|
||||
FORMAT_ELF,
|
||||
OS_WINDOWS,
|
||||
FORMAT_AUTO,
|
||||
FORMAT_SC32,
|
||||
FORMAT_SC64,
|
||||
FORMAT_DOTNET,
|
||||
FORMAT_FREEZE,
|
||||
FORMAT_RESULT,
|
||||
)
|
||||
from capa.features.address import NO_ADDRESS
|
||||
from capa.features.address import NO_ADDRESS, Address
|
||||
from capa.features.extractors.base_extractor import BBHandle, InsnHandle, FunctionHandle, FeatureExtractor
|
||||
|
||||
RULES_PATH_DEFAULT_STRING = "(embedded rules)"
|
||||
SIGNATURES_PATH_DEFAULT_STRING = "(embedded signatures)"
|
||||
BACKEND_VIV = "vivisect"
|
||||
BACKEND_SMDA = "smda"
|
||||
BACKEND_DOTNET = "dotnet"
|
||||
BACKEND_BINJA = "binja"
|
||||
|
||||
E_MISSING_RULES = -10
|
||||
E_MISSING_FILE = -11
|
||||
E_INVALID_RULE = -12
|
||||
E_CORRUPT_FILE = -13
|
||||
E_FILE_LIMITATION = -14
|
||||
E_INVALID_SIG = -15
|
||||
E_INVALID_FILE_TYPE = -16
|
||||
E_INVALID_FILE_ARCH = -17
|
||||
E_INVALID_FILE_OS = -18
|
||||
E_UNSUPPORTED_IDA_VERSION = -19
|
||||
E_MISSING_RULES = 10
|
||||
E_MISSING_FILE = 11
|
||||
E_INVALID_RULE = 12
|
||||
E_CORRUPT_FILE = 13
|
||||
E_FILE_LIMITATION = 14
|
||||
E_INVALID_SIG = 15
|
||||
E_INVALID_FILE_TYPE = 16
|
||||
E_INVALID_FILE_ARCH = 17
|
||||
E_INVALID_FILE_OS = 18
|
||||
E_UNSUPPORTED_IDA_VERSION = 19
|
||||
|
||||
logger = logging.getLogger("capa")
|
||||
|
||||
@@ -262,9 +267,9 @@ def find_capabilities(ruleset: RuleSet, extractor: FeatureExtractor, disable_pro
|
||||
logger.debug("skipping library function 0x%x (%s)", f.address, function_name)
|
||||
meta["library_functions"][f.address] = function_name
|
||||
n_libs = len(meta["library_functions"])
|
||||
percentage = 100 * (n_libs / n_funcs)
|
||||
percentage = round(100 * (n_libs / n_funcs))
|
||||
if isinstance(pb, tqdm.tqdm):
|
||||
pb.set_postfix_str("skipped %d library functions (%d%%)" % (n_libs, percentage))
|
||||
pb.set_postfix_str(f"skipped {n_libs} library functions ({percentage}%)")
|
||||
continue
|
||||
|
||||
function_matches, bb_matches, insn_matches, feature_count = find_code_capabilities(ruleset, extractor, f)
|
||||
@@ -332,7 +337,7 @@ def has_file_limitation(rules: RuleSet, capabilities: MatchResults, is_standalon
|
||||
|
||||
logger.warning("-" * 80)
|
||||
for line in file_limitation_rule.meta.get("description", "").split("\n"):
|
||||
logger.warning(" " + line)
|
||||
logger.warning(" %s", line)
|
||||
logger.warning(" Identified via rule: %s", file_limitation_rule.name)
|
||||
if is_standalone:
|
||||
logger.warning(" ")
|
||||
@@ -398,8 +403,8 @@ def get_meta_str(vw):
|
||||
meta = []
|
||||
for k in ["Format", "Platform", "Architecture"]:
|
||||
if k in vw.metadata:
|
||||
meta.append("%s: %s" % (k.lower(), vw.metadata[k]))
|
||||
return "%s, number of functions: %d" % (", ".join(meta), len(vw.getFunctions()))
|
||||
meta.append(f"{k.lower()}: {vw.metadata[k]}")
|
||||
return f"{', '.join(meta)}, number of functions: {len(vw.getFunctions())}"
|
||||
|
||||
|
||||
def is_running_standalone() -> bool:
|
||||
@@ -433,7 +438,7 @@ def get_default_signatures() -> List[str]:
|
||||
logger.debug("signatures path: %s", sigs_path)
|
||||
|
||||
ret = []
|
||||
for root, dirs, files in os.walk(sigs_path):
|
||||
for root, _, files in os.walk(sigs_path):
|
||||
for file in files:
|
||||
if not (file.endswith(".pat") or file.endswith(".pat.gz") or file.endswith(".sig")):
|
||||
continue
|
||||
@@ -461,6 +466,7 @@ def get_workspace(path, format_, sigpaths):
|
||||
|
||||
# lazy import enables us to not require viv if user wants SMDA, for example.
|
||||
import viv_utils
|
||||
import viv_utils.flirt
|
||||
|
||||
logger.debug("generating vivisect workspace for: %s", path)
|
||||
# TODO should not be auto at this point, anymore
|
||||
@@ -490,7 +496,13 @@ def get_workspace(path, format_, sigpaths):
|
||||
|
||||
# TODO get_extractors -> List[FeatureExtractor]?
|
||||
def get_extractor(
|
||||
path: str, format_: str, backend: str, sigpaths: List[str], should_save_workspace=False, disable_progress=False
|
||||
path: str,
|
||||
format_: str,
|
||||
os_: str,
|
||||
backend: str,
|
||||
sigpaths: List[str],
|
||||
should_save_workspace=False,
|
||||
disable_progress=False,
|
||||
) -> FeatureExtractor:
|
||||
"""
|
||||
raises:
|
||||
@@ -505,7 +517,7 @@ def get_extractor(
|
||||
if not is_supported_arch(path):
|
||||
raise UnsupportedArchError()
|
||||
|
||||
if not is_supported_os(path):
|
||||
if os_ == OS_AUTO and not is_supported_os(path):
|
||||
raise UnsupportedOSError()
|
||||
|
||||
if format_ == FORMAT_DOTNET:
|
||||
@@ -513,22 +525,34 @@ def get_extractor(
|
||||
|
||||
return capa.features.extractors.dnfile.extractor.DnfileFeatureExtractor(path)
|
||||
|
||||
if backend == "smda":
|
||||
from smda.SmdaConfig import SmdaConfig
|
||||
from smda.Disassembler import Disassembler
|
||||
elif backend == BACKEND_BINJA:
|
||||
from capa.features.extractors.binja.find_binja_api import find_binja_path
|
||||
|
||||
import capa.features.extractors.smda.extractor
|
||||
# When we are running as a standalone executable, we cannot directly import binaryninja
|
||||
# We need to fist find the binja API installation path and add it into sys.path
|
||||
if is_running_standalone():
|
||||
bn_api = find_binja_path()
|
||||
if os.path.exists(bn_api):
|
||||
sys.path.append(bn_api)
|
||||
|
||||
try:
|
||||
from binaryninja import BinaryView, BinaryViewType
|
||||
except ImportError:
|
||||
raise RuntimeError(
|
||||
"Cannot import binaryninja module. Please install the Binary Ninja Python API first: "
|
||||
"https://docs.binary.ninja/dev/batch.html#install-the-api)."
|
||||
)
|
||||
|
||||
import capa.features.extractors.binja.extractor
|
||||
|
||||
logger.warning("Deprecation warning: v4.0 will be the last capa version to support the SMDA backend.")
|
||||
warnings.warn("v4.0 will be the last capa version to support the SMDA backend.", DeprecationWarning)
|
||||
smda_report = None
|
||||
with halo.Halo(text="analyzing program", spinner="simpleDots", stream=sys.stderr, enabled=not disable_progress):
|
||||
config = SmdaConfig()
|
||||
config.STORE_BUFFER = True
|
||||
smda_disasm = Disassembler(config)
|
||||
smda_report = smda_disasm.disassembleFile(path)
|
||||
bv: BinaryView = BinaryViewType.get_view_of_file(path)
|
||||
if bv is None:
|
||||
raise RuntimeError(f"Binary Ninja cannot open file {path}")
|
||||
|
||||
return capa.features.extractors.smda.extractor.SmdaFeatureExtractor(smda_report, path)
|
||||
return capa.features.extractors.binja.extractor.BinjaFeatureExtractor(bv)
|
||||
|
||||
# default to use vivisect backend
|
||||
else:
|
||||
import capa.features.extractors.viv.extractor
|
||||
|
||||
@@ -545,18 +569,18 @@ def get_extractor(
|
||||
else:
|
||||
logger.debug("CAPA_SAVE_WORKSPACE unset, not saving workspace")
|
||||
|
||||
return capa.features.extractors.viv.extractor.VivisectFeatureExtractor(vw, path)
|
||||
return capa.features.extractors.viv.extractor.VivisectFeatureExtractor(vw, path, os_)
|
||||
|
||||
|
||||
def get_file_extractors(sample: str, format_: str) -> List[FeatureExtractor]:
|
||||
file_extractors: List[FeatureExtractor] = list()
|
||||
|
||||
if format_ == capa.features.extractors.common.FORMAT_PE:
|
||||
if format_ == FORMAT_PE:
|
||||
file_extractors.append(capa.features.extractors.pefile.PefileFeatureExtractor(sample))
|
||||
|
||||
dnfile_extractor = capa.features.extractors.dnfile_.DnfileFeatureExtractor(sample)
|
||||
if dnfile_extractor.is_dotnet_file():
|
||||
file_extractors.append(dnfile_extractor)
|
||||
elif format_ == FORMAT_DOTNET:
|
||||
file_extractors.append(capa.features.extractors.pefile.PefileFeatureExtractor(sample))
|
||||
file_extractors.append(capa.features.extractors.dnfile_.DnfileFeatureExtractor(sample))
|
||||
|
||||
elif format_ == capa.features.extractors.common.FORMAT_ELF:
|
||||
file_extractors.append(capa.features.extractors.elffile.ElfFeatureExtractor(sample))
|
||||
@@ -577,17 +601,20 @@ def is_nursery_rule_path(path: str) -> bool:
|
||||
return "nursery" in path
|
||||
|
||||
|
||||
def get_rules(rule_paths: List[str], disable_progress=False) -> List[Rule]:
|
||||
def collect_rule_file_paths(rule_paths: List[str]) -> List[str]:
|
||||
"""
|
||||
collect all rule file paths, including those in subdirectories.
|
||||
"""
|
||||
rule_file_paths = []
|
||||
for rule_path in rule_paths:
|
||||
if not os.path.exists(rule_path):
|
||||
raise IOError("rule path %s does not exist or cannot be accessed" % rule_path)
|
||||
raise IOError(f"rule path {rule_path} does not exist or cannot be accessed")
|
||||
|
||||
if os.path.isfile(rule_path):
|
||||
rule_file_paths.append(rule_path)
|
||||
elif os.path.isdir(rule_path):
|
||||
logger.debug("reading rules from directory %s", rule_path)
|
||||
for root, dirs, files in os.walk(rule_path):
|
||||
for root, _, files in os.walk(rule_path):
|
||||
if ".git" in root:
|
||||
# the .github directory contains CI config in capa-rules
|
||||
# this includes some .yml files
|
||||
@@ -605,40 +632,81 @@ def get_rules(rule_paths: List[str], disable_progress=False) -> List[Rule]:
|
||||
rule_path = os.path.join(root, file)
|
||||
rule_file_paths.append(rule_path)
|
||||
|
||||
return rule_file_paths
|
||||
|
||||
|
||||
# TypeAlias. note: using `foo: TypeAlias = bar` is Python 3.10+
|
||||
RulePath = str
|
||||
|
||||
|
||||
def on_load_rule_default(_path: RulePath, i: int, _total: int) -> None:
|
||||
return
|
||||
|
||||
|
||||
def get_rules(
|
||||
rule_paths: List[RulePath],
|
||||
cache_dir=None,
|
||||
on_load_rule: Callable[[RulePath, int, int], None] = on_load_rule_default,
|
||||
) -> RuleSet:
|
||||
"""
|
||||
args:
|
||||
rule_paths: list of paths to rules files or directories containing rules files
|
||||
cache_dir: directory to use for caching rules, or will use the default detected cache directory if None
|
||||
on_load_rule: callback to invoke before a rule is loaded, use for progress or cancellation
|
||||
"""
|
||||
if cache_dir is None:
|
||||
cache_dir = capa.rules.cache.get_default_cache_directory()
|
||||
|
||||
# rule_paths may contain directory paths,
|
||||
# so search for file paths recursively.
|
||||
rule_file_paths = collect_rule_file_paths(rule_paths)
|
||||
|
||||
# this list is parallel to `rule_file_paths`:
|
||||
# rule_file_paths[i] corresponds to rule_contents[i].
|
||||
rule_contents = []
|
||||
for file_path in rule_file_paths:
|
||||
with open(file_path, "rb") as f:
|
||||
rule_contents.append(f.read())
|
||||
|
||||
ruleset = capa.rules.cache.load_cached_ruleset(cache_dir, rule_contents)
|
||||
if ruleset is not None:
|
||||
return ruleset
|
||||
|
||||
rules = [] # type: List[Rule]
|
||||
|
||||
pbar = tqdm.tqdm
|
||||
if disable_progress:
|
||||
# do not use tqdm to avoid unnecessary side effects when caller intends
|
||||
# to disable progress completely
|
||||
pbar = lambda s, *args, **kwargs: s
|
||||
total_rule_count = len(rule_file_paths)
|
||||
for i, (path, content) in enumerate(zip(rule_file_paths, rule_contents)):
|
||||
on_load_rule(path, i, total_rule_count)
|
||||
|
||||
for rule_file_path in pbar(list(rule_file_paths), desc="loading ", unit=" rules"):
|
||||
try:
|
||||
rule = capa.rules.Rule.from_yaml_file(rule_file_path)
|
||||
rule = capa.rules.Rule.from_yaml(content.decode("utf-8"))
|
||||
except capa.rules.InvalidRule:
|
||||
raise
|
||||
else:
|
||||
rule.meta["capa/path"] = rule_file_path
|
||||
if is_nursery_rule_path(rule_file_path):
|
||||
rule.meta["capa/path"] = path
|
||||
if is_nursery_rule_path(path):
|
||||
rule.meta["capa/nursery"] = True
|
||||
|
||||
rules.append(rule)
|
||||
logger.debug("loaded rule: '%s' with scope: %s", rule.name, rule.scope)
|
||||
|
||||
return rules
|
||||
ruleset = capa.rules.RuleSet(rules)
|
||||
|
||||
capa.rules.cache.cache_ruleset(cache_dir, ruleset)
|
||||
|
||||
return ruleset
|
||||
|
||||
|
||||
def get_signatures(sigs_path):
|
||||
if not os.path.exists(sigs_path):
|
||||
raise IOError("signatures path %s does not exist or cannot be accessed" % sigs_path)
|
||||
raise IOError(f"signatures path {sigs_path} does not exist or cannot be accessed")
|
||||
|
||||
paths = []
|
||||
if os.path.isfile(sigs_path):
|
||||
paths.append(sigs_path)
|
||||
elif os.path.isdir(sigs_path):
|
||||
logger.debug("reading signatures from directory %s", os.path.abspath(os.path.normpath(sigs_path)))
|
||||
for root, dirs, files in os.walk(sigs_path):
|
||||
for root, _, files in os.walk(sigs_path):
|
||||
for file in files:
|
||||
if file.endswith((".pat", ".pat.gz", ".sig")):
|
||||
sig_path = os.path.join(root, file)
|
||||
@@ -660,6 +728,8 @@ def get_signatures(sigs_path):
|
||||
def collect_metadata(
|
||||
argv: List[str],
|
||||
sample_path: str,
|
||||
format_: str,
|
||||
os_: str,
|
||||
rules_path: List[str],
|
||||
extractor: capa.features.extractors.base_extractor.FeatureExtractor,
|
||||
):
|
||||
@@ -677,9 +747,9 @@ def collect_metadata(
|
||||
if rules_path != [RULES_PATH_DEFAULT_STRING]:
|
||||
rules_path = [os.path.abspath(os.path.normpath(r)) for r in rules_path]
|
||||
|
||||
format_ = get_format(sample_path)
|
||||
format_ = get_format(sample_path) if format_ == FORMAT_AUTO else format_
|
||||
arch = get_arch(sample_path)
|
||||
os_ = get_os(sample_path)
|
||||
os_ = get_os(sample_path) if os_ == OS_AUTO else os_
|
||||
|
||||
return {
|
||||
"timestamp": datetime.datetime.now().isoformat(),
|
||||
@@ -717,8 +787,8 @@ def compute_layout(rules, extractor, capabilities):
|
||||
otherwise, we may pollute the json document with
|
||||
a large amount of un-referenced data.
|
||||
"""
|
||||
functions_by_bb = {}
|
||||
bbs_by_function = {}
|
||||
functions_by_bb: Dict[Address, Address] = {}
|
||||
bbs_by_function: Dict[Address, List[Address]] = {}
|
||||
for f in extractor.get_functions():
|
||||
bbs_by_function[f.address] = []
|
||||
for bb in extractor.get_basic_blocks(f):
|
||||
@@ -729,7 +799,7 @@ def compute_layout(rules, extractor, capabilities):
|
||||
for rule_name, matches in capabilities.items():
|
||||
rule = rules[rule_name]
|
||||
if rule.meta.get("scope") == capa.rules.BASIC_BLOCK_SCOPE:
|
||||
for (addr, match) in matches:
|
||||
for addr, _ in matches:
|
||||
assert addr in functions_by_bb
|
||||
matched_bbs.add(addr)
|
||||
|
||||
@@ -761,6 +831,7 @@ def install_common_args(parser, wanted=None):
|
||||
wanted (Set[str]): collection of arguments to opt-into, including:
|
||||
- "sample": required positional argument to input file.
|
||||
- "format": flag to override file format.
|
||||
- "os": flag to override file operating system.
|
||||
- "backend": flag to override analysis backend.
|
||||
- "rules": flag to override path to capa rules.
|
||||
- "tag": flag to override/specify which rules to match.
|
||||
@@ -794,6 +865,7 @@ def install_common_args(parser, wanted=None):
|
||||
#
|
||||
# - sample
|
||||
# - format
|
||||
# - os
|
||||
# - rules
|
||||
# - tag
|
||||
#
|
||||
@@ -815,24 +887,39 @@ def install_common_args(parser, wanted=None):
|
||||
(FORMAT_SC64, "64-bit shellcode"),
|
||||
(FORMAT_FREEZE, "features previously frozen by capa"),
|
||||
]
|
||||
format_help = ", ".join(["%s: %s" % (f[0], f[1]) for f in formats])
|
||||
format_help = ", ".join([f"{f[0]}: {f[1]}" for f in formats])
|
||||
parser.add_argument(
|
||||
"-f",
|
||||
"--format",
|
||||
choices=[f[0] for f in formats],
|
||||
default=FORMAT_AUTO,
|
||||
help="select sample format, %s" % format_help,
|
||||
help=f"select sample format, {format_help}",
|
||||
)
|
||||
|
||||
if "backend" in wanted:
|
||||
parser.add_argument(
|
||||
"-b",
|
||||
"--backend",
|
||||
type=str,
|
||||
help="select the backend to use",
|
||||
choices=(BACKEND_VIV, BACKEND_SMDA),
|
||||
default=BACKEND_VIV,
|
||||
)
|
||||
if "backend" in wanted:
|
||||
parser.add_argument(
|
||||
"-b",
|
||||
"--backend",
|
||||
type=str,
|
||||
help="select the backend to use",
|
||||
choices=(BACKEND_VIV, BACKEND_BINJA),
|
||||
default=BACKEND_VIV,
|
||||
)
|
||||
|
||||
if "os" in wanted:
|
||||
oses = [
|
||||
(OS_AUTO, "detect OS automatically - default"),
|
||||
(OS_LINUX,),
|
||||
(OS_MACOS,),
|
||||
(OS_WINDOWS,),
|
||||
]
|
||||
os_help = ", ".join([f"{o[0]} ({o[1]})" if len(o) == 2 else o[0] for o in oses])
|
||||
parser.add_argument(
|
||||
"--os",
|
||||
choices=[o[0] for o in oses],
|
||||
default=OS_AUTO,
|
||||
help=f"select sample OS: {os_help}",
|
||||
)
|
||||
|
||||
if "rules" in wanted:
|
||||
parser.add_argument(
|
||||
@@ -865,6 +952,9 @@ def handle_common_args(args):
|
||||
- rules: file system path to rule files.
|
||||
- signatures: file system path to signature files.
|
||||
|
||||
the following field may be added:
|
||||
- is_default_rules: if the default rules were used.
|
||||
|
||||
args:
|
||||
args (argparse.Namespace): parsed arguments that included at least `install_common_args` args.
|
||||
"""
|
||||
@@ -924,6 +1014,7 @@ def handle_common_args(args):
|
||||
return E_MISSING_RULES
|
||||
|
||||
rules_paths.append(default_rule_path)
|
||||
args.is_default_rules = True
|
||||
else:
|
||||
rules_paths = args.rules
|
||||
|
||||
@@ -933,6 +1024,8 @@ def handle_common_args(args):
|
||||
for rule_path in rules_paths:
|
||||
logger.debug("using rules path: %s", rule_path)
|
||||
|
||||
args.is_default_rules = False
|
||||
|
||||
args.rules = rules_paths
|
||||
|
||||
if hasattr(args, "signatures"):
|
||||
@@ -945,6 +1038,13 @@ def handle_common_args(args):
|
||||
logger.debug("-" * 80)
|
||||
|
||||
sigs_path = os.path.join(get_default_root(), "sigs")
|
||||
if not os.path.exists(sigs_path):
|
||||
logger.error(
|
||||
"Using default signature path, but it doesn't exist. "
|
||||
"Please install the signatures first: "
|
||||
"https://github.com/mandiant/capa/blob/master/doc/installation.md#method-2-using-capa-as-a-python-library."
|
||||
)
|
||||
raise IOError(f"signatures path {sigs_path} does not exist or cannot be accessed")
|
||||
else:
|
||||
sigs_path = args.signatures
|
||||
logger.debug("using signatures path: %s", sigs_path)
|
||||
@@ -991,7 +1091,7 @@ def main(argv=None):
|
||||
parser = argparse.ArgumentParser(
|
||||
description=desc, epilog=epilog, formatter_class=argparse.RawDescriptionHelpFormatter
|
||||
)
|
||||
install_common_args(parser, {"sample", "format", "backend", "signatures", "rules", "tag"})
|
||||
install_common_args(parser, {"sample", "format", "backend", "os", "signatures", "rules", "tag"})
|
||||
parser.add_argument("-j", "--json", action="store_true", help="emit JSON instead of text")
|
||||
args = parser.parse_args(args=argv)
|
||||
ret = handle_common_args(args)
|
||||
@@ -1010,20 +1110,27 @@ def main(argv=None):
|
||||
if format_ == FORMAT_AUTO:
|
||||
try:
|
||||
format_ = get_auto_format(args.sample)
|
||||
except PEFormatError as e:
|
||||
logger.error("Input file '%s' is not a valid PE file: %s", args.sample, str(e))
|
||||
return E_CORRUPT_FILE
|
||||
except UnsupportedFormatError:
|
||||
log_unsupported_format_error()
|
||||
return E_INVALID_FILE_TYPE
|
||||
|
||||
try:
|
||||
rules = get_rules(args.rules, disable_progress=args.quiet)
|
||||
rules = capa.rules.RuleSet(rules)
|
||||
if is_running_standalone() and args.is_default_rules:
|
||||
cache_dir = os.path.join(get_default_root(), "cache")
|
||||
else:
|
||||
cache_dir = capa.rules.cache.get_default_cache_directory()
|
||||
|
||||
rules = get_rules(args.rules, cache_dir=cache_dir)
|
||||
|
||||
logger.debug(
|
||||
"successfully loaded %s rules",
|
||||
# during the load of the RuleSet, we extract subscope statements into their own rules
|
||||
# that are subsequently `match`ed upon. this inflates the total rule count.
|
||||
# so, filter out the subscope rules when reporting total number of loaded rules.
|
||||
len([i for i in filter(lambda r: not r.is_subscope_rule(), rules.rules.values())]),
|
||||
len(list(filter(lambda r: not r.is_subscope_rule(), rules.rules.values()))),
|
||||
)
|
||||
if args.tag:
|
||||
rules = rules.filter_rules_by_meta(args.tag)
|
||||
@@ -1034,12 +1141,12 @@ def main(argv=None):
|
||||
except (IOError, capa.rules.InvalidRule, capa.rules.InvalidRuleSet) as e:
|
||||
logger.error("%s", str(e))
|
||||
logger.error(
|
||||
"Please ensure you're using the rules that correspond to your major version of capa (%s)",
|
||||
capa.version.get_major_version(),
|
||||
"Make sure your file directory contains properly formatted capa rules. You can download the standard "
|
||||
"collection of capa rules from https://github.com/mandiant/capa-rules/releases."
|
||||
)
|
||||
logger.error(
|
||||
"You can check out these rules with the following command:\n %s",
|
||||
capa.version.get_rules_checkout_command(),
|
||||
"Please ensure you're using the rules that correspond to your major version of capa (%s)",
|
||||
capa.version.get_major_version(),
|
||||
)
|
||||
logger.error(
|
||||
"Or, for more details, see the rule set documentation here: %s",
|
||||
@@ -1073,9 +1180,6 @@ def main(argv=None):
|
||||
logger.error("Input file '%s' is not a valid ELF file: %s", args.sample, str(e))
|
||||
return E_CORRUPT_FILE
|
||||
|
||||
if isinstance(file_extractor, capa.features.extractors.dnfile_.DnfileFeatureExtractor):
|
||||
format_ = FORMAT_DOTNET
|
||||
|
||||
# file limitations that rely on non-file scope won't be detected here.
|
||||
# nor on FunctionName features, because pefile doesn't support this.
|
||||
if has_file_limitation(rules, pure_file_capabilities):
|
||||
@@ -1085,47 +1189,72 @@ def main(argv=None):
|
||||
logger.debug("file limitation short circuit, won't analyze fully.")
|
||||
return E_FILE_LIMITATION
|
||||
|
||||
if format_ == FORMAT_FREEZE:
|
||||
with open(args.sample, "rb") as f:
|
||||
extractor = capa.features.freeze.load(f.read())
|
||||
# TODO: #1411 use a real type, not a dict here.
|
||||
meta: Dict[str, Any]
|
||||
capabilities: MatchResults
|
||||
counts: Dict[str, Any]
|
||||
|
||||
if format_ == FORMAT_RESULT:
|
||||
# result document directly parses into meta, capabilities
|
||||
result_doc = capa.render.result_document.ResultDocument.parse_file(args.sample)
|
||||
meta, capabilities = result_doc.to_capa()
|
||||
|
||||
else:
|
||||
try:
|
||||
if format_ == FORMAT_PE:
|
||||
sig_paths = get_signatures(args.signatures)
|
||||
else:
|
||||
sig_paths = []
|
||||
logger.debug("skipping library code matching: only have native PE signatures")
|
||||
except IOError as e:
|
||||
logger.error("%s", str(e))
|
||||
return E_INVALID_SIG
|
||||
# all other formats we must create an extractor
|
||||
# and use that to extract meta and capabilities
|
||||
|
||||
should_save_workspace = os.environ.get("CAPA_SAVE_WORKSPACE") not in ("0", "no", "NO", "n", None)
|
||||
if format_ == FORMAT_FREEZE:
|
||||
# freeze format deserializes directly into an extractor
|
||||
with open(args.sample, "rb") as f:
|
||||
extractor = capa.features.freeze.load(f.read())
|
||||
else:
|
||||
# all other formats we must create an extractor,
|
||||
# such as viv, binary ninja, etc. workspaces
|
||||
# and use those for extracting.
|
||||
|
||||
try:
|
||||
extractor = get_extractor(
|
||||
args.sample, format_, args.backend, sig_paths, should_save_workspace, disable_progress=args.quiet
|
||||
)
|
||||
except UnsupportedFormatError:
|
||||
log_unsupported_format_error()
|
||||
return E_INVALID_FILE_TYPE
|
||||
except UnsupportedArchError:
|
||||
log_unsupported_arch_error()
|
||||
return E_INVALID_FILE_ARCH
|
||||
except UnsupportedOSError:
|
||||
log_unsupported_os_error()
|
||||
return E_INVALID_FILE_OS
|
||||
try:
|
||||
if format_ == FORMAT_PE:
|
||||
sig_paths = get_signatures(args.signatures)
|
||||
else:
|
||||
sig_paths = []
|
||||
logger.debug("skipping library code matching: only have native PE signatures")
|
||||
except IOError as e:
|
||||
logger.error("%s", str(e))
|
||||
return E_INVALID_SIG
|
||||
|
||||
meta = collect_metadata(argv, args.sample, args.rules, extractor)
|
||||
should_save_workspace = os.environ.get("CAPA_SAVE_WORKSPACE") not in ("0", "no", "NO", "n", None)
|
||||
|
||||
capabilities, counts = find_capabilities(rules, extractor, disable_progress=args.quiet)
|
||||
meta["analysis"].update(counts)
|
||||
meta["analysis"]["layout"] = compute_layout(rules, extractor, capabilities)
|
||||
try:
|
||||
extractor = get_extractor(
|
||||
args.sample,
|
||||
format_,
|
||||
args.os,
|
||||
args.backend,
|
||||
sig_paths,
|
||||
should_save_workspace,
|
||||
disable_progress=args.quiet,
|
||||
)
|
||||
except UnsupportedFormatError:
|
||||
log_unsupported_format_error()
|
||||
return E_INVALID_FILE_TYPE
|
||||
except UnsupportedArchError:
|
||||
log_unsupported_arch_error()
|
||||
return E_INVALID_FILE_ARCH
|
||||
except UnsupportedOSError:
|
||||
log_unsupported_os_error()
|
||||
return E_INVALID_FILE_OS
|
||||
|
||||
if has_file_limitation(rules, capabilities):
|
||||
# bail if capa encountered file limitation e.g. a packed binary
|
||||
# do show the output in verbose mode, though.
|
||||
if not (args.verbose or args.vverbose or args.json):
|
||||
return E_FILE_LIMITATION
|
||||
meta = collect_metadata(argv, args.sample, args.format, args.os, args.rules, extractor)
|
||||
|
||||
capabilities, counts = find_capabilities(rules, extractor, disable_progress=args.quiet)
|
||||
meta["analysis"].update(counts)
|
||||
meta["analysis"]["layout"] = compute_layout(rules, extractor, capabilities)
|
||||
|
||||
if has_file_limitation(rules, capabilities):
|
||||
# bail if capa encountered file limitation e.g. a packed binary
|
||||
# do show the output in verbose mode, though.
|
||||
if not (args.verbose or args.vverbose or args.json):
|
||||
return E_FILE_LIMITATION
|
||||
|
||||
if args.json:
|
||||
print(capa.render.json.render(meta, rules, capabilities))
|
||||
@@ -1166,8 +1295,7 @@ def ida_main():
|
||||
|
||||
rules_path = os.path.join(get_default_root(), "rules")
|
||||
logger.debug("rule path: %s", rules_path)
|
||||
rules = get_rules(rules_path)
|
||||
rules = capa.rules.RuleSet(rules)
|
||||
rules = get_rules([rules_path])
|
||||
|
||||
meta = capa.ida.helpers.collect_metadata([rules_path])
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ def optimize_statement(statement):
|
||||
|
||||
if isinstance(statement, (ceng.And, ceng.Or, ceng.Some)):
|
||||
# has .children
|
||||
statement.children = sorted(statement.children, key=lambda n: get_node_cost(n))
|
||||
statement.children = sorted(statement.children, key=get_node_cost)
|
||||
return
|
||||
elif isinstance(statement, (ceng.Not, ceng.Range)):
|
||||
# has .child
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import typing
|
||||
import collections
|
||||
from typing import Dict
|
||||
|
||||
# this structure is unstable and may change before the next major release.
|
||||
counters: Dict[str, int] = collections.Counter()
|
||||
counters: typing.Counter[str] = collections.Counter()
|
||||
|
||||
|
||||
def reset():
|
||||
|
||||
@@ -97,7 +97,7 @@ def render_capabilities(doc: rd.ResultDocument, ostream: StringIO):
|
||||
if count == 1:
|
||||
capability = rutils.bold(rule.meta.name)
|
||||
else:
|
||||
capability = "%s (%d matches)" % (rutils.bold(rule.meta.name), count)
|
||||
capability = f"{rutils.bold(rule.meta.name)} ({count} matches)"
|
||||
rows.append((capability, rule.meta.namespace))
|
||||
|
||||
if rows:
|
||||
@@ -133,11 +133,11 @@ def render_attack(doc: rd.ResultDocument, ostream: StringIO):
|
||||
rows = []
|
||||
for tactic, techniques in sorted(tactics.items()):
|
||||
inner_rows = []
|
||||
for (technique, subtechnique, id) in sorted(techniques):
|
||||
for technique, subtechnique, id in sorted(techniques):
|
||||
if not subtechnique:
|
||||
inner_rows.append("%s %s" % (rutils.bold(technique), id))
|
||||
inner_rows.append(f"{rutils.bold(technique)} {id}")
|
||||
else:
|
||||
inner_rows.append("%s::%s %s" % (rutils.bold(technique), subtechnique, id))
|
||||
inner_rows.append(f"{rutils.bold(technique)}::{subtechnique} {id}")
|
||||
rows.append(
|
||||
(
|
||||
rutils.bold(tactic.upper()),
|
||||
@@ -176,11 +176,11 @@ def render_mbc(doc: rd.ResultDocument, ostream: StringIO):
|
||||
rows = []
|
||||
for objective, behaviors in sorted(objectives.items()):
|
||||
inner_rows = []
|
||||
for (behavior, method, id) in sorted(behaviors):
|
||||
for behavior, method, id in sorted(behaviors):
|
||||
if not method:
|
||||
inner_rows.append("%s [%s]" % (rutils.bold(behavior), id))
|
||||
inner_rows.append(f"{rutils.bold(behavior)} [{id}]")
|
||||
else:
|
||||
inner_rows.append("%s::%s [%s]" % (rutils.bold(behavior), method, id))
|
||||
inner_rows.append(f"{rutils.bold(behavior)}::{method} [{id}]")
|
||||
rows.append(
|
||||
(
|
||||
rutils.bold(objective.upper()),
|
||||
|
||||
741
capa/render/proto/__init__.py
Normal file
741
capa/render/proto/__init__.py
Normal file
@@ -0,0 +1,741 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
"""
|
||||
Convert capa results to protobuf format.
|
||||
The functionality here is similar to the various *from_capa functions, e.g. ResultDocument.from_capa() or
|
||||
feature_from_capa.
|
||||
|
||||
For few classes we can rely on the proto json parser (e.g. RuleMetadata).
|
||||
|
||||
For most classes (e.g. RuleMatches) conversion is tricky, because we use natively unsupported types (e.g. tuples),
|
||||
several classes with unions, and more complex layouts. So, it's more straight forward to convert explicitly vs.
|
||||
massaging the data so the protobuf json parser works.
|
||||
|
||||
Of note, the 3 in `syntax = "proto3"` has nothing to do with the 2 in capa_pb2.py;
|
||||
see details in https://github.com/grpc/grpc/issues/15444#issuecomment-396442980.
|
||||
|
||||
First compile the protobuf to generate an API file and a mypy stub file
|
||||
$ protoc.exe --python_out=. --mypy_out=. <path_to_proto> (e.g. capa/render/proto/capa.proto)
|
||||
|
||||
Alternatively, --pyi_out=. can be used to generate a Python Interface file that supports development
|
||||
"""
|
||||
import sys
|
||||
import json
|
||||
import argparse
|
||||
import datetime
|
||||
from typing import Any, Dict, Union
|
||||
|
||||
import google.protobuf.json_format
|
||||
from google.protobuf.json_format import MessageToJson
|
||||
|
||||
import capa.rules
|
||||
import capa.features.freeze as frz
|
||||
import capa.render.proto.capa_pb2 as capa_pb2
|
||||
import capa.render.result_document as rd
|
||||
import capa.features.freeze.features as frzf
|
||||
from capa.helpers import assert_never
|
||||
from capa.features.freeze import AddressType
|
||||
|
||||
|
||||
def dict_tuple_to_list_values(d: Dict) -> Dict:
|
||||
o = dict()
|
||||
for k, v in d.items():
|
||||
if isinstance(v, tuple):
|
||||
o[k] = list(v)
|
||||
else:
|
||||
o[k] = v
|
||||
return o
|
||||
|
||||
|
||||
def int_to_pb2(v: int) -> capa_pb2.Integer:
|
||||
if v < -2_147_483_648:
|
||||
raise ValueError(f"value underflow: {v}")
|
||||
if v > 0xFFFFFFFFFFFFFFFF:
|
||||
raise ValueError(f"value overflow: {v}")
|
||||
|
||||
if v < 0:
|
||||
return capa_pb2.Integer(i=v)
|
||||
else:
|
||||
return capa_pb2.Integer(u=v)
|
||||
|
||||
|
||||
def number_to_pb2(v: Union[int, float]) -> capa_pb2.Number:
|
||||
if isinstance(v, float):
|
||||
return capa_pb2.Number(f=v)
|
||||
elif isinstance(v, int):
|
||||
i = int_to_pb2(v)
|
||||
if v < 0:
|
||||
return capa_pb2.Number(i=i.i)
|
||||
else:
|
||||
return capa_pb2.Number(u=i.u)
|
||||
else:
|
||||
assert_never(v)
|
||||
|
||||
|
||||
def addr_to_pb2(addr: frz.Address) -> capa_pb2.Address:
|
||||
if addr.type is AddressType.ABSOLUTE:
|
||||
assert isinstance(addr.value, int)
|
||||
return capa_pb2.Address(type=capa_pb2.AddressType.ADDRESSTYPE_ABSOLUTE, v=int_to_pb2(addr.value))
|
||||
|
||||
elif addr.type is AddressType.RELATIVE:
|
||||
assert isinstance(addr.value, int)
|
||||
return capa_pb2.Address(type=capa_pb2.AddressType.ADDRESSTYPE_RELATIVE, v=int_to_pb2(addr.value))
|
||||
|
||||
elif addr.type is AddressType.FILE:
|
||||
assert isinstance(addr.value, int)
|
||||
return capa_pb2.Address(type=capa_pb2.AddressType.ADDRESSTYPE_FILE, v=int_to_pb2(addr.value))
|
||||
|
||||
elif addr.type is AddressType.DN_TOKEN:
|
||||
assert isinstance(addr.value, int)
|
||||
return capa_pb2.Address(type=capa_pb2.AddressType.ADDRESSTYPE_DN_TOKEN, v=int_to_pb2(addr.value))
|
||||
|
||||
elif addr.type is AddressType.DN_TOKEN_OFFSET:
|
||||
assert isinstance(addr.value, tuple)
|
||||
token, offset = addr.value
|
||||
assert isinstance(token, int)
|
||||
assert isinstance(offset, int)
|
||||
return capa_pb2.Address(
|
||||
type=capa_pb2.AddressType.ADDRESSTYPE_DN_TOKEN_OFFSET,
|
||||
token_offset=capa_pb2.Token_Offset(token=int_to_pb2(token), offset=offset),
|
||||
)
|
||||
|
||||
elif addr.type is AddressType.NO_ADDRESS:
|
||||
# value == None, so only set type
|
||||
return capa_pb2.Address(type=capa_pb2.AddressType.ADDRESSTYPE_NO_ADDRESS)
|
||||
|
||||
else:
|
||||
assert_never(addr)
|
||||
|
||||
|
||||
def scope_to_pb2(scope: capa.rules.Scope) -> capa_pb2.Scope.ValueType:
|
||||
if scope == capa.rules.Scope.FILE:
|
||||
return capa_pb2.Scope.SCOPE_FILE
|
||||
elif scope == capa.rules.Scope.FUNCTION:
|
||||
return capa_pb2.Scope.SCOPE_FUNCTION
|
||||
elif scope == capa.rules.Scope.BASIC_BLOCK:
|
||||
return capa_pb2.Scope.SCOPE_BASIC_BLOCK
|
||||
elif scope == capa.rules.Scope.INSTRUCTION:
|
||||
return capa_pb2.Scope.SCOPE_INSTRUCTION
|
||||
else:
|
||||
assert_never(scope)
|
||||
|
||||
|
||||
def metadata_to_pb2(meta: rd.Metadata) -> capa_pb2.Metadata:
|
||||
return capa_pb2.Metadata(
|
||||
timestamp=str(meta.timestamp),
|
||||
version=meta.version,
|
||||
argv=meta.argv,
|
||||
sample=google.protobuf.json_format.ParseDict(meta.sample.dict(), capa_pb2.Sample()),
|
||||
analysis=capa_pb2.Analysis(
|
||||
format=meta.analysis.format,
|
||||
arch=meta.analysis.arch,
|
||||
os=meta.analysis.os,
|
||||
extractor=meta.analysis.extractor,
|
||||
rules=meta.analysis.rules,
|
||||
base_address=addr_to_pb2(meta.analysis.base_address),
|
||||
layout=capa_pb2.Layout(
|
||||
functions=[
|
||||
capa_pb2.FunctionLayout(
|
||||
address=addr_to_pb2(f.address),
|
||||
matched_basic_blocks=[
|
||||
capa_pb2.BasicBlockLayout(address=addr_to_pb2(bb.address)) for bb in f.matched_basic_blocks
|
||||
],
|
||||
)
|
||||
for f in meta.analysis.layout.functions
|
||||
]
|
||||
),
|
||||
feature_counts=capa_pb2.FeatureCounts(
|
||||
file=meta.analysis.feature_counts.file,
|
||||
functions=[
|
||||
capa_pb2.FunctionFeatureCount(address=addr_to_pb2(f.address), count=f.count)
|
||||
for f in meta.analysis.feature_counts.functions
|
||||
],
|
||||
),
|
||||
library_functions=[
|
||||
capa_pb2.LibraryFunction(address=addr_to_pb2(lf.address), name=lf.name)
|
||||
for lf in meta.analysis.library_functions
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def statement_to_pb2(statement: rd.Statement) -> capa_pb2.StatementNode:
|
||||
if isinstance(statement, rd.RangeStatement):
|
||||
return capa_pb2.StatementNode(
|
||||
range=capa_pb2.RangeStatement(
|
||||
type="range",
|
||||
description=statement.description,
|
||||
min=statement.min,
|
||||
max=statement.max,
|
||||
child=feature_to_pb2(statement.child),
|
||||
),
|
||||
type="statement",
|
||||
)
|
||||
|
||||
elif isinstance(statement, rd.SomeStatement):
|
||||
return capa_pb2.StatementNode(
|
||||
some=capa_pb2.SomeStatement(type=statement.type, description=statement.description, count=statement.count),
|
||||
type="statement",
|
||||
)
|
||||
|
||||
elif isinstance(statement, rd.SubscopeStatement):
|
||||
return capa_pb2.StatementNode(
|
||||
subscope=capa_pb2.SubscopeStatement(
|
||||
type=statement.type,
|
||||
description=statement.description,
|
||||
scope=scope_to_pb2(statement.scope),
|
||||
),
|
||||
type="statement",
|
||||
)
|
||||
|
||||
elif isinstance(statement, rd.CompoundStatement):
|
||||
return capa_pb2.StatementNode(
|
||||
compound=capa_pb2.CompoundStatement(type=statement.type, description=statement.description),
|
||||
type="statement",
|
||||
)
|
||||
|
||||
else:
|
||||
assert_never(statement)
|
||||
|
||||
|
||||
def feature_to_pb2(f: frzf.Feature) -> capa_pb2.FeatureNode:
|
||||
if isinstance(f, frzf.OSFeature):
|
||||
return capa_pb2.FeatureNode(
|
||||
type="feature", os=capa_pb2.OSFeature(type=f.type, os=f.os, description=f.description)
|
||||
)
|
||||
|
||||
elif isinstance(f, frzf.ArchFeature):
|
||||
return capa_pb2.FeatureNode(
|
||||
type="feature", arch=capa_pb2.ArchFeature(type=f.type, arch=f.arch, description=f.description)
|
||||
)
|
||||
|
||||
elif isinstance(f, frzf.FormatFeature):
|
||||
return capa_pb2.FeatureNode(
|
||||
type="feature", format=capa_pb2.FormatFeature(type=f.type, format=f.format, description=f.description)
|
||||
)
|
||||
|
||||
elif isinstance(f, frzf.MatchFeature):
|
||||
return capa_pb2.FeatureNode(
|
||||
type="feature",
|
||||
match=capa_pb2.MatchFeature(
|
||||
type=f.type,
|
||||
match=f.match,
|
||||
description=f.description,
|
||||
),
|
||||
)
|
||||
|
||||
elif isinstance(f, frzf.CharacteristicFeature):
|
||||
return capa_pb2.FeatureNode(
|
||||
type="feature",
|
||||
characteristic=capa_pb2.CharacteristicFeature(
|
||||
type=f.type, characteristic=f.characteristic, description=f.description
|
||||
),
|
||||
)
|
||||
|
||||
elif isinstance(f, frzf.ExportFeature):
|
||||
return capa_pb2.FeatureNode(
|
||||
type="feature", export=capa_pb2.ExportFeature(type=f.type, export=f.export, description=f.description)
|
||||
)
|
||||
|
||||
elif isinstance(f, frzf.ImportFeature):
|
||||
return capa_pb2.FeatureNode(
|
||||
type="feature", import_=capa_pb2.ImportFeature(type=f.type, import_=f.import_, description=f.description)
|
||||
)
|
||||
|
||||
elif isinstance(f, frzf.SectionFeature):
|
||||
return capa_pb2.FeatureNode(
|
||||
type="feature", section=capa_pb2.SectionFeature(type=f.type, section=f.section, description=f.description)
|
||||
)
|
||||
|
||||
elif isinstance(f, frzf.FunctionNameFeature):
|
||||
return capa_pb2.FeatureNode(
|
||||
type="function name",
|
||||
function_name=capa_pb2.FunctionNameFeature(
|
||||
type=f.type, function_name=f.function_name, description=f.description
|
||||
),
|
||||
)
|
||||
|
||||
elif isinstance(f, frzf.SubstringFeature):
|
||||
return capa_pb2.FeatureNode(
|
||||
type="feature",
|
||||
substring=capa_pb2.SubstringFeature(type=f.type, substring=f.substring, description=f.description),
|
||||
)
|
||||
|
||||
elif isinstance(f, frzf.RegexFeature):
|
||||
return capa_pb2.FeatureNode(
|
||||
type="feature", regex=capa_pb2.RegexFeature(type=f.type, regex=f.regex, description=f.description)
|
||||
)
|
||||
|
||||
elif isinstance(f, frzf.StringFeature):
|
||||
return capa_pb2.FeatureNode(
|
||||
type="feature",
|
||||
string=capa_pb2.StringFeature(
|
||||
type=f.type,
|
||||
string=f.string,
|
||||
description=f.description,
|
||||
),
|
||||
)
|
||||
|
||||
elif isinstance(f, frzf.ClassFeature):
|
||||
return capa_pb2.FeatureNode(
|
||||
type="feature", class_=capa_pb2.ClassFeature(type=f.type, class_=f.class_, description=f.description)
|
||||
)
|
||||
|
||||
elif isinstance(f, frzf.NamespaceFeature):
|
||||
return capa_pb2.FeatureNode(
|
||||
type="feature",
|
||||
namespace=capa_pb2.NamespaceFeature(type=f.type, namespace=f.namespace, description=f.description),
|
||||
)
|
||||
|
||||
elif isinstance(f, frzf.APIFeature):
|
||||
return capa_pb2.FeatureNode(
|
||||
type="feature", api=capa_pb2.APIFeature(type=f.type, api=f.api, description=f.description)
|
||||
)
|
||||
|
||||
elif isinstance(f, frzf.PropertyFeature):
|
||||
return capa_pb2.FeatureNode(
|
||||
type="feature",
|
||||
property_=capa_pb2.PropertyFeature(
|
||||
type=f.type, access=f.access, property_=f.property, description=f.description
|
||||
),
|
||||
)
|
||||
|
||||
elif isinstance(f, frzf.NumberFeature):
|
||||
return capa_pb2.FeatureNode(
|
||||
type="feature",
|
||||
number=capa_pb2.NumberFeature(type=f.type, number=number_to_pb2(f.number), description=f.description),
|
||||
)
|
||||
|
||||
elif isinstance(f, frzf.BytesFeature):
|
||||
return capa_pb2.FeatureNode(
|
||||
type="feature", bytes=capa_pb2.BytesFeature(type=f.type, bytes=f.bytes, description=f.description)
|
||||
)
|
||||
|
||||
elif isinstance(f, frzf.OffsetFeature):
|
||||
return capa_pb2.FeatureNode(
|
||||
type="feature",
|
||||
offset=capa_pb2.OffsetFeature(type=f.type, offset=int_to_pb2(f.offset), description=f.description),
|
||||
)
|
||||
|
||||
elif isinstance(f, frzf.MnemonicFeature):
|
||||
return capa_pb2.FeatureNode(
|
||||
type="feature",
|
||||
mnemonic=capa_pb2.MnemonicFeature(type=f.type, mnemonic=f.mnemonic, description=f.description),
|
||||
)
|
||||
|
||||
elif isinstance(f, frzf.OperandNumberFeature):
|
||||
return capa_pb2.FeatureNode(
|
||||
type="feature",
|
||||
operand_number=capa_pb2.OperandNumberFeature(
|
||||
type=f.type, index=f.index, operand_number=int_to_pb2(f.operand_number), description=f.description
|
||||
),
|
||||
)
|
||||
|
||||
elif isinstance(f, frzf.OperandOffsetFeature):
|
||||
return capa_pb2.FeatureNode(
|
||||
type="feature",
|
||||
operand_offset=capa_pb2.OperandOffsetFeature(
|
||||
type=f.type, index=f.index, operand_offset=int_to_pb2(f.operand_offset), description=f.description
|
||||
),
|
||||
)
|
||||
|
||||
elif isinstance(f, frzf.BasicBlockFeature):
|
||||
return capa_pb2.FeatureNode(
|
||||
type="feature", basic_block=capa_pb2.BasicBlockFeature(type=f.type, description=f.description)
|
||||
)
|
||||
|
||||
else:
|
||||
assert_never(f)
|
||||
|
||||
|
||||
def node_to_pb2(node: rd.Node) -> Union[capa_pb2.FeatureNode, capa_pb2.StatementNode]:
|
||||
if isinstance(node, rd.StatementNode):
|
||||
return statement_to_pb2(node.statement)
|
||||
|
||||
elif isinstance(node, rd.FeatureNode):
|
||||
return feature_to_pb2(node.feature)
|
||||
|
||||
else:
|
||||
assert_never(node)
|
||||
|
||||
|
||||
def match_to_pb2(match: rd.Match) -> capa_pb2.Match:
|
||||
node = node_to_pb2(match.node)
|
||||
children = list(map(match_to_pb2, match.children))
|
||||
locations = list(map(addr_to_pb2, match.locations))
|
||||
|
||||
if isinstance(node, capa_pb2.StatementNode):
|
||||
return capa_pb2.Match(
|
||||
success=match.success,
|
||||
statement=node,
|
||||
children=children,
|
||||
locations=locations,
|
||||
captures={},
|
||||
)
|
||||
|
||||
elif isinstance(node, capa_pb2.FeatureNode):
|
||||
return capa_pb2.Match(
|
||||
success=match.success,
|
||||
feature=node,
|
||||
children=children,
|
||||
locations=locations,
|
||||
captures={
|
||||
capture: capa_pb2.Addresses(address=list(map(addr_to_pb2, locs)))
|
||||
for capture, locs in match.captures.items()
|
||||
},
|
||||
)
|
||||
|
||||
else:
|
||||
assert_never(match)
|
||||
|
||||
|
||||
def rule_metadata_to_pb2(rule_metadata: rd.RuleMetadata) -> capa_pb2.RuleMetadata:
|
||||
# after manual type conversions to the RuleMetadata, we can rely on the protobuf json parser
|
||||
# conversions include tuple -> list and rd.Enum -> proto.enum
|
||||
meta = dict_tuple_to_list_values(rule_metadata.dict())
|
||||
meta["scope"] = scope_to_pb2(meta["scope"])
|
||||
meta["attack"] = list(map(dict_tuple_to_list_values, meta.get("attack", [])))
|
||||
meta["mbc"] = list(map(dict_tuple_to_list_values, meta.get("mbc", [])))
|
||||
|
||||
return google.protobuf.json_format.ParseDict(meta, capa_pb2.RuleMetadata())
|
||||
|
||||
|
||||
def doc_to_pb2(doc: rd.ResultDocument) -> capa_pb2.ResultDocument:
|
||||
rule_matches: Dict[str, capa_pb2.RuleMatches] = {}
|
||||
for rule_name, matches in doc.rules.items():
|
||||
m = capa_pb2.RuleMatches(
|
||||
meta=rule_metadata_to_pb2(matches.meta),
|
||||
source=matches.source,
|
||||
matches=[
|
||||
capa_pb2.Pair_Address_Match(address=addr_to_pb2(addr), match=match_to_pb2(match))
|
||||
for addr, match in matches.matches
|
||||
],
|
||||
)
|
||||
rule_matches[rule_name] = m
|
||||
|
||||
r = capa_pb2.ResultDocument(meta=metadata_to_pb2(doc.meta), rules=rule_matches)
|
||||
|
||||
return r
|
||||
|
||||
|
||||
def int_from_pb2(v: capa_pb2.Integer) -> int:
|
||||
type = v.WhichOneof("value")
|
||||
if type == "u":
|
||||
return v.u
|
||||
elif type == "i":
|
||||
return v.i
|
||||
else:
|
||||
assert_never(type)
|
||||
|
||||
|
||||
def number_from_pb2(v: capa_pb2.Number) -> Union[int, float]:
|
||||
type = v.WhichOneof("value")
|
||||
if type == "u":
|
||||
return v.u
|
||||
elif type == "i":
|
||||
return v.i
|
||||
elif type == "f":
|
||||
return v.f
|
||||
else:
|
||||
assert_never(type)
|
||||
|
||||
|
||||
def addr_from_pb2(addr: capa_pb2.Address) -> frz.Address:
|
||||
if addr.type == capa_pb2.AddressType.ADDRESSTYPE_ABSOLUTE:
|
||||
return frz.Address(type=frz.AddressType.ABSOLUTE, value=int_from_pb2(addr.v))
|
||||
|
||||
elif addr.type == capa_pb2.AddressType.ADDRESSTYPE_RELATIVE:
|
||||
return frz.Address(type=frz.AddressType.RELATIVE, value=int_from_pb2(addr.v))
|
||||
|
||||
elif addr.type == capa_pb2.AddressType.ADDRESSTYPE_FILE:
|
||||
return frz.Address(type=frz.AddressType.FILE, value=int_from_pb2(addr.v))
|
||||
|
||||
elif addr.type == capa_pb2.AddressType.ADDRESSTYPE_DN_TOKEN:
|
||||
return frz.Address(type=frz.AddressType.DN_TOKEN, value=int_from_pb2(addr.v))
|
||||
|
||||
elif addr.type == capa_pb2.AddressType.ADDRESSTYPE_DN_TOKEN_OFFSET:
|
||||
token = int_from_pb2(addr.token_offset.token)
|
||||
offset = addr.token_offset.offset
|
||||
return frz.Address(type=frz.AddressType.DN_TOKEN_OFFSET, value=(token, offset))
|
||||
|
||||
elif addr.type == capa_pb2.AddressType.ADDRESSTYPE_NO_ADDRESS:
|
||||
return frz.Address(type=frz.AddressType.NO_ADDRESS, value=None)
|
||||
|
||||
else:
|
||||
assert_never(addr)
|
||||
|
||||
|
||||
def scope_from_pb2(scope: capa_pb2.Scope.ValueType) -> capa.rules.Scope:
|
||||
if scope == capa_pb2.Scope.SCOPE_FILE:
|
||||
return capa.rules.Scope.FILE
|
||||
elif scope == capa_pb2.Scope.SCOPE_FUNCTION:
|
||||
return capa.rules.Scope.FUNCTION
|
||||
elif scope == capa_pb2.Scope.SCOPE_BASIC_BLOCK:
|
||||
return capa.rules.Scope.BASIC_BLOCK
|
||||
elif scope == capa_pb2.Scope.SCOPE_INSTRUCTION:
|
||||
return capa.rules.Scope.INSTRUCTION
|
||||
else:
|
||||
assert_never(scope)
|
||||
|
||||
|
||||
def metadata_from_pb2(meta: capa_pb2.Metadata) -> rd.Metadata:
|
||||
return rd.Metadata(
|
||||
timestamp=datetime.datetime.fromisoformat(meta.timestamp),
|
||||
version=meta.version,
|
||||
argv=tuple(meta.argv) if meta.argv else None,
|
||||
sample=rd.Sample(
|
||||
md5=meta.sample.md5,
|
||||
sha1=meta.sample.sha1,
|
||||
sha256=meta.sample.sha256,
|
||||
path=meta.sample.path,
|
||||
),
|
||||
analysis=rd.Analysis(
|
||||
format=meta.analysis.format,
|
||||
arch=meta.analysis.arch,
|
||||
os=meta.analysis.os,
|
||||
extractor=meta.analysis.extractor,
|
||||
rules=tuple(meta.analysis.rules),
|
||||
base_address=addr_from_pb2(meta.analysis.base_address),
|
||||
layout=rd.Layout(
|
||||
functions=tuple(
|
||||
[
|
||||
rd.FunctionLayout(
|
||||
address=addr_from_pb2(f.address),
|
||||
matched_basic_blocks=tuple(
|
||||
[
|
||||
rd.BasicBlockLayout(address=addr_from_pb2(bb.address))
|
||||
for bb in f.matched_basic_blocks
|
||||
]
|
||||
),
|
||||
)
|
||||
for f in meta.analysis.layout.functions
|
||||
]
|
||||
)
|
||||
),
|
||||
feature_counts=rd.FeatureCounts(
|
||||
file=meta.analysis.feature_counts.file,
|
||||
functions=tuple(
|
||||
[
|
||||
rd.FunctionFeatureCount(address=addr_from_pb2(f.address), count=f.count)
|
||||
for f in meta.analysis.feature_counts.functions
|
||||
]
|
||||
),
|
||||
),
|
||||
library_functions=tuple(
|
||||
[
|
||||
rd.LibraryFunction(address=addr_from_pb2(lf.address), name=lf.name)
|
||||
for lf in meta.analysis.library_functions
|
||||
]
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def statement_from_pb2(statement: capa_pb2.StatementNode) -> rd.Statement:
|
||||
type_ = statement.WhichOneof("statement")
|
||||
|
||||
if type_ == "range":
|
||||
return rd.RangeStatement(
|
||||
min=statement.range.min,
|
||||
max=statement.range.max,
|
||||
child=feature_from_pb2(statement.range.child),
|
||||
description=statement.range.description or None,
|
||||
)
|
||||
elif type_ == "some":
|
||||
return rd.SomeStatement(
|
||||
count=statement.some.count,
|
||||
description=statement.some.description or None,
|
||||
)
|
||||
elif type_ == "subscope":
|
||||
return rd.SubscopeStatement(
|
||||
scope=scope_from_pb2(statement.subscope.scope),
|
||||
description=statement.subscope.description or None,
|
||||
)
|
||||
elif type_ == "compound":
|
||||
return rd.CompoundStatement(
|
||||
type=statement.compound.type,
|
||||
description=statement.compound.description or None,
|
||||
)
|
||||
else:
|
||||
assert_never(type_)
|
||||
|
||||
|
||||
def feature_from_pb2(f: capa_pb2.FeatureNode) -> frzf.Feature:
|
||||
type_ = f.WhichOneof("feature")
|
||||
|
||||
# mypy gets angry below because ff may have a different type in each branch,
|
||||
# even though we don't use ff outside each branch.
|
||||
# so we just let mypy know that ff might be any type to silence that warning.
|
||||
# upstream issue: https://github.com/python/mypy/issues/6233
|
||||
ff: Any
|
||||
|
||||
if type_ == "os":
|
||||
ff = f.os
|
||||
return frzf.OSFeature(os=ff.os, description=ff.description or None)
|
||||
elif type_ == "arch":
|
||||
ff = f.arch
|
||||
return frzf.ArchFeature(arch=ff.arch, description=ff.description or None)
|
||||
elif type_ == "format":
|
||||
ff = f.format
|
||||
return frzf.FormatFeature(format=ff.format, description=ff.description or None)
|
||||
elif type_ == "match":
|
||||
ff = f.match
|
||||
return frzf.MatchFeature(match=ff.match, description=ff.description or None)
|
||||
elif type_ == "characteristic":
|
||||
ff = f.characteristic
|
||||
return frzf.CharacteristicFeature(characteristic=ff.characteristic, description=ff.description or None)
|
||||
elif type_ == "export":
|
||||
ff = f.export
|
||||
return frzf.ExportFeature(export=ff.export, description=ff.description or None)
|
||||
elif type_ == "import_":
|
||||
ff = f.import_
|
||||
return frzf.ImportFeature(import_=ff.import_, description=ff.description or None) # type: ignore
|
||||
# Mypy is unable to recognize `import_` as an argument
|
||||
elif type_ == "section":
|
||||
ff = f.section
|
||||
return frzf.SectionFeature(section=ff.section, description=ff.description or None)
|
||||
elif type_ == "function_name":
|
||||
ff = f.function_name
|
||||
return frzf.FunctionNameFeature(function_name=ff.function_name, description=ff.description or None) # type: ignore
|
||||
elif type_ == "substring":
|
||||
ff = f.substring
|
||||
return frzf.SubstringFeature(substring=ff.substring, description=ff.description or None)
|
||||
elif type_ == "regex":
|
||||
ff = f.regex
|
||||
return frzf.RegexFeature(regex=ff.regex, description=ff.description or None)
|
||||
elif type_ == "string":
|
||||
ff = f.string
|
||||
return frzf.StringFeature(string=ff.string, description=ff.description or None)
|
||||
elif type_ == "class_":
|
||||
ff = f.class_
|
||||
return frzf.ClassFeature(class_=ff.class_, description=ff.description or None) # type: ignore
|
||||
# Mypy is unable to recognize `class_` as an argument due to aliasing
|
||||
elif type_ == "namespace":
|
||||
ff = f.namespace
|
||||
return frzf.NamespaceFeature(namespace=ff.namespace, description=ff.description or None)
|
||||
elif type_ == "api":
|
||||
ff = f.api
|
||||
return frzf.APIFeature(api=ff.api, description=ff.description or None)
|
||||
elif type_ == "property_":
|
||||
ff = f.property_
|
||||
return frzf.PropertyFeature(property=ff.property_, access=ff.access or None, description=ff.description or None)
|
||||
elif type_ == "number":
|
||||
ff = f.number
|
||||
return frzf.NumberFeature(number=number_from_pb2(ff.number), description=ff.description or None)
|
||||
elif type_ == "bytes":
|
||||
ff = f.bytes
|
||||
return frzf.BytesFeature(bytes=ff.bytes, description=ff.description or None)
|
||||
elif type_ == "offset":
|
||||
ff = f.offset
|
||||
return frzf.OffsetFeature(offset=int_from_pb2(ff.offset), description=ff.description or None)
|
||||
elif type_ == "mnemonic":
|
||||
ff = f.mnemonic
|
||||
return frzf.MnemonicFeature(mnemonic=ff.mnemonic, description=ff.description or None)
|
||||
elif type_ == "operand_number":
|
||||
ff = f.operand_number
|
||||
return frzf.OperandNumberFeature(
|
||||
index=ff.index, operand_number=number_from_pb2(ff.operand_number), description=ff.description or None
|
||||
) # type: ignore
|
||||
elif type_ == "operand_offset":
|
||||
ff = f.operand_offset
|
||||
return frzf.OperandOffsetFeature(
|
||||
index=ff.index, operand_offset=int_from_pb2(ff.operand_offset), description=ff.description or None
|
||||
) # type: ignore
|
||||
# Mypy is unable to recognize `operand_offset` as an argument due to aliasing
|
||||
elif type_ == "basic_block":
|
||||
ff = f.basic_block
|
||||
return frzf.BasicBlockFeature(description=ff.description or None)
|
||||
else:
|
||||
assert_never(type_)
|
||||
|
||||
|
||||
def match_from_pb2(match: capa_pb2.Match) -> rd.Match:
|
||||
children = list(map(match_from_pb2, match.children))
|
||||
locations = list(map(addr_from_pb2, match.locations))
|
||||
|
||||
node_type = match.WhichOneof("node")
|
||||
if node_type == "statement":
|
||||
return rd.Match(
|
||||
success=match.success,
|
||||
node=rd.StatementNode(statement=statement_from_pb2(match.statement)),
|
||||
children=tuple(children),
|
||||
locations=tuple(locations),
|
||||
captures={},
|
||||
)
|
||||
elif node_type == "feature":
|
||||
return rd.Match(
|
||||
success=match.success,
|
||||
node=rd.FeatureNode(feature=feature_from_pb2(match.feature)),
|
||||
children=tuple(children),
|
||||
locations=tuple(locations),
|
||||
captures={capture: tuple(map(addr_from_pb2, locs.address)) for capture, locs in match.captures.items()},
|
||||
)
|
||||
else:
|
||||
assert_never(node_type)
|
||||
|
||||
|
||||
def attack_from_pb2(pb: capa_pb2.AttackSpec) -> rd.AttackSpec:
|
||||
return rd.AttackSpec(
|
||||
parts=tuple(pb.parts),
|
||||
tactic=pb.tactic,
|
||||
technique=pb.technique,
|
||||
subtechnique=pb.subtechnique,
|
||||
id=pb.id,
|
||||
)
|
||||
|
||||
|
||||
def mbc_from_pb2(pb: capa_pb2.MBCSpec) -> rd.MBCSpec:
|
||||
return rd.MBCSpec(
|
||||
parts=tuple(pb.parts),
|
||||
objective=pb.objective,
|
||||
behavior=pb.behavior,
|
||||
method=pb.method,
|
||||
id=pb.id,
|
||||
)
|
||||
|
||||
|
||||
def maec_from_pb2(pb: capa_pb2.MaecMetadata) -> rd.MaecMetadata:
|
||||
return rd.MaecMetadata(
|
||||
analysis_conclusion=pb.analysis_conclusion or None,
|
||||
analysis_conclusion_ov=pb.analysis_conclusion_ov or None,
|
||||
malware_family=pb.malware_family or None,
|
||||
malware_category=pb.malware_category or None,
|
||||
malware_category_ov=pb.malware_category_ov or None,
|
||||
) # type: ignore
|
||||
# Mypy is unable to recognise arguments due to alias
|
||||
|
||||
|
||||
def rule_metadata_from_pb2(pb: capa_pb2.RuleMetadata) -> rd.RuleMetadata:
|
||||
return rd.RuleMetadata(
|
||||
name=pb.name,
|
||||
namespace=pb.namespace or None,
|
||||
authors=tuple(pb.authors),
|
||||
scope=scope_from_pb2(pb.scope),
|
||||
attack=tuple([attack_from_pb2(attack) for attack in pb.attack]),
|
||||
mbc=tuple([mbc_from_pb2(mbc) for mbc in pb.mbc]),
|
||||
references=tuple(pb.references),
|
||||
examples=tuple(pb.examples),
|
||||
description=pb.description,
|
||||
lib=pb.lib,
|
||||
is_subscope_rule=pb.is_subscope_rule,
|
||||
maec=maec_from_pb2(pb.maec),
|
||||
) # type: ignore
|
||||
# Mypy is unable to recognise `attack` and `is_subscope_rule` as arguments due to alias
|
||||
|
||||
|
||||
def doc_from_pb2(doc: capa_pb2.ResultDocument) -> rd.ResultDocument:
|
||||
rule_matches: Dict[str, rd.RuleMatches] = {}
|
||||
for rule_name, matches in doc.rules.items():
|
||||
m = rd.RuleMatches(
|
||||
meta=rule_metadata_from_pb2(matches.meta),
|
||||
source=matches.source,
|
||||
matches=tuple([(addr_from_pb2(pair.address), match_from_pb2(pair.match)) for pair in matches.matches]),
|
||||
)
|
||||
rule_matches[rule_name] = m
|
||||
|
||||
return rd.ResultDocument(meta=metadata_from_pb2(doc.meta), rules=rule_matches)
|
||||
364
capa/render/proto/capa.proto
Normal file
364
capa/render/proto/capa.proto
Normal file
@@ -0,0 +1,364 @@
|
||||
syntax = "proto3";
|
||||
|
||||
message APIFeature {
|
||||
string type = 1;
|
||||
string api = 2;
|
||||
optional string description = 3;
|
||||
}
|
||||
|
||||
message Address {
|
||||
AddressType type = 1;
|
||||
oneof value {
|
||||
Integer v = 2;
|
||||
Token_Offset token_offset = 3;
|
||||
};
|
||||
}
|
||||
|
||||
enum AddressType {
|
||||
ADDRESSTYPE_UNSPECIFIED = 0;
|
||||
ADDRESSTYPE_ABSOLUTE = 1;
|
||||
ADDRESSTYPE_RELATIVE = 2;
|
||||
ADDRESSTYPE_FILE = 3;
|
||||
ADDRESSTYPE_DN_TOKEN = 4;
|
||||
ADDRESSTYPE_DN_TOKEN_OFFSET = 5;
|
||||
ADDRESSTYPE_NO_ADDRESS = 6;
|
||||
}
|
||||
|
||||
message Analysis {
|
||||
string format = 1;
|
||||
string arch = 2;
|
||||
string os = 3;
|
||||
string extractor = 4;
|
||||
repeated string rules = 5;
|
||||
Address base_address = 6;
|
||||
Layout layout = 7;
|
||||
FeatureCounts feature_counts = 8;
|
||||
repeated LibraryFunction library_functions = 9;
|
||||
}
|
||||
|
||||
message ArchFeature {
|
||||
string type = 1;
|
||||
string arch = 2;
|
||||
optional string description = 3;
|
||||
}
|
||||
|
||||
message AttackSpec {
|
||||
repeated string parts = 1;
|
||||
string tactic = 2;
|
||||
string technique = 3;
|
||||
string subtechnique = 4;
|
||||
string id = 5;
|
||||
}
|
||||
|
||||
message BasicBlockFeature {
|
||||
string type = 1;
|
||||
optional string description = 2;
|
||||
}
|
||||
|
||||
message BasicBlockLayout {
|
||||
Address address = 1;
|
||||
}
|
||||
|
||||
message BytesFeature {
|
||||
string type = 1;
|
||||
string bytes = 2;
|
||||
optional string description = 3;
|
||||
}
|
||||
|
||||
message CharacteristicFeature {
|
||||
string type = 1;
|
||||
string characteristic = 2;
|
||||
optional string description = 3;
|
||||
}
|
||||
|
||||
message ClassFeature {
|
||||
string type = 1;
|
||||
string class_ = 2; // class is protected Python keyword
|
||||
optional string description = 3;
|
||||
}
|
||||
|
||||
message CompoundStatement {
|
||||
string type = 1;
|
||||
optional string description = 2;
|
||||
}
|
||||
|
||||
message ExportFeature {
|
||||
string type = 1;
|
||||
string export = 2;
|
||||
optional string description = 3;
|
||||
}
|
||||
|
||||
message FeatureCounts {
|
||||
uint64 file = 1;
|
||||
repeated FunctionFeatureCount functions = 2;
|
||||
}
|
||||
|
||||
message FeatureNode {
|
||||
string type = 1;
|
||||
oneof feature {
|
||||
OSFeature os = 2;
|
||||
ArchFeature arch = 3;
|
||||
FormatFeature format = 4;
|
||||
MatchFeature match = 5;
|
||||
CharacteristicFeature characteristic = 6;
|
||||
ExportFeature export = 7;
|
||||
ImportFeature import_ = 8; // import is Python keyword
|
||||
SectionFeature section = 9;
|
||||
FunctionNameFeature function_name = 10;
|
||||
SubstringFeature substring = 11;
|
||||
RegexFeature regex = 12;
|
||||
StringFeature string = 13;
|
||||
ClassFeature class_ = 14;
|
||||
NamespaceFeature namespace = 15;
|
||||
APIFeature api = 16;
|
||||
PropertyFeature property_ = 17; // property is a Python top-level decorator name
|
||||
NumberFeature number = 18;
|
||||
BytesFeature bytes = 19;
|
||||
OffsetFeature offset = 20;
|
||||
MnemonicFeature mnemonic = 21;
|
||||
OperandNumberFeature operand_number = 22;
|
||||
OperandOffsetFeature operand_offset = 23;
|
||||
BasicBlockFeature basic_block = 24;
|
||||
};
|
||||
}
|
||||
|
||||
message FormatFeature {
|
||||
string type = 1;
|
||||
string format = 2;
|
||||
optional string description = 3;
|
||||
}
|
||||
|
||||
message FunctionFeatureCount {
|
||||
Address address = 1;
|
||||
uint64 count = 2;
|
||||
}
|
||||
|
||||
message FunctionLayout {
|
||||
Address address = 1;
|
||||
repeated BasicBlockLayout matched_basic_blocks = 2;
|
||||
}
|
||||
|
||||
message FunctionNameFeature {
|
||||
string type = 1;
|
||||
string function_name = 2;
|
||||
optional string description = 3;
|
||||
}
|
||||
|
||||
message ImportFeature {
|
||||
string type = 1;
|
||||
string import_ = 2;
|
||||
optional string description = 3;
|
||||
}
|
||||
|
||||
message Layout {
|
||||
repeated FunctionLayout functions = 1;
|
||||
}
|
||||
|
||||
message LibraryFunction {
|
||||
Address address = 1;
|
||||
string name = 2;
|
||||
}
|
||||
|
||||
message MBCSpec {
|
||||
repeated string parts = 1;
|
||||
string objective = 2;
|
||||
string behavior = 3;
|
||||
string method = 4;
|
||||
string id = 5;
|
||||
}
|
||||
|
||||
message MaecMetadata {
|
||||
string analysis_conclusion = 1;
|
||||
string analysis_conclusion_ov = 2;
|
||||
string malware_family = 3;
|
||||
string malware_category = 4;
|
||||
string malware_category_ov = 5;
|
||||
}
|
||||
|
||||
message Match {
|
||||
bool success = 1;
|
||||
oneof node {
|
||||
StatementNode statement = 2;
|
||||
FeatureNode feature = 3;
|
||||
};
|
||||
repeated Match children = 5;
|
||||
repeated Address locations = 6;
|
||||
map <string, Addresses> captures = 7;
|
||||
}
|
||||
|
||||
message MatchFeature {
|
||||
string type = 1;
|
||||
string match = 2;
|
||||
optional string description = 3;
|
||||
}
|
||||
|
||||
message Metadata {
|
||||
string timestamp = 1; // iso8601 format, like: 2019-01-01T00:00:00Z
|
||||
string version = 2;
|
||||
repeated string argv = 3;
|
||||
Sample sample = 4;
|
||||
Analysis analysis = 5;
|
||||
}
|
||||
|
||||
message MnemonicFeature {
|
||||
string type = 1;
|
||||
string mnemonic = 2;
|
||||
optional string description = 3;
|
||||
}
|
||||
|
||||
message NamespaceFeature {
|
||||
string type = 1;
|
||||
string namespace = 2;
|
||||
optional string description = 3;
|
||||
}
|
||||
|
||||
message NumberFeature {
|
||||
string type = 1;
|
||||
Number number = 2; // this can be positive (range: u64), negative (range: i64), or a double.
|
||||
optional string description = 5;
|
||||
}
|
||||
|
||||
message OSFeature {
|
||||
string type = 1;
|
||||
string os = 2;
|
||||
optional string description = 3;
|
||||
}
|
||||
|
||||
message OffsetFeature {
|
||||
string type = 1;
|
||||
Integer offset = 2; // offset can be negative
|
||||
optional string description = 3;
|
||||
}
|
||||
|
||||
message OperandNumberFeature {
|
||||
string type = 1;
|
||||
uint32 index = 2;
|
||||
Integer operand_number = 3; // this can be positive (range: u64), negative (range: i64), or a double.
|
||||
optional string description = 4;
|
||||
}
|
||||
|
||||
message OperandOffsetFeature {
|
||||
string type = 1;
|
||||
uint32 index = 2;
|
||||
Integer operand_offset = 3;
|
||||
optional string description = 4;
|
||||
}
|
||||
|
||||
message PropertyFeature {
|
||||
string type = 1;
|
||||
string property_ = 2; // property is a Python top-level decorator name
|
||||
optional string access = 3;
|
||||
optional string description = 4;
|
||||
}
|
||||
|
||||
message RangeStatement {
|
||||
string type = 1;
|
||||
uint64 min = 2;
|
||||
uint64 max = 3;
|
||||
// reusing FeatureNode here to avoid duplication and list all features OSFeature, ArchFeature, ... again.
|
||||
FeatureNode child = 4;
|
||||
optional string description = 5;
|
||||
}
|
||||
|
||||
message RegexFeature {
|
||||
string type = 1;
|
||||
string regex = 2;
|
||||
optional string description = 3;
|
||||
}
|
||||
|
||||
message ResultDocument {
|
||||
Metadata meta = 1;
|
||||
map <string, RuleMatches> rules = 2;
|
||||
}
|
||||
|
||||
message RuleMatches {
|
||||
RuleMetadata meta = 1;
|
||||
string source = 2;
|
||||
repeated Pair_Address_Match matches = 3;
|
||||
}
|
||||
|
||||
message RuleMetadata {
|
||||
string name = 1;
|
||||
string namespace = 2;
|
||||
repeated string authors = 3;
|
||||
Scope scope = 4;
|
||||
repeated AttackSpec attack = 5;
|
||||
repeated MBCSpec mbc = 6;
|
||||
repeated string references = 7;
|
||||
repeated string examples = 8;
|
||||
string description = 9;
|
||||
bool lib = 10;
|
||||
MaecMetadata maec = 11;
|
||||
bool is_subscope_rule = 12;
|
||||
}
|
||||
|
||||
message Sample {
|
||||
string md5 = 1;
|
||||
string sha1 = 2;
|
||||
string sha256 = 3;
|
||||
string path = 4;
|
||||
}
|
||||
|
||||
enum Scope {
|
||||
SCOPE_UNSPECIFIED = 0;
|
||||
SCOPE_FILE = 1;
|
||||
SCOPE_FUNCTION = 2;
|
||||
SCOPE_BASIC_BLOCK = 3;
|
||||
SCOPE_INSTRUCTION = 4;
|
||||
}
|
||||
|
||||
message SectionFeature {
|
||||
string type = 1;
|
||||
string section = 2;
|
||||
optional string description = 3;
|
||||
}
|
||||
|
||||
message SomeStatement {
|
||||
string type = 1;
|
||||
uint32 count = 2;
|
||||
optional string description = 3;
|
||||
}
|
||||
|
||||
message StatementNode {
|
||||
string type = 1;
|
||||
oneof statement {
|
||||
RangeStatement range = 2;
|
||||
SomeStatement some = 3;
|
||||
SubscopeStatement subscope = 4;
|
||||
CompoundStatement compound = 5;
|
||||
};
|
||||
}
|
||||
|
||||
message StringFeature {
|
||||
string type = 1;
|
||||
string string = 2;
|
||||
optional string description = 3;
|
||||
}
|
||||
|
||||
message SubscopeStatement {
|
||||
string type = 1;
|
||||
Scope scope = 2;
|
||||
optional string description = 3;
|
||||
}
|
||||
|
||||
message SubstringFeature {
|
||||
string type = 1;
|
||||
string substring = 2;
|
||||
optional string description = 3;
|
||||
}
|
||||
|
||||
message Addresses { repeated Address address = 1; }
|
||||
|
||||
message Pair_Address_Match {
|
||||
Address address = 1;
|
||||
Match match = 2;
|
||||
}
|
||||
|
||||
message Token_Offset {
|
||||
Integer token = 1;
|
||||
uint64 offset = 2; // offset is always >= 0
|
||||
}
|
||||
|
||||
message Integer { oneof value { uint64 u = 1; sint64 i = 2; } } // unsigned or signed int
|
||||
|
||||
message Number { oneof value { uint64 u = 1; sint64 i = 2; double f = 3; } }
|
||||
137
capa/render/proto/capa_pb2.py
Normal file
137
capa/render/proto/capa_pb2.py
Normal file
File diff suppressed because one or more lines are too long
1449
capa/render/proto/capa_pb2.pyi
Normal file
1449
capa/render/proto/capa_pb2.pyi
Normal file
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,8 @@
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
import datetime
|
||||
from typing import Any, Dict, Tuple, Union, Optional
|
||||
import collections
|
||||
from typing import Any, Dict, List, Tuple, Union, Optional
|
||||
|
||||
from pydantic import Field, BaseModel
|
||||
|
||||
@@ -15,6 +16,7 @@ import capa.engine
|
||||
import capa.features.common
|
||||
import capa.features.freeze as frz
|
||||
import capa.features.address
|
||||
import capa.features.freeze.features as frzf
|
||||
from capa.rules import RuleSet
|
||||
from capa.engine import MatchResults
|
||||
from capa.helpers import assert_never
|
||||
@@ -23,6 +25,7 @@ from capa.helpers import assert_never
|
||||
class FrozenModel(BaseModel):
|
||||
class Config:
|
||||
frozen = True
|
||||
extra = "forbid"
|
||||
|
||||
|
||||
class Sample(FrozenModel):
|
||||
@@ -75,7 +78,7 @@ class Analysis(FrozenModel):
|
||||
class Metadata(FrozenModel):
|
||||
timestamp: datetime.datetime
|
||||
version: str
|
||||
argv: Tuple[str, ...]
|
||||
argv: Optional[Tuple[str, ...]]
|
||||
sample: Sample
|
||||
analysis: Analysis
|
||||
|
||||
@@ -99,64 +102,91 @@ class Metadata(FrozenModel):
|
||||
rules=meta["analysis"]["rules"],
|
||||
base_address=frz.Address.from_capa(meta["analysis"]["base_address"]),
|
||||
layout=Layout(
|
||||
functions=[
|
||||
functions=tuple(
|
||||
FunctionLayout(
|
||||
address=frz.Address.from_capa(address),
|
||||
matched_basic_blocks=[
|
||||
matched_basic_blocks=tuple(
|
||||
BasicBlockLayout(address=frz.Address.from_capa(bb)) for bb in f["matched_basic_blocks"]
|
||||
],
|
||||
),
|
||||
)
|
||||
for address, f in meta["analysis"]["layout"]["functions"].items()
|
||||
]
|
||||
)
|
||||
),
|
||||
feature_counts=FeatureCounts(
|
||||
file=meta["analysis"]["feature_counts"]["file"],
|
||||
functions=[
|
||||
functions=tuple(
|
||||
FunctionFeatureCount(address=frz.Address.from_capa(address), count=count)
|
||||
for address, count in meta["analysis"]["feature_counts"]["functions"].items()
|
||||
],
|
||||
),
|
||||
),
|
||||
library_functions=[
|
||||
library_functions=tuple(
|
||||
LibraryFunction(address=frz.Address.from_capa(address), name=name)
|
||||
for address, name in meta["analysis"]["library_functions"].items()
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
def to_capa(self) -> Dict[str, Any]:
|
||||
capa_meta = {
|
||||
"timestamp": self.timestamp.isoformat(),
|
||||
"version": self.version,
|
||||
"sample": {
|
||||
"md5": self.sample.md5,
|
||||
"sha1": self.sample.sha1,
|
||||
"sha256": self.sample.sha256,
|
||||
"path": self.sample.path,
|
||||
},
|
||||
"analysis": {
|
||||
"format": self.analysis.format,
|
||||
"arch": self.analysis.arch,
|
||||
"os": self.analysis.os,
|
||||
"extractor": self.analysis.extractor,
|
||||
"rules": self.analysis.rules,
|
||||
"base_address": self.analysis.base_address.to_capa(),
|
||||
"layout": {
|
||||
"functions": {
|
||||
f.address.to_capa(): {
|
||||
"matched_basic_blocks": [bb.address.to_capa() for bb in f.matched_basic_blocks]
|
||||
}
|
||||
for f in self.analysis.layout.functions
|
||||
}
|
||||
},
|
||||
"feature_counts": {
|
||||
"file": self.analysis.feature_counts.file,
|
||||
"functions": {fc.address.to_capa(): fc.count for fc in self.analysis.feature_counts.functions},
|
||||
},
|
||||
"library_functions": {lf.address.to_capa(): lf.name for lf in self.analysis.library_functions},
|
||||
},
|
||||
}
|
||||
|
||||
return capa_meta
|
||||
|
||||
|
||||
class CompoundStatementType:
|
||||
AND = "and"
|
||||
OR = "or"
|
||||
NOT = "not"
|
||||
OPTIONAL = "optional"
|
||||
|
||||
|
||||
class StatementModel(FrozenModel):
|
||||
...
|
||||
|
||||
|
||||
class AndStatement(StatementModel):
|
||||
type = "and"
|
||||
description: Optional[str]
|
||||
|
||||
|
||||
class OrStatement(StatementModel):
|
||||
type = "or"
|
||||
description: Optional[str]
|
||||
|
||||
|
||||
class NotStatement(StatementModel):
|
||||
type = "not"
|
||||
description: Optional[str]
|
||||
class CompoundStatement(StatementModel):
|
||||
type: str
|
||||
description: Optional[str] = None
|
||||
|
||||
|
||||
class SomeStatement(StatementModel):
|
||||
type = "some"
|
||||
description: Optional[str]
|
||||
description: Optional[str] = None
|
||||
count: int
|
||||
|
||||
|
||||
class OptionalStatement(StatementModel):
|
||||
type = "optional"
|
||||
description: Optional[str]
|
||||
|
||||
|
||||
class RangeStatement(StatementModel):
|
||||
type = "range"
|
||||
description: Optional[str]
|
||||
description: Optional[str] = None
|
||||
min: int
|
||||
max: int
|
||||
child: frz.Feature
|
||||
@@ -164,18 +194,16 @@ class RangeStatement(StatementModel):
|
||||
|
||||
class SubscopeStatement(StatementModel):
|
||||
type = "subscope"
|
||||
description: Optional[str]
|
||||
scope = capa.rules.Scope
|
||||
description: Optional[str] = None
|
||||
scope: capa.rules.Scope
|
||||
|
||||
|
||||
Statement = Union[
|
||||
OptionalStatement,
|
||||
AndStatement,
|
||||
OrStatement,
|
||||
NotStatement,
|
||||
SomeStatement,
|
||||
# Note! order matters, see #1161
|
||||
RangeStatement,
|
||||
SomeStatement,
|
||||
SubscopeStatement,
|
||||
CompoundStatement,
|
||||
]
|
||||
|
||||
|
||||
@@ -185,18 +213,12 @@ class StatementNode(FrozenModel):
|
||||
|
||||
|
||||
def statement_from_capa(node: capa.engine.Statement) -> Statement:
|
||||
if isinstance(node, capa.engine.And):
|
||||
return AndStatement(description=node.description)
|
||||
|
||||
elif isinstance(node, capa.engine.Or):
|
||||
return OrStatement(description=node.description)
|
||||
|
||||
elif isinstance(node, capa.engine.Not):
|
||||
return NotStatement(description=node.description)
|
||||
if isinstance(node, (capa.engine.And, capa.engine.Or, capa.engine.Not)):
|
||||
return CompoundStatement(type=node.__class__.__name__.lower(), description=node.description)
|
||||
|
||||
elif isinstance(node, capa.engine.Some):
|
||||
if node.count == 0:
|
||||
return OptionalStatement(description=node.description)
|
||||
return CompoundStatement(type=CompoundStatementType.OPTIONAL, description=node.description)
|
||||
|
||||
else:
|
||||
return SomeStatement(
|
||||
@@ -241,7 +263,55 @@ def node_from_capa(node: Union[capa.engine.Statement, capa.engine.Feature]) -> N
|
||||
assert_never(node)
|
||||
|
||||
|
||||
class Match(BaseModel):
|
||||
def node_to_capa(
|
||||
node: Node, children: List[Union[capa.engine.Statement, capa.engine.Feature]]
|
||||
) -> Union[capa.engine.Statement, capa.engine.Feature]:
|
||||
if isinstance(node, StatementNode):
|
||||
if isinstance(node.statement, CompoundStatement):
|
||||
if node.statement.type == CompoundStatementType.AND:
|
||||
return capa.engine.And(description=node.statement.description, children=children)
|
||||
|
||||
elif node.statement.type == CompoundStatementType.OR:
|
||||
return capa.engine.Or(description=node.statement.description, children=children)
|
||||
|
||||
elif node.statement.type == CompoundStatementType.NOT:
|
||||
return capa.engine.Not(description=node.statement.description, child=children[0])
|
||||
|
||||
elif node.statement.type == CompoundStatementType.OPTIONAL:
|
||||
return capa.engine.Some(description=node.statement.description, count=0, children=children)
|
||||
|
||||
else:
|
||||
assert_never(node.statement.type)
|
||||
|
||||
elif isinstance(node.statement, SomeStatement):
|
||||
return capa.engine.Some(
|
||||
description=node.statement.description, count=node.statement.count, children=children
|
||||
)
|
||||
|
||||
elif isinstance(node.statement, RangeStatement):
|
||||
return capa.engine.Range(
|
||||
description=node.statement.description,
|
||||
min=node.statement.min,
|
||||
max=node.statement.max,
|
||||
child=node.statement.child.to_capa(),
|
||||
)
|
||||
|
||||
elif isinstance(node.statement, SubscopeStatement):
|
||||
return capa.engine.Subscope(
|
||||
description=node.statement.description, scope=node.statement.scope, child=children[0]
|
||||
)
|
||||
|
||||
else:
|
||||
assert_never(node.statement)
|
||||
|
||||
elif isinstance(node, FeatureNode):
|
||||
return node.feature.to_capa()
|
||||
|
||||
else:
|
||||
assert_never(node)
|
||||
|
||||
|
||||
class Match(FrozenModel):
|
||||
"""
|
||||
args:
|
||||
success: did the node match?
|
||||
@@ -293,7 +363,7 @@ class Match(BaseModel):
|
||||
# finally, splice that logic into this tree.
|
||||
if (
|
||||
isinstance(node, FeatureNode)
|
||||
and isinstance(node.feature, frz.features.MatchFeature)
|
||||
and isinstance(node.feature, frzf.MatchFeature)
|
||||
# only add subtree on success,
|
||||
# because there won't be results for the other rule on failure.
|
||||
and success
|
||||
@@ -368,21 +438,54 @@ class Match(BaseModel):
|
||||
return cls(
|
||||
success=success,
|
||||
node=node,
|
||||
children=tuple(children),
|
||||
locations=tuple(locations),
|
||||
captures={capture: tuple(captures[capture]) for capture in captures},
|
||||
)
|
||||
|
||||
def to_capa(self, rules_by_name: Dict[str, capa.rules.Rule]) -> capa.engine.Result:
|
||||
children = [child.to_capa(rules_by_name) for child in self.children]
|
||||
statement = node_to_capa(self.node, [child.statement for child in children])
|
||||
|
||||
if isinstance(self.node, FeatureNode):
|
||||
feature = self.node.feature
|
||||
|
||||
if isinstance(feature, (frzf.SubstringFeature, frzf.RegexFeature)):
|
||||
matches = {capture: {loc.to_capa() for loc in locs} for capture, locs in self.captures.items()}
|
||||
|
||||
if isinstance(feature, frzf.SubstringFeature):
|
||||
assert isinstance(statement, capa.features.common.Substring)
|
||||
statement = capa.features.common._MatchedSubstring(statement, matches)
|
||||
elif isinstance(feature, frzf.RegexFeature):
|
||||
assert isinstance(statement, capa.features.common.Regex)
|
||||
statement = capa.features.common._MatchedRegex(statement, matches)
|
||||
else:
|
||||
assert_never(feature)
|
||||
|
||||
# apparently we don't have to fixup match and subscope entries here.
|
||||
# at least, default, verbose, and vverbose renderers seem to work well without any special handling here.
|
||||
#
|
||||
# children contains a single tree of results, corresponding to the logic of the matched rule.
|
||||
# self.node.feature.match contains the name of the rule that was matched.
|
||||
# so its all available to reconstruct, if necessary.
|
||||
|
||||
return capa.features.common.Result(
|
||||
success=self.success,
|
||||
statement=statement,
|
||||
locations={loc.to_capa() for loc in self.locations},
|
||||
children=children,
|
||||
locations=locations,
|
||||
captures=captures,
|
||||
)
|
||||
|
||||
|
||||
def parse_parts_id(s: str):
|
||||
id = ""
|
||||
id_ = ""
|
||||
parts = s.split("::")
|
||||
if len(parts) > 0:
|
||||
last = parts.pop()
|
||||
last, _, id = last.rpartition(" ")
|
||||
id = id.lstrip("[").rstrip("]")
|
||||
last, _, id_ = last.rpartition(" ")
|
||||
id_ = id_.lstrip("[").rstrip("]")
|
||||
parts.append(last)
|
||||
return parts, id
|
||||
return tuple(parts), id_
|
||||
|
||||
|
||||
class AttackSpec(FrozenModel):
|
||||
@@ -408,7 +511,7 @@ class AttackSpec(FrozenModel):
|
||||
tactic = ""
|
||||
technique = ""
|
||||
subtechnique = ""
|
||||
parts, id = parse_parts_id(s)
|
||||
parts, id_ = parse_parts_id(s)
|
||||
if len(parts) > 0:
|
||||
tactic = parts[0]
|
||||
if len(parts) > 1:
|
||||
@@ -421,7 +524,7 @@ class AttackSpec(FrozenModel):
|
||||
tactic=tactic,
|
||||
technique=technique,
|
||||
subtechnique=subtechnique,
|
||||
id=id,
|
||||
id=id_,
|
||||
)
|
||||
|
||||
|
||||
@@ -448,7 +551,7 @@ class MBCSpec(FrozenModel):
|
||||
objective = ""
|
||||
behavior = ""
|
||||
method = ""
|
||||
parts, id = parse_parts_id(s)
|
||||
parts, id_ = parse_parts_id(s)
|
||||
if len(parts) > 0:
|
||||
objective = parts[0]
|
||||
if len(parts) > 1:
|
||||
@@ -461,7 +564,7 @@ class MBCSpec(FrozenModel):
|
||||
objective=objective,
|
||||
behavior=behavior,
|
||||
method=method,
|
||||
id=id,
|
||||
id=id_,
|
||||
)
|
||||
|
||||
|
||||
@@ -499,28 +602,30 @@ class RuleMetadata(FrozenModel):
|
||||
namespace=rule.meta.get("namespace"),
|
||||
authors=rule.meta.get("authors"),
|
||||
scope=capa.rules.Scope(rule.meta.get("scope")),
|
||||
attack=list(map(AttackSpec.from_str, rule.meta.get("att&ck", []))),
|
||||
mbc=list(map(MBCSpec.from_str, rule.meta.get("mbc", []))),
|
||||
attack=tuple(map(AttackSpec.from_str, rule.meta.get("att&ck", []))),
|
||||
mbc=tuple(map(MBCSpec.from_str, rule.meta.get("mbc", []))),
|
||||
references=rule.meta.get("references", []),
|
||||
examples=rule.meta.get("examples", []),
|
||||
description=rule.meta.get("description", ""),
|
||||
lib=rule.meta.get("lib", False),
|
||||
capa_subscope=rule.meta.get("capa/subscope", False),
|
||||
is_subscope_rule=rule.meta.get("capa/subscope", False),
|
||||
maec=MaecMetadata(
|
||||
analysis_conclusion=rule.meta.get("maec/analysis-conclusion"),
|
||||
analysis_conclusion_ov=rule.meta.get("maec/analysis-conclusion-ov"),
|
||||
malware_family=rule.meta.get("maec/malware-family"),
|
||||
malware_category=rule.meta.get("maec/malware-category"),
|
||||
malware_category_ov=rule.meta.get("maec/malware-category-ov"),
|
||||
),
|
||||
)
|
||||
), # type: ignore
|
||||
# Mypy is unable to recognise arguments due to alias
|
||||
) # type: ignore
|
||||
# Mypy is unable to recognise arguments due to alias
|
||||
|
||||
class Config:
|
||||
frozen = True
|
||||
allow_population_by_field_name = True
|
||||
|
||||
|
||||
class RuleMatches(BaseModel):
|
||||
class RuleMatches(FrozenModel):
|
||||
"""
|
||||
args:
|
||||
meta: the metadata from the rule
|
||||
@@ -532,7 +637,7 @@ class RuleMatches(BaseModel):
|
||||
matches: Tuple[Tuple[frz.Address, Match], ...]
|
||||
|
||||
|
||||
class ResultDocument(BaseModel):
|
||||
class ResultDocument(FrozenModel):
|
||||
meta: Metadata
|
||||
rules: Dict[str, RuleMatches]
|
||||
|
||||
@@ -548,10 +653,29 @@ class ResultDocument(BaseModel):
|
||||
rule_matches[rule_name] = RuleMatches(
|
||||
meta=RuleMetadata.from_capa(rule),
|
||||
source=rule.definition,
|
||||
matches=[
|
||||
matches=tuple(
|
||||
(frz.Address.from_capa(addr), Match.from_capa(rules, capabilities, match))
|
||||
for addr, match in matches
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
return ResultDocument(meta=Metadata.from_capa(meta), rules=rule_matches)
|
||||
|
||||
def to_capa(self) -> Tuple[Dict, Dict]:
|
||||
meta = self.meta.to_capa()
|
||||
capabilities: Dict[
|
||||
str, List[Tuple[capa.features.address.Address, capa.features.common.Result]]
|
||||
] = collections.defaultdict(list)
|
||||
|
||||
# this doesn't quite work because we don't have the rule source for rules that aren't matched.
|
||||
rules_by_name = {
|
||||
rule_name: capa.rules.Rule.from_yaml(rule_match.source) for rule_name, rule_match in self.rules.items()
|
||||
}
|
||||
|
||||
for rule_name, rule_match in self.rules.items():
|
||||
for addr, match in rule_match.matches:
|
||||
result: capa.engine.Result = match.to_capa(rules_by_name)
|
||||
|
||||
capabilities[rule_name].append((addr.to_capa(), result))
|
||||
|
||||
return meta, capabilities
|
||||
|
||||
@@ -16,7 +16,7 @@ import capa.render.result_document as rd
|
||||
|
||||
def bold(s: str) -> str:
|
||||
"""draw attention to the given string"""
|
||||
return termcolor.colored(s, "blue")
|
||||
return termcolor.colored(s, "cyan")
|
||||
|
||||
|
||||
def bold2(s: str) -> str:
|
||||
@@ -24,24 +24,20 @@ def bold2(s: str) -> str:
|
||||
return termcolor.colored(s, "green")
|
||||
|
||||
|
||||
def hex(n: int) -> str:
|
||||
"""render the given number using upper case hex, like: 0x123ABC"""
|
||||
if n < 0:
|
||||
return "-0x%X" % (-n)
|
||||
else:
|
||||
return "0x%X" % n
|
||||
def warn(s: str) -> str:
|
||||
return termcolor.colored(s, "yellow")
|
||||
|
||||
|
||||
def format_parts_id(data: Union[rd.AttackSpec, rd.MBCSpec]):
|
||||
"""
|
||||
format canonical representation of ATT&CK/MBC parts and ID
|
||||
"""
|
||||
return "%s [%s]" % ("::".join(data.parts), data.id)
|
||||
return f"{'::'.join(data.parts)} [{data.id}]"
|
||||
|
||||
|
||||
def capability_rules(doc: rd.ResultDocument) -> Iterator[rd.RuleMatches]:
|
||||
"""enumerate the rules in (namespace, name) order that are 'capability' rules (not lib/subscope/disposition/etc)."""
|
||||
for (_, _, rule) in sorted(map(lambda rule: (rule.meta.namespace or "", rule.meta.name, rule), doc.rules.values())):
|
||||
for _, _, rule in sorted(map(lambda rule: (rule.meta.namespace or "", rule.meta.name, rule), doc.rules.values())):
|
||||
if rule.meta.lib:
|
||||
continue
|
||||
if rule.meta.is_subscope_rule:
|
||||
|
||||
@@ -22,14 +22,14 @@ Unless required by applicable law or agreed to in writing, software distributed
|
||||
is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and limitations under the License.
|
||||
"""
|
||||
import enum
|
||||
|
||||
import tabulate
|
||||
import dnfile.mdtable
|
||||
import dncil.clr.token
|
||||
|
||||
import capa.rules
|
||||
import capa.helpers
|
||||
import capa.render.utils as rutils
|
||||
import capa.features.freeze as frz
|
||||
import capa.render.result_document
|
||||
import capa.render.result_document as rd
|
||||
from capa.rules import RuleSet
|
||||
from capa.engine import MatchResults
|
||||
@@ -37,18 +37,23 @@ from capa.engine import MatchResults
|
||||
|
||||
def format_address(address: frz.Address) -> str:
|
||||
if address.type == frz.AddressType.ABSOLUTE:
|
||||
return rutils.hex(address.value)
|
||||
assert isinstance(address.value, int)
|
||||
return capa.helpers.hex(address.value)
|
||||
elif address.type == frz.AddressType.RELATIVE:
|
||||
return f"base address+{rutils.hex(address.value)}"
|
||||
assert isinstance(address.value, int)
|
||||
return f"base address+{capa.helpers.hex(address.value)}"
|
||||
elif address.type == frz.AddressType.FILE:
|
||||
return f"file+{rutils.hex(address.value)}"
|
||||
assert isinstance(address.value, int)
|
||||
return f"file+{capa.helpers.hex(address.value)}"
|
||||
elif address.type == frz.AddressType.DN_TOKEN:
|
||||
token = dncil.clr.token.Token(address.value)
|
||||
return f"token({rutils.hex(token.value)})"
|
||||
assert isinstance(address.value, int)
|
||||
return f"token({capa.helpers.hex(address.value)})"
|
||||
elif address.type == frz.AddressType.DN_TOKEN_OFFSET:
|
||||
assert isinstance(address.value, tuple)
|
||||
token, offset = address.value
|
||||
token = dncil.clr.token.Token(token)
|
||||
return f"token({rutils.hex(token.value)})+{rutils.hex(offset)}"
|
||||
assert isinstance(token, int)
|
||||
assert isinstance(offset, int)
|
||||
return f"token({capa.helpers.hex(token)})+{capa.helpers.hex(offset)}"
|
||||
elif address.type == frz.AddressType.NO_ADDRESS:
|
||||
return "global"
|
||||
else:
|
||||
@@ -116,7 +121,7 @@ def render_rules(ostream, doc: rd.ResultDocument):
|
||||
if count == 1:
|
||||
capability = rutils.bold(rule.meta.name)
|
||||
else:
|
||||
capability = "%s (%d matches)" % (rutils.bold(rule.meta.name), count)
|
||||
capability = f"{rutils.bold(rule.meta.name)} ({count} matches)"
|
||||
|
||||
ostream.writeln(capability)
|
||||
had_match = True
|
||||
@@ -130,6 +135,9 @@ def render_rules(ostream, doc: rd.ResultDocument):
|
||||
if isinstance(v, list) and len(v) == 1:
|
||||
v = v[0]
|
||||
|
||||
if isinstance(v, enum.Enum):
|
||||
v = v.value
|
||||
|
||||
rows.append((key, v))
|
||||
|
||||
if rule.meta.scope != capa.rules.FILE_SCOPE:
|
||||
|
||||
@@ -6,11 +6,12 @@
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
from typing import Dict, List, Iterable
|
||||
from typing import Dict, Iterable
|
||||
|
||||
import tabulate
|
||||
|
||||
import capa.rules
|
||||
import capa.helpers
|
||||
import capa.render.utils as rutils
|
||||
import capa.render.verbose
|
||||
import capa.features.common
|
||||
@@ -42,7 +43,7 @@ def render_locations(ostream, locations: Iterable[frz.Address]):
|
||||
# don't display too many locations, because it becomes very noisy.
|
||||
# probably only the first handful of locations will be useful for inspection.
|
||||
ostream.write(", ".join(map(v.format_address, locations[0:4])))
|
||||
ostream.write(", and %d more..." % (len(locations) - 4))
|
||||
ostream.write(f", and {(len(locations) - 4)} more...")
|
||||
|
||||
elif len(locations) > 1:
|
||||
ostream.write(", ".join(map(v.format_address, locations)))
|
||||
@@ -61,23 +62,23 @@ def render_statement(ostream, match: rd.Match, statement: rd.Statement, indent=0
|
||||
|
||||
ostream.write(":")
|
||||
if statement.description:
|
||||
ostream.write(" = %s" % statement.description)
|
||||
ostream.write(f" = {statement.description}")
|
||||
ostream.writeln("")
|
||||
|
||||
elif isinstance(statement, (rd.AndStatement, rd.OrStatement, rd.OptionalStatement, rd.NotStatement)):
|
||||
elif isinstance(statement, (rd.CompoundStatement)):
|
||||
# emit `and:` `or:` `optional:` `not:`
|
||||
ostream.write(statement.type)
|
||||
|
||||
ostream.write(":")
|
||||
if statement.description:
|
||||
ostream.write(" = %s" % statement.description)
|
||||
ostream.write(f" = {statement.description}")
|
||||
ostream.writeln("")
|
||||
|
||||
elif isinstance(statement, rd.SomeStatement):
|
||||
ostream.write("%d or more:" % (statement.count))
|
||||
ostream.write(f"{statement.count} or more:")
|
||||
|
||||
if statement.description:
|
||||
ostream.write(" = %s" % statement.description)
|
||||
ostream.write(f" = {statement.description}")
|
||||
ostream.writeln("")
|
||||
|
||||
elif isinstance(statement, rd.RangeStatement):
|
||||
@@ -87,32 +88,32 @@ def render_statement(ostream, match: rd.Match, statement: rd.Statement, indent=0
|
||||
# so, we have to inline some of the feature rendering here.
|
||||
|
||||
child = statement.child
|
||||
value = getattr(child, child.type)
|
||||
value = child.dict(by_alias=True).get(child.type)
|
||||
|
||||
if value:
|
||||
if isinstance(child, frzf.StringFeature):
|
||||
value = '"%s"' % capa.features.common.escape_string(value)
|
||||
value = f'"{capa.features.common.escape_string(value)}"'
|
||||
|
||||
value = rutils.bold2(value)
|
||||
|
||||
if child.description:
|
||||
ostream.write("count(%s(%s = %s)): " % (child.type, value, child.description))
|
||||
ostream.write(f"count({child.type}({value} = {child.description})): ")
|
||||
else:
|
||||
ostream.write("count(%s(%s)): " % (child.type, value))
|
||||
ostream.write(f"count({child.type}({value})): ")
|
||||
else:
|
||||
ostream.write("count(%s): " % child.type)
|
||||
ostream.write(f"count({child.type}): ")
|
||||
|
||||
if statement.max == statement.min:
|
||||
ostream.write("%d" % (statement.min))
|
||||
ostream.write(f"{statement.min}")
|
||||
elif statement.min == 0:
|
||||
ostream.write("%d or fewer" % (statement.max))
|
||||
ostream.write(f"{statement.max} or fewer")
|
||||
elif statement.max == (1 << 64 - 1):
|
||||
ostream.write("%d or more" % (statement.min))
|
||||
ostream.write(f"{statement.min} or more")
|
||||
else:
|
||||
ostream.write("between %d and %d" % (statement.min, statement.max))
|
||||
ostream.write(f"between {statement.min} and {statement.max}")
|
||||
|
||||
if statement.description:
|
||||
ostream.write(" = %s" % statement.description)
|
||||
ostream.write(f" = {statement.description}")
|
||||
render_locations(ostream, match.locations)
|
||||
ostream.writeln("")
|
||||
|
||||
@@ -121,33 +122,51 @@ def render_statement(ostream, match: rd.Match, statement: rd.Statement, indent=0
|
||||
|
||||
|
||||
def render_string_value(s: str) -> str:
|
||||
return '"%s"' % capa.features.common.escape_string(s)
|
||||
return f'"{capa.features.common.escape_string(s)}"'
|
||||
|
||||
|
||||
def render_feature(ostream, match: rd.Match, feature: frzf.Feature, indent=0):
|
||||
ostream.write(" " * indent)
|
||||
|
||||
key = feature.type
|
||||
if isinstance(feature, frzf.ImportFeature):
|
||||
if isinstance(feature, frzf.BasicBlockFeature):
|
||||
# i don't think it makes sense to have standalone basic block features.
|
||||
# we don't parse them from rules, only things like: `count(basic block) > 1`
|
||||
raise ValueError("cannot render basic block feature directly")
|
||||
elif isinstance(feature, frzf.ImportFeature):
|
||||
# fixup access to Python reserved name
|
||||
value = feature.import_
|
||||
if isinstance(feature, frzf.ClassFeature):
|
||||
elif isinstance(feature, frzf.ClassFeature):
|
||||
value = feature.class_
|
||||
else:
|
||||
value = getattr(feature, key)
|
||||
# convert attributes to dictionary using aliased names, if applicable
|
||||
value = feature.dict(by_alias=True).get(key, None)
|
||||
|
||||
if key not in ("regex", "substring"):
|
||||
if value is None:
|
||||
raise ValueError(f"{key} contains None")
|
||||
|
||||
if not isinstance(feature, (frzf.RegexFeature, frzf.SubstringFeature)):
|
||||
# like:
|
||||
# number: 10 = SOME_CONSTANT @ 0x401000
|
||||
if key == "string":
|
||||
if isinstance(feature, frzf.StringFeature):
|
||||
value = render_string_value(value)
|
||||
|
||||
if key == "number":
|
||||
elif isinstance(
|
||||
feature, (frzf.NumberFeature, frzf.OffsetFeature, frzf.OperandNumberFeature, frzf.OperandOffsetFeature)
|
||||
):
|
||||
assert isinstance(value, int)
|
||||
value = hex(value)
|
||||
value = capa.helpers.hex(value)
|
||||
|
||||
ostream.write(key)
|
||||
ostream.write(": ")
|
||||
if isinstance(feature, frzf.PropertyFeature) and feature.access is not None:
|
||||
key = f"property/{feature.access}"
|
||||
|
||||
elif isinstance(feature, frzf.OperandNumberFeature):
|
||||
key = f"operand[{feature.index}].number"
|
||||
|
||||
elif isinstance(feature, frzf.OperandOffsetFeature):
|
||||
key = f"operand[{feature.index}].offset"
|
||||
|
||||
ostream.write(f"{key}: ")
|
||||
|
||||
if value:
|
||||
ostream.write(rutils.bold2(value))
|
||||
@@ -156,7 +175,7 @@ def render_feature(ostream, match: rd.Match, feature: frzf.Feature, indent=0):
|
||||
ostream.write(capa.rules.DESCRIPTION_SEPARATOR)
|
||||
ostream.write(feature.description)
|
||||
|
||||
if key not in ("os", "arch"):
|
||||
if not isinstance(feature, (frzf.OSFeature, frzf.ArchFeature, frzf.FormatFeature)):
|
||||
render_locations(ostream, match.locations)
|
||||
ostream.write("\n")
|
||||
else:
|
||||
@@ -202,12 +221,12 @@ def render_match(ostream, match: rd.Match, indent=0, mode=MODE_SUCCESS):
|
||||
return
|
||||
|
||||
# optional statement with no successful children is empty
|
||||
if isinstance(match.node, rd.StatementNode) and isinstance(match.node.statement, rd.OptionalStatement):
|
||||
if isinstance(match.node, rd.StatementNode) and match.node.statement.type == rd.CompoundStatementType.OPTIONAL:
|
||||
if not any(map(lambda m: m.success, match.children)):
|
||||
return
|
||||
|
||||
# not statement, so invert the child mode to show failed evaluations
|
||||
if isinstance(match.node, rd.StatementNode) and isinstance(match.node.statement, rd.NotStatement):
|
||||
if isinstance(match.node, rd.StatementNode) and match.node.statement.type == rd.CompoundStatementType.NOT:
|
||||
child_mode = MODE_FAILURE
|
||||
|
||||
elif mode == MODE_FAILURE:
|
||||
@@ -216,12 +235,12 @@ def render_match(ostream, match: rd.Match, indent=0, mode=MODE_SUCCESS):
|
||||
return
|
||||
|
||||
# optional statement with successful children is not relevant
|
||||
if isinstance(match.node, rd.StatementNode) and isinstance(match.node.statement, rd.OptionalStatement):
|
||||
if isinstance(match.node, rd.StatementNode) and match.node.statement.type == rd.CompoundStatementType.OPTIONAL:
|
||||
if any(map(lambda m: m.success, match.children)):
|
||||
return
|
||||
|
||||
# not statement, so invert the child mode to show successful evaluations
|
||||
if isinstance(match.node, rd.StatementNode) and isinstance(match.node.statement, rd.NotStatement):
|
||||
if isinstance(match.node, rd.StatementNode) and match.node.statement.type == rd.CompoundStatementType.NOT:
|
||||
child_mode = MODE_SUCCESS
|
||||
else:
|
||||
raise RuntimeError("unexpected mode: " + mode)
|
||||
@@ -258,7 +277,7 @@ def render_rules(ostream, doc: rd.ResultDocument):
|
||||
|
||||
had_match = False
|
||||
|
||||
for (_, _, rule) in sorted(map(lambda rule: (rule.meta.namespace or "", rule.meta.name, rule), doc.rules.values())):
|
||||
for _, _, rule in sorted(map(lambda rule: (rule.meta.namespace or "", rule.meta.name, rule), doc.rules.values())):
|
||||
# default scope hides things like lib rules, malware-category rules, etc.
|
||||
# but in vverbose mode, we really want to show everything.
|
||||
#
|
||||
@@ -266,17 +285,24 @@ def render_rules(ostream, doc: rd.ResultDocument):
|
||||
if rule.meta.is_subscope_rule:
|
||||
continue
|
||||
|
||||
lib_info = ""
|
||||
count = len(rule.matches)
|
||||
if count == 1:
|
||||
capability = rutils.bold(rule.meta.name)
|
||||
if rule.meta.lib:
|
||||
lib_info = " (library rule)"
|
||||
capability = f"{rutils.bold(rule.meta.name)}{lib_info}"
|
||||
else:
|
||||
capability = "%s (%d matches)" % (rutils.bold(rule.meta.name), count)
|
||||
if rule.meta.lib:
|
||||
lib_info = ", only showing first match of library rule"
|
||||
capability = f"{rutils.bold(rule.meta.name)} ({count} matches{lib_info})"
|
||||
|
||||
ostream.writeln(capability)
|
||||
had_match = True
|
||||
|
||||
rows = []
|
||||
rows.append(("namespace", rule.meta.namespace))
|
||||
if not rule.meta.lib:
|
||||
# library rules should not have a namespace
|
||||
rows.append(("namespace", rule.meta.namespace))
|
||||
|
||||
if rule.meta.maec.analysis_conclusion or rule.meta.maec.analysis_conclusion_ov:
|
||||
rows.append(
|
||||
@@ -289,7 +315,7 @@ def render_rules(ostream, doc: rd.ResultDocument):
|
||||
if rule.meta.maec.malware_family:
|
||||
rows.append(("maec/malware-family", rule.meta.maec.malware_family))
|
||||
|
||||
if rule.meta.maec.malware_category or rule.meta.maec.malware_category:
|
||||
if rule.meta.maec.malware_category or rule.meta.maec.malware_category_ov:
|
||||
rows.append(
|
||||
("maec/malware-category", rule.meta.maec.malware_category or rule.meta.maec.malware_category_ov)
|
||||
)
|
||||
@@ -319,7 +345,7 @@ def render_rules(ostream, doc: rd.ResultDocument):
|
||||
# because we do the file-scope evaluation a single time.
|
||||
# but i'm not 100% sure if this is/will always be true.
|
||||
# so, lets be explicit about our assumptions and raise an exception if they fail.
|
||||
raise RuntimeError("unexpected file scope match count: %d" % (len(matches)))
|
||||
raise RuntimeError(f"unexpected file scope match count: {len(matches)}")
|
||||
first_address, first_match = matches[0]
|
||||
render_match(ostream, first_match, indent=0)
|
||||
else:
|
||||
@@ -336,6 +362,10 @@ def render_rules(ostream, doc: rd.ResultDocument):
|
||||
|
||||
ostream.write("\n")
|
||||
render_match(ostream, match, indent=1)
|
||||
if rule.meta.lib:
|
||||
# only show first match
|
||||
break
|
||||
|
||||
ostream.write("\n")
|
||||
|
||||
if not had_match:
|
||||
|
||||
@@ -27,7 +27,9 @@ except ImportError:
|
||||
from typing import Any, Set, Dict, List, Tuple, Union, Iterator
|
||||
|
||||
import yaml
|
||||
import pydantic
|
||||
import ruamel.yaml
|
||||
import yaml.parser
|
||||
|
||||
import capa.perf
|
||||
import capa.engine as ceng
|
||||
@@ -122,6 +124,7 @@ SUPPORTED_FEATURES: Dict[str, Set] = {
|
||||
INSTRUCTION_SCOPE: {
|
||||
capa.features.common.MatchedRule,
|
||||
capa.features.insn.API,
|
||||
capa.features.insn.Property,
|
||||
capa.features.insn.Number,
|
||||
capa.features.common.String,
|
||||
capa.features.common.Bytes,
|
||||
@@ -156,11 +159,11 @@ SUPPORTED_FEATURES[FUNCTION_SCOPE].update(SUPPORTED_FEATURES[BASIC_BLOCK_SCOPE])
|
||||
|
||||
class InvalidRule(ValueError):
|
||||
def __init__(self, msg):
|
||||
super(InvalidRule, self).__init__()
|
||||
super().__init__()
|
||||
self.msg = msg
|
||||
|
||||
def __str__(self):
|
||||
return "invalid rule: %s" % (self.msg)
|
||||
return f"invalid rule: {self.msg}"
|
||||
|
||||
def __repr__(self):
|
||||
return str(self)
|
||||
@@ -168,22 +171,22 @@ class InvalidRule(ValueError):
|
||||
|
||||
class InvalidRuleWithPath(InvalidRule):
|
||||
def __init__(self, path, msg):
|
||||
super(InvalidRuleWithPath, self).__init__(msg)
|
||||
super().__init__(msg)
|
||||
self.path = path
|
||||
self.msg = msg
|
||||
self.__cause__ = None
|
||||
|
||||
def __str__(self):
|
||||
return "invalid rule: %s: %s" % (self.path, self.msg)
|
||||
return f"invalid rule: {self.path}: {self.msg}"
|
||||
|
||||
|
||||
class InvalidRuleSet(ValueError):
|
||||
def __init__(self, msg):
|
||||
super(InvalidRuleSet, self).__init__()
|
||||
super().__init__()
|
||||
self.msg = msg
|
||||
|
||||
def __str__(self):
|
||||
return "invalid rule set: %s" % (self.msg)
|
||||
return f"invalid rule set: {self.msg}"
|
||||
|
||||
def __repr__(self):
|
||||
return str(self)
|
||||
@@ -197,14 +200,14 @@ def ensure_feature_valid_for_scope(scope: str, feature: Union[Feature, Statement
|
||||
and isinstance(feature.value, str)
|
||||
and capa.features.common.Characteristic(feature.value) not in SUPPORTED_FEATURES[scope]
|
||||
):
|
||||
raise InvalidRule("feature %s not supported for scope %s" % (feature, scope))
|
||||
raise InvalidRule(f"feature {feature} not supported for scope {scope}")
|
||||
|
||||
if not isinstance(feature, capa.features.common.Characteristic):
|
||||
# features of this scope that are not Characteristics will be Type instances.
|
||||
# check that the given feature is one of these types.
|
||||
types_for_scope = filter(lambda t: isinstance(t, type), SUPPORTED_FEATURES[scope])
|
||||
if not isinstance(feature, tuple(types_for_scope)): # type: ignore
|
||||
raise InvalidRule("feature %s not supported for scope %s" % (feature, scope))
|
||||
raise InvalidRule(f"feature {feature} not supported for scope {scope}")
|
||||
|
||||
|
||||
def parse_int(s: str) -> int:
|
||||
@@ -221,33 +224,33 @@ def parse_range(s: str):
|
||||
"""
|
||||
# we want to use `{` characters, but this is a dict in yaml.
|
||||
if not s.startswith("("):
|
||||
raise InvalidRule("invalid range: %s" % (s))
|
||||
raise InvalidRule(f"invalid range: {s}")
|
||||
|
||||
if not s.endswith(")"):
|
||||
raise InvalidRule("invalid range: %s" % (s))
|
||||
raise InvalidRule(f"invalid range: {s}")
|
||||
|
||||
s = s[len("(") : -len(")")]
|
||||
min_spec, _, max_spec = s.partition(",")
|
||||
min_spec = min_spec.strip()
|
||||
max_spec = max_spec.strip()
|
||||
|
||||
min = None
|
||||
min_ = None
|
||||
if min_spec:
|
||||
min = parse_int(min_spec)
|
||||
if min < 0:
|
||||
min_ = parse_int(min_spec)
|
||||
if min_ < 0:
|
||||
raise InvalidRule("range min less than zero")
|
||||
|
||||
max = None
|
||||
max_ = None
|
||||
if max_spec:
|
||||
max = parse_int(max_spec)
|
||||
if max < 0:
|
||||
max_ = parse_int(max_spec)
|
||||
if max_ < 0:
|
||||
raise InvalidRule("range max less than zero")
|
||||
|
||||
if min is not None and max is not None:
|
||||
if max < min:
|
||||
if min_ is not None and max_ is not None:
|
||||
if max_ < min_:
|
||||
raise InvalidRule("range max less than min")
|
||||
|
||||
return min, max
|
||||
return min_, max_
|
||||
|
||||
|
||||
def parse_feature(key: str):
|
||||
@@ -290,8 +293,10 @@ def parse_feature(key: str):
|
||||
return capa.features.common.Class
|
||||
elif key == "namespace":
|
||||
return capa.features.common.Namespace
|
||||
elif key == "property":
|
||||
return capa.features.insn.Property
|
||||
else:
|
||||
raise InvalidRule("unexpected statement: %s" % key)
|
||||
raise InvalidRule(f"unexpected statement: {key}")
|
||||
|
||||
|
||||
# this is the separator between a feature value and its description
|
||||
@@ -305,11 +310,11 @@ def parse_bytes(s: str) -> bytes:
|
||||
try:
|
||||
b = codecs.decode(s.replace(" ", "").encode("ascii"), "hex")
|
||||
except binascii.Error:
|
||||
raise InvalidRule('unexpected bytes value: must be a valid hex sequence: "%s"' % s)
|
||||
raise InvalidRule(f'unexpected bytes value: must be a valid hex sequence: "{s}"')
|
||||
|
||||
if len(b) > MAX_BYTES_FEATURE_SIZE:
|
||||
raise InvalidRule(
|
||||
"unexpected bytes value: byte sequences must be no larger than %s bytes" % MAX_BYTES_FEATURE_SIZE
|
||||
f"unexpected bytes value: byte sequences must be no larger than {MAX_BYTES_FEATURE_SIZE} bytes"
|
||||
)
|
||||
|
||||
return b
|
||||
@@ -332,15 +337,14 @@ def parse_description(s: Union[str, int, bytes], value_type: str, description=No
|
||||
# - number: 10 = CONST_FOO
|
||||
# description: CONST_FOO
|
||||
raise InvalidRule(
|
||||
'unexpected value: "%s", only one description allowed (inline description with `%s`)'
|
||||
% (s, DESCRIPTION_SEPARATOR)
|
||||
f'unexpected value: "{s}", only one description allowed (inline description with `{DESCRIPTION_SEPARATOR}`)'
|
||||
)
|
||||
|
||||
value, _, description = s.partition(DESCRIPTION_SEPARATOR)
|
||||
if description == "":
|
||||
# sanity check:
|
||||
# there is an empty description, like `number: 10 =`
|
||||
raise InvalidRule('unexpected value: "%s", description cannot be empty' % s)
|
||||
raise InvalidRule(f'unexpected value: "{s}", description cannot be empty')
|
||||
else:
|
||||
# this is a string, but there is no description,
|
||||
# like: `api: CreateFileA`
|
||||
@@ -367,7 +371,7 @@ def parse_description(s: Union[str, int, bytes], value_type: str, description=No
|
||||
try:
|
||||
value = parse_int(value)
|
||||
except ValueError:
|
||||
raise InvalidRule('unexpected value: "%s", must begin with numerical value' % value)
|
||||
raise InvalidRule(f'unexpected value: "{value}", must begin with numerical value')
|
||||
|
||||
else:
|
||||
# the value might be a number, like: `number: 10`
|
||||
@@ -527,22 +531,23 @@ def build_statements(d, scope: str):
|
||||
min, max = parse_range(count)
|
||||
return ceng.Range(feature, min=min, max=max, description=description)
|
||||
else:
|
||||
raise InvalidRule("unexpected range: %s" % (count))
|
||||
raise InvalidRule(f"unexpected range: {count}")
|
||||
elif key == "string" and not isinstance(d[key], str):
|
||||
raise InvalidRule("ambiguous string value %s, must be defined as explicit string" % d[key])
|
||||
raise InvalidRule(f"ambiguous string value {d[key]}, must be defined as explicit string")
|
||||
|
||||
elif key.startswith("operand[") and key.endswith("].number"):
|
||||
index = key[len("operand[") : -len("].number")]
|
||||
try:
|
||||
index = int(index)
|
||||
except ValueError:
|
||||
raise InvalidRule("operand index must be an integer")
|
||||
except ValueError as e:
|
||||
raise InvalidRule("operand index must be an integer") from e
|
||||
|
||||
value, description = parse_description(d[key], key, d.get("description"))
|
||||
assert isinstance(value, int)
|
||||
try:
|
||||
feature = capa.features.insn.OperandNumber(index, value, description=description)
|
||||
except ValueError as e:
|
||||
raise InvalidRule(str(e))
|
||||
raise InvalidRule(str(e)) from e
|
||||
ensure_feature_valid_for_scope(scope, feature)
|
||||
return feature
|
||||
|
||||
@@ -550,14 +555,15 @@ def build_statements(d, scope: str):
|
||||
index = key[len("operand[") : -len("].offset")]
|
||||
try:
|
||||
index = int(index)
|
||||
except ValueError:
|
||||
raise InvalidRule("operand index must be an integer")
|
||||
except ValueError as e:
|
||||
raise InvalidRule("operand index must be an integer") from e
|
||||
|
||||
value, description = parse_description(d[key], key, d.get("description"))
|
||||
assert isinstance(value, int)
|
||||
try:
|
||||
feature = capa.features.insn.OperandOffset(index, value, description=description)
|
||||
except ValueError as e:
|
||||
raise InvalidRule(str(e))
|
||||
raise InvalidRule(str(e)) from e
|
||||
ensure_feature_valid_for_scope(scope, feature)
|
||||
return feature
|
||||
|
||||
@@ -566,14 +572,28 @@ def build_statements(d, scope: str):
|
||||
or (key == "format" and d[key] not in capa.features.common.VALID_FORMAT)
|
||||
or (key == "arch" and d[key] not in capa.features.common.VALID_ARCH)
|
||||
):
|
||||
raise InvalidRule("unexpected %s value %s" % (key, d[key]))
|
||||
raise InvalidRule(f"unexpected {key} value {d[key]}")
|
||||
|
||||
elif key.startswith("property/"):
|
||||
access = key[len("property/") :]
|
||||
if access not in capa.features.common.VALID_FEATURE_ACCESS:
|
||||
raise InvalidRule(f"unexpected {key} access {access}")
|
||||
|
||||
value, description = parse_description(d[key], key, d.get("description"))
|
||||
try:
|
||||
feature = capa.features.insn.Property(value, access=access, description=description)
|
||||
except ValueError as e:
|
||||
raise InvalidRule(str(e)) from e
|
||||
ensure_feature_valid_for_scope(scope, feature)
|
||||
return feature
|
||||
|
||||
else:
|
||||
Feature = parse_feature(key)
|
||||
value, description = parse_description(d[key], key, d.get("description"))
|
||||
try:
|
||||
feature = Feature(value, description=description)
|
||||
except ValueError as e:
|
||||
raise InvalidRule(str(e))
|
||||
raise InvalidRule(str(e)) from e
|
||||
ensure_feature_valid_for_scope(scope, feature)
|
||||
return feature
|
||||
|
||||
@@ -588,7 +608,7 @@ def second(s: List[Any]) -> Any:
|
||||
|
||||
class Rule:
|
||||
def __init__(self, name: str, scope: str, statement: Statement, meta, definition=""):
|
||||
super(Rule, self).__init__()
|
||||
super().__init__()
|
||||
self.name = name
|
||||
self.scope = scope
|
||||
self.statement = statement
|
||||
@@ -596,10 +616,10 @@ class Rule:
|
||||
self.definition = definition
|
||||
|
||||
def __str__(self):
|
||||
return "Rule(name=%s)" % (self.name)
|
||||
return f"Rule(name={self.name})"
|
||||
|
||||
def __repr__(self):
|
||||
return "Rule(scope=%s, name=%s)" % (self.scope, self.name)
|
||||
return f"Rule(scope={self.scope}, name={self.name})"
|
||||
|
||||
def get_dependencies(self, namespaces):
|
||||
"""
|
||||
@@ -614,7 +634,7 @@ class Rule:
|
||||
Returns:
|
||||
List[str]: names of rules upon which this rule depends.
|
||||
"""
|
||||
deps = set([])
|
||||
deps: Set[str] = set([])
|
||||
|
||||
def rec(statement):
|
||||
if isinstance(statement, capa.features.common.MatchedRule):
|
||||
@@ -631,6 +651,7 @@ class Rule:
|
||||
deps.update(map(lambda r: r.name, namespaces[statement.value]))
|
||||
else:
|
||||
# not a namespace, assume its a rule name.
|
||||
assert isinstance(statement.value, str)
|
||||
deps.add(statement.value)
|
||||
|
||||
elif isinstance(statement, ceng.Statement):
|
||||
@@ -646,7 +667,11 @@ class Rule:
|
||||
def _extract_subscope_rules_rec(self, statement):
|
||||
if isinstance(statement, ceng.Statement):
|
||||
# for each child that is a subscope,
|
||||
for subscope in filter(lambda statement: isinstance(statement, ceng.Subscope), statement.get_children()):
|
||||
for child in statement.get_children():
|
||||
if not isinstance(child, ceng.Subscope):
|
||||
continue
|
||||
|
||||
subscope = child
|
||||
|
||||
# create a new rule from it.
|
||||
# the name is a randomly generated, hopefully unique value.
|
||||
@@ -717,7 +742,7 @@ class Rule:
|
||||
return self.statement.evaluate(features, short_circuit=short_circuit)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d, definition):
|
||||
def from_dict(cls, d: Dict[str, Any], definition: str) -> "Rule":
|
||||
meta = d["rule"]["meta"]
|
||||
name = meta["name"]
|
||||
# if scope is not specified, default to function scope.
|
||||
@@ -751,14 +776,12 @@ class Rule:
|
||||
# prefer to use CLoader to be fast, see #306
|
||||
# on Linux, make sure you install libyaml-dev or similar
|
||||
# on Windows, get WHLs from pyyaml.org/pypi
|
||||
loader = yaml.CLoader
|
||||
logger.debug("using libyaml CLoader.")
|
||||
return yaml.CLoader
|
||||
except:
|
||||
loader = yaml.Loader
|
||||
logger.debug("unable to import libyaml CLoader, falling back to Python yaml parser.")
|
||||
logger.debug("this will be slower to load rules.")
|
||||
|
||||
return loader
|
||||
return yaml.Loader
|
||||
|
||||
@staticmethod
|
||||
def _get_ruamel_yaml_parser():
|
||||
@@ -770,8 +793,9 @@ class Rule:
|
||||
# use block mode, not inline json-like mode
|
||||
y.default_flow_style = False
|
||||
|
||||
# leave quotes unchanged
|
||||
y.preserve_quotes = True
|
||||
# leave quotes unchanged.
|
||||
# manually verified this property exists, even if mypy complains.
|
||||
y.preserve_quotes = True # type: ignore
|
||||
|
||||
# indent lists by two spaces below their parent
|
||||
#
|
||||
@@ -782,12 +806,13 @@ class Rule:
|
||||
y.indent(sequence=2, offset=2)
|
||||
|
||||
# avoid word wrapping
|
||||
y.width = 4096
|
||||
# manually verified this property exists, even if mypy complains.
|
||||
y.width = 4096 # type: ignore
|
||||
|
||||
return y
|
||||
|
||||
@classmethod
|
||||
def from_yaml(cls, s, use_ruamel=False):
|
||||
def from_yaml(cls, s: str, use_ruamel=False) -> "Rule":
|
||||
if use_ruamel:
|
||||
# ruamel enables nice formatting and doc roundtripping with comments
|
||||
doc = cls._get_ruamel_yaml_parser().load(s)
|
||||
@@ -797,14 +822,24 @@ class Rule:
|
||||
return cls.from_dict(doc, s)
|
||||
|
||||
@classmethod
|
||||
def from_yaml_file(cls, path, use_ruamel=False):
|
||||
def from_yaml_file(cls, path, use_ruamel=False) -> "Rule":
|
||||
with open(path, "rb") as f:
|
||||
try:
|
||||
return cls.from_yaml(f.read().decode("utf-8"), use_ruamel=use_ruamel)
|
||||
except InvalidRule as e:
|
||||
raise InvalidRuleWithPath(path, str(e))
|
||||
rule = cls.from_yaml(f.read().decode("utf-8"), use_ruamel=use_ruamel)
|
||||
# import here to avoid circular dependency
|
||||
from capa.render.result_document import RuleMetadata
|
||||
|
||||
def to_yaml(self):
|
||||
# validate meta data fields
|
||||
_ = RuleMetadata.from_capa(rule)
|
||||
return rule
|
||||
except InvalidRule as e:
|
||||
raise InvalidRuleWithPath(path, str(e)) from e
|
||||
except pydantic.ValidationError as e:
|
||||
raise InvalidRuleWithPath(path, str(e)) from e
|
||||
except yaml.parser.ParserError as e:
|
||||
raise InvalidRuleWithPath(path, str(e)) from e
|
||||
|
||||
def to_yaml(self) -> str:
|
||||
# reformat the yaml document with a common style.
|
||||
# this includes:
|
||||
# - ordering the meta elements
|
||||
@@ -962,7 +997,7 @@ def ensure_rule_dependencies_are_met(rules: List[Rule]) -> None:
|
||||
for rule in rules_by_name.values():
|
||||
for dep in rule.get_dependencies(namespaces):
|
||||
if dep not in rules_by_name:
|
||||
raise InvalidRule('rule "%s" depends on missing rule "%s"' % (rule.name, dep))
|
||||
raise InvalidRule(f'rule "{rule.name}" depends on missing rule "{dep}"')
|
||||
|
||||
|
||||
def index_rules_by_namespace(rules: List[Rule]) -> Dict[str, List[Rule]]:
|
||||
@@ -1041,10 +1076,18 @@ class RuleSet:
|
||||
"""
|
||||
|
||||
def __init__(self, rules: List[Rule]):
|
||||
super(RuleSet, self).__init__()
|
||||
super().__init__()
|
||||
|
||||
ensure_rules_are_unique(rules)
|
||||
|
||||
# in the next step we extract subscope rules,
|
||||
# which may inflate the number of rules tracked in this ruleset.
|
||||
# so record number of rules initially provided to this ruleset.
|
||||
#
|
||||
# this number is really only meaningful to the user,
|
||||
# who may compare it against the number of files on their file system.
|
||||
self.source_rule_count = len(rules)
|
||||
|
||||
rules = self._extract_subscope_rules(rules)
|
||||
|
||||
ensure_rule_dependencies_are_met(rules)
|
||||
@@ -1233,7 +1276,7 @@ class RuleSet:
|
||||
return (easy_rules_by_feature, hard_rules)
|
||||
|
||||
@staticmethod
|
||||
def _get_rules_for_scope(rules, scope):
|
||||
def _get_rules_for_scope(rules, scope) -> List[Rule]:
|
||||
"""
|
||||
given a collection of rules, collect the rules that are needed at the given scope.
|
||||
these rules are ordered topologically.
|
||||
@@ -1241,7 +1284,7 @@ class RuleSet:
|
||||
don't include auto-generated "subscope" rules.
|
||||
we want to include general "lib" rules here - even if they are not dependencies of other rules, see #398
|
||||
"""
|
||||
scope_rules = set([])
|
||||
scope_rules: Set[Rule] = set([])
|
||||
|
||||
# we need to process all rules, not just rules with the given scope.
|
||||
# this is because rules with a higher scope, e.g. file scope, may have subscope rules
|
||||
@@ -1255,7 +1298,7 @@ class RuleSet:
|
||||
return get_rules_with_scope(topologically_order_rules(list(scope_rules)), scope)
|
||||
|
||||
@staticmethod
|
||||
def _extract_subscope_rules(rules):
|
||||
def _extract_subscope_rules(rules) -> List[Rule]:
|
||||
"""
|
||||
process the given sequence of rules.
|
||||
for each one, extract any embedded subscope rules into their own rule.
|
||||
@@ -1291,13 +1334,13 @@ class RuleSet:
|
||||
for k, v in rule.meta.items():
|
||||
if isinstance(v, str) and tag in v:
|
||||
logger.debug('using rule "%s" and dependencies, found tag in meta.%s: %s', rule.name, k, v)
|
||||
rules_filtered.update(set(capa.rules.get_rules_and_dependencies(rules, rule.name)))
|
||||
rules_filtered.update(set(get_rules_and_dependencies(rules, rule.name)))
|
||||
break
|
||||
if isinstance(v, list):
|
||||
for vv in v:
|
||||
if tag in vv:
|
||||
logger.debug('using rule "%s" and dependencies, found tag in meta.%s: %s', rule.name, k, vv)
|
||||
rules_filtered.update(set(capa.rules.get_rules_and_dependencies(rules, rule.name)))
|
||||
rules_filtered.update(set(get_rules_and_dependencies(rules, rule.name)))
|
||||
break
|
||||
return RuleSet(list(rules_filtered))
|
||||
|
||||
155
capa/rules/cache.py
Normal file
155
capa/rules/cache.py
Normal file
@@ -0,0 +1,155 @@
|
||||
import sys
|
||||
import zlib
|
||||
import pickle
|
||||
import hashlib
|
||||
import logging
|
||||
import os.path
|
||||
from typing import List, Optional
|
||||
from dataclasses import dataclass
|
||||
|
||||
import capa.rules
|
||||
import capa.helpers
|
||||
import capa.version
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# TypeAlias. note: using `foo: TypeAlias = bar` is Python 3.10+
|
||||
CacheIdentifier = str
|
||||
|
||||
|
||||
def compute_cache_identifier(rule_content: List[bytes]) -> CacheIdentifier:
|
||||
hash = hashlib.sha256()
|
||||
|
||||
# note that this changes with each release,
|
||||
# so cache identifiers will never collide across releases.
|
||||
version = capa.version.__version__
|
||||
|
||||
hash.update(version.encode("utf-8"))
|
||||
hash.update(b"\x00")
|
||||
|
||||
rule_hashes = list(sorted([hashlib.sha256(buf).hexdigest() for buf in rule_content]))
|
||||
for rule_hash in rule_hashes:
|
||||
hash.update(rule_hash.encode("ascii"))
|
||||
hash.update(b"\x00")
|
||||
|
||||
return hash.hexdigest()
|
||||
|
||||
|
||||
def get_default_cache_directory() -> str:
|
||||
# ref: https://github.com/mandiant/capa/issues/1212#issuecomment-1361259813
|
||||
#
|
||||
# Linux: $XDG_CACHE_HOME/capa/
|
||||
# Windows: %LOCALAPPDATA%\flare\capa\cache
|
||||
# MacOS: ~/Library/Caches/capa
|
||||
|
||||
# ref: https://stackoverflow.com/a/8220141/87207
|
||||
if sys.platform == "linux" or sys.platform == "linux2":
|
||||
directory = os.environ.get("XDG_CACHE_HOME", os.path.join(os.environ["HOME"], ".cache", "capa"))
|
||||
elif sys.platform == "darwin":
|
||||
directory = os.path.join(os.environ["HOME"], "Library", "Caches", "capa")
|
||||
elif sys.platform == "win32":
|
||||
directory = os.path.join(os.environ["LOCALAPPDATA"], "flare", "capa", "cache")
|
||||
else:
|
||||
raise NotImplementedError(f"unsupported platform: {sys.platform}")
|
||||
|
||||
os.makedirs(directory, exist_ok=True)
|
||||
|
||||
return directory
|
||||
|
||||
|
||||
def get_cache_path(cache_dir: str, id: CacheIdentifier) -> str:
|
||||
filename = "capa-" + id[:8] + ".cache"
|
||||
return os.path.join(cache_dir, filename)
|
||||
|
||||
|
||||
MAGIC = b"capa"
|
||||
VERSION = b"\x00\x00\x00\x01"
|
||||
|
||||
|
||||
@dataclass
|
||||
class RuleCache:
|
||||
id: CacheIdentifier
|
||||
ruleset: capa.rules.RuleSet
|
||||
|
||||
def dump(self):
|
||||
return MAGIC + VERSION + self.id.encode("ascii") + zlib.compress(pickle.dumps(self))
|
||||
|
||||
@staticmethod
|
||||
def load(data):
|
||||
assert data.startswith(MAGIC + VERSION)
|
||||
|
||||
id = data[0x8:0x48].decode("ascii")
|
||||
cache = pickle.loads(zlib.decompress(data[0x48:]))
|
||||
|
||||
assert isinstance(cache, RuleCache)
|
||||
assert cache.id == id
|
||||
|
||||
return cache
|
||||
|
||||
|
||||
def get_ruleset_content(ruleset: capa.rules.RuleSet) -> List[bytes]:
|
||||
rule_contents = []
|
||||
for rule in ruleset.rules.values():
|
||||
if rule.is_subscope_rule():
|
||||
continue
|
||||
rule_contents.append(rule.definition.encode("utf-8"))
|
||||
return rule_contents
|
||||
|
||||
|
||||
def compute_ruleset_cache_identifier(ruleset: capa.rules.RuleSet) -> CacheIdentifier:
|
||||
rule_contents = get_ruleset_content(ruleset)
|
||||
return compute_cache_identifier(rule_contents)
|
||||
|
||||
|
||||
def cache_ruleset(cache_dir: str, ruleset: capa.rules.RuleSet):
|
||||
"""
|
||||
cache the given ruleset to disk, using the given cache directory.
|
||||
this can subsequently be reloaded via `load_cached_ruleset`,
|
||||
assuming the capa version and rule content does not change.
|
||||
|
||||
callers should use this function to avoid the performance overhead
|
||||
of validating rules on each run.
|
||||
"""
|
||||
id = compute_ruleset_cache_identifier(ruleset)
|
||||
path = get_cache_path(cache_dir, id)
|
||||
if os.path.exists(path):
|
||||
logger.debug("rule set already cached to %s", path)
|
||||
return
|
||||
|
||||
cache = RuleCache(id, ruleset)
|
||||
with open(path, "wb") as f:
|
||||
f.write(cache.dump())
|
||||
|
||||
logger.debug("rule set cached to %s", path)
|
||||
return
|
||||
|
||||
|
||||
def load_cached_ruleset(cache_dir: str, rule_contents: List[bytes]) -> Optional[capa.rules.RuleSet]:
|
||||
"""
|
||||
load a cached ruleset from disk, using the given cache directory.
|
||||
the raw rule contents are required here to prove that the rules haven't changed
|
||||
and to avoid stale cache entries.
|
||||
|
||||
callers should use this function to avoid the performance overhead
|
||||
of validating rules on each run.
|
||||
"""
|
||||
id = compute_cache_identifier(rule_contents)
|
||||
path = get_cache_path(cache_dir, id)
|
||||
if not os.path.exists(path):
|
||||
logger.debug("rule set cache does not exist: %s", path)
|
||||
return None
|
||||
|
||||
logger.debug("loading rule set from cache: %s", path)
|
||||
with open(path, "rb") as f:
|
||||
buf = f.read()
|
||||
|
||||
try:
|
||||
cache = RuleCache.load(buf)
|
||||
except AssertionError:
|
||||
logger.debug("rule set cache is invalid: %s", path)
|
||||
# delete the cache that seems to be invalid.
|
||||
os.remove(path)
|
||||
return None
|
||||
else:
|
||||
return cache.ruleset
|
||||
@@ -1,13 +1,5 @@
|
||||
__version__ = "4.0.0"
|
||||
__version__ = "5.1.0"
|
||||
|
||||
|
||||
def get_major_version():
|
||||
return int(__version__.partition(".")[0])
|
||||
|
||||
|
||||
def get_rules_branch():
|
||||
return f"v{get_major_version()}"
|
||||
|
||||
|
||||
def get_rules_checkout_command():
|
||||
return f"$ git clone https://github.com/mandiant/capa-rules.git -b {get_rules_branch()} /local/path/to/rules"
|
||||
|
||||
@@ -6,13 +6,11 @@ If you simply want to use capa, use the standalone binaries we host on GitHub: h
|
||||
|
||||
We use PyInstaller to create these packages.
|
||||
|
||||
The capa [README](../README.md#download) also links to nightly builds of standalone binaries from the latest development branch.
|
||||
|
||||
### Linux Standalone installation
|
||||
|
||||
The Linux Standalone binary has been built using GLIB 2.26.
|
||||
Consequently it works when using GLIB >= 2.26.
|
||||
This requirement is satisfied by default in most newer distribution such as Ubuntu >= 18, Debian >= 10, openSUSE >= 15.1 and CentOS >= 8.
|
||||
Consequently, it works when using GLIB >= 2.26.
|
||||
This requirement is satisfied by default in newer distribution such as Ubuntu >= 18, Debian >= 10, openSUSE >= 15.1 and CentOS >= 8.
|
||||
But the binary may not work in older distributions.
|
||||
|
||||
### MacOS Standalone installation
|
||||
@@ -24,24 +22,27 @@ By default, on MacOS Catalina or greater, Gatekeeper will block execution of the
|
||||
## Method 2: Using capa as a Python library
|
||||
To install capa as a Python library use `pip` to fetch the `flare-capa` module.
|
||||
|
||||
#### *Note*:
|
||||
### 1. Install capa module
|
||||
Use `pip` to install the capa module to your local Python environment. This fetches the library code to your computer but does not keep editable source files around for you to hack on. If you'd like to edit the source files, see below. `$ pip install flare-capa`
|
||||
|
||||
#### *Note on capa rules and library identification signatures*
|
||||
This method is appropriate for integrating capa in an existing project.
|
||||
This technique doesn't pull the default rule set, so you should check it out separately from [capa-rules](https://github.com/mandiant/capa-rules/) and pass the directory to the entrypoint using `-r` or set the rules path in the IDA Pro plugin:
|
||||
This technique doesn't pull the default rule set. You can obtain rule releases from [capa-rules](https://github.com/mandiant/capa-rules/releases) and pass the directory to the entrypoint using `-r`. In the IDA Pro plugin you need to configure the rules directory path once.
|
||||
|
||||
```console
|
||||
$ git clone https://github.com/mandiant/capa-rules.git -b v3 /local/path/to/rules
|
||||
$ capa -r /local/path/to/rules suspicious.exe
|
||||
$ wget https://github.com/mandiant/capa-rules/archive/refs/tags/v4.0.0.zip
|
||||
$ unzip v4.0.0.zip
|
||||
$ capa -r /path/to/capa-rules suspicious.exe
|
||||
```
|
||||
|
||||
This technique also doesn't set up the default library identification [signatures](https://github.com/mandiant/capa/tree/master/sigs). You can pass the signature directory using the `-s` argument.
|
||||
For example, to run capa with both a rule path and a signature path:
|
||||
```console
|
||||
$ capa -s /path/to/capa-sigs suspicious.exe
|
||||
```
|
||||
|
||||
capa -r /path/to/capa-rules -s /path/to/capa-sigs suspicious.exe
|
||||
Alternatively, see Method 3 below.
|
||||
|
||||
### 1. Install capa module
|
||||
Use `pip` to install the capa module to your local Python environment. This fetches the library code to your computer but does not keep editable source files around for you to hack on. If you'd like to edit the source files, see below. `$ pip install flare-capa`
|
||||
|
||||
### 2. Use capa
|
||||
You can now import the `capa` module from a Python script or use the IDA Pro plugins from the `capa/ida` directory. For more information please see the [usage](usage.md) documentation.
|
||||
|
||||
@@ -49,18 +50,20 @@ You can now import the `capa` module from a Python script or use the IDA Pro plu
|
||||
If you'd like to review and modify the capa source code, you'll need to check it out from GitHub and install it locally. By following these instructions, you'll maintain a local directory of source code that you can modify and run easily.
|
||||
|
||||
### 1. Check out source code
|
||||
Next, clone the capa git repository.
|
||||
Clone the capa git repository.
|
||||
We use submodules to separate [code](https://github.com/mandiant/capa), [rules](https://github.com/mandiant/capa-rules), and [test data](https://github.com/mandiant/capa-testfiles).
|
||||
|
||||
To clone everything use the `--recurse-submodules` option:
|
||||
- CAUTION: The capa testfiles repository contains many malware samples. If you pull down everything using this method, you may want to install to a directory that won't trigger your anti-virus software.
|
||||
- CAUTION: The capa testfiles repository contains many malware samples. If you pull down everything using this method, you may want to install to a directory that is ignored by your anti-virus software.
|
||||
- `$ git clone --recurse-submodules https://github.com/mandiant/capa.git /local/path/to/src` (HTTPS)
|
||||
- `$ git clone --recurse-submodules git@github.com:mandiant/capa.git /local/path/to/src` (SSH)
|
||||
|
||||
To only get the source code and our provided rules (common), follow these steps:
|
||||
To only get the source code and our provided rules (a more common use-case), follow these steps:
|
||||
- clone repository
|
||||
- `$ git clone https://github.com/mandiant/capa.git /local/path/to/src` (HTTPS)
|
||||
- `$ git clone git@github.com:mandiant/capa.git /local/path/to/src` (SSH)
|
||||
- `$ cd /local/path/to/src`
|
||||
- initialize the rules submodule and pull rules
|
||||
- `$ git submodule update --init rules`
|
||||
|
||||
### 2. Install the local source code
|
||||
@@ -76,8 +79,7 @@ You'll find that the `capa.exe` (Windows) or `capa` (Linux/MacOS) executables in
|
||||
|
||||
For development, we recommend to use [venv](https://docs.python.org/3/tutorial/venv.html). It allows you to create a virtual environment: a self-contained directory tree that contains a Python installation for a particular version of Python, plus a number of additional packages. This approach avoids conflicts between the requirements of different applications on your computer. It also ensures that you don't overlook to add a new requirement to `setup.up` using a library already installed on your system.
|
||||
|
||||
To create an environment (in the parent directory, to avoid commiting it by accident or messing with the linters), run:
|
||||
`$ python3 -m venv ../capa-env`
|
||||
To create an environment (in the parent directory, to avoid committing it by accident or messing with the linters), run: `$ python3 -m venv ../capa-env`
|
||||
|
||||
To activate `capa-env` in Linux or MacOS, run:
|
||||
`$ source ../capa-env/bin/activate`
|
||||
@@ -90,8 +92,8 @@ For more details about creating and using virtual environments, check out the [v
|
||||
##### Install development dependencies
|
||||
|
||||
We use the following tools to ensure consistent code style and formatting:
|
||||
- [black](https://github.com/psf/black) code formatter, with `-l 120`
|
||||
- [isort 5](https://pypi.org/project/isort/) code formatter, with `--profile black --length-sort --line-width 120`
|
||||
- [black](https://github.com/psf/black) code formatter
|
||||
- [isort 5](https://pypi.org/project/isort/) code formatter
|
||||
- [dos2unix](https://linux.die.net/man/1/dos2unix) for UNIX-style LF newlines
|
||||
- [capafmt](https://github.com/mandiant/capa/blob/master/scripts/capafmt.py) rule formatter
|
||||
|
||||
@@ -104,7 +106,7 @@ You can run it with the argument `no_tests` to skip the tests and only run the c
|
||||
|
||||
##### Setup hooks [optional]
|
||||
|
||||
If you plan to contribute to capa, you may want to setup the hooks.
|
||||
If you plan to contribute to capa, you may want to setup the provided hooks.
|
||||
Run `scripts/setup-hooks.sh` to set the following hooks up:
|
||||
- The `pre-commit` hook runs checks before every `git commit`.
|
||||
It runs `scripts/ci.sh no_tests` aborting the commit if there are code style or rule linter offenses you need to fix.
|
||||
@@ -112,13 +114,23 @@ Run `scripts/setup-hooks.sh` to set the following hooks up:
|
||||
It runs `scripts/ci.sh` aborting the push if there are code style or rule linter offenses or if the tests fail.
|
||||
This way you can ensure everything is alright before sending a pull request.
|
||||
|
||||
You can skip the checks by using the `--no-verify` git option.
|
||||
You can skip the checks by using the `-n`/`--no-verify` git option.
|
||||
|
||||
### 3. Compile binary using PyInstaller
|
||||
We compile capa standalone binaries using PyInstaller. To reproduce the build process check out the source code as described above and follow these steps.
|
||||
We compile capa standalone binaries using PyInstaller. To reproduce the build process check out the source code as described above and follow the following steps.
|
||||
|
||||
#### Install PyInstaller:
|
||||
`$ pip install pyinstaller` (Python 3)
|
||||
`$ pip install pyinstaller`
|
||||
|
||||
Or install capa with build dependencies:
|
||||
|
||||
`$ pip install -e /local/path/to/src[build]`
|
||||
|
||||
#### Generate rule cache
|
||||
|
||||
Generate cache for all rules in the `rules` folder and save the output in the `cache` folder.
|
||||
|
||||
`$ python scripts/cache-ruleset.py rules/ cache/`
|
||||
|
||||
#### Run Pyinstaller
|
||||
`$ pyinstaller .github/pyinstaller/pyinstaller.spec`
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
- [ ] Ensure all [milestoned issues/PRs](https://github.com/mandiant/capa/milestones) are addressed, or reassign to a new milestone.
|
||||
- [ ] Add the `dont merge` label to all PRs that are close to be ready to merge (or merge them if they are ready) in [capa](https://github.com/mandiant/capa/pulls) and [capa-rules](https://github.com/mandiant/capa-rules/pulls).
|
||||
- [ ] Ensure the [CI workflow succeeds in master](https://github.com/mandiant/capa/actions/workflows/tests.yml?query=branch%3Amaster).
|
||||
- [ ] Ensure that `python scripts/lint.py rules/ --thorough` succeeds (only `missing examples` offenses are allowed in the nursery).
|
||||
- [ ] Ensure that `python scripts/lint.py rules/ --thorough` succeeds (only `missing examples` offenses are allowed in the nursery). You can [manually trigger a thorough lint](https://github.com/mandiant/capa-rules/actions/workflows/tests.yml) in CI via the "Run workflow" option.
|
||||
- [ ] Review changes
|
||||
- capa https://github.com/mandiant/capa/compare/\<last-release\>...master
|
||||
- capa-rules https://github.com/mandiant/capa-rules/compare/\<last-release>\...master
|
||||
@@ -37,13 +37,10 @@
|
||||
- [ ] Update [capa/version.py](https://github.com/mandiant/capa/blob/master/capa/version.py)
|
||||
- [ ] Create a PR with the updated [CHANGELOG.md](https://github.com/mandiant/capa/blob/master/CHANGELOG.md) and [capa/version.py](https://github.com/mandiant/capa/blob/master/capa/version.py). Copy this checklist in the PR description.
|
||||
- [ ] After PR review, merge the PR and [create the release in GH](https://github.com/mandiant/capa/releases/new) using text from the [CHANGELOG.md](https://github.com/mandiant/capa/blob/master/CHANGELOG.md).
|
||||
- [ ] Verify GH actions [upload artifacts](https://github.com/mandiant/capa/releases), [publish to PyPI](https://pypi.org/project/flare-capa) and [create a tag in capa rules](https://github.com/mandiant/capa-rules/tags) upon completion.
|
||||
- [ ] Manually update capa rules major version rule branch
|
||||
```commandline
|
||||
[capa/rules] $ git pull master
|
||||
[capa/rules] $ git checkout v3 # create if new major version: git checkout -b vX
|
||||
[capa/rules] $ git merge master
|
||||
[capa/rules] $ git push origin v3
|
||||
```
|
||||
- Verify GH actions
|
||||
- [ ] [upload artifacts](https://github.com/mandiant/capa/releases)
|
||||
- [ ] [publish to PyPI](https://pypi.org/project/flare-capa)
|
||||
- [ ] [create tag in capa rules](https://github.com/mandiant/capa-rules/tags)
|
||||
- [ ] [create release in capa rules](https://github.com/mandiant/capa-rules/releases)
|
||||
- [ ] [Spread the word](https://twitter.com)
|
||||
- [ ] Update internal service
|
||||
|
||||
41
doc/rules.md
41
doc/rules.md
@@ -1,6 +1,5 @@
|
||||
### rules
|
||||
|
||||
|
||||
capa uses a collection of rules to identify capabilities within a program.
|
||||
The [github.com/mandiant/capa-rules](https://github.com/mandiant/capa-rules) repository contains hundreds of standard library rules that are distributed with capa.
|
||||
|
||||
@@ -12,8 +11,8 @@ $ capa suspicious.exe
|
||||
|
||||
However, you may want to modify the rules for a variety of reasons:
|
||||
|
||||
- develop new rules to find behaviors, and/or
|
||||
- tweak existing rules to reduce false positives, and/or
|
||||
- develop new rules to find behaviors,
|
||||
- tweak existing rules to reduce false positives,
|
||||
- collect a private selection of rules not shared publicly.
|
||||
|
||||
Or, you may want to use capa as a Python library within another application.
|
||||
@@ -21,22 +20,18 @@ Or, you may want to use capa as a Python library within another application.
|
||||
In these scenarios, you must provide the rule set to capa as a directory on your file system. Do this using the `-r`/`--rules` parameter:
|
||||
|
||||
```console
|
||||
$ capa --rules /local/path/to/rules suspicious.exe
|
||||
$ capa --rules /local/path/to/rules suspicious.exe
|
||||
```
|
||||
|
||||
You can collect the standard set of rules in two ways:
|
||||
You can download the standard set of rules as ZIP or TGZ archives from the [capa-rules release page](https://github.com/mandiant/capa-rules/releases).
|
||||
|
||||
- [download from the Github releases page](#download-release-archive), or
|
||||
- [clone from Github](#clone-with-git).
|
||||
|
||||
Note that you must use match the rules major version with the capa major version,
|
||||
i.e., use `v1` rules with `v1` of capa.
|
||||
Note that you must use match the rules major version with the capa major version, i.e., use `v1` rules with `v1` of capa.
|
||||
This is so that new versions of capa can update rule syntax, such as by adding new fields and logic.
|
||||
|
||||
Otherwise, using rules with a mismatched version of capa may lead to errors like:
|
||||
|
||||
```
|
||||
$ capa --rules /path/to/mismatched/rules suspicious.exe
|
||||
$ capa --rules /path/to/mismatched/rules suspicious.exe
|
||||
ERROR:lint:invalid rule: injection.yml: invalid rule: unexpected statement: instruction
|
||||
```
|
||||
|
||||
@@ -46,27 +41,3 @@ You can check the version of capa you're currently using like this:
|
||||
$ capa --version
|
||||
capa 3.0.3
|
||||
```
|
||||
|
||||
#### download release archive
|
||||
|
||||
The releases page is [here](https://github.com/mandiant/capa-rules/tags/).
|
||||
Find the most recent release corresponding to your major version of capa and download the ZIP archive.
|
||||
Here are some quick links:
|
||||
- v1: [v1](https://github.com/mandiant/capa-rules/releases/tag/v1)
|
||||
- v2: [v2](https://github.com/mandiant/capa-rules/releases/tag/v2)
|
||||
- v3: [v3](https://github.com/mandiant/capa-rules/releases/tag/v3)
|
||||
|
||||
#### clone with git
|
||||
|
||||
To fetch with git, clone the appropriate branch like this:
|
||||
|
||||
```console
|
||||
$ git clone https://github.com/mandiant/capa-rules.git -b v3 /local/path/to/rules
|
||||
```
|
||||
|
||||
Note that the branch name (`v3` in the example above) must match the major version of capa you're using.
|
||||
|
||||
- [v1](https://github.com/mandiant/capa-rules/tree/v1): `v1`
|
||||
- [v2](https://github.com/mandiant/capa-rules/tree/v2): `v2`
|
||||
- [v3](https://github.com/mandiant/capa-rules/tree/v3): `v3`
|
||||
|
||||
|
||||
2
rules
2
rules
Submodule rules updated: ad4da12d90...a10ccf3fd8
@@ -69,6 +69,7 @@ import capa.main
|
||||
import capa.rules
|
||||
import capa.render.json
|
||||
import capa.render.result_document as rd
|
||||
from capa.features.common import OS_AUTO
|
||||
|
||||
logger = logging.getLogger("capa")
|
||||
|
||||
@@ -81,6 +82,7 @@ def get_capa_results(args):
|
||||
rules (capa.rules.RuleSet): the rules to match
|
||||
signatures (List[str]): list of file system paths to signature files
|
||||
format (str): the name of the sample file format
|
||||
os (str): the name of the operating system
|
||||
path (str): the file system path to the sample to process
|
||||
|
||||
args is a tuple because i'm not quite sure how to unpack multiple arguments using `map`.
|
||||
@@ -96,12 +98,12 @@ def get_capa_results(args):
|
||||
meta (dict): the meta analysis results
|
||||
capabilities (dict): the matched capabilities and their result objects
|
||||
"""
|
||||
rules, sigpaths, format, path = args
|
||||
rules, sigpaths, format, os_, path = args
|
||||
should_save_workspace = os.environ.get("CAPA_SAVE_WORKSPACE") not in ("0", "no", "NO", "n", None)
|
||||
logger.info("computing capa results for: %s", path)
|
||||
try:
|
||||
extractor = capa.main.get_extractor(
|
||||
path, format, capa.main.BACKEND_VIV, sigpaths, should_save_workspace, disable_progress=True
|
||||
path, format, os_, capa.main.BACKEND_VIV, sigpaths, should_save_workspace, disable_progress=True
|
||||
)
|
||||
except capa.main.UnsupportedFormatError:
|
||||
# i'm 100% sure if multiprocessing will reliably raise exceptions across process boundaries.
|
||||
@@ -112,7 +114,7 @@ def get_capa_results(args):
|
||||
return {
|
||||
"path": path,
|
||||
"status": "error",
|
||||
"error": "input file does not appear to be a PE file: %s" % path,
|
||||
"error": f"input file does not appear to be a PE file: {path}",
|
||||
}
|
||||
except capa.main.UnsupportedRuntimeError:
|
||||
return {
|
||||
@@ -124,10 +126,10 @@ def get_capa_results(args):
|
||||
return {
|
||||
"path": path,
|
||||
"status": "error",
|
||||
"error": "unexpected error: %s" % (e),
|
||||
"error": f"unexpected error: {e}",
|
||||
}
|
||||
|
||||
meta = capa.main.collect_metadata([], path, [], extractor)
|
||||
meta = capa.main.collect_metadata([], path, format, os_, [], extractor)
|
||||
capabilities, counts = capa.main.find_capabilities(rules, extractor, disable_progress=True)
|
||||
meta["analysis"].update(counts)
|
||||
meta["analysis"]["layout"] = capa.main.compute_layout(rules, extractor, capabilities)
|
||||
@@ -142,7 +144,7 @@ def main(argv=None):
|
||||
argv = sys.argv[1:]
|
||||
|
||||
parser = argparse.ArgumentParser(description="detect capabilities in programs.")
|
||||
capa.main.install_common_args(parser, wanted={"rules", "signatures"})
|
||||
capa.main.install_common_args(parser, wanted={"rules", "signatures", "format", "os"})
|
||||
parser.add_argument("input", type=str, help="Path to directory of files to recursively analyze")
|
||||
parser.add_argument(
|
||||
"-n", "--parallelism", type=int, default=multiprocessing.cpu_count(), help="parallelism factor"
|
||||
@@ -153,7 +155,6 @@ def main(argv=None):
|
||||
|
||||
try:
|
||||
rules = capa.main.get_rules(args.rules)
|
||||
rules = capa.rules.RuleSet(rules)
|
||||
logger.info("successfully loaded %s rules", len(rules))
|
||||
except (IOError, capa.rules.InvalidRule, capa.rules.InvalidRuleSet) as e:
|
||||
logger.error("%s", str(e))
|
||||
@@ -161,12 +162,12 @@ def main(argv=None):
|
||||
|
||||
try:
|
||||
sig_paths = capa.main.get_signatures(args.signatures)
|
||||
except (IOError) as e:
|
||||
except IOError as e:
|
||||
logger.error("%s", str(e))
|
||||
return -1
|
||||
|
||||
samples = []
|
||||
for (base, directories, files) in os.walk(args.input):
|
||||
for base, directories, files in os.walk(args.input):
|
||||
for file in files:
|
||||
samples.append(os.path.join(base, file))
|
||||
|
||||
@@ -196,14 +197,16 @@ def main(argv=None):
|
||||
|
||||
results = {}
|
||||
for result in mapper(
|
||||
get_capa_results, [(rules, sig_paths, "pe", sample) for sample in samples], parallelism=args.parallelism
|
||||
get_capa_results,
|
||||
[(rules, sig_paths, "pe", OS_AUTO, sample) for sample in samples],
|
||||
parallelism=args.parallelism,
|
||||
):
|
||||
if result["status"] == "error":
|
||||
logger.warning(result["error"])
|
||||
elif result["status"] == "ok":
|
||||
results[result["path"]] = rd.ResultDocument.parse_obj(result["ok"]).json(exclude_none=True)
|
||||
else:
|
||||
raise ValueError("unexpected status: %s" % (result["status"]))
|
||||
raise ValueError(f"unexpected status: {result['status']}")
|
||||
|
||||
print(json.dumps(results))
|
||||
|
||||
|
||||
67
scripts/cache-ruleset.py
Normal file
67
scripts/cache-ruleset.py
Normal file
@@ -0,0 +1,67 @@
|
||||
"""
|
||||
Create a cache of the given rules.
|
||||
This is only really intended to be used by CI to pre-cache rulesets
|
||||
that will be distributed within PyInstaller binaries.
|
||||
|
||||
Usage:
|
||||
|
||||
$ python scripts/cache-ruleset.py rules/ /path/to/cache/directory
|
||||
|
||||
Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and limitations under the License.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import logging
|
||||
import argparse
|
||||
|
||||
import capa.main
|
||||
import capa.rules
|
||||
import capa.engine
|
||||
import capa.helpers
|
||||
import capa.rules.cache
|
||||
import capa.features.insn
|
||||
|
||||
logger = logging.getLogger("cache-ruleset")
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
if argv is None:
|
||||
argv = sys.argv[1:]
|
||||
|
||||
parser = argparse.ArgumentParser(description="Cache ruleset.")
|
||||
capa.main.install_common_args(parser)
|
||||
parser.add_argument("rules", type=str, action="append", help="Path to rules")
|
||||
parser.add_argument("cache", type=str, help="Path to cache directory")
|
||||
args = parser.parse_args(args=argv)
|
||||
capa.main.handle_common_args(args)
|
||||
|
||||
if args.debug:
|
||||
logging.getLogger("capa").setLevel(logging.DEBUG)
|
||||
else:
|
||||
logging.getLogger("capa").setLevel(logging.ERROR)
|
||||
|
||||
try:
|
||||
os.makedirs(args.cache, exist_ok=True)
|
||||
rules = capa.main.get_rules(args.rules, cache_dir=args.cache)
|
||||
logger.info("successfully loaded %s rules", len(rules))
|
||||
except (IOError, capa.rules.InvalidRule, capa.rules.InvalidRuleSet) as e:
|
||||
logger.error("%s", str(e))
|
||||
return -1
|
||||
|
||||
content = capa.rules.cache.get_ruleset_content(rules)
|
||||
id = capa.rules.cache.compute_cache_identifier(content)
|
||||
path = capa.rules.cache.get_cache_path(args.cache, id)
|
||||
|
||||
assert os.path.exists(path)
|
||||
logger.info("cached to: %s", path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -64,7 +64,6 @@ unsupported = ["characteristic", "mnemonic", "offset", "subscope", "Range"]
|
||||
|
||||
# collect all converted rules to be able to check if we have needed sub rules for match:
|
||||
converted_rules = []
|
||||
count_incomplete = 0
|
||||
|
||||
default_tags = "CAPA "
|
||||
|
||||
@@ -129,8 +128,7 @@ def convert_capa_number_to_yara_bytes(number):
|
||||
|
||||
|
||||
def convert_rule_name(rule_name):
|
||||
|
||||
# yara rule names: "Identifiers must follow the same lexical conventions of the C programming language, they can contain any alphanumeric character and the underscore character, but the first character cannot be a digit. Rule identifiers are case sensitive and cannot exceed 128 characters." so we replace any non-alpanum with _
|
||||
# yara rule names: "Identifiers must follow the same lexical conventions of the C programming language, they can contain any alphanumeric character and the underscore character, but the first character cannot be a digit. Rule identifiers are case sensitive and cannot exceed 128 characters." so we replace any non-alphanum with _
|
||||
rule_name = re.sub(r"\W", "_", rule_name)
|
||||
rule_name = "capa_" + rule_name
|
||||
|
||||
@@ -152,7 +150,6 @@ def convert_description(statement):
|
||||
|
||||
|
||||
def convert_rule(rule, rulename, cround, depth):
|
||||
|
||||
depth += 1
|
||||
logger.info("recursion depth: " + str(depth))
|
||||
|
||||
@@ -284,12 +281,12 @@ def convert_rule(rule, rulename, cround, depth):
|
||||
# change capas /xxx/i to yaras /xxx/ nocase, count will be used later to decide appending 'nocase'
|
||||
regex, count = re.subn(r"/i$", "/", regex)
|
||||
|
||||
# remove / in the begining and end
|
||||
# remove / in the beginning and end
|
||||
regex = regex[1:-1]
|
||||
|
||||
# all .* in the regexes of capa look like they should be maximum 100 chars so take 1000 to speed up rules and prevent yara warnings on poor performance
|
||||
regex = regex.replace(".*", ".{,1000}")
|
||||
# strange: capa accepts regexes with unsescaped / like - string: /com/exe4j/runtime/exe4jcontroller/i in capa-rules/compiler/exe4j/compiled-with-exe4j.yml, needs a fix for yara:
|
||||
# strange: capa accepts regexes with unescaped / like - string: /com/exe4j/runtime/exe4jcontroller/i in capa-rules/compiler/exe4j/compiled-with-exe4j.yml, needs a fix for yara:
|
||||
# would assume that get_value_str() gives the raw string
|
||||
regex = re.sub(r"(?<!\\)/", r"\/", regex)
|
||||
|
||||
@@ -297,7 +294,7 @@ def convert_rule(rule, rulename, cround, depth):
|
||||
# /reg(|.exe)/ => /reg(.exe)?/
|
||||
regex = re.sub(r"\(\|([^\)]+)\)", r"(\1)?", regex)
|
||||
|
||||
# change begining of line to null byte, e.g. /^open => /\x00open (not word boundary because we're not looking for the begining of a word in a text but usually a function name if there's ^ in a capa rule)
|
||||
# change beginning of line to null byte, e.g. /^open => /\x00open (not word boundary because we're not looking for the beginning of a word in a text but usually a function name if there's ^ in a capa rule)
|
||||
regex = re.sub(r"^\^", r"\\x00", regex)
|
||||
|
||||
# regex = re.sub(r"^\^", r"\\b", regex)
|
||||
@@ -378,7 +375,7 @@ def convert_rule(rule, rulename, cround, depth):
|
||||
|
||||
if s_type == "Some":
|
||||
cmin = kid.count
|
||||
logger.info("Some type with mininum: " + str(cmin))
|
||||
logger.info("Some type with minimum: " + str(cmin))
|
||||
|
||||
if not cmin:
|
||||
logger.info("this is optional: which means, we can just ignore it")
|
||||
@@ -483,7 +480,7 @@ def convert_rule(rule, rulename, cround, depth):
|
||||
|
||||
elif statement == "Some":
|
||||
cmin = rule.count
|
||||
logger.info("Some type with mininum at2: " + str(cmin))
|
||||
logger.info("Some type with minimum at2: " + str(cmin))
|
||||
|
||||
if not cmin:
|
||||
logger.info("this is optional: which means, we can just ignore it")
|
||||
@@ -516,7 +513,6 @@ def output_yar(yara):
|
||||
|
||||
|
||||
def output_unsupported_capa_rules(yaml, capa_rulename, url, reason):
|
||||
|
||||
if reason != "NOLOG":
|
||||
if capa_rulename not in unsupported_capa_rules_list:
|
||||
logger.info("unsupported: " + capa_rulename + " - reason: " + reason + " - url: " + url)
|
||||
@@ -537,9 +533,9 @@ def output_unsupported_capa_rules(yaml, capa_rulename, url, reason):
|
||||
unsupported_capa_rules_names.write(url.encode("utf-8") + b"\n")
|
||||
|
||||
|
||||
def convert_rules(rules, namespaces, cround):
|
||||
def convert_rules(rules, namespaces, cround, make_priv):
|
||||
count_incomplete = 0
|
||||
for rule in rules.rules.values():
|
||||
|
||||
rule_name = convert_rule_name(rule.name)
|
||||
|
||||
if rule.is_subscope_rule():
|
||||
@@ -579,7 +575,6 @@ def convert_rules(rules, namespaces, cround):
|
||||
output_unsupported_capa_rules(rule.to_yaml(), rule.name, url, yara_condition)
|
||||
logger.info("Unknown feature at5: " + rule.name)
|
||||
else:
|
||||
|
||||
yara_meta = ""
|
||||
metas = rule.meta
|
||||
rule_tags = ""
|
||||
@@ -623,7 +618,7 @@ def convert_rules(rules, namespaces, cround):
|
||||
value = re.sub(r"^([0-9a-f]{20,64}):0x[0-9a-f]{1,10}$", r"\1", value, flags=re.IGNORECASE)
|
||||
|
||||
# examples in capa can contain the same hash several times with different offset, so check if it's already there:
|
||||
# (keeping the offset might be interessting for some but breaks yara-ci for checking of the final rules
|
||||
# (keeping the offset might be interesting for some but breaks yara-ci for checking of the final rules
|
||||
if value not in seen_hashes:
|
||||
yara_meta += "\t" + meta_name + ' = "' + value + '"\n'
|
||||
seen_hashes.append(value)
|
||||
@@ -652,7 +647,6 @@ def convert_rules(rules, namespaces, cround):
|
||||
if meta_name and meta_value:
|
||||
yara_meta += "\t" + meta_name + ' = "' + meta_value + '"\n'
|
||||
|
||||
rule_name_bonus = ""
|
||||
if rule_comment:
|
||||
yara_meta += '\tcomment = "' + rule_comment + '"\n'
|
||||
yara_meta += '\tdate = "' + today + '"\n'
|
||||
@@ -662,7 +656,6 @@ def convert_rules(rules, namespaces, cround):
|
||||
# check if there's some beef in condition:
|
||||
tmp_yc = re.sub(r"(and|or|not)", "", yara_condition)
|
||||
if re.search(r"\w", tmp_yc):
|
||||
|
||||
yara = ""
|
||||
if make_priv:
|
||||
yara = "private "
|
||||
@@ -679,12 +672,13 @@ def convert_rules(rules, namespaces, cround):
|
||||
# TODO: now the rule is finished and could be automatically checked with the capa-testfile(s) named in meta (doing it for all of them using yara-ci upload at the moment)
|
||||
output_yar(yara)
|
||||
converted_rules.append(rule_name)
|
||||
global count_incomplete
|
||||
count_incomplete += incomplete
|
||||
else:
|
||||
output_unsupported_capa_rules(rule.to_yaml(), rule.name, url, yara_condition)
|
||||
pass
|
||||
|
||||
return count_incomplete
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
if argv is None:
|
||||
@@ -696,7 +690,6 @@ def main(argv=None):
|
||||
capa.main.install_common_args(parser, wanted={"tag"})
|
||||
|
||||
args = parser.parse_args(args=argv)
|
||||
global make_priv
|
||||
make_priv = args.private
|
||||
|
||||
if args.verbose:
|
||||
@@ -710,9 +703,8 @@ def main(argv=None):
|
||||
logging.getLogger("capa2yara").setLevel(level)
|
||||
|
||||
try:
|
||||
rules = capa.main.get_rules([args.rules], disable_progress=True)
|
||||
namespaces = capa.rules.index_rules_by_namespace(list(rules))
|
||||
rules = capa.rules.RuleSet(rules)
|
||||
rules = capa.main.get_rules([args.rules])
|
||||
namespaces = capa.rules.index_rules_by_namespace(list(rules.rules.values()))
|
||||
logger.info("successfully loaded %s rules (including subscope rules which will be ignored)", len(rules))
|
||||
if args.tag:
|
||||
rules = rules.filter_rules_by_meta(args.tag)
|
||||
@@ -745,14 +737,15 @@ def main(argv=None):
|
||||
# do several rounds of converting rules because some rules for match: might not be converted in the 1st run
|
||||
num_rules = 9999999
|
||||
cround = 0
|
||||
count_incomplete = 0
|
||||
while num_rules != len(converted_rules) or cround < min_rounds:
|
||||
cround += 1
|
||||
logger.info("doing convert_rules(), round: " + str(cround))
|
||||
num_rules = len(converted_rules)
|
||||
convert_rules(rules, namespaces, cround)
|
||||
count_incomplete += convert_rules(rules, namespaces, cround, make_priv)
|
||||
|
||||
# one last round to collect all unconverted rules
|
||||
convert_rules(rules, namespaces, 9000)
|
||||
count_incomplete += convert_rules(rules, namespaces, 9000, make_priv)
|
||||
|
||||
stats = "\n// converted rules : " + str(len(converted_rules))
|
||||
stats += "\n// among those are incomplete : " + str(count_incomplete)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user