Compare commits
1053 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77a86e33bd | ||
|
|
4f44b5a60a | ||
|
|
9361b3deb1 | ||
|
|
9a0ec51f00 | ||
|
|
5979892d29 | ||
|
|
96f2536c34 | ||
|
|
52a3d35987 | ||
|
|
de4827e8fa | ||
|
|
b6d5409691 | ||
|
|
818f532ca9 | ||
|
|
895b548f34 | ||
|
|
d9f1d0918f | ||
|
|
35abdb8ecf | ||
|
|
e77bbd68cf | ||
|
|
4c73e5df3c | ||
|
|
933789d02b | ||
|
|
e88bb4814e | ||
|
|
17b7694170 | ||
|
|
f191c4f145 | ||
|
|
6fc2037f45 | ||
|
|
b5f23e7baf | ||
|
|
f7e4273523 | ||
|
|
6860b9a040 | ||
|
|
5c8a4aafd7 | ||
|
|
02658d6962 | ||
|
|
b2b94e6a8e | ||
|
|
65b3c046a3 | ||
|
|
04b5949a05 | ||
|
|
18c87e4e55 | ||
|
|
b84cc3128d | ||
|
|
f83ef470cb | ||
|
|
2928dd279c | ||
|
|
f96d3fd8ba | ||
|
|
d094272e4a | ||
|
|
7eeab35ae8 | ||
|
|
4e7b490bc3 | ||
|
|
4ca9e168fe | ||
|
|
e579edecb4 | ||
|
|
58aa3e33bf | ||
|
|
0685d36220 | ||
|
|
2158be0a2e | ||
|
|
7922d08fd4 | ||
|
|
44b47eb39c | ||
|
|
45c4b4019a | ||
|
|
831dc577f4 | ||
|
|
229d5ca549 | ||
|
|
2872db8b23 | ||
|
|
7152525dbc | ||
|
|
d7d7aa76c8 | ||
|
|
565bb96c9e | ||
|
|
9fd6098e1e | ||
|
|
0c0929fd94 | ||
|
|
1343baa250 | ||
|
|
6977477a39 | ||
|
|
86b3438a2d | ||
|
|
a00c3b6d32 | ||
|
|
544ffdea8f | ||
|
|
e4b89f1d7b | ||
|
|
73dd49ed21 | ||
|
|
0511eec67c | ||
|
|
c7e2ca0b1a | ||
|
|
03b15ce289 | ||
|
|
2d7ac73caa | ||
|
|
7fe53073fe | ||
|
|
d1407f0a1e | ||
|
|
f5a0e1cd08 | ||
|
|
94485285f3 | ||
|
|
466bc4995b | ||
|
|
7bce202122 | ||
|
|
40c7401f0a | ||
|
|
a7ebd5a309 | ||
|
|
d510840bb7 | ||
|
|
09ad0ec184 | ||
|
|
7f03db9fe4 | ||
|
|
96b9bce93c | ||
|
|
48858e114d | ||
|
|
1b4a087c4b | ||
|
|
6f1f928434 | ||
|
|
efd02915ab | ||
|
|
9484fadd0f | ||
|
|
b47b398b07 | ||
|
|
5867e880c6 | ||
|
|
c1acf702b6 | ||
|
|
9a7c83b26f | ||
|
|
dd2671aac2 | ||
|
|
c2981d5091 | ||
|
|
ae2baebf6c | ||
|
|
7372aa91c6 | ||
|
|
48756a7621 | ||
|
|
aca6ad2f52 | ||
|
|
24d61d8634 | ||
|
|
6411732bea | ||
|
|
152060a28a | ||
|
|
919aef90c0 | ||
|
|
853d7285bd | ||
|
|
6842b92ca2 | ||
|
|
dba250ca86 | ||
|
|
b8c524d2f5 | ||
|
|
0ff5db9397 | ||
|
|
15334cf5d4 | ||
|
|
f5cb5d462d | ||
|
|
79459d4a14 | ||
|
|
addd4683ca | ||
|
|
6d8399684b | ||
|
|
4583692539 | ||
|
|
9b7e67443b | ||
|
|
83909b2be4 | ||
|
|
247d330f79 | ||
|
|
1a31c84eef | ||
|
|
9ce92cfb5b | ||
|
|
1f44a2dec8 | ||
|
|
b7cd467363 | ||
|
|
ff3cc421eb | ||
|
|
205798865d | ||
|
|
10f499d230 | ||
|
|
a21b53d737 | ||
|
|
0f15895b36 | ||
|
|
2ba2aec0d3 | ||
|
|
11d50aa5b1 | ||
|
|
b066af9506 | ||
|
|
059909c027 | ||
|
|
d61ff0c69f | ||
|
|
f6c2394bdf | ||
|
|
df5ed6bbf2 | ||
|
|
0b653aa47a | ||
|
|
b5a18de4a3 | ||
|
|
5408481606 | ||
|
|
1c66ebe638 | ||
|
|
3e79dfd0e7 | ||
|
|
459df37b13 | ||
|
|
3d8edc513c | ||
|
|
ab7bf53f67 | ||
|
|
c30a56bc11 | ||
|
|
6918a039e9 | ||
|
|
469e2ff870 | ||
|
|
3416f7bc61 | ||
|
|
a75d7576f8 | ||
|
|
23addda29a | ||
|
|
14e2efa309 | ||
|
|
faa363cd8f | ||
|
|
e29922af57 | ||
|
|
8a0ae7ae55 | ||
|
|
6f67619621 | ||
|
|
3f55f678ca | ||
|
|
ee41d47e4d | ||
|
|
527e993bb4 | ||
|
|
6b4d7266e6 | ||
|
|
954ed3a408 | ||
|
|
ac59e50b5f | ||
|
|
7029ad32c4 | ||
|
|
766dcacdbe | ||
|
|
fc9ad6c737 | ||
|
|
7d2e664320 | ||
|
|
6187317a4e | ||
|
|
d81b0bcbfa | ||
|
|
9c8e18acb4 | ||
|
|
8aed58c1d4 | ||
|
|
325c726f0e | ||
|
|
9a4e9b6586 | ||
|
|
23354ec452 | ||
|
|
f698f4e79b | ||
|
|
c05a8bf910 | ||
|
|
9ffbb82f4c | ||
|
|
0508d31a35 | ||
|
|
901a398b31 | ||
|
|
fd0f87ca6e | ||
|
|
84d2f9f324 | ||
|
|
f9bad7e5e4 | ||
|
|
40b6575db6 | ||
|
|
64d849aafc | ||
|
|
3b6e6dcc00 | ||
|
|
d17ac2928f | ||
|
|
8b58723f40 | ||
|
|
bed2e3777e | ||
|
|
c039e98d3f | ||
|
|
c3ba6a9025 | ||
|
|
2691fb400e | ||
|
|
e0075573d9 | ||
|
|
1bb8c78b60 | ||
|
|
ff66346d2a | ||
|
|
6f51324cca | ||
|
|
700259eab6 | ||
|
|
438677b129 | ||
|
|
3f51e787e4 | ||
|
|
2bbf00d603 | ||
|
|
b21b041dab | ||
|
|
734b1702e6 | ||
|
|
a39e2e7e0f | ||
|
|
d9e1732766 | ||
|
|
6dd5bbeffd | ||
|
|
3c4388e280 | ||
|
|
6ffa5ef53e | ||
|
|
90ec848bf6 | ||
|
|
e0be7f1b8e | ||
|
|
4ef3830b6b | ||
|
|
e737595339 | ||
|
|
94cb090afe | ||
|
|
32e0a5dce2 | ||
|
|
f304bdbd20 | ||
|
|
1a3286beda | ||
|
|
63cd70029f | ||
|
|
94089ff43f | ||
|
|
8f1ce68e96 | ||
|
|
37208aabd3 | ||
|
|
8c3605c886 | ||
|
|
2706a7171e | ||
|
|
8f3d443247 | ||
|
|
9968d16f21 | ||
|
|
2756c05889 | ||
|
|
8a65c565a5 | ||
|
|
17eeecc526 | ||
|
|
3b245ea201 | ||
|
|
3cd348e8f7 | ||
|
|
6d08695b38 | ||
|
|
66b2c07af4 | ||
|
|
b8a67553d0 | ||
|
|
82eae4324e | ||
|
|
ac9c132c91 | ||
|
|
c2953b9733 | ||
|
|
30de93b81f | ||
|
|
e6f45b63d6 | ||
|
|
c1b689a375 | ||
|
|
c1546cf6a8 | ||
|
|
de96bb763b | ||
|
|
9e62bd1b24 | ||
|
|
54d21a043e | ||
|
|
f593592ff0 | ||
|
|
ed02088c82 | ||
|
|
b3fff51002 | ||
|
|
51884fea2d | ||
|
|
84b0bc6439 | ||
|
|
38d41e2f59 | ||
|
|
23ff9e719f | ||
|
|
7a0a6f9cf1 | ||
|
|
f6960e4deb | ||
|
|
bd63ded1dd | ||
|
|
3c90e909a1 | ||
|
|
70396ffa36 | ||
|
|
56efb2adfe | ||
|
|
868b5ed6a3 | ||
|
|
0a226e8b01 | ||
|
|
7df29b491c | ||
|
|
f0fb5fb346 | ||
|
|
342497b72f | ||
|
|
2b19257c5c | ||
|
|
4ebbdcd00c | ||
|
|
204d8b36df | ||
|
|
8e4e9fc616 | ||
|
|
826d472c07 | ||
|
|
57f416d62d | ||
|
|
a79a547682 | ||
|
|
bd9812cee4 | ||
|
|
2a36894d85 | ||
|
|
c33c4c45dc | ||
|
|
9cd07a0cee | ||
|
|
4f85d85ea6 | ||
|
|
8699003597 | ||
|
|
4cada67b21 | ||
|
|
0a203b54cd | ||
|
|
cf1e9dc425 | ||
|
|
6b8bb0520d | ||
|
|
7759d2dd79 | ||
|
|
73f121cf03 | ||
|
|
91f914f5c0 | ||
|
|
af5613250f | ||
|
|
72da8f3aed | ||
|
|
a8e353fe31 | ||
|
|
8a386b6909 | ||
|
|
83606bbc0f | ||
|
|
caaeded278 | ||
|
|
dcf4a056ee | ||
|
|
f9cec64c2d | ||
|
|
9b1400c23a | ||
|
|
60d77759f2 | ||
|
|
5fc705856d | ||
|
|
0a1adb99e0 | ||
|
|
3eef034a94 | ||
|
|
66d96201cb | ||
|
|
586726fb13 | ||
|
|
656cdfc41c | ||
|
|
7b62b589f7 | ||
|
|
e7884c9a53 | ||
|
|
2f2849dee0 | ||
|
|
ff88393248 | ||
|
|
9ed6e12e7c | ||
|
|
ec5cec619d | ||
|
|
760867b81e | ||
|
|
abeaac0675 | ||
|
|
010866a3bd | ||
|
|
8f9f792930 | ||
|
|
9ccdce9896 | ||
|
|
0dc212f53e | ||
|
|
3cf4a47773 | ||
|
|
bbf59d65ad | ||
|
|
6b738f754e | ||
|
|
83a4e054d1 | ||
|
|
9843776460 | ||
|
|
2626572ddc | ||
|
|
e3af23f209 | ||
|
|
0f16787ef9 | ||
|
|
495a270c99 | ||
|
|
424a25cb91 | ||
|
|
fa0809685e | ||
|
|
188966a94b | ||
|
|
d7b7e0111e | ||
|
|
be11223e4b | ||
|
|
2cbf5147c0 | ||
|
|
5b026df5f4 | ||
|
|
ac842c95d3 | ||
|
|
aaaeec4de7 | ||
|
|
99a7380faf | ||
|
|
f43ffabded | ||
|
|
52c0cfd5d0 | ||
|
|
1caf4a7fbf | ||
|
|
98a976fa72 | ||
|
|
3a883807e5 | ||
|
|
b1b34db0b6 | ||
|
|
4901cd1da1 | ||
|
|
272471e158 | ||
|
|
8f0ce11ff6 | ||
|
|
e8c807b993 | ||
|
|
0b1c80d4d5 | ||
|
|
82ce223c9b | ||
|
|
f190b630b7 | ||
|
|
614a6caee6 | ||
|
|
ddda87373d | ||
|
|
9ceebb9bb2 | ||
|
|
7d2bb6f61b | ||
|
|
c7fe132389 | ||
|
|
404c7a7e88 | ||
|
|
9a2827935f | ||
|
|
55b83fc2b5 | ||
|
|
b89a29b997 | ||
|
|
5aa7c57798 | ||
|
|
e46d1bbbfb | ||
|
|
14abb7d4f6 | ||
|
|
b0c27f5890 | ||
|
|
bd92933030 | ||
|
|
249332a9dd | ||
|
|
1a99ff8ccb | ||
|
|
7373437317 | ||
|
|
4e7364f25b | ||
|
|
ce9fd73fa9 | ||
|
|
9ca1a7ebb6 | ||
|
|
e8457c7abf | ||
|
|
f4ba5a5eb9 | ||
|
|
fc126451a7 | ||
|
|
89ad582af5 | ||
|
|
e66d74764a | ||
|
|
4962fcfcde | ||
|
|
582e45f72f | ||
|
|
6ec89baf26 | ||
|
|
76cd530a0f | ||
|
|
f6a105bcc1 | ||
|
|
75eed82d33 | ||
|
|
fbe307d26a | ||
|
|
c4a0c3d54a | ||
|
|
c79f461e39 | ||
|
|
24cd301fa8 | ||
|
|
a32d609ead | ||
|
|
a0e045dc52 | ||
|
|
3111593ab8 | ||
|
|
75d9ff5fff | ||
|
|
42877b0b6e | ||
|
|
f54b697187 | ||
|
|
e4a001170c | ||
|
|
bb15023b0b | ||
|
|
54531ebf35 | ||
|
|
9257e326f3 | ||
|
|
b59b83a86a | ||
|
|
caec649a5d | ||
|
|
09d0286b1b | ||
|
|
1ebe9766c0 | ||
|
|
3e3b1579c3 | ||
|
|
ec6b380acd | ||
|
|
5ceb515325 | ||
|
|
8938744e3e | ||
|
|
d0f6b47f58 | ||
|
|
a07bcbff2e | ||
|
|
3023634536 | ||
|
|
a11d04e92b | ||
|
|
2140a3d762 | ||
|
|
1f6debc6e0 | ||
|
|
eb5c705083 | ||
|
|
f01044e453 | ||
|
|
8ef3eb85a2 | ||
|
|
d1cd4ef259 | ||
|
|
a8bef0d9c0 | ||
|
|
309a9abb8a | ||
|
|
cc13a7681a | ||
|
|
503a723611 | ||
|
|
998f4a6bad | ||
|
|
1be3613063 | ||
|
|
9ffbe5cd76 | ||
|
|
255d6ea176 | ||
|
|
628e2ef3f4 | ||
|
|
64465a7a31 | ||
|
|
9d79baa96a | ||
|
|
3013269a1c | ||
|
|
bbff3016fe | ||
|
|
e9d190799e | ||
|
|
0465333aa4 | ||
|
|
28406dafa1 | ||
|
|
73a49c6a1f | ||
|
|
4028171f59 | ||
|
|
5d341ba078 | ||
|
|
dfb7cf4888 | ||
|
|
d640c57e29 | ||
|
|
c0d6468347 | ||
|
|
058b61b10c | ||
|
|
aa4d6305af | ||
|
|
407ecab162 | ||
|
|
cbc1f57b21 | ||
|
|
374a9e4337 | ||
|
|
83e2f80d10 | ||
|
|
576211c4ef | ||
|
|
31fc5a31d6 | ||
|
|
eb08943d4f | ||
|
|
c36ed71353 | ||
|
|
fa52dbcf84 | ||
|
|
d412e66cea | ||
|
|
efe50d3313 | ||
|
|
1062ba995e | ||
|
|
7f93bd5b59 | ||
|
|
275d170680 | ||
|
|
6d7e10b804 | ||
|
|
25944864f7 | ||
|
|
5e84a16eba | ||
|
|
244ec163a3 | ||
|
|
dabd2174d4 | ||
|
|
f8d2b41a86 | ||
|
|
902972a1ee | ||
|
|
bddb5fbd2f | ||
|
|
adfd769963 | ||
|
|
c75e70ec74 | ||
|
|
6118183105 | ||
|
|
da755d8411 | ||
|
|
742e03d90f | ||
|
|
744228a03e | ||
|
|
5d1c6f54cd | ||
|
|
0a3dd4600b | ||
|
|
0289891d07 | ||
|
|
87cdf837e6 | ||
|
|
ea4c7d6403 | ||
|
|
2807549564 | ||
|
|
c0fe96cec6 | ||
|
|
8c967ac237 | ||
|
|
c48b46e932 | ||
|
|
49d1af7798 | ||
|
|
d44fd008ae | ||
|
|
c0c9ea3403 | ||
|
|
21359da766 | ||
|
|
e51c79c241 | ||
|
|
e22113c20d | ||
|
|
195bae903f | ||
|
|
5aff21a9a1 | ||
|
|
6f289d1b8e | ||
|
|
71b21aec59 | ||
|
|
42a87d4eaa | ||
|
|
51d125642f | ||
|
|
ddebf2e1cb | ||
|
|
7f3e8f1fb1 | ||
|
|
ab7dbcd2e4 | ||
|
|
7e5cbddf5d | ||
|
|
44f517c20d | ||
|
|
7bf8c6e3a1 | ||
|
|
31ea683335 | ||
|
|
29d8f1fd27 | ||
|
|
a6c472bb2a | ||
|
|
b880d419a3 | ||
|
|
a2ff87af8a | ||
|
|
5b9c577380 | ||
|
|
4775e124db | ||
|
|
c243158d7c | ||
|
|
8afc3f46f6 | ||
|
|
8b5dc54397 | ||
|
|
1dbb34df9f | ||
|
|
9383f0bc77 | ||
|
|
900a03c172 | ||
|
|
13306b71e0 | ||
|
|
8719a23de4 | ||
|
|
7e0b5236af | ||
|
|
c7798b3254 | ||
|
|
7d668550f5 | ||
|
|
c945eaf804 | ||
|
|
1bfe0e0874 | ||
|
|
153c6a7b01 | ||
|
|
30a83fa382 | ||
|
|
c0bcefe0bf | ||
|
|
5d16a77891 | ||
|
|
cd01a01894 | ||
|
|
df36bb9f35 | ||
|
|
8a3f5e423b | ||
|
|
177605aaf8 | ||
|
|
030893e125 | ||
|
|
b2ab8ab54c | ||
|
|
12eb1b96de | ||
|
|
cff7d4bad4 | ||
|
|
a31c616a21 | ||
|
|
3d2b4dcc26 | ||
|
|
c7d24ee290 | ||
|
|
06c958f081 | ||
|
|
b8efe585d5 | ||
|
|
e7eb2152cc | ||
|
|
e1a8641399 | ||
|
|
cffac62e68 | ||
|
|
7a8c0572e9 | ||
|
|
5596d5f8b2 | ||
|
|
06fd02cd61 | ||
|
|
6b9d1047cf | ||
|
|
a7b3fd72ca | ||
|
|
dd3deb2358 | ||
|
|
c99fce3183 | ||
|
|
4db6227d84 | ||
|
|
30e1d409dd | ||
|
|
ff8a6f1d57 | ||
|
|
9b5d6f8df0 | ||
|
|
1e8919c6e6 | ||
|
|
1ee7b7b856 | ||
|
|
3e55581bf7 | ||
|
|
dfbe1418d4 | ||
|
|
7671fca373 | ||
|
|
c01dde3fb2 | ||
|
|
bb17adeda2 | ||
|
|
9f743f1c59 | ||
|
|
ee85c929da | ||
|
|
6f9c660082 | ||
|
|
e02bb7f5a1 | ||
|
|
9aaaa044da | ||
|
|
54da8444df | ||
|
|
063e1229bc | ||
|
|
eacd70329a | ||
|
|
3a1d5d068c | ||
|
|
f2749d884f | ||
|
|
bdea61f93b | ||
|
|
6006e87c5e | ||
|
|
1e8161b24e | ||
|
|
a3e6d1b611 | ||
|
|
1a93999cc0 | ||
|
|
53684adbdd | ||
|
|
d3caecc551 | ||
|
|
004ddb3e66 | ||
|
|
20894124e6 | ||
|
|
22c4e3b8c2 | ||
|
|
c2a4629c62 | ||
|
|
c0f4fe6867 | ||
|
|
f2c95568bd | ||
|
|
358aab85e7 | ||
|
|
829274cd5e | ||
|
|
c522f5094a | ||
|
|
29b6772721 | ||
|
|
695b5b50ab | ||
|
|
42af7b2d8b | ||
|
|
079a9b5204 | ||
|
|
e5048fd3ac | ||
|
|
18eaea95fa | ||
|
|
a4a0a56448 | ||
|
|
40ed2f39a4 | ||
|
|
2859b037aa | ||
|
|
bbb7878e0a | ||
|
|
fc438866ec | ||
|
|
2da2f498a2 | ||
|
|
29dffffe1b | ||
|
|
1ecaad5413 | ||
|
|
cd56d672c0 | ||
|
|
68aed3c190 | ||
|
|
f16ecd837e | ||
|
|
68fcc03d5c | ||
|
|
bfcae0e754 | ||
|
|
1b2c8880ee | ||
|
|
fa7d58d01a | ||
|
|
ec558f377a | ||
|
|
186eba7197 | ||
|
|
d28ba3c628 | ||
|
|
a026cb84d1 | ||
|
|
3acc3eeabd | ||
|
|
a92d2af7f8 | ||
|
|
adcb683458 | ||
|
|
939b29bf60 | ||
|
|
e4925613b3 | ||
|
|
2f6a6e4628 | ||
|
|
7938ea34d0 | ||
|
|
ed94e36f7a | ||
|
|
1c3a8df136 | ||
|
|
9f254b22ee | ||
|
|
753f8ce84e | ||
|
|
acf3b549de | ||
|
|
669f6dcf98 | ||
|
|
e4f7c4aab1 | ||
|
|
5836d55e21 | ||
|
|
e17bf1a1f4 | ||
|
|
acb253ae9c | ||
|
|
cc0aaa301f | ||
|
|
4256316045 | ||
|
|
78ab0c9400 | ||
|
|
944a670af0 | ||
|
|
e4e517b334 | ||
|
|
ccd7f1ee4b | ||
|
|
9db7ed88aa | ||
|
|
a5e7497f56 | ||
|
|
754f302493 | ||
|
|
7783543153 | ||
|
|
b02f92b3ea | ||
|
|
47b3ef29be | ||
|
|
1eb615f97c | ||
|
|
cfa904a0a0 | ||
|
|
2d34458d10 | ||
|
|
e39713c4fd | ||
|
|
320b734da8 | ||
|
|
887848625c | ||
|
|
685f06582d | ||
|
|
a3c21dba32 | ||
|
|
9744cde8aa | ||
|
|
0ba8c9ec00 | ||
|
|
0764c603b4 | ||
|
|
2d4f7a6946 | ||
|
|
5346eec84d | ||
|
|
b704dd967b | ||
|
|
84ace24b35 | ||
|
|
ea42f76cff | ||
|
|
dd147dd040 | ||
|
|
9a79136d15 | ||
|
|
b722dd016a | ||
|
|
054853dc06 | ||
|
|
e5ceef52c6 | ||
|
|
92747e8efc | ||
|
|
6171de54f9 | ||
|
|
287ef31081 | ||
|
|
8121f291c3 | ||
|
|
b721b5fcff | ||
|
|
521dfe0337 | ||
|
|
7dc78b7837 | ||
|
|
1a804ed97b | ||
|
|
6636b9d56c | ||
|
|
325c6cc805 | ||
|
|
6a6e205973 | ||
|
|
46ec25d286 | ||
|
|
6e33a22676 | ||
|
|
6e81de9e44 | ||
|
|
03f7bbc3a5 | ||
|
|
4354bc9108 | ||
|
|
b8fcc2ff0c | ||
|
|
55b7ae10a7 | ||
|
|
6d2a6c98d1 | ||
|
|
05998b5d05 | ||
|
|
1063f3fcda | ||
|
|
93c5e4637b | ||
|
|
073c2b5754 | ||
|
|
ef41d74b82 | ||
|
|
84b3f38810 | ||
|
|
2288f38a11 | ||
|
|
dbc4e06657 | ||
|
|
2433777a76 | ||
|
|
bb7001f5f2 | ||
|
|
9b5aaa40de | ||
|
|
96d74f48f4 | ||
|
|
c8a99c247c | ||
|
|
9f50a37e40 | ||
|
|
54c9e39654 | ||
|
|
3386a1e9f9 | ||
|
|
b413f2eafe | ||
|
|
f07af25a6a | ||
|
|
14e65c4601 | ||
|
|
b5c2fb0259 | ||
|
|
92d98db7bb | ||
|
|
9caafedb8d | ||
|
|
e6f7ef604a | ||
|
|
0eb8d3e47c | ||
|
|
072e30498b | ||
|
|
d6e73577af | ||
|
|
a81f98be8e | ||
|
|
0980e35c29 | ||
|
|
336c2a3aff | ||
|
|
e3055bc740 | ||
|
|
9406e3dbfb | ||
|
|
5307b7e1b1 | ||
|
|
f18a8f5b31 | ||
|
|
cfe99c4b72 | ||
|
|
0d439c0f55 | ||
|
|
6288a96a8b | ||
|
|
819b6f6ccf | ||
|
|
4bc06aa8cd | ||
|
|
7b64425c24 | ||
|
|
44c9d6a22b | ||
|
|
c750447d62 | ||
|
|
b1c99d82fd | ||
|
|
10db79f636 | ||
|
|
059ec8f3f2 | ||
|
|
2c5508febd | ||
|
|
905fff041b | ||
|
|
cd27a64f4e | ||
|
|
d1b7a5c2e4 | ||
|
|
4b81b086db | ||
|
|
0db42c28a7 | ||
|
|
0eca6ce2e3 | ||
|
|
34685bf80e | ||
|
|
271dc2a6a9 | ||
|
|
bf0376f73f | ||
|
|
cf8656eb2d | ||
|
|
20ce29b033 | ||
|
|
4bd93a680e | ||
|
|
c9bf7f424d | ||
|
|
4cde2e1a78 | ||
|
|
15625b5f8c | ||
|
|
e5f9da1f2b | ||
|
|
ab33c46c87 | ||
|
|
48c045d381 | ||
|
|
2b385ead7f | ||
|
|
0fcc9f3df6 | ||
|
|
b251202804 | ||
|
|
6967010281 | ||
|
|
7e0846e66a | ||
|
|
4e3daad96d | ||
|
|
37fb3da5db | ||
|
|
762f48957c | ||
|
|
c1af7b8783 | ||
|
|
f89084677d | ||
|
|
0716084bbb | ||
|
|
a6c946e6c9 | ||
|
|
3f6e088faa | ||
|
|
9abdd5813b | ||
|
|
f33ea36e6f | ||
|
|
8788e0a9c9 | ||
|
|
b1c1cb4b9b | ||
|
|
982d4ac472 | ||
|
|
b7a8d667b9 | ||
|
|
8f8729df05 | ||
|
|
e928d281dd | ||
|
|
625583f5ab | ||
|
|
ab54553dd2 | ||
|
|
47bf7b1325 | ||
|
|
145d75f579 | ||
|
|
01d976d7f7 | ||
|
|
095e3720ab | ||
|
|
d62a37fe1f | ||
|
|
5323f2fc31 | ||
|
|
5539cb0d08 | ||
|
|
76e80106d6 | ||
|
|
9ab7b9a033 | ||
|
|
fe97d6a349 | ||
|
|
2242c2afe8 | ||
|
|
ec25fb5c36 | ||
|
|
ce25f5cadd | ||
|
|
1099f40f19 | ||
|
|
70368b3f1e | ||
|
|
0181ebad45 | ||
|
|
e158e3f13c | ||
|
|
b1bbded23c | ||
|
|
b77d9d3738 | ||
|
|
d0b2421752 | ||
|
|
96b65a7c60 | ||
|
|
177c90093e | ||
|
|
28ee091107 | ||
|
|
64c71d8e6d | ||
|
|
9ce0c94e17 | ||
|
|
08c3372635 | ||
|
|
2fafc70b69 | ||
|
|
0e62ebe3a2 | ||
|
|
1cc4d20b89 | ||
|
|
af4889894a | ||
|
|
429a5e1ea3 | ||
|
|
4ef860eb07 | ||
|
|
b59ebf30c6 | ||
|
|
a1ae8d54a6 | ||
|
|
8155207bea | ||
|
|
337d2cfa6d | ||
|
|
df2229782b | ||
|
|
5920552649 | ||
|
|
b4827fcb00 | ||
|
|
63983ccb65 | ||
|
|
eac7e2b749 | ||
|
|
65a365bca1 | ||
|
|
fecd0e11eb | ||
|
|
51ad526cfc | ||
|
|
10a062017d | ||
|
|
0d351794db | ||
|
|
067e3ffced | ||
|
|
50d55fae56 | ||
|
|
ce63628d3d | ||
|
|
13df7f90f6 | ||
|
|
f5099b873d | ||
|
|
70eb38895d | ||
|
|
7aea9fa1d2 | ||
|
|
5d30be31e0 | ||
|
|
7abe66e3de | ||
|
|
49ef5e5e64 | ||
|
|
c2266bc105 | ||
|
|
a813e219e6 | ||
|
|
1c1fb20546 | ||
|
|
65feb60bb8 | ||
|
|
f7492c7dc7 | ||
|
|
dfc805b89b | ||
|
|
75defc13a0 | ||
|
|
7d4888bb77 | ||
|
|
1a34029171 | ||
|
|
f6ad4652e4 | ||
|
|
1e25604b0b | ||
|
|
3a43ffa641 | ||
|
|
8f6bcf3d98 | ||
|
|
0fd9753681 | ||
|
|
76a04dfe25 | ||
|
|
16317182e3 | ||
|
|
6bcdf64f67 | ||
|
|
d276a07a71 | ||
|
|
f3b59b342a | ||
|
|
4a0f1f22ba | ||
|
|
0c85e7604c | ||
|
|
8f6a46e2d8 | ||
|
|
74b2c18296 | ||
|
|
b12d0b6424 | ||
|
|
60ddf0400e | ||
|
|
669d3484c0 | ||
|
|
5420ad97a3 | ||
|
|
36822926af | ||
|
|
eef8f2e781 | ||
|
|
31ac667623 | ||
|
|
868ceb25bf | ||
|
|
ee3ab94774 | ||
|
|
1c47877a8c | ||
|
|
84698462f3 | ||
|
|
da7dc793e7 | ||
|
|
044ee83fbc | ||
|
|
aea324c4a8 | ||
|
|
4d05b20830 | ||
|
|
276928951c | ||
|
|
9486654e77 | ||
|
|
2a2b4cbb06 | ||
|
|
3ba4a8cdd8 | ||
|
|
8820dabab9 | ||
|
|
f9d89301df | ||
|
|
7edb93d3ad | ||
|
|
5c5d9974e1 | ||
|
|
b0bf4f8f8e | ||
|
|
04ea03caf6 | ||
|
|
cf0841bdcc | ||
|
|
cc4f5f66d8 | ||
|
|
e6d75ee7c4 | ||
|
|
61986fc98c | ||
|
|
0e009c7c12 | ||
|
|
425613ee42 | ||
|
|
679316946e | ||
|
|
8bb305038b | ||
|
|
fbe104d254 | ||
|
|
cb44cb0ee2 | ||
|
|
2163f64877 | ||
|
|
a14d958ef0 | ||
|
|
c65ef12783 | ||
|
|
8eb1727c76 | ||
|
|
fafe24295a | ||
|
|
d900a6c145 | ||
|
|
03df2fa3e9 | ||
|
|
69a4b99d70 | ||
|
|
39d95b2fd2 | ||
|
|
1e3b29de2e | ||
|
|
d5186f160d | ||
|
|
5d7dbd15c7 | ||
|
|
12d5fe0afe | ||
|
|
3df1cc9038 | ||
|
|
d46152b73e | ||
|
|
9fc6e0d6a2 | ||
|
|
4994d0597f | ||
|
|
76b46d7957 | ||
|
|
0a369c548b | ||
|
|
9a738ba413 | ||
|
|
a442536246 | ||
|
|
f85b6fde7b | ||
|
|
8dc6a5109a | ||
|
|
235d9d4ab5 | ||
|
|
3572de058b | ||
|
|
93068aff1b | ||
|
|
49e7d75ce5 | ||
|
|
6aa1ecd1a8 | ||
|
|
b442fbb19c | ||
|
|
46fc4f0c25 | ||
|
|
155de6f2b9 | ||
|
|
459af7ab1b | ||
|
|
2bd408a274 | ||
|
|
bc1c5a59f8 | ||
|
|
49cecdc75d | ||
|
|
2a6aeae763 | ||
|
|
f295e1da31 | ||
|
|
1981859343 | ||
|
|
9de237e1a3 | ||
|
|
77b412c1e8 | ||
|
|
a31529bb79 | ||
|
|
00bc1a169e | ||
|
|
3e98cac397 | ||
|
|
8cd0777683 | ||
|
|
8bac77c2ab | ||
|
|
3312e1b20b | ||
|
|
d55e2a2647 | ||
|
|
e87d9cd1b5 | ||
|
|
5dda95385d | ||
|
|
d60bdb561e | ||
|
|
fab89beba0 | ||
|
|
1cb9ed9c01 | ||
|
|
00b7f2e02f | ||
|
|
4691302a78 | ||
|
|
d8a32630fb | ||
|
|
29b6bd8aad | ||
|
|
c2516e7453 | ||
|
|
1fd8c3c068 | ||
|
|
314757a235 | ||
|
|
5b613903e5 | ||
|
|
b2caad9b4b | ||
|
|
4b066e908c | ||
|
|
041e443619 | ||
|
|
999bd84a86 | ||
|
|
2a894fb5f6 | ||
|
|
79bf5c2d6b | ||
|
|
98298a3b2d | ||
|
|
71454c6400 | ||
|
|
5e2e316474 | ||
|
|
6bca211267 | ||
|
|
f8cbc0a12d | ||
|
|
9708c89772 | ||
|
|
29492bfdc8 | ||
|
|
d2e05f03cc | ||
|
|
01bf7b3bd3 | ||
|
|
db790ab20c | ||
|
|
71c19a1fbc | ||
|
|
73e9b6e804 | ||
|
|
199e9fc81d | ||
|
|
a9591aad1b | ||
|
|
0168f444d9 | ||
|
|
4659ab0649 | ||
|
|
49700ffb9f | ||
|
|
6c6062d5a8 | ||
|
|
01e8b198c0 | ||
|
|
90b070296b | ||
|
|
9302c0a98e | ||
|
|
6d98efb1e4 | ||
|
|
04e6e1964d | ||
|
|
a02235e894 | ||
|
|
69751ab8c5 | ||
|
|
c4fdd0db8a | ||
|
|
a45dbba4b1 | ||
|
|
89e409157f | ||
|
|
b64ad56caa | ||
|
|
498fd3fe62 | ||
|
|
0d93df7d59 | ||
|
|
725361c949 | ||
|
|
8510f04651 | ||
|
|
ddf7f0d0e6 | ||
|
|
cfbc906cb3 | ||
|
|
5915ec68bc | ||
|
|
ffae162955 | ||
|
|
4aaeed8c88 | ||
|
|
33ac728af8 | ||
|
|
7846ffa818 | ||
|
|
2e8d02c0ab | ||
|
|
1cb45f35be | ||
|
|
ca47a6ca51 | ||
|
|
1cee930055 | ||
|
|
196d394ebd | ||
|
|
883af122f1 | ||
|
|
0cb1b6a74f | ||
|
|
59f3a1894a | ||
|
|
f076d0e00e | ||
|
|
697ec9736e | ||
|
|
793c9a276b | ||
|
|
ae48671168 | ||
|
|
e48e966794 | ||
|
|
6f3560c680 | ||
|
|
146caed7aa | ||
|
|
95b4c55ea2 | ||
|
|
8cd90e5c2d | ||
|
|
5d02410e1e | ||
|
|
09da1d1af0 | ||
|
|
e1c7993731 | ||
|
|
84aea98448 | ||
|
|
93039df3ef | ||
|
|
f9451feb18 | ||
|
|
35e46654df | ||
|
|
df32d3f195 | ||
|
|
4457207a87 | ||
|
|
fa5f4d209a | ||
|
|
aecf939366 | ||
|
|
2c6e244b3c | ||
|
|
6243e85b6f | ||
|
|
3f194f6584 | ||
|
|
47dc4d39eb | ||
|
|
5f184b278f | ||
|
|
854e586f40 | ||
|
|
6044275346 | ||
|
|
e10f6a2d58 | ||
|
|
c4eab0de2b | ||
|
|
cf961a7c92 | ||
|
|
8f820e4bb8 | ||
|
|
e23e552084 | ||
|
|
d964e82fdc | ||
|
|
f6f7b46fa0 | ||
|
|
e45151cdb8 | ||
|
|
e8cf19caf4 | ||
|
|
aebdc60c7e | ||
|
|
e5f2ed4920 | ||
|
|
5506175bff | ||
|
|
e2c0a702b1 | ||
|
|
398f685b08 | ||
|
|
2e0ab52a77 | ||
|
|
a2a65b7553 | ||
|
|
881c7984aa | ||
|
|
7de0a5414a | ||
|
|
98143d13f8 | ||
|
|
a25a86e2d6 | ||
|
|
0833f06439 | ||
|
|
7e9a3d649a | ||
|
|
d6aa10164a | ||
|
|
198fabdd2d | ||
|
|
ba47455a0c | ||
|
|
e65e2b8706 | ||
|
|
e28c8a16eb | ||
|
|
76ab5da49b | ||
|
|
3d6d38c4fb | ||
|
|
ea6698e27a | ||
|
|
b611ddeb6e | ||
|
|
bf90dc075e | ||
|
|
99d5f06383 | ||
|
|
b386933a04 | ||
|
|
76447d65a0 | ||
|
|
08099f93a1 | ||
|
|
cbabf5650d | ||
|
|
82f20f102e | ||
|
|
2b2656c2a3 | ||
|
|
330c0f055e | ||
|
|
d272006873 | ||
|
|
5f7f718fe4 | ||
|
|
13abd175aa | ||
|
|
090ec46ca4 | ||
|
|
5b349c1df8 | ||
|
|
7310b0feda | ||
|
|
7e0ebb8c5b | ||
|
|
0734edf6f0 | ||
|
|
4656275ee0 | ||
|
|
076a47de1c | ||
|
|
2bd0c03f70 | ||
|
|
322d2ad549 | ||
|
|
e18eb5f463 | ||
|
|
fb4ef6b993 | ||
|
|
863b7b58c5 | ||
|
|
3bac5e7e43 | ||
|
|
846b40de9f | ||
|
|
d48bfe81ac | ||
|
|
4d03856c26 | ||
|
|
ed0f4f994c | ||
|
|
f9eed2d5b2 | ||
|
|
6b5d3978cf | ||
|
|
381e4abd17 | ||
|
|
7ab42d9889 | ||
|
|
b3c3c5579b | ||
|
|
2d20fe20c4 | ||
|
|
c4e4eb27fb | ||
|
|
96eaf311d0 |
9
.gitattributes
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
# Set the default behavior, in case people don't have core.autocrlf set.
|
||||
* text=auto
|
||||
|
||||
# Explicitly declare text files you want to always be normalized and converted
|
||||
# to native line endings on checkout.
|
||||
*.py text
|
||||
*.yml text
|
||||
*.md text
|
||||
*.txt text
|
||||
92
.github/CODE_OF_CONDUCT.md
vendored
@@ -1,46 +1,46 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [https://contributor-covenant.org/version/1/4][version]
|
||||
|
||||
[homepage]: https://contributor-covenant.org
|
||||
[version]: https://contributor-covenant.org/version/1/4/
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [https://contributor-covenant.org/version/1/4][version]
|
||||
|
||||
[homepage]: https://contributor-covenant.org
|
||||
[version]: https://contributor-covenant.org/version/1/4/
|
||||
|
||||
394
.github/CONTRIBUTING.md
vendored
@@ -1,197 +1,197 @@
|
||||
# Contributing to Capa
|
||||
|
||||
First off, thanks for taking the time to contribute!
|
||||
|
||||
The following is a set of guidelines for contributing to capa and its packages, which are hosted in the [FireEye Organization](https://github.com/fireeye) on GitHub. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request.
|
||||
|
||||
#### Table Of Contents
|
||||
|
||||
[Code of Conduct](#code-of-conduct)
|
||||
|
||||
[What should I know before I get started?](#what-should-i-know-before-i-get-started)
|
||||
* [Capa and its Repositories](#capa-and-its-repositories)
|
||||
* [Capa Design Decisions](#design-decisions)
|
||||
|
||||
[How Can I Contribute?](#how-can-i-contribute)
|
||||
* [Reporting Bugs](#reporting-bugs)
|
||||
* [Suggesting Enhancements](#suggesting-enhancements)
|
||||
* [Your First Code Contribution](#your-first-code-contribution)
|
||||
* [Pull Requests](#pull-requests)
|
||||
|
||||
[Styleguides](#styleguides)
|
||||
* [Git Commit Messages](#git-commit-messages)
|
||||
* [Python Styleguide](#python-styleguide)
|
||||
* [Rules Styleguide](#rules-styleguide)
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
This project and everyone participating in it is governed by the [Capa Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to the maintainers.
|
||||
|
||||
## What should I know before I get started?
|
||||
|
||||
### Capa and its repositories
|
||||
|
||||
We host the capa project as three Github repositories:
|
||||
- [capa](https://github.com/fireeye/capa)
|
||||
- [capa-rules](https://github.com/fireeye/capa-rules)
|
||||
- [capa-testfiles](https://github.com/fireeye/capa-testfiles)
|
||||
|
||||
The command line tools, logic engine, and other Python source code are found in the `capa` repository.
|
||||
This is the repository to fork when you want to enhance the features, performance, or user interface of capa.
|
||||
Do *not* push rules directly to this repository, instead...
|
||||
|
||||
The standard rules contributed by the community are found in the `capa-rules` repository.
|
||||
When you have an idea for a new rule, you should open a PR against `capa-rules`.
|
||||
We keep `capa` and `capa-rules` separate to distinguish where ideas, bugs, and discussions should happen.
|
||||
If you're writing yaml it probably goes in `capa-rules` and if you're writing Python it probably goes in `capa`.
|
||||
Also, we encourage users to develop their own rule repositories, so we treat our default set of rules in the same way.
|
||||
|
||||
Test fixtures, such as malware samples and analysis workspaces, are found in the `capa-testfiles` repository.
|
||||
These are files you'll need in order to run the linter (in `--thorough` mode) and full test suites;
|
||||
however, they take up a lot of space (1GB+), so by keeping `capa-testfiles` separate,
|
||||
a shallow checkout of `capa` and `capa-rules` doesn't take much bandwidth.
|
||||
|
||||
### Design Decisions
|
||||
|
||||
When we make a significant decision in how we maintain the project and what we can or cannot support,
|
||||
we will document it in the [capa issues tracker](https://github.com/fireeye/capa/issues).
|
||||
This is the best place review our discussions about what/how/why we do things in the project.
|
||||
If you have a question, check to see if it is documented there.
|
||||
If it is *not* documented there, or you can't find an answer, please open a issue.
|
||||
We'll link to existing issues when appropriate to keep discussions in one place.
|
||||
|
||||
## How Can I Contribute?
|
||||
|
||||
### Reporting Bugs
|
||||
|
||||
This section guides you through submitting a bug report for capa.
|
||||
Following these guidelines helps maintainers and the community understand your report, reproduce the behavior, and find related reports.
|
||||
|
||||
Before creating bug reports, please check [this list](#before-submitting-a-bug-report)
|
||||
as you might find out that you don't need to create one.
|
||||
When you are creating a bug report, please [include as many details as possible](#how-do-i-submit-a-good-bug-report).
|
||||
Fill out [the required template](./ISSUE_TEMPLATE/bug_report.md),
|
||||
the information it asks for helps us resolve issues faster.
|
||||
|
||||
> **Note:** If you find a **Closed** issue that seems like it is the same thing that you're experiencing, open a new issue and include a link to the original issue in the body of your new one.
|
||||
|
||||
#### Before Submitting A Bug Report
|
||||
|
||||
* **Determine [which repository the problem should be reported in](#capa-and-its-repositories)**.
|
||||
* **Perform a [cursory search](https://github.com/fireeye/capa/issues?q=is%3Aissue)** to see if the problem has already been reported. If it has **and the issue is still open**, add a comment to the existing issue instead of opening a new one.
|
||||
|
||||
#### How Do I Submit A (Good) Bug Report?
|
||||
|
||||
Bugs are tracked as [GitHub issues](https://guides.github.com/features/issues/).
|
||||
After you've determined [which repository](#capa-and-its-repositories) your bug is related to,
|
||||
create an issue on that repository and provide the following information by filling in
|
||||
[the template](./ISSUE_TEMPLATE/bug_report.md).
|
||||
|
||||
Explain the problem and include additional details to help maintainers reproduce the problem:
|
||||
|
||||
* **Use a clear and descriptive title** for the issue to identify the problem.
|
||||
* **Describe the exact steps which reproduce the problem** in as many details as possible. For example, start by explaining how you started capa, e.g. which command exactly you used in the terminal, or how you started capa otherwise.
|
||||
* **Provide specific examples to demonstrate the steps**. Include links to files or GitHub projects, or copy/pasteable snippets, which you use in those examples. If you're providing snippets in the issue, use [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines).
|
||||
* **Describe the behavior you observed after following the steps** and point out what exactly is the problem with that behavior.
|
||||
* **Explain which behavior you expected to see instead and why.**
|
||||
* **Include screenshots and animated GIFs** which show you following the described steps and clearly demonstrate the problem. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux.
|
||||
* **If you're reporting that capa crashed**, include the stack trace from the terminal. Include the stack trace in the issue in a [code block](https://help.github.com/articles/markdown-basics/#multiple-lines), a [file attachment](https://help.github.com/articles/file-attachments-on-issues-and-pull-requests/), or put it in a [gist](https://gist.github.com/) and provide link to that gist.
|
||||
* **If the problem wasn't triggered by a specific action**, describe what you were doing before the problem happened and share more information using the guidelines below.
|
||||
|
||||
Provide more context by answering these questions:
|
||||
|
||||
* **Did the problem start happening recently** (e.g. after updating to a new version of capa) or was this always a problem?
|
||||
* If the problem started happening recently, **can you reproduce the problem in an older version of capa?** What's the most recent version in which the problem doesn't happen? You can download older versions of capa from [the releases page](https://github.com/fireeye/capa/releases).
|
||||
* **Can you reliably reproduce the issue?** If not, provide details about how often the problem happens and under which conditions it normally happens.
|
||||
* If the problem is related to working with files (e.g. opening and editing files), **does the problem happen for all files and projects or only some?** Does the problem happen only when working with local or remote files (e.g. on network drives), with files of a specific type (e.g. only JavaScript or Python files), with large files or files with very long lines, or with files in a specific encoding? Is there anything else special about the files you are using?
|
||||
|
||||
Include details about your configuration and environment:
|
||||
|
||||
* **Which version of capa are you using?** You can get the exact version by running `capa --version` in your terminal.
|
||||
* **What's the name and version of the OS you're using**?
|
||||
|
||||
### Suggesting Enhancements
|
||||
|
||||
This section guides you through submitting an enhancement suggestion for capa, including completely new features and minor improvements to existing functionality. Following these guidelines helps maintainers and the community understand your suggestion and find related suggestions.
|
||||
|
||||
Before creating enhancement suggestions, please check [this list](#before-submitting-an-enhancement-suggestion) as you might find out that you don't need to create one. When you are creating an enhancement suggestion, please [include as many details as possible](#how-do-i-submit-a-good-enhancement-suggestion). Fill in [the template](./ISSUE_TEMPLATE/feature_request.md), including the steps that you imagine you would take if the feature you're requesting existed.
|
||||
|
||||
#### Before Submitting An Enhancement Suggestion
|
||||
|
||||
* **Determine [which repository the enhancement should be suggested in](#capa-and-its-repositories).**
|
||||
* **Perform a [cursory search](https://github.com/fireeye/capa/issues?q=is%3Aissue)** to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one.
|
||||
|
||||
#### How Do I Submit A (Good) Enhancement Suggestion?
|
||||
|
||||
Enhancement suggestions are tracked as [GitHub issues](https://guides.github.com/features/issues/). After you've determined [which repository](#capa-and-its-repositories) your enhancement suggestion is related to, create an issue on that repository and provide the following information:
|
||||
|
||||
* **Use a clear and descriptive title** for the issue to identify the suggestion.
|
||||
* **Provide a step-by-step description of the suggested enhancement** in as many details as possible.
|
||||
* **Provide specific examples to demonstrate the steps**. Include copy/pasteable snippets which you use in those examples, as [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines).
|
||||
* **Describe the current behavior** and **explain which behavior you expected to see instead** and why.
|
||||
* **Include screenshots and animated GIFs** which help you demonstrate the steps or point out the part of capa which the suggestion is related to. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux.
|
||||
* **Explain why this enhancement would be useful** to most capa users and isn't something that can or should be implemented as an external tool that uses capa as a library.
|
||||
* **Specify which version of capa you're using.** You can get the exact version by running `capa --version` in your terminal.
|
||||
* **Specify the name and version of the OS you're using.**
|
||||
|
||||
### Your First Code Contribution
|
||||
|
||||
Unsure where to begin contributing to capa? You can start by looking through these `good-first-issue` and `rule-idea` issues:
|
||||
|
||||
* [good-first-issue](https://github.com/fireeye/capa/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) - issues which should only require a few lines of code, and a test or two.
|
||||
* [rule-idea](https://github.com/fireeye/capa-rules/issues?q=is%3Aissue+is%3Aopen+label%3A%22rule+idea%22) - issues that describe potential new rule ideas.
|
||||
|
||||
Both issue lists are sorted by total number of comments. While not perfect, number of comments is a reasonable proxy for impact a given change will have.
|
||||
|
||||
#### Local development
|
||||
|
||||
capa and all its resources can be developed locally.
|
||||
For instructions on how to do this, see the "Method 3" section of the [installation guide](https://github.com/fireeye/capa/blob/master/doc/installation.md).
|
||||
|
||||
### Pull Requests
|
||||
|
||||
The process described here has several goals:
|
||||
|
||||
- Maintain capa's quality
|
||||
- Fix problems that are important to users
|
||||
- Engage the community in working toward the best possible capa
|
||||
- Enable a sustainable system for capa's maintainers to review contributions
|
||||
|
||||
Please follow these steps to have your contribution considered by the maintainers:
|
||||
|
||||
1. Follow all instructions in [the template](PULL_REQUEST_TEMPLATE.md)
|
||||
2. Follow the [styleguides](#styleguides)
|
||||
3. After you submit your pull request, verify that all [status checks](https://help.github.com/articles/about-status-checks/) are passing <details><summary>What if the status checks are failing? </summary>If a status check is failing, and you believe that the failure is unrelated to your change, please leave a comment on the pull request explaining why you believe the failure is unrelated. A maintainer will re-run the status check for you. If we conclude that the failure was a false positive, then we will open an issue to track that problem with our status check suite.</details>
|
||||
|
||||
While the prerequisites above must be satisfied prior to having your pull request reviewed, the reviewer(s) may ask you to complete additional design work, tests, or other changes before your pull request can be ultimately accepted.
|
||||
|
||||
## Styleguides
|
||||
|
||||
### Git Commit Messages
|
||||
|
||||
* Use the present tense ("Add feature" not "Added feature")
|
||||
* Use the imperative mood ("Move cursor to..." not "Moves cursor to...")
|
||||
* Prefix the first line with the component in question ("rules: ..." or "render: ...")
|
||||
* Reference issues and pull requests liberally after the first line
|
||||
|
||||
### Python Styleguide
|
||||
|
||||
All Python code must adhere to the style guide used by capa:
|
||||
|
||||
1. [PEP8](https://www.python.org/dev/peps/pep-0008/), with clarifications from
|
||||
2. [Willi's style guide](https://docs.google.com/document/d/1iRpeg-w4DtibwytUyC_dDT7IGhNGBP25-nQfuBa-Fyk/edit?usp=sharing), formatted with
|
||||
3. [isort](https://pypi.org/project/isort/) (with line width 120 and ordered by line length), and formatted with
|
||||
4. [black](https://github.com/psf/black) (with line width 120), and formatted with
|
||||
5. [dos2unix](https://linux.die.net/man/1/dos2unix)
|
||||
|
||||
Our CI pipeline will reformat and enforce the Python styleguide.
|
||||
|
||||
### Rules Styleguide
|
||||
|
||||
All (non-nursery) capa rules must:
|
||||
|
||||
1. pass the [linter](https://github.com/fireeye/capa/blob/master/scripts/lint.py), and
|
||||
2. be formatted with [capafmt](https://github.com/fireeye/capa/blob/master/scripts/capafmt.py)
|
||||
|
||||
This ensures that all rules meet the same minimum level of quality and are structured in a consistent way.
|
||||
Our CI pipeline will reformat and enforce the capa rules styleguide.
|
||||
# Contributing to Capa
|
||||
|
||||
First off, thanks for taking the time to contribute!
|
||||
|
||||
The following is a set of guidelines for contributing to capa and its packages, which are hosted in the [FireEye Organization](https://github.com/fireeye) on GitHub. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request.
|
||||
|
||||
#### Table Of Contents
|
||||
|
||||
[Code of Conduct](#code-of-conduct)
|
||||
|
||||
[What should I know before I get started?](#what-should-i-know-before-i-get-started)
|
||||
* [Capa and its Repositories](#capa-and-its-repositories)
|
||||
* [Capa Design Decisions](#design-decisions)
|
||||
|
||||
[How Can I Contribute?](#how-can-i-contribute)
|
||||
* [Reporting Bugs](#reporting-bugs)
|
||||
* [Suggesting Enhancements](#suggesting-enhancements)
|
||||
* [Your First Code Contribution](#your-first-code-contribution)
|
||||
* [Pull Requests](#pull-requests)
|
||||
|
||||
[Styleguides](#styleguides)
|
||||
* [Git Commit Messages](#git-commit-messages)
|
||||
* [Python Styleguide](#python-styleguide)
|
||||
* [Rules Styleguide](#rules-styleguide)
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
This project and everyone participating in it is governed by the [Capa Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to the maintainers.
|
||||
|
||||
## What should I know before I get started?
|
||||
|
||||
### Capa and its repositories
|
||||
|
||||
We host the capa project as three Github repositories:
|
||||
- [capa](https://github.com/fireeye/capa)
|
||||
- [capa-rules](https://github.com/fireeye/capa-rules)
|
||||
- [capa-testfiles](https://github.com/fireeye/capa-testfiles)
|
||||
|
||||
The command line tools, logic engine, and other Python source code are found in the `capa` repository.
|
||||
This is the repository to fork when you want to enhance the features, performance, or user interface of capa.
|
||||
Do *not* push rules directly to this repository, instead...
|
||||
|
||||
The standard rules contributed by the community are found in the `capa-rules` repository.
|
||||
When you have an idea for a new rule, you should open a PR against `capa-rules`.
|
||||
We keep `capa` and `capa-rules` separate to distinguish where ideas, bugs, and discussions should happen.
|
||||
If you're writing yaml it probably goes in `capa-rules` and if you're writing Python it probably goes in `capa`.
|
||||
Also, we encourage users to develop their own rule repositories, so we treat our default set of rules in the same way.
|
||||
|
||||
Test fixtures, such as malware samples and analysis workspaces, are found in the `capa-testfiles` repository.
|
||||
These are files you'll need in order to run the linter (in `--thorough` mode) and full test suites;
|
||||
however, they take up a lot of space (1GB+), so by keeping `capa-testfiles` separate,
|
||||
a shallow checkout of `capa` and `capa-rules` doesn't take much bandwidth.
|
||||
|
||||
### Design Decisions
|
||||
|
||||
When we make a significant decision in how we maintain the project and what we can or cannot support,
|
||||
we will document it in the [capa issues tracker](https://github.com/fireeye/capa/issues).
|
||||
This is the best place review our discussions about what/how/why we do things in the project.
|
||||
If you have a question, check to see if it is documented there.
|
||||
If it is *not* documented there, or you can't find an answer, please open a issue.
|
||||
We'll link to existing issues when appropriate to keep discussions in one place.
|
||||
|
||||
## How Can I Contribute?
|
||||
|
||||
### Reporting Bugs
|
||||
|
||||
This section guides you through submitting a bug report for capa.
|
||||
Following these guidelines helps maintainers and the community understand your report, reproduce the behavior, and find related reports.
|
||||
|
||||
Before creating bug reports, please check [this list](#before-submitting-a-bug-report)
|
||||
as you might find out that you don't need to create one.
|
||||
When you are creating a bug report, please [include as many details as possible](#how-do-i-submit-a-good-bug-report).
|
||||
Fill out [the required template](./ISSUE_TEMPLATE/bug_report.md),
|
||||
the information it asks for helps us resolve issues faster.
|
||||
|
||||
> **Note:** If you find a **Closed** issue that seems like it is the same thing that you're experiencing, open a new issue and include a link to the original issue in the body of your new one.
|
||||
|
||||
#### Before Submitting A Bug Report
|
||||
|
||||
* **Determine [which repository the problem should be reported in](#capa-and-its-repositories)**.
|
||||
* **Perform a [cursory search](https://github.com/fireeye/capa/issues?q=is%3Aissue)** to see if the problem has already been reported. If it has **and the issue is still open**, add a comment to the existing issue instead of opening a new one.
|
||||
|
||||
#### How Do I Submit A (Good) Bug Report?
|
||||
|
||||
Bugs are tracked as [GitHub issues](https://guides.github.com/features/issues/).
|
||||
After you've determined [which repository](#capa-and-its-repositories) your bug is related to,
|
||||
create an issue on that repository and provide the following information by filling in
|
||||
[the template](./ISSUE_TEMPLATE/bug_report.md).
|
||||
|
||||
Explain the problem and include additional details to help maintainers reproduce the problem:
|
||||
|
||||
* **Use a clear and descriptive title** for the issue to identify the problem.
|
||||
* **Describe the exact steps which reproduce the problem** in as many details as possible. For example, start by explaining how you started capa, e.g. which command exactly you used in the terminal, or how you started capa otherwise.
|
||||
* **Provide specific examples to demonstrate the steps**. Include links to files or GitHub projects, or copy/pasteable snippets, which you use in those examples. If you're providing snippets in the issue, use [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines).
|
||||
* **Describe the behavior you observed after following the steps** and point out what exactly is the problem with that behavior.
|
||||
* **Explain which behavior you expected to see instead and why.**
|
||||
* **Include screenshots and animated GIFs** which show you following the described steps and clearly demonstrate the problem. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux.
|
||||
* **If you're reporting that capa crashed**, include the stack trace from the terminal. Include the stack trace in the issue in a [code block](https://help.github.com/articles/markdown-basics/#multiple-lines), a [file attachment](https://help.github.com/articles/file-attachments-on-issues-and-pull-requests/), or put it in a [gist](https://gist.github.com/) and provide link to that gist.
|
||||
* **If the problem wasn't triggered by a specific action**, describe what you were doing before the problem happened and share more information using the guidelines below.
|
||||
|
||||
Provide more context by answering these questions:
|
||||
|
||||
* **Did the problem start happening recently** (e.g. after updating to a new version of capa) or was this always a problem?
|
||||
* If the problem started happening recently, **can you reproduce the problem in an older version of capa?** What's the most recent version in which the problem doesn't happen? You can download older versions of capa from [the releases page](https://github.com/fireeye/capa/releases).
|
||||
* **Can you reliably reproduce the issue?** If not, provide details about how often the problem happens and under which conditions it normally happens.
|
||||
* If the problem is related to working with files (e.g. opening and editing files), **does the problem happen for all files and projects or only some?** Does the problem happen only when working with local or remote files (e.g. on network drives), with files of a specific type (e.g. only JavaScript or Python files), with large files or files with very long lines, or with files in a specific encoding? Is there anything else special about the files you are using?
|
||||
|
||||
Include details about your configuration and environment:
|
||||
|
||||
* **Which version of capa are you using?** You can get the exact version by running `capa --version` in your terminal.
|
||||
* **What's the name and version of the OS you're using**?
|
||||
|
||||
### Suggesting Enhancements
|
||||
|
||||
This section guides you through submitting an enhancement suggestion for capa, including completely new features and minor improvements to existing functionality. Following these guidelines helps maintainers and the community understand your suggestion and find related suggestions.
|
||||
|
||||
Before creating enhancement suggestions, please check [this list](#before-submitting-an-enhancement-suggestion) as you might find out that you don't need to create one. When you are creating an enhancement suggestion, please [include as many details as possible](#how-do-i-submit-a-good-enhancement-suggestion). Fill in [the template](./ISSUE_TEMPLATE/feature_request.md), including the steps that you imagine you would take if the feature you're requesting existed.
|
||||
|
||||
#### Before Submitting An Enhancement Suggestion
|
||||
|
||||
* **Determine [which repository the enhancement should be suggested in](#capa-and-its-repositories).**
|
||||
* **Perform a [cursory search](https://github.com/fireeye/capa/issues?q=is%3Aissue)** to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one.
|
||||
|
||||
#### How Do I Submit A (Good) Enhancement Suggestion?
|
||||
|
||||
Enhancement suggestions are tracked as [GitHub issues](https://guides.github.com/features/issues/). After you've determined [which repository](#capa-and-its-repositories) your enhancement suggestion is related to, create an issue on that repository and provide the following information:
|
||||
|
||||
* **Use a clear and descriptive title** for the issue to identify the suggestion.
|
||||
* **Provide a step-by-step description of the suggested enhancement** in as many details as possible.
|
||||
* **Provide specific examples to demonstrate the steps**. Include copy/pasteable snippets which you use in those examples, as [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines).
|
||||
* **Describe the current behavior** and **explain which behavior you expected to see instead** and why.
|
||||
* **Include screenshots and animated GIFs** which help you demonstrate the steps or point out the part of capa which the suggestion is related to. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux.
|
||||
* **Explain why this enhancement would be useful** to most capa users and isn't something that can or should be implemented as an external tool that uses capa as a library.
|
||||
* **Specify which version of capa you're using.** You can get the exact version by running `capa --version` in your terminal.
|
||||
* **Specify the name and version of the OS you're using.**
|
||||
|
||||
### Your First Code Contribution
|
||||
|
||||
Unsure where to begin contributing to capa? You can start by looking through these `good-first-issue` and `rule-idea` issues:
|
||||
|
||||
* [good-first-issue](https://github.com/fireeye/capa/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) - issues which should only require a few lines of code, and a test or two.
|
||||
* [rule-idea](https://github.com/fireeye/capa-rules/issues?q=is%3Aissue+is%3Aopen+label%3A%22rule+idea%22) - issues that describe potential new rule ideas.
|
||||
|
||||
Both issue lists are sorted by total number of comments. While not perfect, number of comments is a reasonable proxy for impact a given change will have.
|
||||
|
||||
#### Local development
|
||||
|
||||
capa and all its resources can be developed locally.
|
||||
For instructions on how to do this, see the "Method 3" section of the [installation guide](https://github.com/fireeye/capa/blob/master/doc/installation.md).
|
||||
|
||||
### Pull Requests
|
||||
|
||||
The process described here has several goals:
|
||||
|
||||
- Maintain capa's quality
|
||||
- Fix problems that are important to users
|
||||
- Engage the community in working toward the best possible capa
|
||||
- Enable a sustainable system for capa's maintainers to review contributions
|
||||
|
||||
Please follow these steps to have your contribution considered by the maintainers:
|
||||
|
||||
1. Follow the [styleguides](#styleguides)
|
||||
2. Update the CHANGELOG and add tests and documentation. In case they are not needed, indicate it in [the PR template](pull_request_template.md).
|
||||
3. After you submit your pull request, verify that all [status checks](https://help.github.com/articles/about-status-checks/) are passing <details><summary>What if the status checks are failing? </summary>If a status check is failing, and you believe that the failure is unrelated to your change, please leave a comment on the pull request explaining why you believe the failure is unrelated. A maintainer will re-run the status check for you. If we conclude that the failure was a false positive, then we will open an issue to track that problem with our status check suite.</details>
|
||||
|
||||
While the prerequisites above must be satisfied prior to having your pull request reviewed, the reviewer(s) may ask you to complete additional design work, tests, or other changes before your pull request can be ultimately accepted.
|
||||
|
||||
## Styleguides
|
||||
|
||||
### Git Commit Messages
|
||||
|
||||
* Use the present tense ("Add feature" not "Added feature")
|
||||
* Use the imperative mood ("Move cursor to..." not "Moves cursor to...")
|
||||
* Prefix the first line with the component in question ("rules: ..." or "render: ...")
|
||||
* Reference issues and pull requests liberally after the first line
|
||||
|
||||
### Python Styleguide
|
||||
|
||||
All Python code must adhere to the style guide used by capa:
|
||||
|
||||
1. [PEP8](https://www.python.org/dev/peps/pep-0008/), with clarifications from
|
||||
2. [Willi's style guide](https://docs.google.com/document/d/1iRpeg-w4DtibwytUyC_dDT7IGhNGBP25-nQfuBa-Fyk/edit?usp=sharing), formatted with
|
||||
3. [isort](https://pypi.org/project/isort/) (with line width 120 and ordered by line length), and formatted with
|
||||
4. [black](https://github.com/psf/black) (with line width 120), and formatted with
|
||||
5. [dos2unix](https://linux.die.net/man/1/dos2unix)
|
||||
|
||||
Our CI pipeline will reformat and enforce the Python styleguide.
|
||||
|
||||
### Rules Styleguide
|
||||
|
||||
All (non-nursery) capa rules must:
|
||||
|
||||
1. pass the [linter](https://github.com/fireeye/capa/blob/master/scripts/lint.py), and
|
||||
2. be formatted with [capafmt](https://github.com/fireeye/capa/blob/master/scripts/capafmt.py)
|
||||
|
||||
This ensures that all rules meet the same minimum level of quality and are structured in a consistent way.
|
||||
Our CI pipeline will reformat and enforce the capa rules styleguide.
|
||||
|
||||
94
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,47 +1,47 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
|
||||
---
|
||||
<!--
|
||||
# Is your bug report related to capa rules (for example a false positive)?
|
||||
We use sybmodules to separate code, rules and test data. If your issue is related to capa rules, please report it at https://github.com/fireeye/capa-rules/issues.
|
||||
|
||||
# Have you checked that your issue isn't already filed?
|
||||
Please search if there is a similar issue at https://github.com/fireeye/capa/issues. If there is already a similar issue, please add more details there instead of opening a new one.
|
||||
|
||||
# Have you read capa's Code of Conduct?
|
||||
By filing an Issue, you are expected to comply with it, including treating everyone with respect: https://github.com/fireeye/capa/blob/master/.github/CODE_OF_CONDUCT.md
|
||||
|
||||
# Have you read capa's CONTRIBUTING guide?
|
||||
It contains helpful information about how to contribute to capa. Check https://github.com/fireeye/capa/blob/master/.github/CONTRIBUTING.md#reporting-bugs
|
||||
-->
|
||||
|
||||
### Description
|
||||
|
||||
<!-- Description of the issue -->
|
||||
|
||||
### Steps to Reproduce
|
||||
|
||||
<!-- 1. First Step -->
|
||||
<!-- 2. Second Step -->
|
||||
<!-- 3. and so on… -->
|
||||
|
||||
**Expected behavior:**
|
||||
|
||||
<!-- What you expect to happen -->
|
||||
|
||||
**Actual behavior:**
|
||||
|
||||
<!-- What actually happens -->
|
||||
|
||||
### Versions
|
||||
|
||||
<!-- You can get this information from copy and pasting the output of `capa --version` from the command line.
|
||||
Please specify the component you're using (e.g. standalone tool or IDA Pro integration) and your Python version.
|
||||
Also, please include the OS and what version of the OS you're running. -->
|
||||
|
||||
### Additional Information
|
||||
|
||||
<!-- Any additional information, configuration or data that might be necessary to reproduce the issue. -->
|
||||
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
|
||||
---
|
||||
<!--
|
||||
# Is your bug report related to capa rules (for example a false positive)?
|
||||
We use submodules to separate code, rules and test data. If your issue is related to capa rules, please report it at https://github.com/fireeye/capa-rules/issues.
|
||||
|
||||
# Have you checked that your issue isn't already filed?
|
||||
Please search if there is a similar issue at https://github.com/fireeye/capa/issues. If there is already a similar issue, please add more details there instead of opening a new one.
|
||||
|
||||
# Have you read capa's Code of Conduct?
|
||||
By filing an Issue, you are expected to comply with it, including treating everyone with respect: https://github.com/fireeye/capa/blob/master/.github/CODE_OF_CONDUCT.md
|
||||
|
||||
# Have you read capa's CONTRIBUTING guide?
|
||||
It contains helpful information about how to contribute to capa. Check https://github.com/fireeye/capa/blob/master/.github/CONTRIBUTING.md#reporting-bugs
|
||||
-->
|
||||
|
||||
### Description
|
||||
|
||||
<!-- Description of the issue -->
|
||||
|
||||
### Steps to Reproduce
|
||||
|
||||
<!-- 1. First Step -->
|
||||
<!-- 2. Second Step -->
|
||||
<!-- 3. and so on… -->
|
||||
|
||||
**Expected behavior:**
|
||||
|
||||
<!-- What you expect to happen -->
|
||||
|
||||
**Actual behavior:**
|
||||
|
||||
<!-- What actually happens -->
|
||||
|
||||
### Versions
|
||||
|
||||
<!-- You can get this information from copy and pasting the output of `capa --version` from the command line.
|
||||
Please specify the component you're using (e.g. standalone tool or IDA Pro integration) and your Python version.
|
||||
Also, please include the OS and what version of the OS you're running. -->
|
||||
|
||||
### Additional Information
|
||||
|
||||
<!-- Any additional information, configuration or data that might be necessary to reproduce the issue. -->
|
||||
|
||||
|
||||
70
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,35 +1,35 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for capa
|
||||
|
||||
---
|
||||
<!--
|
||||
# Is your issue related to capa rules (for example an idea for a new rule)?
|
||||
We use sybmodules to separate code, rules and test data. If your issue is related to capa rules, please report it at https://github.com/fireeye/capa-rules/issues.
|
||||
|
||||
# Have you checked that your issue isn't already filed?
|
||||
Please search if there is a similar issue at https://github.com/fireeye/capa/issues. If there is already a similar issue, please add more details there instead of opening a new one.
|
||||
|
||||
# Have you read capa's Code of Conduct?
|
||||
By filing an Issue, you are expected to comply with it, including treating everyone with respect: https://github.com/fireeye/capa/blob/master/.github/CODE_OF_CONDUCT.md
|
||||
|
||||
# Have you read capa's CONTRIBUTING guide?
|
||||
It contains helpful information about how to contribute to capa. Check https://github.com/fireeye/capa/blob/master/.github/CONTRIBUTING.md#suggesting-enhancements
|
||||
-->
|
||||
|
||||
### Summary
|
||||
|
||||
<!-- One paragraph explanation of the feature. -->
|
||||
|
||||
### Motivation
|
||||
|
||||
<!-- Why are we doing this? What use cases does it support? What is the expected outcome? -->
|
||||
|
||||
### Describe alternatives you've considered
|
||||
|
||||
<!-- A clear and concise description of the alternative solutions you've considered. -->
|
||||
|
||||
## Additional context
|
||||
|
||||
<!-- Add any other context or screenshots about the feature request here. -->
|
||||
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for capa
|
||||
|
||||
---
|
||||
<!--
|
||||
# Is your issue related to capa rules (for example an idea for a new rule)?
|
||||
We use submodules to separate code, rules and test data. If your issue is related to capa rules, please report it at https://github.com/fireeye/capa-rules/issues.
|
||||
|
||||
# Have you checked that your issue isn't already filed?
|
||||
Please search if there is a similar issue at https://github.com/fireeye/capa/issues. If there is already a similar issue, please add more details there instead of opening a new one.
|
||||
|
||||
# Have you read capa's Code of Conduct?
|
||||
By filing an Issue, you are expected to comply with it, including treating everyone with respect: https://github.com/fireeye/capa/blob/master/.github/CODE_OF_CONDUCT.md
|
||||
|
||||
# Have you read capa's CONTRIBUTING guide?
|
||||
It contains helpful information about how to contribute to capa. Check https://github.com/fireeye/capa/blob/master/.github/CONTRIBUTING.md#suggesting-enhancements
|
||||
-->
|
||||
|
||||
### Summary
|
||||
|
||||
<!-- One paragraph explanation of the feature. -->
|
||||
|
||||
### Motivation
|
||||
|
||||
<!-- Why are we doing this? What use cases does it support? What is the expected outcome? -->
|
||||
|
||||
### Describe alternatives you've considered
|
||||
|
||||
<!-- A clear and concise description of the alternative solutions you've considered. -->
|
||||
|
||||
## Additional context
|
||||
|
||||
<!-- Add any other context or screenshots about the feature request here. -->
|
||||
|
||||
|
||||
BIN
.github/capa-explorer-logo.png
vendored
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
.github/capa-ida.jpg
vendored
|
Before Width: | Height: | Size: 453 KiB |
6
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
67
.github/mypy/mypy.ini
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
[mypy]
|
||||
|
||||
[mypy-halo.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-tqdm.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-ruamel.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-networkx.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-pefile.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-viv_utils.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-flirt.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-smda.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-lief.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-idc.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-vivisect.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-envi.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-PE.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-idaapi.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-idautils.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-ida_bytes.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-ida_kernwin.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-ida_settings.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-ida_funcs.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-PyQt5.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-binaryninja.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-pytest.*]
|
||||
ignore_missing_imports = True
|
||||
22
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
|
||||
<!--
|
||||
Thank you for contributing to capa! <3
|
||||
|
||||
Please read capa's CONTRIBUTING guide if you haven't done so already.
|
||||
It contains helpful information about how to contribute to capa. Check https://github.com/fireeye/capa/blob/master/.github/CONTRIBUTING.md
|
||||
|
||||
Please describe the changes in this pull request (PR). Include your motivation and context to help us review.
|
||||
|
||||
Please mention the issue your PR addresses (if any):
|
||||
closes #issue_number
|
||||
-->
|
||||
|
||||
|
||||
### Checklist
|
||||
|
||||
<!-- CHANGELOG.md has a `master (unreleased)` section. Please add bug fixes, new features, breaking changes and anything else you think is worthwhile mentioning in the release notes to this file. -->
|
||||
- [ ] No CHANGELOG update needed
|
||||
<!-- Tests prove that your fix/work as expected and ensure it doesn't break on the feature. -->
|
||||
- [ ] No new tests needed
|
||||
<!-- Please help us keeping capa documentation up-to-date -->
|
||||
- [ ] No documentation update needed
|
||||
5
.github/pyinstaller/hooks/hook-smda.py
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
# Copyright (C) 2020 FireEye, 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")
|
||||
141
.github/pyinstaller/hooks/hook-vivisect.py
vendored
@@ -13,3 +13,144 @@ from PyInstaller.utils.hooks import copy_metadata
|
||||
#
|
||||
# ref: https://github.com/pyinstaller/pyinstaller/issues/1713#issuecomment-162682084
|
||||
datas = copy_metadata("vivisect")
|
||||
|
||||
excludedimports = [
|
||||
# viv gui requires these heavy libraries,
|
||||
# but viv as a library doesn't.
|
||||
# they shouldn't be installed in our configuration,
|
||||
# but we'll ensure they don't slip in here (such as on developers' systems).
|
||||
"PyQt5",
|
||||
"qt5",
|
||||
"pyqtwebengine",
|
||||
# the above are imported by these viv modules.
|
||||
# so really, we'd want to exclude these submodules of viv.
|
||||
# but i dont think this works.
|
||||
"vqt",
|
||||
"vdb.qt",
|
||||
"envi.qt",
|
||||
# unused by capa
|
||||
"pyasn1",
|
||||
]
|
||||
|
||||
hiddenimports = [
|
||||
# vivisect does manual/runtime importing of its modules,
|
||||
# so declare the things that could be imported here.
|
||||
"vivisect",
|
||||
"vivisect.analysis",
|
||||
"vivisect.analysis.amd64",
|
||||
"vivisect.analysis.amd64",
|
||||
"vivisect.analysis.amd64.emulation",
|
||||
"vivisect.analysis.amd64.golang",
|
||||
"vivisect.analysis.crypto",
|
||||
"vivisect.analysis.crypto",
|
||||
"vivisect.analysis.crypto.constants",
|
||||
"vivisect.analysis.elf",
|
||||
"vivisect.analysis.elf",
|
||||
"vivisect.analysis.elf.elfplt",
|
||||
"vivisect.analysis.elf.libc_start_main",
|
||||
"vivisect.analysis.generic",
|
||||
"vivisect.analysis.generic",
|
||||
"vivisect.analysis.generic.codeblocks",
|
||||
"vivisect.analysis.generic.emucode",
|
||||
"vivisect.analysis.generic.entrypoints",
|
||||
"vivisect.analysis.generic.funcentries",
|
||||
"vivisect.analysis.generic.impapi",
|
||||
"vivisect.analysis.generic.mkpointers",
|
||||
"vivisect.analysis.generic.pointers",
|
||||
"vivisect.analysis.generic.pointertables",
|
||||
"vivisect.analysis.generic.relocations",
|
||||
"vivisect.analysis.generic.strconst",
|
||||
"vivisect.analysis.generic.switchcase",
|
||||
"vivisect.analysis.generic.thunks",
|
||||
"vivisect.analysis.generic.noret",
|
||||
"vivisect.analysis.i386",
|
||||
"vivisect.analysis.i386",
|
||||
"vivisect.analysis.i386.calling",
|
||||
"vivisect.analysis.i386.golang",
|
||||
"vivisect.analysis.i386.importcalls",
|
||||
"vivisect.analysis.i386.instrhook",
|
||||
"vivisect.analysis.i386.thunk_bx",
|
||||
"vivisect.analysis.ms",
|
||||
"vivisect.analysis.ms",
|
||||
"vivisect.analysis.ms.hotpatch",
|
||||
"vivisect.analysis.ms.localhints",
|
||||
"vivisect.analysis.ms.msvc",
|
||||
"vivisect.analysis.ms.msvcfunc",
|
||||
"vivisect.analysis.ms.vftables",
|
||||
"vivisect.analysis.pe",
|
||||
"vivisect.impapi.posix.amd64",
|
||||
"vivisect.impapi.posix.i386",
|
||||
"vivisect.impapi.windows",
|
||||
"vivisect.impapi.windows.amd64",
|
||||
"vivisect.impapi.windows.i386",
|
||||
"vivisect.impapi.winkern.i386",
|
||||
"vivisect.impapi.winkern.amd64",
|
||||
"vivisect.parsers.blob",
|
||||
"vivisect.parsers.elf",
|
||||
"vivisect.parsers.ihex",
|
||||
"vivisect.parsers.macho",
|
||||
"vivisect.parsers.pe",
|
||||
"vivisect.storage",
|
||||
"vivisect.storage.basicfile",
|
||||
"vstruct.constants",
|
||||
"vstruct.constants.ntstatus",
|
||||
"vstruct.defs",
|
||||
"vstruct.defs.arm7",
|
||||
"vstruct.defs.bmp",
|
||||
"vstruct.defs.dns",
|
||||
"vstruct.defs.elf",
|
||||
"vstruct.defs.gif",
|
||||
"vstruct.defs.ihex",
|
||||
"vstruct.defs.inet",
|
||||
"vstruct.defs.java",
|
||||
"vstruct.defs.kdcom",
|
||||
"vstruct.defs.macho",
|
||||
"vstruct.defs.macho.const",
|
||||
"vstruct.defs.macho.fat",
|
||||
"vstruct.defs.macho.loader",
|
||||
"vstruct.defs.macho.stabs",
|
||||
"vstruct.defs.minidump",
|
||||
"vstruct.defs.pcap",
|
||||
"vstruct.defs.pe",
|
||||
"vstruct.defs.pptp",
|
||||
"vstruct.defs.rar",
|
||||
"vstruct.defs.swf",
|
||||
"vstruct.defs.win32",
|
||||
"vstruct.defs.windows",
|
||||
"vstruct.defs.windows.win_5_1_i386",
|
||||
"vstruct.defs.windows.win_5_1_i386.ntdll",
|
||||
"vstruct.defs.windows.win_5_1_i386.ntoskrnl",
|
||||
"vstruct.defs.windows.win_5_1_i386.win32k",
|
||||
"vstruct.defs.windows.win_5_2_i386",
|
||||
"vstruct.defs.windows.win_5_2_i386.ntdll",
|
||||
"vstruct.defs.windows.win_5_2_i386.ntoskrnl",
|
||||
"vstruct.defs.windows.win_5_2_i386.win32k",
|
||||
"vstruct.defs.windows.win_6_1_amd64",
|
||||
"vstruct.defs.windows.win_6_1_amd64.ntdll",
|
||||
"vstruct.defs.windows.win_6_1_amd64.ntoskrnl",
|
||||
"vstruct.defs.windows.win_6_1_amd64.win32k",
|
||||
"vstruct.defs.windows.win_6_1_i386",
|
||||
"vstruct.defs.windows.win_6_1_i386.ntdll",
|
||||
"vstruct.defs.windows.win_6_1_i386.ntoskrnl",
|
||||
"vstruct.defs.windows.win_6_1_i386.win32k",
|
||||
"vstruct.defs.windows.win_6_1_wow64",
|
||||
"vstruct.defs.windows.win_6_1_wow64.ntdll",
|
||||
"vstruct.defs.windows.win_6_2_amd64",
|
||||
"vstruct.defs.windows.win_6_2_amd64.ntdll",
|
||||
"vstruct.defs.windows.win_6_2_amd64.ntoskrnl",
|
||||
"vstruct.defs.windows.win_6_2_amd64.win32k",
|
||||
"vstruct.defs.windows.win_6_2_i386",
|
||||
"vstruct.defs.windows.win_6_2_i386.ntdll",
|
||||
"vstruct.defs.windows.win_6_2_i386.ntoskrnl",
|
||||
"vstruct.defs.windows.win_6_2_i386.win32k",
|
||||
"vstruct.defs.windows.win_6_2_wow64",
|
||||
"vstruct.defs.windows.win_6_2_wow64.ntdll",
|
||||
"vstruct.defs.windows.win_6_3_amd64",
|
||||
"vstruct.defs.windows.win_6_3_amd64.ntdll",
|
||||
"vstruct.defs.windows.win_6_3_amd64.ntoskrnl",
|
||||
"vstruct.defs.windows.win_6_3_i386",
|
||||
"vstruct.defs.windows.win_6_3_i386.ntdll",
|
||||
"vstruct.defs.windows.win_6_3_i386.ntoskrnl",
|
||||
"vstruct.defs.windows.win_6_3_wow64",
|
||||
"vstruct.defs.windows.win_6_3_wow64.ntdll",
|
||||
]
|
||||
|
||||
147
.github/pyinstaller/pyinstaller.spec
vendored
@@ -16,9 +16,10 @@ with open('./capa/version.py', 'wb') as f:
|
||||
# - 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)
|
||||
f.write(("__version__ = '%s'" % version).encode("utf-8"))
|
||||
|
||||
a = Analysis(
|
||||
# when invoking pyinstaller from the project root,
|
||||
@@ -32,6 +33,7 @@ a = Analysis(
|
||||
# this gets invoked from the directory of the spec file,
|
||||
# i.e. ./.github/pyinstaller
|
||||
('../../rules', 'rules'),
|
||||
('../../sigs', 'sigs'),
|
||||
|
||||
# capa.render.default uses tabulate that depends on wcwidth.
|
||||
# it seems wcwidth uses a json file `version.json`
|
||||
@@ -41,127 +43,6 @@ a = Analysis(
|
||||
# ref: https://stackoverflow.com/a/62278462/87207
|
||||
(os.path.dirname(wcwidth.__file__), 'wcwidth')
|
||||
],
|
||||
hiddenimports=[
|
||||
# vivisect does manual/runtime importing of its modules,
|
||||
# so declare the things that could be imported here.
|
||||
"pycparser",
|
||||
"vivisect",
|
||||
"vivisect.analysis",
|
||||
"vivisect.analysis.amd64",
|
||||
"vivisect.analysis.amd64",
|
||||
"vivisect.analysis.amd64.emulation",
|
||||
"vivisect.analysis.amd64.golang",
|
||||
"vivisect.analysis.crypto",
|
||||
"vivisect.analysis.crypto",
|
||||
"vivisect.analysis.crypto.constants",
|
||||
"vivisect.analysis.elf",
|
||||
"vivisect.analysis.elf",
|
||||
"vivisect.analysis.elf.elfplt",
|
||||
"vivisect.analysis.elf.libc_start_main",
|
||||
"vivisect.analysis.generic",
|
||||
"vivisect.analysis.generic",
|
||||
"vivisect.analysis.generic.codeblocks",
|
||||
"vivisect.analysis.generic.emucode",
|
||||
"vivisect.analysis.generic.entrypoints",
|
||||
"vivisect.analysis.generic.funcentries",
|
||||
"vivisect.analysis.generic.impapi",
|
||||
"vivisect.analysis.generic.mkpointers",
|
||||
"vivisect.analysis.generic.pointers",
|
||||
"vivisect.analysis.generic.pointertables",
|
||||
"vivisect.analysis.generic.relocations",
|
||||
"vivisect.analysis.generic.strconst",
|
||||
"vivisect.analysis.generic.switchcase",
|
||||
"vivisect.analysis.generic.thunks",
|
||||
"vivisect.analysis.i386",
|
||||
"vivisect.analysis.i386",
|
||||
"vivisect.analysis.i386.calling",
|
||||
"vivisect.analysis.i386.golang",
|
||||
"vivisect.analysis.i386.importcalls",
|
||||
"vivisect.analysis.i386.instrhook",
|
||||
"vivisect.analysis.i386.thunk_bx",
|
||||
"vivisect.analysis.ms",
|
||||
"vivisect.analysis.ms",
|
||||
"vivisect.analysis.ms.hotpatch",
|
||||
"vivisect.analysis.ms.localhints",
|
||||
"vivisect.analysis.ms.msvc",
|
||||
"vivisect.analysis.ms.msvcfunc",
|
||||
"vivisect.analysis.ms.vftables",
|
||||
"vivisect.analysis.pe",
|
||||
"vivisect.impapi.posix.amd64",
|
||||
"vivisect.impapi.posix.i386",
|
||||
"vivisect.impapi.windows",
|
||||
"vivisect.impapi.windows.amd64",
|
||||
"vivisect.impapi.windows.i386",
|
||||
"vivisect.parsers.blob",
|
||||
"vivisect.parsers.elf",
|
||||
"vivisect.parsers.ihex",
|
||||
"vivisect.parsers.macho",
|
||||
"vivisect.parsers.parse_pe",
|
||||
"vivisect.parsers.utils",
|
||||
"vivisect.storage",
|
||||
"vivisect.storage.basicfile",
|
||||
"vstruct.constants",
|
||||
"vstruct.constants.ntstatus",
|
||||
"vstruct.defs",
|
||||
"vstruct.defs.arm7",
|
||||
"vstruct.defs.bmp",
|
||||
"vstruct.defs.dns",
|
||||
"vstruct.defs.elf",
|
||||
"vstruct.defs.gif",
|
||||
"vstruct.defs.ihex",
|
||||
"vstruct.defs.inet",
|
||||
"vstruct.defs.java",
|
||||
"vstruct.defs.kdcom",
|
||||
"vstruct.defs.macho",
|
||||
"vstruct.defs.macho.const",
|
||||
"vstruct.defs.macho.fat",
|
||||
"vstruct.defs.macho.loader",
|
||||
"vstruct.defs.macho.stabs",
|
||||
"vstruct.defs.minidump",
|
||||
"vstruct.defs.pcap",
|
||||
"vstruct.defs.pe",
|
||||
"vstruct.defs.pptp",
|
||||
"vstruct.defs.rar",
|
||||
"vstruct.defs.swf",
|
||||
"vstruct.defs.win32",
|
||||
"vstruct.defs.windows",
|
||||
"vstruct.defs.windows.win_5_1_i386",
|
||||
"vstruct.defs.windows.win_5_1_i386.ntdll",
|
||||
"vstruct.defs.windows.win_5_1_i386.ntoskrnl",
|
||||
"vstruct.defs.windows.win_5_1_i386.win32k",
|
||||
"vstruct.defs.windows.win_5_2_i386",
|
||||
"vstruct.defs.windows.win_5_2_i386.ntdll",
|
||||
"vstruct.defs.windows.win_5_2_i386.ntoskrnl",
|
||||
"vstruct.defs.windows.win_5_2_i386.win32k",
|
||||
"vstruct.defs.windows.win_6_1_amd64",
|
||||
"vstruct.defs.windows.win_6_1_amd64.ntdll",
|
||||
"vstruct.defs.windows.win_6_1_amd64.ntoskrnl",
|
||||
"vstruct.defs.windows.win_6_1_amd64.win32k",
|
||||
"vstruct.defs.windows.win_6_1_i386",
|
||||
"vstruct.defs.windows.win_6_1_i386.ntdll",
|
||||
"vstruct.defs.windows.win_6_1_i386.ntoskrnl",
|
||||
"vstruct.defs.windows.win_6_1_i386.win32k",
|
||||
"vstruct.defs.windows.win_6_1_wow64",
|
||||
"vstruct.defs.windows.win_6_1_wow64.ntdll",
|
||||
"vstruct.defs.windows.win_6_2_amd64",
|
||||
"vstruct.defs.windows.win_6_2_amd64.ntdll",
|
||||
"vstruct.defs.windows.win_6_2_amd64.ntoskrnl",
|
||||
"vstruct.defs.windows.win_6_2_amd64.win32k",
|
||||
"vstruct.defs.windows.win_6_2_i386",
|
||||
"vstruct.defs.windows.win_6_2_i386.ntdll",
|
||||
"vstruct.defs.windows.win_6_2_i386.ntoskrnl",
|
||||
"vstruct.defs.windows.win_6_2_i386.win32k",
|
||||
"vstruct.defs.windows.win_6_2_wow64",
|
||||
"vstruct.defs.windows.win_6_2_wow64.ntdll",
|
||||
"vstruct.defs.windows.win_6_3_amd64",
|
||||
"vstruct.defs.windows.win_6_3_amd64.ntdll",
|
||||
"vstruct.defs.windows.win_6_3_amd64.ntoskrnl",
|
||||
"vstruct.defs.windows.win_6_3_i386",
|
||||
"vstruct.defs.windows.win_6_3_i386.ntdll",
|
||||
"vstruct.defs.windows.win_6_3_i386.ntoskrnl",
|
||||
"vstruct.defs.windows.win_6_3_wow64",
|
||||
"vstruct.defs.windows.win_6_3_wow64.ntdll",
|
||||
],
|
||||
# when invoking pyinstaller from the project root,
|
||||
# this gets run from the project root.
|
||||
hookspath=['.github/pyinstaller/hooks'],
|
||||
@@ -179,6 +60,25 @@ 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",
|
||||
"scipy",
|
||||
"matplotlib",
|
||||
"pandas",
|
||||
"pytest",
|
||||
|
||||
# deps from viv that we don't use.
|
||||
# this duplicates the entries in `hook-vivisect`,
|
||||
# but works better this way.
|
||||
"vqt",
|
||||
"vdb.qt",
|
||||
"envi.qt",
|
||||
"PyQt5",
|
||||
"qt5",
|
||||
"pyqtwebengine",
|
||||
"pyasn1"
|
||||
])
|
||||
|
||||
a.binaries = a.binaries - TOC([
|
||||
@@ -209,5 +109,4 @@ exe = EXE(pyz,
|
||||
# a.datas,
|
||||
# strip=None,
|
||||
# upx=True,
|
||||
# name='capa-dat')
|
||||
|
||||
# name='capa-dat')
|
||||
189
.github/workflows/build.yml
vendored
@@ -1,77 +1,112 @@
|
||||
name: build
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [created, edited]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: PyInstaller for ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-16.04
|
||||
# use old linux so that the shared library versioning is more portable
|
||||
artifact_name: capa
|
||||
asset_name: linux
|
||||
- os: windows-latest
|
||||
artifact_name: capa.exe
|
||||
asset_name: windows
|
||||
- os: macos-latest
|
||||
artifact_name: capa
|
||||
asset_name: macos
|
||||
steps:
|
||||
- name: Checkout capa
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: true
|
||||
- name: Set up Python 2.7
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 2.7
|
||||
- name: Install PyInstaller
|
||||
# pyinstaller 4 doesn't support Python 2.7
|
||||
run: pip install 'pyinstaller==3.*'
|
||||
- name: Install capa
|
||||
run: pip install -e .
|
||||
- name: Build standalone executable
|
||||
run: pyinstaller .github/pyinstaller/pyinstaller.spec
|
||||
- name: Does it run?
|
||||
run: dist/capa "tests/data/Practical Malware Analysis Lab 01-01.dll_"
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: ${{ matrix.asset_name }}
|
||||
path: dist/${{ matrix.artifact_name }}
|
||||
|
||||
zip:
|
||||
name: zip ${{ matrix.asset_name }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- asset_name: linux
|
||||
artifact_name: capa
|
||||
- asset_name: windows
|
||||
artifact_name: capa.exe
|
||||
- asset_name: macos
|
||||
artifact_name: capa
|
||||
steps:
|
||||
- name: Download ${{ matrix.asset_name }}
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: ${{ matrix.asset_name }}
|
||||
- name: Set executable flag
|
||||
run: chmod +x ${{ matrix.artifact_name }}
|
||||
- name: Set zip name
|
||||
run: echo ::set-env name=zip_name::capa-${GITHUB_REF#refs/tags/}-${{ matrix.asset_name }}.zip
|
||||
- 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
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN}}
|
||||
file: ${{ env.zip_name }}
|
||||
tag: ${{ github.ref }}
|
||||
|
||||
name: build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
release:
|
||||
types: [edited, published]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: PyInstaller for ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-16.04
|
||||
# use old linux so that the shared library versioning is more portable
|
||||
artifact_name: capa
|
||||
asset_name: linux
|
||||
- os: windows-2019
|
||||
artifact_name: capa.exe
|
||||
asset_name: windows
|
||||
- os: macos-10.15
|
||||
artifact_name: capa
|
||||
asset_name: macos
|
||||
steps:
|
||||
- name: Checkout capa
|
||||
uses: actions/checkout@v2
|
||||
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
|
||||
with:
|
||||
python-version: 3.8
|
||||
- if: matrix.os == 'ubuntu-16.04'
|
||||
run: sudo apt-get install -y libyaml-dev
|
||||
- name: Install PyInstaller
|
||||
run: pip install 'pyinstaller==4.2'
|
||||
- name: Install capa
|
||||
run: pip install -e .
|
||||
- name: Build standalone executable
|
||||
run: pyinstaller .github/pyinstaller/pyinstaller.spec
|
||||
- name: Does it run?
|
||||
run: dist/capa "tests/data/Practical Malware Analysis Lab 01-01.dll_"
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: ${{ matrix.asset_name }}
|
||||
path: dist/${{ matrix.artifact_name }}
|
||||
|
||||
test_run:
|
||||
# test that binaries run on push to master
|
||||
if: github.event_name == 'push'
|
||||
name: Test run on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: [build]
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
# OSs not already tested above
|
||||
- os: ubuntu-18.04
|
||||
artifact_name: capa
|
||||
asset_name: linux
|
||||
- os: ubuntu-20.04
|
||||
artifact_name: capa
|
||||
asset_name: linux
|
||||
- os: windows-2016
|
||||
artifact_name: capa.exe
|
||||
asset_name: windows
|
||||
steps:
|
||||
- name: Download ${{ matrix.asset_name }}
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: ${{ matrix.asset_name }}
|
||||
- name: Set executable flag
|
||||
if: matrix.os != 'windows-2016'
|
||||
run: chmod +x ${{ matrix.artifact_name }}
|
||||
- name: Run capa
|
||||
run: ./${{ matrix.artifact_name }} -h
|
||||
|
||||
zip_and_upload:
|
||||
# upload zipped binaries to Release page
|
||||
if: github.event_name == 'release'
|
||||
name: zip and upload ${{ matrix.asset_name }}
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [build]
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- asset_name: linux
|
||||
artifact_name: capa
|
||||
- asset_name: windows
|
||||
artifact_name: capa.exe
|
||||
- asset_name: macos
|
||||
artifact_name: capa
|
||||
steps:
|
||||
- name: Download ${{ matrix.asset_name }}
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: ${{ matrix.asset_name }}
|
||||
- name: Set executable flag
|
||||
run: chmod +x ${{ matrix.artifact_name }}
|
||||
- name: Set zip name
|
||||
run: echo "zip_name=capa-${GITHUB_REF#refs/tags/}-${{ matrix.asset_name }}.zip" >> $GITHUB_ENV
|
||||
- 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
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN}}
|
||||
file: ${{ env.zip_name }}
|
||||
tag: ${{ github.ref }}
|
||||
|
||||
41
.github/workflows/changelog.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
name: changelog
|
||||
|
||||
on:
|
||||
# We need pull_request_target instead of pull_request because a write
|
||||
# repository token is needed to add a review to a PR. DO NOT BUILD
|
||||
# OR RUN UNTRUSTED CODE FROM PRs IN THIS ACTION
|
||||
pull_request_target:
|
||||
types: [opened, edited, synchronize]
|
||||
|
||||
jobs:
|
||||
check_changelog:
|
||||
# no need to check for dependency updates via dependabot
|
||||
if: github.actor != 'dependabot[bot]' && github.actor != 'dependabot-preview[bot]'
|
||||
runs-on: ubuntu-20.04
|
||||
env:
|
||||
NO_CHANGELOG: '[x] No CHANGELOG update needed'
|
||||
steps:
|
||||
- name: Get changed files
|
||||
id: files
|
||||
uses: Ana06/get-changed-files@v1.2
|
||||
- name: check changelog updated
|
||||
id: changelog_updated
|
||||
env:
|
||||
PR_BODY: ${{ github.event.pull_request.body }}
|
||||
FILES: ${{ steps.files.outputs.modified }}
|
||||
run: |
|
||||
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
|
||||
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
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
event: DISMISS
|
||||
body: "CHANGELOG updated or no update needed, thanks! :smile:"
|
||||
30
.github/workflows/publish.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
# This workflows will upload a Python Package using Twine when a release is created
|
||||
# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
|
||||
|
||||
name: publish to pypi
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.6'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install setuptools wheel twine
|
||||
- name: Build and publish
|
||||
env:
|
||||
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
|
||||
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
|
||||
run: |
|
||||
python setup.py sdist bdist_wheel
|
||||
twine upload --skip-existing dist/*
|
||||
|
||||
29
.github/workflows/tag.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
name: tag
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
tag:
|
||||
name: Tag capa rules
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout capa-rules
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: fireeye/capa-rules
|
||||
token: ${{ secrets.CAPA_TOKEN }}
|
||||
- name: Tag capa-rules
|
||||
run: |
|
||||
# user information is needed to create annotated tags (with a message)
|
||||
git config user.email 'capa-dev@fireeye.com'
|
||||
git config user.name 'Capa Bot'
|
||||
name=${{ github.event.release.tag_name }}
|
||||
git tag $name -m "https://github.com/fireeye/capa/releases/$name"
|
||||
- name: Push tag to capa-rules
|
||||
uses: ad-m/github-push-action@master
|
||||
with:
|
||||
repository: fireeye/capa-rules
|
||||
github_token: ${{ secrets.CAPA_TOKEN }}
|
||||
tags: true
|
||||
54
.github/workflows/tests.yml
vendored
@@ -6,9 +6,24 @@ on:
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
# save workspaces to speed up testing
|
||||
env:
|
||||
CAPA_SAVE_WORKSPACE: "True"
|
||||
|
||||
jobs:
|
||||
changelog_format:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout capa
|
||||
uses: actions/checkout@v2
|
||||
# The sync GH action in capa-rules relies on a single '- *$' in the CHANGELOG file
|
||||
- name: Ensure CHANGELOG has '- *$'
|
||||
run: |
|
||||
number=$(grep '\- *$' CHANGELOG.md | wc -l)
|
||||
if [ $number != 1 ]; then exit 1; fi
|
||||
|
||||
code_style:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout capa
|
||||
uses: actions/checkout@v2
|
||||
@@ -17,16 +32,18 @@ jobs:
|
||||
with:
|
||||
python-version: 3.8
|
||||
- name: Install dependencies
|
||||
run: pip install 'isort==5.*' black
|
||||
run: pip install -e .[dev]
|
||||
- name: Lint with isort
|
||||
run: isort --profile black --length-sort --line-width 120 -c .
|
||||
- name: Lint with black
|
||||
run: black -l 120 --check .
|
||||
- name: Check types with mypy
|
||||
run: mypy --config-file .github/mypy/mypy.ini capa/ scripts/ tests/
|
||||
|
||||
rule_linter:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout capa with rules submodule
|
||||
- name: Checkout capa with submodules
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: true
|
||||
@@ -34,35 +51,40 @@ jobs:
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
# We don't need vivisect, so we can install capa using Python3
|
||||
- name: Install capa
|
||||
run: pip install -e .
|
||||
- name: Run rule linter
|
||||
run: python scripts/lint.py rules/
|
||||
|
||||
tests:
|
||||
name: Tests in ${{ matrix.python }}
|
||||
runs-on: ubuntu-latest
|
||||
name: Tests in ${{ matrix.python-version }} on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: [code_style, rule_linter]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-20.04, windows-2019, macos-10.15]
|
||||
# across all operating systems
|
||||
python-version: [3.6, 3.9]
|
||||
include:
|
||||
- python: 2.7
|
||||
- python: 3.6
|
||||
- python: 3.7
|
||||
- python: 3.8
|
||||
- python: '3.9.0-alpha - 3.9.x' # Python latest
|
||||
# on Ubuntu run these as well
|
||||
- os: ubuntu-20.04
|
||||
python-version: 3.7
|
||||
- os: ubuntu-20.04
|
||||
python-version: 3.8
|
||||
steps:
|
||||
- name: Checkout capa with submodules
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: true
|
||||
- name: Set up Python ${{ matrix.python }}
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install pyyaml
|
||||
if: matrix.os == 'ubuntu-20.04'
|
||||
run: sudo apt-get install -y libyaml-dev
|
||||
- name: Install capa
|
||||
run: pip install -e .[dev]
|
||||
- name: Run tests
|
||||
run: pytest tests/
|
||||
|
||||
run: pytest -v tests/
|
||||
|
||||
1036
CHANGELOG.md
39
README.md
@@ -1,7 +1,10 @@
|
||||

|
||||

|
||||
|
||||
[](https://pypi.org/project/flare-capa)
|
||||
[](https://github.com/fireeye/capa/releases)
|
||||
[](https://github.com/fireeye/capa-rules)
|
||||
[](https://github.com/fireeye/capa/actions?query=workflow%3ACI+event%3Apush+branch%3Amaster)
|
||||
[](https://github.com/fireeye/capa-rules)
|
||||
[](https://github.com/fireeye/capa/releases)
|
||||
[](LICENSE.txt)
|
||||
|
||||
capa detects capabilities in executable files.
|
||||
@@ -62,16 +65,9 @@ $ capa.exe suspicious.exe
|
||||
|
||||
Download stable releases of the standalone capa binaries [here](https://github.com/fireeye/capa/releases). You can run the standalone binaries without installation. capa is a command line tool that should be run from the terminal.
|
||||
|
||||
<!--
|
||||
Alternatively, you can fetch a nightly build of a standalone binary from one of the following links. These are built using the latest development branch.
|
||||
- Windows 64bit: TODO
|
||||
- Linux: TODO
|
||||
- OSX: TODO
|
||||
-->
|
||||
To use capa as a library or integrate with another tool, see [doc/installation.md](https://github.com/fireeye/capa/blob/master/doc/installation.md) for further setup instructions.
|
||||
|
||||
To use capa as a library or integrate with another tool, see [doc/installation.md](doc/installation.md) for further setup instructions.
|
||||
|
||||
For more information about how to use capa, including running it as an IDA script/plugin see [doc/usage.md](doc/usage.md).
|
||||
For more information about how to use capa, see [doc/usage.md](https://github.com/fireeye/capa/blob/master/doc/usage.md).
|
||||
|
||||
# example
|
||||
|
||||
@@ -88,7 +84,7 @@ This is useful for at least two reasons:
|
||||
- it shows where within the binary an experienced analyst might study with IDA Pro
|
||||
|
||||
```
|
||||
λ capa.exe suspicious.exe -vv
|
||||
$ capa.exe suspicious.exe -vv
|
||||
...
|
||||
execute shell command and capture output
|
||||
namespace c2/shell
|
||||
@@ -146,20 +142,21 @@ rule:
|
||||
The [github.com/fireeye/capa-rules](https://github.com/fireeye/capa-rules) repository contains hundreds of standard library rules that are distributed with capa.
|
||||
Please learn to write rules and contribute new entries as you find interesting techniques in malware.
|
||||
|
||||
If you use IDA Pro, then you use can use the [IDA Pro plugin for capa](./capa/ida/ida_capa_explorer.py).
|
||||
This script adds new user interface elements to IDA, including an interactive tree view of rule matches and their locations within the current database.
|
||||
As you select the checkboxes, the plugin will highlight the addresses associated with the features.
|
||||
We use this plugin all the time to quickly jump to interesting parts of a program.
|
||||
If you use IDA Pro, then you can use the [capa explorer](https://github.com/fireeye/capa/tree/master/capa/ida/plugin) plugin.
|
||||
capa explorer helps you identify interesting areas of a program and build new capa rules using features extracted directly from your IDA Pro database.
|
||||
|
||||

|
||||

|
||||
|
||||
# further information
|
||||
## capa
|
||||
- [doc/installation](doc/installation.md)
|
||||
- [doc/usage](doc/usage.md)
|
||||
- [doc/limitations](doc/limitations.md)
|
||||
- [Contributing Guide](.github/CONTRIBUTING.md)
|
||||
- [Installation](https://github.com/fireeye/capa/blob/master/doc/installation.md)
|
||||
- [Usage](https://github.com/fireeye/capa/blob/master/doc/usage.md)
|
||||
- [Limitations](https://github.com/fireeye/capa/blob/master/doc/limitations.md)
|
||||
- [Contributing Guide](https://github.com/fireeye/capa/blob/master/.github/CONTRIBUTING.md)
|
||||
|
||||
## capa rules
|
||||
- [capa-rules repository](https://github.com/fireeye/capa-rules)
|
||||
- [capa-rules rule format](https://github.com/fireeye/capa-rules/blob/master/doc/format.md)
|
||||
|
||||
## capa testfiles
|
||||
The [capa-testfiles repository](https://github.com/fireeye/capa-testfiles) contains the data we use to test capa's code and rules
|
||||
|
||||
@@ -6,14 +6,25 @@
|
||||
# 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 copy
|
||||
import collections
|
||||
from typing import Set, Dict, List, Tuple, Union, Mapping
|
||||
|
||||
import capa.features
|
||||
import capa.rules
|
||||
import capa.features.common
|
||||
from capa.features.common import Feature
|
||||
|
||||
# a collection of features and the locations at which they are found.
|
||||
#
|
||||
# used throughout matching as the context in which features are searched:
|
||||
# to check if a feature exists, do: `Number(0x10) in features`.
|
||||
# to collect the locations of a feature, do: `features[Number(0x10)]`
|
||||
#
|
||||
# aliased here so that the type can be documented and xref'd.
|
||||
FeatureSet = Dict[Feature, Set[int]]
|
||||
|
||||
|
||||
class Statement(object):
|
||||
class Statement:
|
||||
"""
|
||||
superclass for structural nodes, such as and/or/not.
|
||||
this exists to provide a default impl for `__str__` and `__repr__`,
|
||||
@@ -34,7 +45,7 @@ class Statement(object):
|
||||
def __repr__(self):
|
||||
return str(self)
|
||||
|
||||
def evaluate(self, ctx):
|
||||
def evaluate(self, features: FeatureSet) -> "Result":
|
||||
"""
|
||||
classes that inherit `Statement` must implement `evaluate`
|
||||
|
||||
@@ -51,7 +62,7 @@ class Statement(object):
|
||||
yield self.child
|
||||
|
||||
if hasattr(self, "children"):
|
||||
for child in self.children:
|
||||
for child in getattr(self, "children"):
|
||||
yield child
|
||||
|
||||
def replace_child(self, existing, new):
|
||||
@@ -60,12 +71,13 @@ class Statement(object):
|
||||
self.child = new
|
||||
|
||||
if hasattr(self, "children"):
|
||||
for i, child in enumerate(self.children):
|
||||
children = getattr(self, "children")
|
||||
for i, child in enumerate(children):
|
||||
if child is existing:
|
||||
self.children[i] = new
|
||||
children[i] = new
|
||||
|
||||
|
||||
class Result(object):
|
||||
class Result:
|
||||
"""
|
||||
represents the results of an evaluation of statements against features.
|
||||
|
||||
@@ -79,7 +91,7 @@ class Result(object):
|
||||
we need this so that we can render the tree of expressions and their results.
|
||||
"""
|
||||
|
||||
def __init__(self, success, statement, children, locations=None):
|
||||
def __init__(self, success: bool, statement: Union[Statement, Feature], children: List["Result"], locations=None):
|
||||
"""
|
||||
args:
|
||||
success (bool)
|
||||
@@ -200,38 +212,23 @@ class Subscope(Statement):
|
||||
raise ValueError("cannot evaluate a subscope directly!")
|
||||
|
||||
|
||||
def topologically_order_rules(rules):
|
||||
"""
|
||||
order the given rules such that dependencies show up before dependents.
|
||||
this means that as we match rules, we can add features for the matches, and these
|
||||
will be matched by subsequent rules if they follow this order.
|
||||
|
||||
assumes that the rule dependency graph is a DAG.
|
||||
"""
|
||||
# we evaluate `rules` multiple times, so if its a generator, realize it into a list.
|
||||
rules = list(rules)
|
||||
namespaces = capa.rules.index_rules_by_namespace(rules)
|
||||
rules = {rule.name: rule for rule in rules}
|
||||
seen = set([])
|
||||
ret = []
|
||||
|
||||
def rec(rule):
|
||||
if rule.name in seen:
|
||||
return
|
||||
|
||||
for dep in rule.get_dependencies(namespaces):
|
||||
rec(rules[dep])
|
||||
|
||||
ret.append(rule)
|
||||
seen.add(rule.name)
|
||||
|
||||
for rule in rules.values():
|
||||
rec(rule)
|
||||
|
||||
return ret
|
||||
# mapping from rule name to list of: (location of match, result object)
|
||||
#
|
||||
# used throughout matching and rendering to collection the results
|
||||
# of statement evaluation and their locations.
|
||||
#
|
||||
# to check if a rule matched, do: `"TCP client" in matches`.
|
||||
# to find where a rule matched, do: `map(first, matches["TCP client"])`
|
||||
# to see how a rule matched, do:
|
||||
#
|
||||
# for address, match_details in matches["TCP client"]:
|
||||
# inspect(match_details)
|
||||
#
|
||||
# aliased here so that the type can be documented and xref'd.
|
||||
MatchResults = Mapping[str, List[Tuple[int, Result]]]
|
||||
|
||||
|
||||
def match(rules, features, va):
|
||||
def match(rules: List["capa.rules.Rule"], features: FeatureSet, va: int) -> Tuple[FeatureSet, MatchResults]:
|
||||
"""
|
||||
Args:
|
||||
rules (List[capa.rules.Rule]): these must already be ordered topologically by dependency.
|
||||
@@ -239,11 +236,11 @@ def match(rules, features, va):
|
||||
va (int): location of the features
|
||||
|
||||
Returns:
|
||||
Tuple[List[capa.features.Feature], Dict[str, Tuple[int, capa.engine.Result]]]: two-tuple with entries:
|
||||
- list of features used for matching (which may be greater than argument, due to rule match features), and
|
||||
- mapping from rule name to (location of match, result object)
|
||||
Tuple[FeatureSet, MatchResults]: two-tuple with entries:
|
||||
- set of features used for matching (which may be greater than argument, due to rule match features), and
|
||||
- mapping from rule name to [(location of match, result object)]
|
||||
"""
|
||||
results = collections.defaultdict(list)
|
||||
results = collections.defaultdict(list) # type: MatchResults
|
||||
|
||||
# copy features so that we can modify it
|
||||
# without affecting the caller (keep this function pure)
|
||||
@@ -255,12 +252,12 @@ def match(rules, features, va):
|
||||
res = rule.evaluate(features)
|
||||
if res:
|
||||
results[rule.name].append((va, res))
|
||||
features[capa.features.MatchedRule(rule.name)].add(va)
|
||||
features[capa.features.common.MatchedRule(rule.name)].add(va)
|
||||
|
||||
namespace = rule.meta.get("namespace")
|
||||
if namespace:
|
||||
while namespace:
|
||||
features[capa.features.MatchedRule(namespace)].add(va)
|
||||
features[capa.features.common.MatchedRule(namespace)].add(va)
|
||||
namespace, _, _ = namespace.rpartition("/")
|
||||
|
||||
return (features, results)
|
||||
|
||||
@@ -1,192 +0,0 @@
|
||||
# Copyright (C) 2020 FireEye, 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
|
||||
import sys
|
||||
import codecs
|
||||
import logging
|
||||
|
||||
import capa.engine
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
MAX_BYTES_FEATURE_SIZE = 0x100
|
||||
|
||||
# identifiers for supported architectures names that tweak a feature
|
||||
# for example, offset/x32
|
||||
ARCH_X32 = "x32"
|
||||
ARCH_X64 = "x64"
|
||||
VALID_ARCH = (ARCH_X32, ARCH_X64)
|
||||
|
||||
|
||||
def bytes_to_str(b):
|
||||
if sys.version_info[0] >= 3:
|
||||
return str(codecs.encode(b, "hex").decode("utf-8"))
|
||||
else:
|
||||
return codecs.encode(b, "hex")
|
||||
|
||||
|
||||
def hex_string(h):
|
||||
""" render hex string e.g. "0a40b1" as "0A 40 B1" """
|
||||
return " ".join(h[i : i + 2] for i in range(0, len(h), 2)).upper()
|
||||
|
||||
|
||||
class Feature(object):
|
||||
def __init__(self, value, arch=None, description=None):
|
||||
"""
|
||||
Args:
|
||||
value (any): the value of the feature, such as the number or string.
|
||||
arch (str): one of the VALID_ARCH values, or None.
|
||||
When None, then the feature applies to any architecture.
|
||||
Modifies the feature name from `feature` to `feature/arch`, like `offset/x32`.
|
||||
description (str): a human-readable description that explains the feature value.
|
||||
"""
|
||||
super(Feature, self).__init__()
|
||||
|
||||
if arch is not None:
|
||||
if arch not in VALID_ARCH:
|
||||
raise ValueError("arch '%s' must be one of %s" % (arch, VALID_ARCH))
|
||||
self.name = self.__class__.__name__.lower() + "/" + arch
|
||||
else:
|
||||
self.name = self.__class__.__name__.lower()
|
||||
|
||||
self.value = value
|
||||
self.arch = arch
|
||||
self.description = description
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.name, self.value, self.arch))
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.name == other.name and self.value == other.value and self.arch == other.arch
|
||||
|
||||
def get_value_str(self):
|
||||
"""
|
||||
render the value of this feature, for use by `__str__` and friends.
|
||||
subclasses should override to customize the rendering.
|
||||
|
||||
Returns: any
|
||||
"""
|
||||
return self.value
|
||||
|
||||
def __str__(self):
|
||||
if self.value:
|
||||
if self.description:
|
||||
return "%s(%s = %s)" % (self.name, self.get_value_str(), self.description)
|
||||
else:
|
||||
return "%s(%s)" % (self.name, self.get_value_str())
|
||||
else:
|
||||
return "%s" % self.name
|
||||
|
||||
def __repr__(self):
|
||||
return str(self)
|
||||
|
||||
def evaluate(self, ctx):
|
||||
return capa.engine.Result(self in ctx, self, [], locations=ctx.get(self, []))
|
||||
|
||||
def freeze_serialize(self):
|
||||
if self.arch is not None:
|
||||
return (self.__class__.__name__, [self.value, {"arch": self.arch}])
|
||||
else:
|
||||
return (self.__class__.__name__, [self.value])
|
||||
|
||||
@classmethod
|
||||
def freeze_deserialize(cls, args):
|
||||
# as you can see below in code,
|
||||
# if the last argument is a dictionary,
|
||||
# consider it to be kwargs passed to the feature constructor.
|
||||
if len(args) == 1:
|
||||
return cls(*args)
|
||||
elif isinstance(args[-1], dict):
|
||||
kwargs = args[-1]
|
||||
args = args[:-1]
|
||||
return cls(*args, **kwargs)
|
||||
|
||||
|
||||
class MatchedRule(Feature):
|
||||
def __init__(self, value, description=None):
|
||||
super(MatchedRule, self).__init__(value, description=description)
|
||||
self.name = "match"
|
||||
|
||||
|
||||
class Characteristic(Feature):
|
||||
def __init__(self, value, description=None):
|
||||
super(Characteristic, self).__init__(value, description=description)
|
||||
|
||||
|
||||
class String(Feature):
|
||||
def __init__(self, value, description=None):
|
||||
super(String, self).__init__(value, description=description)
|
||||
|
||||
|
||||
class Regex(String):
|
||||
def __init__(self, value, description=None):
|
||||
super(Regex, self).__init__(value, description=description)
|
||||
pat = self.value[len("/") : -len("/")]
|
||||
flags = re.DOTALL
|
||||
if value.endswith("/i"):
|
||||
pat = self.value[len("/") : -len("/i")]
|
||||
flags |= re.IGNORECASE
|
||||
try:
|
||||
self.re = re.compile(pat, flags)
|
||||
except re.error:
|
||||
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
|
||||
)
|
||||
self.match = None
|
||||
|
||||
def evaluate(self, ctx):
|
||||
for feature, locations in ctx.items():
|
||||
if not isinstance(feature, (capa.features.String,)):
|
||||
continue
|
||||
|
||||
# `re.search` finds a match anywhere in the given string
|
||||
# which implies leading and/or trailing whitespace.
|
||||
# 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):
|
||||
self.match = feature.value
|
||||
return capa.engine.Result(True, self, [], locations=locations)
|
||||
|
||||
return capa.engine.Result(False, self, [])
|
||||
|
||||
def __str__(self):
|
||||
return 'regex(string =~ %s, matched = "%s")' % (self.value, self.match)
|
||||
|
||||
|
||||
class StringFactory(object):
|
||||
def __new__(self, value, description=None):
|
||||
if value.startswith("/") and (value.endswith("/") or value.endswith("/i")):
|
||||
return Regex(value, description=description)
|
||||
return String(value, description=description)
|
||||
|
||||
|
||||
class Bytes(Feature):
|
||||
def __init__(self, value, description=None):
|
||||
super(Bytes, self).__init__(value, description=description)
|
||||
|
||||
def evaluate(self, ctx):
|
||||
for feature, locations in ctx.items():
|
||||
if not isinstance(feature, (capa.features.Bytes,)):
|
||||
continue
|
||||
|
||||
if feature.value.startswith(self.value):
|
||||
return capa.engine.Result(True, self, [], locations=locations)
|
||||
|
||||
return capa.engine.Result(False, self, [])
|
||||
|
||||
def get_value_str(self):
|
||||
return hex_string(bytes_to_str(self.value))
|
||||
|
||||
def freeze_serialize(self):
|
||||
return (self.__class__.__name__, [bytes_to_str(self.value).upper()])
|
||||
|
||||
@classmethod
|
||||
def freeze_deserialize(cls, args):
|
||||
return cls(*[codecs.decode(x, "hex") for x in args])
|
||||
|
||||
@@ -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 capa.features import Feature
|
||||
from capa.features.common import Feature
|
||||
|
||||
|
||||
class BasicBlock(Feature):
|
||||
|
||||
262
capa/features/common.py
Normal file
@@ -0,0 +1,262 @@
|
||||
# Copyright (C) 2020 FireEye, 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
|
||||
import codecs
|
||||
import logging
|
||||
import collections
|
||||
from typing import Set, Dict, Union
|
||||
|
||||
import capa.engine
|
||||
import capa.features
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
MAX_BYTES_FEATURE_SIZE = 0x100
|
||||
|
||||
# thunks may be chained so we specify a delta to control the depth to which these chains are explored
|
||||
THUNK_CHAIN_DEPTH_DELTA = 5
|
||||
|
||||
# identifiers for supported architectures names that tweak a feature
|
||||
# for example, offset/x32
|
||||
ARCH_X32 = "x32"
|
||||
ARCH_X64 = "x64"
|
||||
VALID_ARCH = (ARCH_X32, ARCH_X64)
|
||||
|
||||
|
||||
def bytes_to_str(b: bytes) -> str:
|
||||
return str(codecs.encode(b, "hex").decode("utf-8"))
|
||||
|
||||
|
||||
def hex_string(h: str) -> str:
|
||||
"""render hex string e.g. "0a40b1" as "0A 40 B1" """
|
||||
return " ".join(h[i : i + 2] for i in range(0, len(h), 2)).upper()
|
||||
|
||||
|
||||
def escape_string(s: str) -> str:
|
||||
"""escape special characters"""
|
||||
s = repr(s)
|
||||
if not s.startswith(('"', "'")):
|
||||
# u'hello\r\nworld' -> hello\\r\\nworld
|
||||
s = s[2:-1]
|
||||
else:
|
||||
# 'hello\r\nworld' -> hello\\r\\nworld
|
||||
s = s[1:-1]
|
||||
s = s.replace("\\'", "'") # repr() may escape "'" in some edge cases, remove
|
||||
s = s.replace('"', '\\"') # repr() does not escape '"', add
|
||||
return s
|
||||
|
||||
|
||||
class Feature:
|
||||
def __init__(self, value: Union[str, int, bytes], arch=None, description=None):
|
||||
"""
|
||||
Args:
|
||||
value (any): the value of the feature, such as the number or string.
|
||||
arch (str): one of the VALID_ARCH values, or None.
|
||||
When None, then the feature applies to any architecture.
|
||||
Modifies the feature name from `feature` to `feature/arch`, like `offset/x32`.
|
||||
description (str): a human-readable description that explains the feature value.
|
||||
"""
|
||||
super(Feature, self).__init__()
|
||||
|
||||
if arch is not None:
|
||||
if arch not in VALID_ARCH:
|
||||
raise ValueError("arch '%s' must be one of %s" % (arch, VALID_ARCH))
|
||||
self.name = self.__class__.__name__.lower() + "/" + arch
|
||||
else:
|
||||
self.name = self.__class__.__name__.lower()
|
||||
|
||||
self.value = value
|
||||
self.arch = arch
|
||||
self.description = description
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.name, self.value, self.arch))
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.name == other.name and self.value == other.value and self.arch == other.arch
|
||||
|
||||
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)
|
||||
else:
|
||||
return "%s(%s)" % (self.name, self.get_value_str())
|
||||
else:
|
||||
return "%s" % self.name
|
||||
|
||||
def __repr__(self):
|
||||
return str(self)
|
||||
|
||||
def evaluate(self, ctx: Dict["Feature", Set[int]]) -> "capa.engine.Result":
|
||||
return capa.engine.Result(self in ctx, self, [], locations=ctx.get(self, []))
|
||||
|
||||
def freeze_serialize(self):
|
||||
if self.arch is not None:
|
||||
return (self.__class__.__name__, [self.value, {"arch": self.arch}])
|
||||
else:
|
||||
return (self.__class__.__name__, [self.value])
|
||||
|
||||
@classmethod
|
||||
def freeze_deserialize(cls, args):
|
||||
# as you can see below in code,
|
||||
# if the last argument is a dictionary,
|
||||
# consider it to be kwargs passed to the feature constructor.
|
||||
if len(args) == 1:
|
||||
return cls(*args)
|
||||
elif isinstance(args[-1], dict):
|
||||
kwargs = args[-1]
|
||||
args = args[:-1]
|
||||
return cls(*args, **kwargs)
|
||||
|
||||
|
||||
class MatchedRule(Feature):
|
||||
def __init__(self, value: str, description=None):
|
||||
super(MatchedRule, self).__init__(value, description=description)
|
||||
self.name = "match"
|
||||
|
||||
|
||||
class Characteristic(Feature):
|
||||
def __init__(self, value: str, description=None):
|
||||
super(Characteristic, self).__init__(value, description=description)
|
||||
|
||||
|
||||
class String(Feature):
|
||||
def __init__(self, value: str, description=None):
|
||||
super(String, self).__init__(value, description=description)
|
||||
|
||||
|
||||
class Regex(String):
|
||||
def __init__(self, value: str, description=None):
|
||||
super(Regex, self).__init__(value, description=description)
|
||||
self.value = value
|
||||
|
||||
pat = self.value[len("/") : -len("/")]
|
||||
flags = re.DOTALL
|
||||
if value.endswith("/i"):
|
||||
pat = self.value[len("/") : -len("/i")]
|
||||
flags |= re.IGNORECASE
|
||||
try:
|
||||
self.re = re.compile(pat, flags)
|
||||
except re.error:
|
||||
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
|
||||
)
|
||||
|
||||
def evaluate(self, ctx):
|
||||
# mapping from string value to list of locations.
|
||||
# will unique the locations later on.
|
||||
matches = collections.defaultdict(list)
|
||||
|
||||
for feature, locations in ctx.items():
|
||||
if not isinstance(feature, (String,)):
|
||||
continue
|
||||
|
||||
if not isinstance(feature.value, str):
|
||||
# this is a programming error: String should only contain str
|
||||
raise ValueError("unexpected feature value type")
|
||||
|
||||
# `re.search` finds a match anywhere in the given string
|
||||
# which implies leading and/or trailing whitespace.
|
||||
# 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)
|
||||
|
||||
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])
|
||||
|
||||
# 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 capa.engine.Result(True, _MatchedRegex(self, matches), [], locations=locations)
|
||||
else:
|
||||
return capa.engine.Result(False, _MatchedRegex(self, None), [])
|
||||
|
||||
def __str__(self):
|
||||
return "regex(string =~ %s)" % self.value
|
||||
|
||||
|
||||
class _MatchedRegex(Regex):
|
||||
"""
|
||||
this represents specific match instances of a regular expression feature.
|
||||
treat it the same as a `Regex` except it has the `matches` field that contains the complete strings that matched.
|
||||
|
||||
note: this type should only ever be constructed by `Regex.evaluate()`. it is not part of the public API.
|
||||
"""
|
||||
|
||||
def __init__(self, regex: Regex, matches):
|
||||
"""
|
||||
args:
|
||||
regex (Regex): the regex feature that matches.
|
||||
match (Dict[string, List[int]]|None): mapping from matching string to its locations.
|
||||
"""
|
||||
super(_MatchedRegex, self).__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"
|
||||
# this may be None if the regex doesn't match
|
||||
self.matches = matches
|
||||
|
||||
def __str__(self):
|
||||
return "regex(string =~ %s, matches = %s)" % (
|
||||
self.value,
|
||||
", ".join(map(lambda s: '"' + s + '"', (self.matches or {}).keys())),
|
||||
)
|
||||
|
||||
|
||||
class StringFactory:
|
||||
def __new__(cls, value: str, description=None):
|
||||
if value.startswith("/") and (value.endswith("/") or value.endswith("/i")):
|
||||
return Regex(value, description=description)
|
||||
return String(value, description=description)
|
||||
|
||||
|
||||
class Bytes(Feature):
|
||||
def __init__(self, value: bytes, description=None):
|
||||
super(Bytes, self).__init__(value, description=description)
|
||||
self.value = value
|
||||
|
||||
def evaluate(self, ctx):
|
||||
for feature, locations in ctx.items():
|
||||
if not isinstance(feature, (Bytes,)):
|
||||
continue
|
||||
|
||||
if feature.value.startswith(self.value):
|
||||
return capa.engine.Result(True, self, [], locations=locations)
|
||||
|
||||
return capa.engine.Result(False, self, [])
|
||||
|
||||
def get_value_str(self):
|
||||
return hex_string(bytes_to_str(self.value))
|
||||
|
||||
def freeze_serialize(self):
|
||||
return (self.__class__.__name__, [bytes_to_str(self.value).upper()])
|
||||
|
||||
@classmethod
|
||||
def freeze_deserialize(cls, args):
|
||||
return cls(*[codecs.decode(x, "hex") for x in args])
|
||||
@@ -1,286 +0,0 @@
|
||||
# Copyright (C) 2020 FireEye, 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 abc
|
||||
|
||||
|
||||
class FeatureExtractor(object):
|
||||
"""
|
||||
FeatureExtractor defines the interface for fetching features from a sample.
|
||||
|
||||
There may be multiple backends that support fetching features for capa.
|
||||
For example, we use vivisect by default, but also want to support saving
|
||||
and restoring features from a JSON file.
|
||||
When we restore the features, we'd like to use exactly the same matching logic
|
||||
to find matching rules.
|
||||
Therefore, we can define a FeatureExtractor that provides features from the
|
||||
serialized JSON file and do matching without a binary analysis pass.
|
||||
Also, this provides a way to hook in an IDA backend.
|
||||
|
||||
This class is not instantiated directly; it is the base class for other implementations.
|
||||
"""
|
||||
|
||||
__metaclass__ = abc.ABCMeta
|
||||
|
||||
def __init__(self):
|
||||
#
|
||||
# note: a subclass should define ctor parameters for its own use.
|
||||
# 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__()
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_base_address(self):
|
||||
"""
|
||||
fetch the preferred load address at which the sample was analyzed.
|
||||
|
||||
returns: int
|
||||
"""
|
||||
raise NotImplemented
|
||||
|
||||
@abc.abstractmethod
|
||||
def extract_file_features(self):
|
||||
"""
|
||||
extract file-scope features.
|
||||
|
||||
example::
|
||||
|
||||
extractor = VivisectFeatureExtractor(vw, path)
|
||||
for feature, va in extractor.get_file_features():
|
||||
print('0x%x: %s', va, feature)
|
||||
|
||||
yields:
|
||||
Tuple[capa.features.Feature, int]: feature and its location
|
||||
"""
|
||||
raise NotImplemented
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_functions(self):
|
||||
"""
|
||||
enumerate the functions and provide opaque values that will
|
||||
subsequently be provided to `.extract_function_features()`, etc.
|
||||
|
||||
by "opaque value", we mean that this can be any object, as long as it
|
||||
provides enough context to `.extract_function_features()`.
|
||||
|
||||
the opaque value should support casting to int (`__int__`) for the function start address.
|
||||
|
||||
yields:
|
||||
any: the opaque function value.
|
||||
"""
|
||||
raise NotImplemented
|
||||
|
||||
@abc.abstractmethod
|
||||
def extract_function_features(self, f):
|
||||
"""
|
||||
extract function-scope features.
|
||||
the arguments are opaque values previously provided by `.get_functions()`, etc.
|
||||
|
||||
example::
|
||||
|
||||
extractor = VivisectFeatureExtractor(vw, path)
|
||||
for function in extractor.get_functions():
|
||||
for feature, va in extractor.extract_function_features(function):
|
||||
print('0x%x: %s', va, feature)
|
||||
|
||||
args:
|
||||
f [any]: an opaque value previously fetched from `.get_functions()`.
|
||||
|
||||
yields:
|
||||
Tuple[capa.features.Feature, int]: feature and its location
|
||||
"""
|
||||
raise NotImplemented
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_basic_blocks(self, f):
|
||||
"""
|
||||
enumerate the basic blocks in the given function and provide opaque values that will
|
||||
subsequently be provided to `.extract_basic_block_features()`, etc.
|
||||
|
||||
by "opaque value", we mean that this can be any object, as long as it
|
||||
provides enough context to `.extract_basic_block_features()`.
|
||||
|
||||
the opaque value should support casting to int (`__int__`) for the basic block start address.
|
||||
|
||||
yields:
|
||||
any: the opaque basic block value.
|
||||
"""
|
||||
raise NotImplemented
|
||||
|
||||
@abc.abstractmethod
|
||||
def extract_basic_block_features(self, f, bb):
|
||||
"""
|
||||
extract basic block-scope features.
|
||||
the arguments are opaque values previously provided by `.get_functions()`, etc.
|
||||
|
||||
example::
|
||||
|
||||
extractor = VivisectFeatureExtractor(vw, path)
|
||||
for function in extractor.get_functions():
|
||||
for bb in extractor.get_basic_blocks(function):
|
||||
for feature, va in extractor.extract_basic_block_features(function, bb):
|
||||
print('0x%x: %s', va, feature)
|
||||
|
||||
args:
|
||||
f [any]: an opaque value previously fetched from `.get_functions()`.
|
||||
bb [any]: an opaque value previously fetched from `.get_basic_blocks()`.
|
||||
|
||||
yields:
|
||||
Tuple[capa.features.Feature, int]: feature and its location
|
||||
"""
|
||||
raise NotImplemented
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_instructions(self, f, bb):
|
||||
"""
|
||||
enumerate the instructions in the given basic block and provide opaque values that will
|
||||
subsequently be provided to `.extract_insn_features()`, etc.
|
||||
|
||||
by "opaque value", we mean that this can be any object, as long as it
|
||||
provides enough context to `.extract_insn_features()`.
|
||||
|
||||
the opaque value should support casting to int (`__int__`) for the instruction address.
|
||||
|
||||
yields:
|
||||
any: the opaque function value.
|
||||
"""
|
||||
raise NotImplemented
|
||||
|
||||
@abc.abstractmethod
|
||||
def extract_insn_features(self, f, bb, insn):
|
||||
"""
|
||||
extract instruction-scope features.
|
||||
the arguments are opaque values previously provided by `.get_functions()`, etc.
|
||||
|
||||
example::
|
||||
|
||||
extractor = VivisectFeatureExtractor(vw, path)
|
||||
for function in extractor.get_functions():
|
||||
for bb in extractor.get_basic_blocks(function):
|
||||
for insn in extractor.get_instructions(function, bb):
|
||||
for feature, va in extractor.extract_insn_features(function, bb, insn):
|
||||
print('0x%x: %s', va, feature)
|
||||
|
||||
args:
|
||||
f [any]: an opaque value previously fetched from `.get_functions()`.
|
||||
bb [any]: an opaque value previously fetched from `.get_basic_blocks()`.
|
||||
insn [any]: an opaque value previously fetched from `.get_instructions()`.
|
||||
|
||||
yields:
|
||||
Tuple[capa.features.Feature, int]: feature and its location
|
||||
"""
|
||||
raise NotImplemented
|
||||
|
||||
|
||||
class NullFeatureExtractor(FeatureExtractor):
|
||||
"""
|
||||
An extractor that extracts some user-provided features.
|
||||
The structure of the single parameter is demonstrated in the example below.
|
||||
|
||||
This is useful for testing, as we can provide expected values and see if matching works.
|
||||
Also, this is how we represent features deserialized from a freeze file.
|
||||
|
||||
example::
|
||||
|
||||
extractor = NullFeatureExtractor({
|
||||
'base address: 0x401000,
|
||||
'file features': [
|
||||
(0x402345, capa.features.Characteristic('embedded pe')),
|
||||
],
|
||||
'functions': {
|
||||
0x401000: {
|
||||
'features': [
|
||||
(0x401000, capa.features.Characteristic('nzxor')),
|
||||
],
|
||||
'basic blocks': {
|
||||
0x401000: {
|
||||
'features': [
|
||||
(0x401000, capa.features.Characteristic('tight-loop')),
|
||||
],
|
||||
'instructions': {
|
||||
0x401000: {
|
||||
'features': [
|
||||
(0x401000, capa.features.Characteristic('nzxor')),
|
||||
],
|
||||
},
|
||||
0x401002: ...
|
||||
}
|
||||
},
|
||||
0x401005: ...
|
||||
}
|
||||
},
|
||||
0x40200: ...
|
||||
}
|
||||
)
|
||||
"""
|
||||
|
||||
def __init__(self, features):
|
||||
super(NullFeatureExtractor, self).__init__()
|
||||
self.features = features
|
||||
|
||||
def get_base_address(self):
|
||||
return self.features["base address"]
|
||||
|
||||
def extract_file_features(self):
|
||||
for p in self.features.get("file features", []):
|
||||
va, feature = p
|
||||
yield feature, va
|
||||
|
||||
def get_functions(self):
|
||||
for va in sorted(self.features["functions"].keys()):
|
||||
yield va
|
||||
|
||||
def extract_function_features(self, f):
|
||||
for p in self.features.get("functions", {}).get(f, {}).get("features", []): # noqa: E127 line over-indented
|
||||
va, feature = p
|
||||
yield feature, va
|
||||
|
||||
def get_basic_blocks(self, f):
|
||||
for va in sorted(
|
||||
self.features.get("functions", {}) # noqa: E127 line over-indented
|
||||
.get(f, {})
|
||||
.get("basic blocks", {})
|
||||
.keys()
|
||||
):
|
||||
yield va
|
||||
|
||||
def extract_basic_block_features(self, f, bb):
|
||||
for p in (
|
||||
self.features.get("functions", {}) # noqa: E127 line over-indented
|
||||
.get(f, {})
|
||||
.get("basic blocks", {})
|
||||
.get(bb, {})
|
||||
.get("features", [])
|
||||
):
|
||||
va, feature = p
|
||||
yield feature, va
|
||||
|
||||
def get_instructions(self, f, bb):
|
||||
for va in sorted(
|
||||
self.features.get("functions", {}) # noqa: E127 line over-indented
|
||||
.get(f, {})
|
||||
.get("basic blocks", {})
|
||||
.get(bb, {})
|
||||
.get("instructions", {})
|
||||
.keys()
|
||||
):
|
||||
yield va
|
||||
|
||||
def extract_insn_features(self, f, bb, insn):
|
||||
for p in (
|
||||
self.features.get("functions", {}) # noqa: E127 line over-indented
|
||||
.get(f, {})
|
||||
.get("basic blocks", {})
|
||||
.get(bb, {})
|
||||
.get("instructions", {})
|
||||
.get(insn, {})
|
||||
.get("features", [])
|
||||
):
|
||||
va, feature = p
|
||||
yield feature, va
|
||||
|
||||
312
capa/features/extractors/base_extractor.py
Normal file
@@ -0,0 +1,312 @@
|
||||
# Copyright (C) 2020 FireEye, 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 abc
|
||||
from typing import Tuple, Iterator, SupportsInt
|
||||
|
||||
from capa.features.basicblock import Feature
|
||||
|
||||
# feature extractors may reference functions, BBs, insns by opaque handle values.
|
||||
# the only requirement of these handles are that they support `__int__`,
|
||||
# so that they can be rendered as addresses.
|
||||
#
|
||||
# these handles are only consumed by routines on
|
||||
# the feature extractor from which they were created.
|
||||
#
|
||||
# int(FunctionHandle) -> function start address
|
||||
# int(BBHandle) -> BasicBlock start address
|
||||
# int(InsnHandle) -> instruction address
|
||||
FunctionHandle = SupportsInt
|
||||
BBHandle = SupportsInt
|
||||
InsnHandle = SupportsInt
|
||||
|
||||
|
||||
class FeatureExtractor:
|
||||
"""
|
||||
FeatureExtractor defines the interface for fetching features from a sample.
|
||||
|
||||
There may be multiple backends that support fetching features for capa.
|
||||
For example, we use vivisect by default, but also want to support saving
|
||||
and restoring features from a JSON file.
|
||||
When we restore the features, we'd like to use exactly the same matching logic
|
||||
to find matching rules.
|
||||
Therefore, we can define a FeatureExtractor that provides features from the
|
||||
serialized JSON file and do matching without a binary analysis pass.
|
||||
Also, this provides a way to hook in an IDA backend.
|
||||
|
||||
This class is not instantiated directly; it is the base class for other implementations.
|
||||
"""
|
||||
|
||||
__metaclass__ = abc.ABCMeta
|
||||
|
||||
def __init__(self):
|
||||
#
|
||||
# note: a subclass should define ctor parameters for its own use.
|
||||
# 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__()
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_base_address(self) -> int:
|
||||
"""
|
||||
fetch the preferred load address at which the sample was analyzed.
|
||||
"""
|
||||
raise NotImplemented
|
||||
|
||||
@abc.abstractmethod
|
||||
def extract_file_features(self) -> Iterator[Tuple[Feature, int]]:
|
||||
"""
|
||||
extract file-scope features.
|
||||
|
||||
example::
|
||||
|
||||
extractor = VivisectFeatureExtractor(vw, path)
|
||||
for feature, va in extractor.get_file_features():
|
||||
print('0x%x: %s', va, feature)
|
||||
|
||||
yields:
|
||||
Tuple[Feature, int]: feature and its location
|
||||
"""
|
||||
raise NotImplemented
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_functions(self) -> Iterator[FunctionHandle]:
|
||||
"""
|
||||
enumerate the functions and provide opaque values that will
|
||||
subsequently be provided to `.extract_function_features()`, etc.
|
||||
"""
|
||||
raise NotImplemented
|
||||
|
||||
def is_library_function(self, va: int) -> bool:
|
||||
"""
|
||||
is the given address a library function?
|
||||
the backend may implement its own function matching algorithm, or none at all.
|
||||
we accept a VA here, rather than function object, to handle addresses identified in instructions.
|
||||
|
||||
this information is used to:
|
||||
- filter out matches in library functions (by default), and
|
||||
- recognize when to fetch symbol names for called (non-API) functions
|
||||
|
||||
args:
|
||||
va (int): the virtual address of a function.
|
||||
|
||||
returns:
|
||||
bool: True if the given address is the start of a library function.
|
||||
"""
|
||||
return False
|
||||
|
||||
def get_function_name(self, va: int) -> str:
|
||||
"""
|
||||
fetch any recognized name for the given address.
|
||||
this is only guaranteed to return a value when the given function is a recognized library function.
|
||||
we accept a VA here, rather than function object, to handle addresses identified in instructions.
|
||||
|
||||
args:
|
||||
va (int): the virtual address of a function.
|
||||
|
||||
returns:
|
||||
str: the function name
|
||||
|
||||
raises:
|
||||
KeyError: when the given function does not have a name.
|
||||
"""
|
||||
raise KeyError(va)
|
||||
|
||||
@abc.abstractmethod
|
||||
def extract_function_features(self, f: FunctionHandle) -> Iterator[Tuple[Feature, int]]:
|
||||
"""
|
||||
extract function-scope features.
|
||||
the arguments are opaque values previously provided by `.get_functions()`, etc.
|
||||
|
||||
example::
|
||||
|
||||
extractor = VivisectFeatureExtractor(vw, path)
|
||||
for function in extractor.get_functions():
|
||||
for feature, va in extractor.extract_function_features(function):
|
||||
print('0x%x: %s', va, feature)
|
||||
|
||||
args:
|
||||
f [FunctionHandle]: an opaque value previously fetched from `.get_functions()`.
|
||||
|
||||
yields:
|
||||
Tuple[Feature, int]: feature and its location
|
||||
"""
|
||||
raise NotImplemented
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_basic_blocks(self, f: FunctionHandle) -> Iterator[BBHandle]:
|
||||
"""
|
||||
enumerate the basic blocks in the given function and provide opaque values that will
|
||||
subsequently be provided to `.extract_basic_block_features()`, etc.
|
||||
"""
|
||||
raise NotImplemented
|
||||
|
||||
@abc.abstractmethod
|
||||
def extract_basic_block_features(self, f: FunctionHandle, bb: BBHandle) -> Iterator[Tuple[Feature, int]]:
|
||||
"""
|
||||
extract basic block-scope features.
|
||||
the arguments are opaque values previously provided by `.get_functions()`, etc.
|
||||
|
||||
example::
|
||||
|
||||
extractor = VivisectFeatureExtractor(vw, path)
|
||||
for function in extractor.get_functions():
|
||||
for bb in extractor.get_basic_blocks(function):
|
||||
for feature, va in extractor.extract_basic_block_features(function, bb):
|
||||
print('0x%x: %s', va, feature)
|
||||
|
||||
args:
|
||||
f [FunctionHandle]: an opaque value previously fetched from `.get_functions()`.
|
||||
bb [BBHandle]: an opaque value previously fetched from `.get_basic_blocks()`.
|
||||
|
||||
yields:
|
||||
Tuple[Feature, int]: feature and its location
|
||||
"""
|
||||
raise NotImplemented
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_instructions(self, f: FunctionHandle, bb: BBHandle) -> Iterator[InsnHandle]:
|
||||
"""
|
||||
enumerate the instructions in the given basic block and provide opaque values that will
|
||||
subsequently be provided to `.extract_insn_features()`, etc.
|
||||
"""
|
||||
raise NotImplemented
|
||||
|
||||
@abc.abstractmethod
|
||||
def extract_insn_features(self, f: FunctionHandle, bb: BBHandle, insn: InsnHandle) -> Iterator[Tuple[Feature, int]]:
|
||||
"""
|
||||
extract instruction-scope features.
|
||||
the arguments are opaque values previously provided by `.get_functions()`, etc.
|
||||
|
||||
example::
|
||||
|
||||
extractor = VivisectFeatureExtractor(vw, path)
|
||||
for function in extractor.get_functions():
|
||||
for bb in extractor.get_basic_blocks(function):
|
||||
for insn in extractor.get_instructions(function, bb):
|
||||
for feature, va in extractor.extract_insn_features(function, bb, insn):
|
||||
print('0x%x: %s', va, feature)
|
||||
|
||||
args:
|
||||
f [FunctionHandle]: an opaque value previously fetched from `.get_functions()`.
|
||||
bb [BBHandle]: an opaque value previously fetched from `.get_basic_blocks()`.
|
||||
insn [InsnHandle]: an opaque value previously fetched from `.get_instructions()`.
|
||||
|
||||
yields:
|
||||
Tuple[Feature, int]: feature and its location
|
||||
"""
|
||||
raise NotImplemented
|
||||
|
||||
|
||||
class NullFeatureExtractor(FeatureExtractor):
|
||||
"""
|
||||
An extractor that extracts some user-provided features.
|
||||
The structure of the single parameter is demonstrated in the example below.
|
||||
|
||||
This is useful for testing, as we can provide expected values and see if matching works.
|
||||
Also, this is how we represent features deserialized from a freeze file.
|
||||
|
||||
example::
|
||||
|
||||
extractor = NullFeatureExtractor({
|
||||
'base address: 0x401000,
|
||||
'file features': [
|
||||
(0x402345, capa.features.Characteristic('embedded pe')),
|
||||
],
|
||||
'functions': {
|
||||
0x401000: {
|
||||
'features': [
|
||||
(0x401000, capa.features.Characteristic('nzxor')),
|
||||
],
|
||||
'basic blocks': {
|
||||
0x401000: {
|
||||
'features': [
|
||||
(0x401000, capa.features.Characteristic('tight-loop')),
|
||||
],
|
||||
'instructions': {
|
||||
0x401000: {
|
||||
'features': [
|
||||
(0x401000, capa.features.Characteristic('nzxor')),
|
||||
],
|
||||
},
|
||||
0x401002: ...
|
||||
}
|
||||
},
|
||||
0x401005: ...
|
||||
}
|
||||
},
|
||||
0x40200: ...
|
||||
}
|
||||
)
|
||||
"""
|
||||
|
||||
def __init__(self, features):
|
||||
super(NullFeatureExtractor, self).__init__()
|
||||
self.features = features
|
||||
|
||||
def get_base_address(self):
|
||||
return self.features["base address"]
|
||||
|
||||
def extract_file_features(self):
|
||||
for p in self.features.get("file features", []):
|
||||
va, feature = p
|
||||
yield feature, va
|
||||
|
||||
def get_functions(self):
|
||||
for va in sorted(self.features["functions"].keys()):
|
||||
yield va
|
||||
|
||||
def extract_function_features(self, f):
|
||||
for p in self.features.get("functions", {}).get(f, {}).get("features", []): # noqa: E127 line over-indented
|
||||
va, feature = p
|
||||
yield feature, va
|
||||
|
||||
def get_basic_blocks(self, f):
|
||||
for va in sorted(
|
||||
self.features.get("functions", {}) # noqa: E127 line over-indented
|
||||
.get(f, {})
|
||||
.get("basic blocks", {})
|
||||
.keys()
|
||||
):
|
||||
yield va
|
||||
|
||||
def extract_basic_block_features(self, f, bb):
|
||||
for p in (
|
||||
self.features.get("functions", {}) # noqa: E127 line over-indented
|
||||
.get(f, {})
|
||||
.get("basic blocks", {})
|
||||
.get(bb, {})
|
||||
.get("features", [])
|
||||
):
|
||||
va, feature = p
|
||||
yield feature, va
|
||||
|
||||
def get_instructions(self, f, bb):
|
||||
for va in sorted(
|
||||
self.features.get("functions", {}) # noqa: E127 line over-indented
|
||||
.get(f, {})
|
||||
.get("basic blocks", {})
|
||||
.get(bb, {})
|
||||
.get("instructions", {})
|
||||
.keys()
|
||||
):
|
||||
yield va
|
||||
|
||||
def extract_insn_features(self, f, bb, insn):
|
||||
for p in (
|
||||
self.features.get("functions", {}) # noqa: E127 line over-indented
|
||||
.get(f, {})
|
||||
.get("basic blocks", {})
|
||||
.get(bb, {})
|
||||
.get("instructions", {})
|
||||
.get(insn, {})
|
||||
.get("features", [])
|
||||
):
|
||||
va, feature = p
|
||||
yield feature, va
|
||||
@@ -6,40 +6,45 @@
|
||||
# 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
|
||||
import builtins
|
||||
|
||||
from capa.features.insn import API
|
||||
from typing import Tuple, Iterator
|
||||
|
||||
MIN_STACKSTRING_LEN = 8
|
||||
|
||||
|
||||
def xor_static(data, i):
|
||||
if sys.version_info >= (3, 0):
|
||||
return bytes(c ^ i for c in data)
|
||||
else:
|
||||
return "".join(chr(ord(c) ^ i) for c in data)
|
||||
def xor_static(data: bytes, i: int) -> bytes:
|
||||
return bytes(c ^ i for c in data)
|
||||
|
||||
|
||||
def is_aw_function(function_name):
|
||||
def is_aw_function(symbol: str) -> bool:
|
||||
"""
|
||||
is the given function name an A/W function?
|
||||
these are variants of functions that, on Windows, accept either a narrow or wide string.
|
||||
"""
|
||||
if len(function_name) < 2:
|
||||
if len(symbol) < 2:
|
||||
return False
|
||||
|
||||
# last character should be 'A' or 'W'
|
||||
if function_name[-1] not in ("A", "W"):
|
||||
if symbol[-1] not in ("A", "W"):
|
||||
return False
|
||||
|
||||
# second to last character should be lowercase letter
|
||||
return "a" <= function_name[-2] <= "z" or "0" <= function_name[-2] <= "9"
|
||||
return "a" <= symbol[-2] <= "z" or "0" <= symbol[-2] <= "9"
|
||||
|
||||
|
||||
def generate_api_features(apiname, va):
|
||||
def is_ordinal(symbol: str) -> bool:
|
||||
"""
|
||||
for a given function name and address, generate API names.
|
||||
is the given symbol an ordinal that is prefixed by "#"?
|
||||
"""
|
||||
if symbol:
|
||||
return symbol[0] == "#"
|
||||
return False
|
||||
|
||||
|
||||
def generate_symbols(dll: str, symbol: str) -> Iterator[str]:
|
||||
"""
|
||||
for a given dll and symbol name, generate variants.
|
||||
we over-generate features to make matching easier.
|
||||
these include:
|
||||
- kernel32.CreateFileA
|
||||
@@ -47,29 +52,27 @@ def generate_api_features(apiname, va):
|
||||
- CreateFileA
|
||||
- CreateFile
|
||||
"""
|
||||
# (kernel32.CreateFileA, 0x401000)
|
||||
yield API(apiname), va
|
||||
# kernel32.CreateFileA
|
||||
yield "%s.%s" % (dll, symbol)
|
||||
|
||||
if is_aw_function(apiname):
|
||||
# (kernel32.CreateFile, 0x401000)
|
||||
yield API(apiname[:-1]), va
|
||||
if not is_ordinal(symbol):
|
||||
# CreateFileA
|
||||
yield symbol
|
||||
|
||||
if "." in apiname:
|
||||
modname, impname = apiname.split(".")
|
||||
# strip modname to support importname-only matching
|
||||
# (CreateFileA, 0x401000)
|
||||
yield API(impname), va
|
||||
if is_aw_function(symbol):
|
||||
# kernel32.CreateFile
|
||||
yield "%s.%s" % (dll, symbol[:-1])
|
||||
|
||||
if is_aw_function(impname):
|
||||
# (CreateFile, 0x401000)
|
||||
yield API(impname[:-1]), va
|
||||
if not is_ordinal(symbol):
|
||||
# CreateFile
|
||||
yield symbol[:-1]
|
||||
|
||||
|
||||
def all_zeros(bytez):
|
||||
def all_zeros(bytez: bytes) -> bool:
|
||||
return all(b == 0 for b in builtins.bytes(bytez))
|
||||
|
||||
|
||||
def twos_complement(val, bits):
|
||||
def twos_complement(val: int, bits: int) -> int:
|
||||
"""
|
||||
compute the 2's complement of int value val
|
||||
|
||||
@@ -82,3 +85,49 @@ def twos_complement(val, bits):
|
||||
else:
|
||||
# return positive value as is
|
||||
return val
|
||||
|
||||
|
||||
def carve_pe(pbytes: bytes, offset: int = 0) -> Iterator[Tuple[int, int]]:
|
||||
"""
|
||||
Generate (offset, key) tuples of embedded PEs
|
||||
|
||||
Based on the version from vivisect:
|
||||
https://github.com/vivisect/vivisect/blob/7be4037b1cecc4551b397f840405a1fc606f9b53/PE/carve.py#L19
|
||||
And its IDA adaptation:
|
||||
capa/features/extractors/ida/file.py
|
||||
"""
|
||||
mz_xor = [
|
||||
(
|
||||
xor_static(b"MZ", key),
|
||||
xor_static(b"PE", key),
|
||||
key,
|
||||
)
|
||||
for key in range(256)
|
||||
]
|
||||
|
||||
pblen = len(pbytes)
|
||||
todo = [(pbytes.find(mzx, offset), mzx, pex, key) for mzx, pex, key in mz_xor]
|
||||
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
|
||||
# e_lfanew is at 0x3c
|
||||
e_lfanew = off + 0x3C
|
||||
if pblen < (e_lfanew + 4):
|
||||
continue
|
||||
|
||||
newoff = struct.unpack("<I", xor_static(pbytes[e_lfanew : e_lfanew + 4], key))[0]
|
||||
|
||||
nextres = pbytes.find(mzx, off + 1)
|
||||
if nextres != -1:
|
||||
todo.append((nextres, mzx, pex, key))
|
||||
|
||||
peoff = off + newoff
|
||||
if pblen < (peoff + 2):
|
||||
continue
|
||||
|
||||
if pbytes[peoff : peoff + 2] == pex:
|
||||
yield (off, key)
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
# Copyright (C) 2020 FireEye, 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 types
|
||||
|
||||
import idaapi
|
||||
|
||||
import capa.features.extractors.ida.file
|
||||
import capa.features.extractors.ida.insn
|
||||
import capa.features.extractors.ida.function
|
||||
import capa.features.extractors.ida.basicblock
|
||||
from capa.features.extractors import FeatureExtractor
|
||||
|
||||
|
||||
def get_ea(self):
|
||||
""" """
|
||||
if isinstance(self, (idaapi.BasicBlock, idaapi.func_t)):
|
||||
return self.start_ea
|
||||
if isinstance(self, idaapi.insn_t):
|
||||
return self.ea
|
||||
raise TypeError
|
||||
|
||||
|
||||
def add_ea_int_cast(o):
|
||||
"""
|
||||
dynamically add a cast-to-int (`__int__`) method to the given object
|
||||
that returns the value of the `.ea` property.
|
||||
this bit of skullduggery lets use cast viv-utils objects as ints.
|
||||
the correct way of doing this is to update viv-utils (or subclass the objects here).
|
||||
"""
|
||||
if sys.version_info[0] >= 3:
|
||||
setattr(o, "__int__", types.MethodType(get_ea, o))
|
||||
else:
|
||||
setattr(o, "__int__", types.MethodType(get_ea, o, type(o)))
|
||||
return o
|
||||
|
||||
|
||||
class IdaFeatureExtractor(FeatureExtractor):
|
||||
def __init__(self):
|
||||
super(IdaFeatureExtractor, self).__init__()
|
||||
|
||||
def get_base_address(self):
|
||||
return idaapi.get_imagebase()
|
||||
|
||||
def extract_file_features(self):
|
||||
for (feature, ea) in capa.features.extractors.ida.file.extract_features():
|
||||
yield feature, ea
|
||||
|
||||
def get_functions(self):
|
||||
import capa.features.extractors.ida.helpers as ida_helpers
|
||||
|
||||
# data structure shared across functions yielded here.
|
||||
# useful for caching analysis relevant across a single workspace.
|
||||
ctx = {}
|
||||
|
||||
# ignore library functions and thunk functions as identified by IDA
|
||||
for f in ida_helpers.get_functions(skip_thunks=True, skip_libs=True):
|
||||
setattr(f, "ctx", ctx)
|
||||
yield add_ea_int_cast(f)
|
||||
|
||||
@staticmethod
|
||||
def get_function(ea):
|
||||
f = idaapi.get_func(ea)
|
||||
setattr(f, "ctx", {})
|
||||
return add_ea_int_cast(f)
|
||||
|
||||
def extract_function_features(self, f):
|
||||
for (feature, ea) in capa.features.extractors.ida.function.extract_features(f):
|
||||
yield feature, ea
|
||||
|
||||
def get_basic_blocks(self, f):
|
||||
for bb in capa.features.extractors.ida.helpers.get_function_blocks(f):
|
||||
yield add_ea_int_cast(bb)
|
||||
|
||||
def extract_basic_block_features(self, f, bb):
|
||||
for (feature, ea) in capa.features.extractors.ida.basicblock.extract_features(f, bb):
|
||||
yield feature, ea
|
||||
|
||||
def get_instructions(self, f, bb):
|
||||
import capa.features.extractors.ida.helpers as ida_helpers
|
||||
|
||||
for insn in ida_helpers.get_instructions_in_range(bb.start_ea, bb.end_ea):
|
||||
yield add_ea_int_cast(insn)
|
||||
|
||||
def extract_insn_features(self, f, bb, insn):
|
||||
for (feature, ea) in capa.features.extractors.ida.insn.extract_features(f, bb, insn):
|
||||
yield feature, ea
|
||||
|
||||
@@ -6,14 +6,13 @@
|
||||
# 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
|
||||
|
||||
import idaapi
|
||||
|
||||
import capa.features.extractors.ida.helpers
|
||||
from capa.features import Characteristic
|
||||
from capa.features.common import Characteristic
|
||||
from capa.features.basicblock import BasicBlock
|
||||
from capa.features.extractors.ida import helpers
|
||||
from capa.features.extractors.helpers import MIN_STACKSTRING_LEN
|
||||
@@ -39,18 +38,11 @@ def get_printable_len(op):
|
||||
raise ValueError("Unhandled operand data type 0x%x." % op.dtype)
|
||||
|
||||
def is_printable_ascii(chars):
|
||||
if sys.version_info[0] >= 3:
|
||||
return all(c < 127 and chr(c) in string.printable for c in chars)
|
||||
else:
|
||||
return all(ord(c) < 127 and c in string.printable for c in chars)
|
||||
return all(c < 127 and chr(c) in string.printable for c in chars)
|
||||
|
||||
def is_printable_utf16le(chars):
|
||||
if sys.version_info[0] >= 3:
|
||||
if all(c == 0x00 for c in chars[1::2]):
|
||||
return is_printable_ascii(chars[::2])
|
||||
else:
|
||||
if all(c == "\x00" for c in chars[1::2]):
|
||||
return is_printable_ascii(chars[::2])
|
||||
if all(c == 0x00 for c in chars[1::2]):
|
||||
return is_printable_ascii(chars[::2])
|
||||
|
||||
if is_printable_ascii(chars):
|
||||
return idaapi.get_dtype_size(op.dtype)
|
||||
|
||||
108
capa/features/extractors/ida/extractor.py
Normal file
@@ -0,0 +1,108 @@
|
||||
# Copyright (C) 2020 FireEye, 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 idaapi
|
||||
|
||||
import capa.features.extractors.ida.file
|
||||
import capa.features.extractors.ida.insn
|
||||
import capa.features.extractors.ida.function
|
||||
import capa.features.extractors.ida.basicblock
|
||||
from capa.features.extractors.base_extractor import FeatureExtractor
|
||||
|
||||
|
||||
class FunctionHandle:
|
||||
"""this acts like an idaapi.func_t but with __int__()"""
|
||||
|
||||
def __init__(self, inner):
|
||||
self._inner = inner
|
||||
|
||||
def __int__(self):
|
||||
return self.start_ea
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._inner, name)
|
||||
|
||||
|
||||
class BasicBlockHandle:
|
||||
"""this acts like an idaapi.BasicBlock but with __int__()"""
|
||||
|
||||
def __init__(self, inner):
|
||||
self._inner = inner
|
||||
|
||||
def __int__(self):
|
||||
return self.start_ea
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._inner, name)
|
||||
|
||||
|
||||
class InstructionHandle:
|
||||
"""this acts like an idaapi.insn_t but with __int__()"""
|
||||
|
||||
def __init__(self, inner):
|
||||
self._inner = inner
|
||||
|
||||
def __int__(self):
|
||||
return self.ea
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._inner, name)
|
||||
|
||||
|
||||
class IdaFeatureExtractor(FeatureExtractor):
|
||||
def __init__(self):
|
||||
super(IdaFeatureExtractor, self).__init__()
|
||||
|
||||
def get_base_address(self):
|
||||
return idaapi.get_imagebase()
|
||||
|
||||
def extract_file_features(self):
|
||||
for (feature, ea) in capa.features.extractors.ida.file.extract_features():
|
||||
yield feature, ea
|
||||
|
||||
def get_functions(self):
|
||||
import capa.features.extractors.ida.helpers as ida_helpers
|
||||
|
||||
# data structure shared across functions yielded here.
|
||||
# useful for caching analysis relevant across a single workspace.
|
||||
ctx = {}
|
||||
|
||||
# ignore library functions and thunk functions as identified by IDA
|
||||
for f in ida_helpers.get_functions(skip_thunks=True, skip_libs=True):
|
||||
setattr(f, "ctx", ctx)
|
||||
yield FunctionHandle(f)
|
||||
|
||||
@staticmethod
|
||||
def get_function(ea):
|
||||
f = idaapi.get_func(ea)
|
||||
setattr(f, "ctx", {})
|
||||
return FunctionHandle(f)
|
||||
|
||||
def extract_function_features(self, f):
|
||||
for (feature, ea) in capa.features.extractors.ida.function.extract_features(f):
|
||||
yield feature, ea
|
||||
|
||||
def get_basic_blocks(self, f):
|
||||
import capa.features.extractors.ida.helpers as ida_helpers
|
||||
|
||||
for bb in ida_helpers.get_function_blocks(f):
|
||||
yield BasicBlockHandle(bb)
|
||||
|
||||
def extract_basic_block_features(self, f, bb):
|
||||
for (feature, ea) in capa.features.extractors.ida.basicblock.extract_features(f, bb):
|
||||
yield feature, ea
|
||||
|
||||
def get_instructions(self, f, bb):
|
||||
import capa.features.extractors.ida.helpers as ida_helpers
|
||||
|
||||
for insn in ida_helpers.get_instructions_in_range(bb.start_ea, bb.end_ea):
|
||||
yield InstructionHandle(insn)
|
||||
|
||||
def extract_insn_features(self, f, bb, insn):
|
||||
for (feature, ea) in capa.features.extractors.ida.insn.extract_features(f, bb, insn):
|
||||
yield feature, ea
|
||||
@@ -15,8 +15,8 @@ import idautils
|
||||
import capa.features.extractors.helpers
|
||||
import capa.features.extractors.strings
|
||||
import capa.features.extractors.ida.helpers
|
||||
from capa.features import String, Characteristic
|
||||
from capa.features.file import Export, Import, Section
|
||||
from capa.features.file import Export, Import, Section, FunctionName
|
||||
from capa.features.common import String, Characteristic
|
||||
|
||||
|
||||
def check_segment_for_pe(seg):
|
||||
@@ -37,11 +37,11 @@ def check_segment_for_pe(seg):
|
||||
)
|
||||
for i in range(256)
|
||||
]
|
||||
todo = [
|
||||
(capa.features.extractors.ida.helpers.find_byte_sequence(seg.start_ea, seg.end_ea, mzx), mzx, pex, i)
|
||||
for mzx, pex, i in mz_xor
|
||||
]
|
||||
todo = [(off, mzx, pex, i) for (off, mzx, pex, i) in todo if off != idaapi.BADADDR]
|
||||
|
||||
todo = []
|
||||
for (mzx, pex, i) in mz_xor:
|
||||
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()
|
||||
@@ -61,8 +61,7 @@ def check_segment_for_pe(seg):
|
||||
if idc.get_bytes(peoff, 2) == pex:
|
||||
yield (off, i)
|
||||
|
||||
nextres = capa.features.extractors.ida.helpers.find_byte_sequence(off + 1, seg.end_ea, mzx)
|
||||
if nextres != -1:
|
||||
for nextres in capa.features.extractors.ida.helpers.find_byte_sequence(off + 1, seg.end_ea, mzx):
|
||||
todo.append((nextres, mzx, pex, i))
|
||||
|
||||
|
||||
@@ -79,7 +78,7 @@ def extract_file_embedded_pe():
|
||||
|
||||
|
||||
def extract_file_export_names():
|
||||
""" extract function exports """
|
||||
"""extract function exports"""
|
||||
for (_, _, ea, name) in idautils.Entries():
|
||||
yield Export(name), ea
|
||||
|
||||
@@ -96,11 +95,24 @@ def extract_file_import_names():
|
||||
- importname
|
||||
"""
|
||||
for (ea, info) in capa.features.extractors.ida.helpers.get_file_imports().items():
|
||||
if info[1]:
|
||||
yield Import("%s.%s" % (info[0], info[1])), ea
|
||||
yield Import(info[1]), ea
|
||||
if info[2]:
|
||||
yield Import("%s.#%s" % (info[0], str(info[2]))), ea
|
||||
if info[1] and info[2]:
|
||||
# e.g. in mimikatz: ('cabinet', 'FCIAddFile', 11L)
|
||||
# extract by name here and by ordinal below
|
||||
for name in capa.features.extractors.helpers.generate_symbols(info[0], info[1]):
|
||||
yield Import(name), ea
|
||||
dll = info[0]
|
||||
symbol = "#%d" % (info[2])
|
||||
elif info[1]:
|
||||
dll = info[0]
|
||||
symbol = info[1]
|
||||
elif info[2]:
|
||||
dll = info[0]
|
||||
symbol = "#%d" % (info[2])
|
||||
else:
|
||||
continue
|
||||
|
||||
for name in capa.features.extractors.helpers.generate_symbols(dll, symbol):
|
||||
yield Import(name), ea
|
||||
|
||||
|
||||
def extract_file_section_names():
|
||||
@@ -131,8 +143,18 @@ def extract_file_strings():
|
||||
yield String(s.s), (seg.start_ea + s.offset)
|
||||
|
||||
|
||||
def extract_file_function_names():
|
||||
"""
|
||||
extract the names of statically-linked library functions.
|
||||
"""
|
||||
for ea in idautils.Functions():
|
||||
if idaapi.get_func(ea).flags & idaapi.FUNC_LIB:
|
||||
name = idaapi.get_name(ea)
|
||||
yield FunctionName(name), ea
|
||||
|
||||
|
||||
def extract_features():
|
||||
""" extract file features """
|
||||
"""extract file features"""
|
||||
for file_handler in FILE_HANDLERS:
|
||||
for feature, va in file_handler():
|
||||
yield feature, va
|
||||
@@ -144,6 +166,7 @@ FILE_HANDLERS = (
|
||||
extract_file_strings,
|
||||
extract_file_section_names,
|
||||
extract_file_embedded_pe,
|
||||
extract_file_function_names,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import idaapi
|
||||
import idautils
|
||||
|
||||
import capa.features.extractors.ida.helpers
|
||||
from capa.features import Characteristic
|
||||
from capa.features.common import Characteristic
|
||||
from capa.features.extractors import loops
|
||||
|
||||
|
||||
|
||||
@@ -6,26 +6,27 @@
|
||||
# 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 idc
|
||||
import idaapi
|
||||
import idautils
|
||||
import ida_bytes
|
||||
|
||||
|
||||
def find_byte_sequence(start, end, seq):
|
||||
"""find byte sequence
|
||||
"""yield all ea of a given byte sequence
|
||||
|
||||
args:
|
||||
start: min virtual address
|
||||
end: max virtual address
|
||||
seq: bytes to search e.g. b'\x01\x03'
|
||||
seq: bytes to search e.g. b"\x01\x03"
|
||||
"""
|
||||
if sys.version_info[0] >= 3:
|
||||
return idaapi.find_binary(start, end, " ".join(["%02x" % b for b in seq]), 0, idaapi.SEARCH_DOWN)
|
||||
else:
|
||||
return idaapi.find_binary(start, end, " ".join(["%02x" % ord(b) for b in seq]), 0, idaapi.SEARCH_DOWN)
|
||||
seq = " ".join(["%02x" % b for b in seq])
|
||||
while True:
|
||||
ea = idaapi.find_binary(start, end, seq, 0, idaapi.SEARCH_DOWN)
|
||||
if ea == idaapi.BADADDR:
|
||||
break
|
||||
start = ea + 1
|
||||
yield ea
|
||||
|
||||
|
||||
def get_functions(start=None, end=None, skip_thunks=False, skip_libs=False):
|
||||
@@ -75,7 +76,7 @@ def get_segment_buffer(seg):
|
||||
|
||||
|
||||
def get_file_imports():
|
||||
""" get file imports """
|
||||
"""get file imports"""
|
||||
imports = {}
|
||||
|
||||
for idx in range(idaapi.get_import_module_qty()):
|
||||
@@ -112,7 +113,7 @@ def get_instructions_in_range(start, end):
|
||||
|
||||
|
||||
def is_operand_equal(op1, op2):
|
||||
""" compare two IDA op_t """
|
||||
"""compare two IDA op_t"""
|
||||
if op1.flags != op2.flags:
|
||||
return False
|
||||
|
||||
@@ -138,7 +139,7 @@ def is_operand_equal(op1, op2):
|
||||
|
||||
|
||||
def is_basic_block_equal(bb1, bb2):
|
||||
""" compare two IDA BasicBlock """
|
||||
"""compare two IDA BasicBlock"""
|
||||
if bb1.start_ea != bb2.start_ea:
|
||||
return False
|
||||
|
||||
@@ -152,12 +153,16 @@ def is_basic_block_equal(bb1, bb2):
|
||||
|
||||
|
||||
def basic_block_size(bb):
|
||||
""" calculate size of basic block """
|
||||
"""calculate size of basic block"""
|
||||
return bb.end_ea - bb.start_ea
|
||||
|
||||
|
||||
def read_bytes_at(ea, count):
|
||||
""" """
|
||||
# check if byte has a value, see get_wide_byte doc
|
||||
if not idc.is_loaded(ea):
|
||||
return b""
|
||||
|
||||
segm_end = idc.get_segm_end(ea)
|
||||
if ea + count > segm_end:
|
||||
return idc.get_bytes(ea, segm_end - ea)
|
||||
@@ -166,7 +171,7 @@ def read_bytes_at(ea, count):
|
||||
|
||||
|
||||
def find_string_at(ea, min=4):
|
||||
""" check if ASCII string exists at a given virtual address """
|
||||
"""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:
|
||||
try:
|
||||
@@ -220,17 +225,23 @@ def get_op_phrase_info(op):
|
||||
|
||||
|
||||
def is_op_write(insn, op):
|
||||
""" Check if an operand is written to (destination operand) """
|
||||
"""Check if an operand is written to (destination operand)"""
|
||||
return idaapi.has_cf_chg(insn.get_canon_feature(), op.n)
|
||||
|
||||
|
||||
def is_op_read(insn, op):
|
||||
""" Check if an operand is read from (source operand) """
|
||||
"""Check if an operand is read from (source operand)"""
|
||||
return idaapi.has_cf_use(insn.get_canon_feature(), op.n)
|
||||
|
||||
|
||||
def is_op_offset(insn, op):
|
||||
"""Check is an operand has been marked as an offset (by auto-analysis or manually)"""
|
||||
flags = idaapi.get_flags(insn.ea)
|
||||
return ida_bytes.is_off(flags, op.n)
|
||||
|
||||
|
||||
def is_sp_modified(insn):
|
||||
""" determine if instruction modifies SP, ESP, RSP """
|
||||
"""determine if instruction modifies SP, ESP, RSP"""
|
||||
for op in get_insn_ops(insn, target_ops=(idaapi.o_reg,)):
|
||||
if op.reg == idautils.procregs.sp.reg and is_op_write(insn, op):
|
||||
# register is stack and written
|
||||
@@ -239,7 +250,7 @@ def is_sp_modified(insn):
|
||||
|
||||
|
||||
def is_bp_modified(insn):
|
||||
""" check if instruction modifies BP, EBP, RBP """
|
||||
"""check if instruction modifies BP, EBP, RBP"""
|
||||
for op in get_insn_ops(insn, target_ops=(idaapi.o_reg,)):
|
||||
if op.reg == idautils.procregs.bp.reg and is_op_write(insn, op):
|
||||
# register is base and written
|
||||
@@ -248,12 +259,12 @@ def is_bp_modified(insn):
|
||||
|
||||
|
||||
def is_frame_register(reg):
|
||||
""" check if register is sp or bp """
|
||||
"""check if register is sp or bp"""
|
||||
return reg in (idautils.procregs.sp.reg, idautils.procregs.bp.reg)
|
||||
|
||||
|
||||
def get_insn_ops(insn, target_ops=()):
|
||||
""" yield op_t for instruction, filter on type if specified """
|
||||
"""yield op_t for instruction, filter on type if specified"""
|
||||
for op in insn.ops:
|
||||
if op.type == idaapi.o_void:
|
||||
# avoid looping all 6 ops if only subset exists
|
||||
@@ -264,7 +275,7 @@ def get_insn_ops(insn, target_ops=()):
|
||||
|
||||
|
||||
def is_op_stack_var(ea, index):
|
||||
""" check if operand is a stack variable """
|
||||
"""check if operand is a stack variable"""
|
||||
return idaapi.is_stkvar(idaapi.get_flags(ea), index)
|
||||
|
||||
|
||||
@@ -318,7 +329,7 @@ def is_basic_block_tight_loop(bb):
|
||||
|
||||
|
||||
def find_data_reference_from_insn(insn, max_depth=10):
|
||||
""" search for data reference from instruction, return address of instruction if no reference exists """
|
||||
"""search for data reference from instruction, return address of instruction if no reference exists"""
|
||||
depth = 0
|
||||
ea = insn.ea
|
||||
|
||||
@@ -333,6 +344,10 @@ def find_data_reference_from_insn(insn, max_depth=10):
|
||||
# break if circular reference
|
||||
break
|
||||
|
||||
if not idaapi.is_mapped(data_refs[0]):
|
||||
# break if address is not mapped
|
||||
break
|
||||
|
||||
depth += 1
|
||||
if depth > max_depth:
|
||||
# break if max depth
|
||||
@@ -357,5 +372,5 @@ def get_function_blocks(f):
|
||||
|
||||
|
||||
def is_basic_block_return(bb):
|
||||
""" check if basic block is return block """
|
||||
"""check if basic block is return block"""
|
||||
return bb.type == idaapi.fcb_ret
|
||||
|
||||
@@ -12,8 +12,16 @@ import idautils
|
||||
|
||||
import capa.features.extractors.helpers
|
||||
import capa.features.extractors.ida.helpers
|
||||
from capa.features import ARCH_X32, ARCH_X64, MAX_BYTES_FEATURE_SIZE, Bytes, String, Characteristic
|
||||
from capa.features.insn import Number, Offset, Mnemonic
|
||||
from capa.features.insn import API, Number, Offset, Mnemonic
|
||||
from capa.features.common import (
|
||||
ARCH_X32,
|
||||
ARCH_X64,
|
||||
MAX_BYTES_FEATURE_SIZE,
|
||||
THUNK_CHAIN_DEPTH_DELTA,
|
||||
Bytes,
|
||||
String,
|
||||
Characteristic,
|
||||
)
|
||||
|
||||
# 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
|
||||
@@ -45,24 +53,32 @@ def get_imports(ctx):
|
||||
|
||||
|
||||
def check_for_api_call(ctx, insn):
|
||||
""" check instruction for API call """
|
||||
if not idaapi.is_call_insn(insn):
|
||||
return
|
||||
"""check instruction for API call"""
|
||||
info = ()
|
||||
ref = insn.ea
|
||||
|
||||
# attempt to resolve API calls by following chained thunks to a reasonable depth
|
||||
for _ in range(THUNK_CHAIN_DEPTH_DELTA):
|
||||
# assume only one code/data ref when resolving "call" or "jmp"
|
||||
try:
|
||||
ref = tuple(idautils.CodeRefsFrom(ref, False))[0]
|
||||
except IndexError:
|
||||
try:
|
||||
# thunks may be marked as data refs
|
||||
ref = tuple(idautils.DataRefsFrom(ref))[0]
|
||||
except IndexError:
|
||||
break
|
||||
|
||||
for ref in idautils.CodeRefsFrom(insn.ea, False):
|
||||
info = get_imports(ctx).get(ref, ())
|
||||
if info:
|
||||
yield "%s.%s" % (info[0], info[1])
|
||||
else:
|
||||
f = idaapi.get_func(ref)
|
||||
# check if call to thunk
|
||||
# TODO: first instruction might not always be the thunk
|
||||
if f and (f.flags & idaapi.FUNC_THUNK):
|
||||
for thunk_ref in idautils.DataRefsFrom(ref):
|
||||
# TODO: always data ref for thunk??
|
||||
info = get_imports(ctx).get(thunk_ref, ())
|
||||
if info:
|
||||
yield "%s.%s" % (info[0], info[1])
|
||||
break
|
||||
|
||||
f = idaapi.get_func(ref)
|
||||
if not f or not (f.flags & idaapi.FUNC_THUNK):
|
||||
break
|
||||
|
||||
if info:
|
||||
yield "%s.%s" % (info[0], info[1])
|
||||
|
||||
|
||||
def extract_insn_api_features(f, bb, insn):
|
||||
@@ -76,9 +92,28 @@ def extract_insn_api_features(f, bb, insn):
|
||||
example:
|
||||
call dword [0x00473038]
|
||||
"""
|
||||
if not insn.get_canon_mnem() in ("call", "jmp"):
|
||||
return
|
||||
|
||||
for api in check_for_api_call(f.ctx, insn):
|
||||
for (feature, ea) in capa.features.extractors.helpers.generate_api_features(api, insn.ea):
|
||||
yield feature, ea
|
||||
dll, _, symbol = api.rpartition(".")
|
||||
for name in capa.features.extractors.helpers.generate_symbols(dll, symbol):
|
||||
yield API(name), insn.ea
|
||||
|
||||
# extract IDA/FLIRT recognized API functions
|
||||
targets = tuple(idautils.CodeRefsFrom(insn.ea, False))
|
||||
if not targets:
|
||||
return
|
||||
|
||||
target = targets[0]
|
||||
target_func = idaapi.get_func(target)
|
||||
if not target_func or target_func.start_ea != target:
|
||||
# not a function (start)
|
||||
return
|
||||
|
||||
if target_func.flags & idaapi.FUNC_LIB:
|
||||
name = idaapi.get_name(target_func.start_ea)
|
||||
yield API(name), insn.ea
|
||||
|
||||
|
||||
def extract_insn_number_features(f, bb, insn):
|
||||
@@ -103,13 +138,18 @@ def extract_insn_number_features(f, bb, insn):
|
||||
return
|
||||
|
||||
for op in capa.features.extractors.ida.helpers.get_insn_ops(insn, target_ops=(idaapi.o_imm, idaapi.o_mem)):
|
||||
# skip things like:
|
||||
# .text:00401100 shr eax, offset loc_C
|
||||
if capa.features.extractors.ida.helpers.is_op_offset(insn, op):
|
||||
continue
|
||||
|
||||
if op.type == idaapi.o_imm:
|
||||
const = capa.features.extractors.ida.helpers.mask_op_val(op)
|
||||
else:
|
||||
const = op.addr
|
||||
if not idaapi.is_mapped(const):
|
||||
yield Number(const), insn.ea
|
||||
yield Number(const, arch=get_arch(f.ctx)), insn.ea
|
||||
|
||||
yield Number(const), insn.ea
|
||||
yield Number(const, arch=get_arch(f.ctx)), insn.ea
|
||||
|
||||
|
||||
def extract_insn_bytes_features(f, bb, insn):
|
||||
@@ -123,6 +163,9 @@ def extract_insn_bytes_features(f, bb, insn):
|
||||
example:
|
||||
push offset iid_004118d4_IShellLinkA ; riid
|
||||
"""
|
||||
if idaapi.is_call_insn(insn):
|
||||
return
|
||||
|
||||
ref = capa.features.extractors.ida.helpers.find_data_reference_from_insn(insn)
|
||||
if ref != insn.ea:
|
||||
extracted_bytes = capa.features.extractors.ida.helpers.read_bytes_at(ref, MAX_BYTES_FEATURE_SIZE)
|
||||
@@ -228,7 +271,7 @@ def bb_stack_cookie_registers(bb):
|
||||
|
||||
|
||||
def is_nzxor_stack_cookie_delta(f, bb, insn):
|
||||
""" check if nzxor exists within stack cookie delta """
|
||||
"""check if nzxor exists within stack cookie delta"""
|
||||
# security cookie check should use SP or BP
|
||||
if not capa.features.extractors.ida.helpers.is_frame_register(insn.Op2.reg):
|
||||
return False
|
||||
@@ -251,7 +294,7 @@ def is_nzxor_stack_cookie_delta(f, bb, insn):
|
||||
|
||||
|
||||
def is_nzxor_stack_cookie(f, bb, insn):
|
||||
""" check if nzxor is related to stack cookie """
|
||||
"""check if nzxor is related to stack cookie"""
|
||||
if contains_stack_cookie_keywords(idaapi.get_cmt(insn.ea, False)):
|
||||
# Example:
|
||||
# xor ecx, ebp ; StackCookie
|
||||
@@ -277,7 +320,7 @@ def extract_insn_nzxor_characteristic_features(f, bb, insn):
|
||||
bb (IDA BasicBlock)
|
||||
insn (IDA insn_t)
|
||||
"""
|
||||
if insn.itype != idaapi.NN_xor:
|
||||
if insn.itype not in (idaapi.NN_xor, idaapi.NN_xorpd, idaapi.NN_xorps, idaapi.NN_pxor):
|
||||
return
|
||||
if capa.features.extractors.ida.helpers.is_operand_equal(insn.Op1, insn.Op2):
|
||||
return
|
||||
|
||||
173
capa/features/extractors/pefile.py
Normal file
@@ -0,0 +1,173 @@
|
||||
# Copyright (C) 2020 FireEye, 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 logging
|
||||
|
||||
import pefile
|
||||
|
||||
import capa.features.extractors
|
||||
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.extractors.base_extractor import FeatureExtractor
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def extract_file_embedded_pe(pe, file_path):
|
||||
with open(file_path, "rb") as f:
|
||||
fbytes = f.read()
|
||||
|
||||
for offset, i in capa.features.extractors.helpers.carve_pe(fbytes, 1):
|
||||
yield Characteristic("embedded pe"), offset
|
||||
|
||||
|
||||
def extract_file_export_names(pe, file_path):
|
||||
base_address = pe.OPTIONAL_HEADER.ImageBase
|
||||
|
||||
if hasattr(pe, "DIRECTORY_ENTRY_EXPORT"):
|
||||
for export in pe.DIRECTORY_ENTRY_EXPORT.symbols:
|
||||
try:
|
||||
name = export.name.partition(b"\x00")[0].decode("ascii")
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
va = base_address + export.address
|
||||
yield Export(name), va
|
||||
|
||||
|
||||
def extract_file_import_names(pe, file_path):
|
||||
"""
|
||||
extract imported function names
|
||||
1. imports by ordinal:
|
||||
- modulename.#ordinal
|
||||
2. imports by name, results in two features to support importname-only matching:
|
||||
- modulename.importname
|
||||
- importname
|
||||
"""
|
||||
if hasattr(pe, "DIRECTORY_ENTRY_IMPORT"):
|
||||
for dll in pe.DIRECTORY_ENTRY_IMPORT:
|
||||
try:
|
||||
modname = dll.dll.partition(b"\x00")[0].decode("ascii")
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
|
||||
# strip extension
|
||||
modname = modname.rpartition(".")[0].lower()
|
||||
|
||||
for imp in dll.imports:
|
||||
if imp.import_by_ordinal:
|
||||
impname = "#%s" % imp.ordinal
|
||||
else:
|
||||
try:
|
||||
impname = imp.name.partition(b"\x00")[0].decode("ascii")
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
|
||||
for name in capa.features.extractors.helpers.generate_symbols(modname, impname):
|
||||
yield Import(name), imp.address
|
||||
|
||||
|
||||
def extract_file_section_names(pe, file_path):
|
||||
base_address = pe.OPTIONAL_HEADER.ImageBase
|
||||
|
||||
for section in pe.sections:
|
||||
try:
|
||||
name = section.Name.partition(b"\x00")[0].decode("ascii")
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
|
||||
yield Section(name), base_address + section.VirtualAddress
|
||||
|
||||
|
||||
def extract_file_strings(pe, file_path):
|
||||
"""
|
||||
extract ASCII and UTF-16 LE strings from file
|
||||
"""
|
||||
with open(file_path, "rb") as f:
|
||||
b = f.read()
|
||||
|
||||
for s in capa.features.extractors.strings.extract_ascii_strings(b):
|
||||
yield String(s.s), s.offset
|
||||
|
||||
for s in capa.features.extractors.strings.extract_unicode_strings(b):
|
||||
yield String(s.s), s.offset
|
||||
|
||||
|
||||
def extract_file_function_names(pe, file_path):
|
||||
"""
|
||||
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("pefile doesn't have library matching")
|
||||
return
|
||||
|
||||
|
||||
def extract_file_features(pe, file_path):
|
||||
"""
|
||||
extract file features from given workspace
|
||||
|
||||
args:
|
||||
pe (pefile.PE): the parsed PE
|
||||
file_path: path to the input file
|
||||
|
||||
yields:
|
||||
Tuple[Feature, VA]: a feature and its location.
|
||||
"""
|
||||
|
||||
for file_handler in FILE_HANDLERS:
|
||||
for feature, va in file_handler(pe, file_path):
|
||||
yield feature, va
|
||||
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
class PefileFeatureExtractor(FeatureExtractor):
|
||||
def __init__(self, path: str):
|
||||
super(PefileFeatureExtractor, self).__init__()
|
||||
self.path = path
|
||||
self.pe = pefile.PE(path)
|
||||
|
||||
def get_base_address(self):
|
||||
return self.pe.OPTIONAL_HEADER.ImageBase
|
||||
|
||||
def extract_file_features(self):
|
||||
for feature, va in extract_file_features(self.pe, self.path):
|
||||
yield feature, va
|
||||
|
||||
def get_functions(self):
|
||||
raise NotImplementedError("PefileFeatureExtract can only be used to extract file features")
|
||||
|
||||
def extract_function_features(self, f):
|
||||
raise NotImplementedError("PefileFeatureExtract can only be used to extract file features")
|
||||
|
||||
def get_basic_blocks(self, f):
|
||||
raise NotImplementedError("PefileFeatureExtract can only be used to extract file features")
|
||||
|
||||
def extract_basic_block_features(self, f, bb):
|
||||
raise NotImplementedError("PefileFeatureExtract can only be used to extract file features")
|
||||
|
||||
def get_instructions(self, f, bb):
|
||||
raise NotImplementedError("PefileFeatureExtract can only be used to extract file features")
|
||||
|
||||
def extract_insn_features(self, f, bb, insn):
|
||||
raise NotImplementedError("PefileFeatureExtract can only be used to extract file features")
|
||||
|
||||
def is_library_function(self, va):
|
||||
raise NotImplementedError("PefileFeatureExtract can only be used to extract file features")
|
||||
|
||||
def get_function_name(self, va):
|
||||
raise NotImplementedError("PefileFeatureExtract can only be used to extract file features")
|
||||
130
capa/features/extractors/smda/basicblock.py
Normal file
@@ -0,0 +1,130 @@
|
||||
import string
|
||||
import struct
|
||||
|
||||
from capa.features.common import Characteristic
|
||||
from capa.features.basicblock import BasicBlock
|
||||
from capa.features.extractors.helpers import MIN_STACKSTRING_LEN
|
||||
|
||||
|
||||
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, bb):
|
||||
"""check basic block for tight loop indicators"""
|
||||
if _bb_has_tight_loop(f, bb):
|
||||
yield Characteristic("tight loop"), bb.offset
|
||||
|
||||
|
||||
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, bb):
|
||||
"""check basic block for stackstring indicators"""
|
||||
if _bb_has_stackstring(f, bb):
|
||||
yield Characteristic("stack string"), bb.offset
|
||||
|
||||
|
||||
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, bb):
|
||||
"""
|
||||
extract features from the given basic block.
|
||||
|
||||
args:
|
||||
f (smda.common.SmdaFunction): the function from which to extract features
|
||||
bb (smda.common.SmdaBasicBlock): the basic block to process.
|
||||
|
||||
yields:
|
||||
Tuple[Feature, int]: the features and their location found in this basic block.
|
||||
"""
|
||||
yield BasicBlock(), bb.offset
|
||||
for bb_handler in BASIC_BLOCK_HANDLERS:
|
||||
for feature, va in bb_handler(f, bb):
|
||||
yield feature, va
|
||||
|
||||
|
||||
BASIC_BLOCK_HANDLERS = (
|
||||
extract_bb_tight_loop,
|
||||
extract_stackstring,
|
||||
)
|
||||
45
capa/features/extractors/smda/extractor.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from smda.common.SmdaReport import SmdaReport
|
||||
|
||||
import capa.features.extractors.smda.file
|
||||
import capa.features.extractors.smda.insn
|
||||
import capa.features.extractors.smda.function
|
||||
import capa.features.extractors.smda.basicblock
|
||||
from capa.features.extractors.base_extractor import FeatureExtractor
|
||||
|
||||
|
||||
class SmdaFeatureExtractor(FeatureExtractor):
|
||||
def __init__(self, smda_report: SmdaReport, path):
|
||||
super(SmdaFeatureExtractor, self).__init__()
|
||||
self.smda_report = smda_report
|
||||
self.path = path
|
||||
|
||||
def get_base_address(self):
|
||||
return self.smda_report.base_addr
|
||||
|
||||
def extract_file_features(self):
|
||||
for feature, va in capa.features.extractors.smda.file.extract_features(self.smda_report, self.path):
|
||||
yield feature, va
|
||||
|
||||
def get_functions(self):
|
||||
for function in self.smda_report.getFunctions():
|
||||
yield function
|
||||
|
||||
def extract_function_features(self, f):
|
||||
for feature, va in capa.features.extractors.smda.function.extract_features(f):
|
||||
yield feature, va
|
||||
|
||||
def get_basic_blocks(self, f):
|
||||
for bb in f.getBlocks():
|
||||
yield bb
|
||||
|
||||
def extract_basic_block_features(self, f, bb):
|
||||
for feature, va in capa.features.extractors.smda.basicblock.extract_features(f, bb):
|
||||
yield feature, va
|
||||
|
||||
def get_instructions(self, f, bb):
|
||||
for smda_ins in bb.getInstructions():
|
||||
yield smda_ins
|
||||
|
||||
def extract_insn_features(self, f, bb, insn):
|
||||
for feature, va in capa.features.extractors.smda.insn.extract_features(f, bb, insn):
|
||||
yield feature, va
|
||||
101
capa/features/extractors/smda/file.py
Normal file
@@ -0,0 +1,101 @@
|
||||
# if we have SMDA we definitely have lief
|
||||
import lief
|
||||
|
||||
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
|
||||
|
||||
|
||||
def extract_file_embedded_pe(smda_report, file_path):
|
||||
with open(file_path, "rb") as f:
|
||||
fbytes = f.read()
|
||||
|
||||
for offset, i in capa.features.extractors.helpers.carve_pe(fbytes, 1):
|
||||
yield Characteristic("embedded pe"), offset
|
||||
|
||||
|
||||
def extract_file_export_names(smda_report, file_path):
|
||||
lief_binary = lief.parse(file_path)
|
||||
if lief_binary is not None:
|
||||
for function in lief_binary.exported_functions:
|
||||
yield Export(function.name), function.address
|
||||
|
||||
|
||||
def extract_file_import_names(smda_report, file_path):
|
||||
# extract import table info via LIEF
|
||||
lief_binary = lief.parse(file_path)
|
||||
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), va
|
||||
elif func.is_ordinal:
|
||||
for name in capa.features.extractors.helpers.generate_symbols(library_name, "#%s" % func.ordinal):
|
||||
yield Import(name), va
|
||||
|
||||
|
||||
def extract_file_section_names(smda_report, file_path):
|
||||
lief_binary = lief.parse(file_path)
|
||||
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), base_address + section.virtual_address
|
||||
|
||||
|
||||
def extract_file_strings(smda_report, file_path):
|
||||
"""
|
||||
extract ASCII and UTF-16 LE strings from file
|
||||
"""
|
||||
with open(file_path, "rb") as f:
|
||||
b = f.read()
|
||||
|
||||
for s in capa.features.extractors.strings.extract_ascii_strings(b):
|
||||
yield String(s.s), s.offset
|
||||
|
||||
for s in capa.features.extractors.strings.extract_unicode_strings(b):
|
||||
yield String(s.s), s.offset
|
||||
|
||||
|
||||
def extract_file_function_names(smda_report, file_path):
|
||||
"""
|
||||
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_features(smda_report, file_path):
|
||||
"""
|
||||
extract file features from given workspace
|
||||
|
||||
args:
|
||||
smda_report (smda.common.SmdaReport): a SmdaReport
|
||||
file_path: path to the input file
|
||||
|
||||
yields:
|
||||
Tuple[Feature, VA]: a feature and its location.
|
||||
"""
|
||||
|
||||
for file_handler in FILE_HANDLERS:
|
||||
for feature, va in file_handler(smda_report, file_path):
|
||||
yield feature, va
|
||||
|
||||
|
||||
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,
|
||||
)
|
||||
38
capa/features/extractors/smda/function.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from capa.features.common import Characteristic
|
||||
from capa.features.extractors import loops
|
||||
|
||||
|
||||
def extract_function_calls_to(f):
|
||||
for inref in f.inrefs:
|
||||
yield Characteristic("calls to"), inref
|
||||
|
||||
|
||||
def extract_function_loop(f):
|
||||
"""
|
||||
parse if a function has a loop
|
||||
"""
|
||||
edges = []
|
||||
for bb_from, bb_tos in f.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.offset
|
||||
|
||||
|
||||
def extract_features(f):
|
||||
"""
|
||||
extract features from the given function.
|
||||
|
||||
args:
|
||||
f (smda.common.SmdaFunction): the function from which to extract features
|
||||
|
||||
yields:
|
||||
Tuple[Feature, int]: the features and their location found in this function.
|
||||
"""
|
||||
for func_handler in FUNCTION_HANDLERS:
|
||||
for feature, va in func_handler(f):
|
||||
yield feature, va
|
||||
|
||||
|
||||
FUNCTION_HANDLERS = (extract_function_calls_to, extract_function_loop)
|
||||
393
capa/features/extractors/smda/insn.py
Normal file
@@ -0,0 +1,393 @@
|
||||
import re
|
||||
import string
|
||||
import struct
|
||||
|
||||
from smda.common.SmdaReport import SmdaReport
|
||||
|
||||
import capa.features.extractors.helpers
|
||||
from capa.features.insn import API, Number, Offset, Mnemonic
|
||||
from capa.features.common import (
|
||||
ARCH_X32,
|
||||
ARCH_X64,
|
||||
MAX_BYTES_FEATURE_SIZE,
|
||||
THUNK_CHAIN_DEPTH_DELTA,
|
||||
Bytes,
|
||||
String,
|
||||
Characteristic,
|
||||
)
|
||||
|
||||
# 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 get_arch(smda_report):
|
||||
if smda_report.architecture == "intel":
|
||||
if smda_report.bitness == 32:
|
||||
return ARCH_X32
|
||||
elif smda_report.bitness == 64:
|
||||
return ARCH_X64
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def extract_insn_api_features(f, bb, insn):
|
||||
"""parse API features from the given instruction."""
|
||||
if insn.offset in f.apirefs:
|
||||
api_entry = f.apirefs[insn.offset]
|
||||
# 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), insn.offset
|
||||
elif insn.offset 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), insn.offset
|
||||
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(f, bb, insn):
|
||||
"""parse number features from the given instruction."""
|
||||
# example:
|
||||
#
|
||||
# push 3136B0h ; dwControlCode
|
||||
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 operand in operands:
|
||||
try:
|
||||
yield Number(int(operand, 16)), insn.offset
|
||||
yield Number(int(operand, 16), arch=get_arch(f.smda_report)), insn.offset
|
||||
except:
|
||||
continue
|
||||
|
||||
|
||||
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(f, bb, insn):
|
||||
"""
|
||||
parse byte sequence features from the given instruction.
|
||||
example:
|
||||
# push offset iid_004118d4_IShellLinkA ; riid
|
||||
"""
|
||||
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), insn.offset
|
||||
|
||||
|
||||
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(f, bb, insn):
|
||||
"""parse string features from the given instruction."""
|
||||
# example:
|
||||
#
|
||||
# push offset aAcr ; "ACR > "
|
||||
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")), insn.offset
|
||||
|
||||
|
||||
def extract_insn_offset_features(f, bb, insn):
|
||||
"""parse structure offset features from the given instruction."""
|
||||
# examples:
|
||||
#
|
||||
# mov eax, [esi + 4]
|
||||
# mov eax, [esi + ecx + 16384]
|
||||
operands = [o.strip() for o in insn.operands.split(",")]
|
||||
for operand in operands:
|
||||
if not "ptr" in operand:
|
||||
continue
|
||||
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
|
||||
yield Offset(number), insn.offset
|
||||
yield Offset(number, arch=get_arch(f.smda_report)), insn.offset
|
||||
|
||||
|
||||
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(f, bb, insn):
|
||||
"""
|
||||
parse non-zeroing XOR instruction from the given instruction.
|
||||
ignore expected non-zeroing XORs, e.g. security cookies.
|
||||
"""
|
||||
|
||||
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"), insn.offset
|
||||
|
||||
|
||||
def extract_insn_mnemonic_features(f, bb, insn):
|
||||
"""parse mnemonic features from the given instruction."""
|
||||
yield Mnemonic(insn.mnemonic), insn.offset
|
||||
|
||||
|
||||
def extract_insn_peb_access_characteristic_features(f, bb, insn):
|
||||
"""
|
||||
parse peb access from the given function. fs:[0x30] on x86, gs:[0x60] on x64
|
||||
"""
|
||||
|
||||
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"), insn.offset
|
||||
elif "gs:" in operand and "0x60" in operand:
|
||||
yield Characteristic("peb access"), insn.offset
|
||||
|
||||
|
||||
def extract_insn_segment_access_features(f, bb, insn):
|
||||
"""parse the instruction for access to fs or gs"""
|
||||
operands = [o.strip() for o in insn.operands.split(",")]
|
||||
for operand in operands:
|
||||
if "fs:" in operand:
|
||||
yield Characteristic("fs access"), insn.offset
|
||||
elif "gs:" in operand:
|
||||
yield Characteristic("gs access"), insn.offset
|
||||
|
||||
|
||||
def extract_insn_cross_section_cflow(f, bb, insn):
|
||||
"""
|
||||
inspect the instruction for a CALL or JMP that crosses section boundaries.
|
||||
"""
|
||||
if insn.mnemonic in ["call", "jmp"]:
|
||||
if insn.offset in f.apirefs:
|
||||
return
|
||||
|
||||
smda_report = insn.smda_function.smda_report
|
||||
if insn.offset in f.outrefs:
|
||||
for target in f.outrefs[insn.offset]:
|
||||
if smda_report.getSection(insn.offset) != smda_report.getSection(target):
|
||||
yield Characteristic("cross section flow"), insn.offset
|
||||
elif insn.operands.startswith("0x"):
|
||||
target = int(insn.operands, 16)
|
||||
if smda_report.getSection(insn.offset) != smda_report.getSection(target):
|
||||
yield Characteristic("cross section flow"), insn.offset
|
||||
|
||||
|
||||
# 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(f, bb, insn):
|
||||
if insn.mnemonic != "call":
|
||||
return
|
||||
|
||||
if insn.offset in f.outrefs:
|
||||
for outref in f.outrefs[insn.offset]:
|
||||
yield Characteristic("calls from"), outref
|
||||
|
||||
if outref == f.offset:
|
||||
# if we found a jump target and it's the function address
|
||||
# mark as recursive
|
||||
yield Characteristic("recursive call"), outref
|
||||
if insn.offset in f.apirefs:
|
||||
yield Characteristic("calls from"), insn.offset
|
||||
|
||||
|
||||
# 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, insn):
|
||||
"""
|
||||
extract indirect function call characteristic (e.g., call eax or call dword ptr [edx+4])
|
||||
does not include calls like => call ds:dword_ABD4974
|
||||
"""
|
||||
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"), insn.offset
|
||||
|
||||
|
||||
def extract_features(f, bb, insn):
|
||||
"""
|
||||
extract features from the given insn.
|
||||
|
||||
args:
|
||||
f (smda.common.SmdaFunction): the function to process.
|
||||
bb (smda.common.SmdaBasicBlock): the basic block to process.
|
||||
insn (smda.common.SmdaInstruction): the instruction to process.
|
||||
|
||||
yields:
|
||||
Tuple[Feature, int]: the features and their location found in this insn.
|
||||
"""
|
||||
for insn_handler in INSTRUCTION_HANDLERS:
|
||||
for feature, va in insn_handler(f, bb, insn):
|
||||
yield feature, va
|
||||
|
||||
|
||||
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_peb_access_characteristic_features,
|
||||
extract_insn_cross_section_cflow,
|
||||
extract_insn_segment_access_features,
|
||||
extract_function_calls_from,
|
||||
extract_function_indirect_call_characteristic_features,
|
||||
)
|
||||
@@ -1,85 +0,0 @@
|
||||
# Copyright (C) 2020 FireEye, 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 types
|
||||
|
||||
import file
|
||||
import insn
|
||||
import function
|
||||
import viv_utils
|
||||
import basicblock
|
||||
|
||||
import capa.features.extractors
|
||||
import capa.features.extractors.viv.file
|
||||
import capa.features.extractors.viv.insn
|
||||
import capa.features.extractors.viv.function
|
||||
import capa.features.extractors.viv.basicblock
|
||||
from capa.features.extractors import FeatureExtractor
|
||||
|
||||
__all__ = ["file", "function", "basicblock", "insn"]
|
||||
|
||||
|
||||
def get_va(self):
|
||||
try:
|
||||
# vivisect type
|
||||
return self.va
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
raise TypeError()
|
||||
|
||||
|
||||
def add_va_int_cast(o):
|
||||
"""
|
||||
dynamically add a cast-to-int (`__int__`) method to the given object
|
||||
that returns the value of the `.va` property.
|
||||
|
||||
this bit of skullduggery lets use cast viv-utils objects as ints.
|
||||
the correct way of doing this is to update viv-utils (or subclass the objects here).
|
||||
"""
|
||||
setattr(o, "__int__", types.MethodType(get_va, o, type(o)))
|
||||
return o
|
||||
|
||||
|
||||
class VivisectFeatureExtractor(FeatureExtractor):
|
||||
def __init__(self, vw, path):
|
||||
super(VivisectFeatureExtractor, self).__init__()
|
||||
self.vw = vw
|
||||
self.path = path
|
||||
|
||||
def get_base_address(self):
|
||||
# assume there is only one file loaded into the vw
|
||||
return list(self.vw.filemeta.values())[0]["imagebase"]
|
||||
|
||||
def extract_file_features(self):
|
||||
for feature, va in capa.features.extractors.viv.file.extract_features(self.vw, self.path):
|
||||
yield feature, va
|
||||
|
||||
def get_functions(self):
|
||||
for va in sorted(self.vw.getFunctions()):
|
||||
yield add_va_int_cast(viv_utils.Function(self.vw, va))
|
||||
|
||||
def extract_function_features(self, f):
|
||||
for feature, va in capa.features.extractors.viv.function.extract_features(f):
|
||||
yield feature, va
|
||||
|
||||
def get_basic_blocks(self, f):
|
||||
for bb in f.basic_blocks:
|
||||
yield add_va_int_cast(bb)
|
||||
|
||||
def extract_basic_block_features(self, f, bb):
|
||||
for feature, va in capa.features.extractors.viv.basicblock.extract_features(f, bb):
|
||||
yield feature, va
|
||||
|
||||
def get_instructions(self, f, bb):
|
||||
for insn in bb.instructions:
|
||||
yield add_va_int_cast(insn)
|
||||
|
||||
def extract_insn_features(self, f, bb, insn):
|
||||
for feature, va in capa.features.extractors.viv.insn.extract_features(f, bb, insn):
|
||||
yield feature, va
|
||||
|
||||
@@ -10,9 +10,9 @@ import string
|
||||
import struct
|
||||
|
||||
import envi
|
||||
import vivisect.const
|
||||
import envi.archs.i386.disasm
|
||||
|
||||
from capa.features import Characteristic
|
||||
from capa.features.common import Characteristic
|
||||
from capa.features.basicblock import BasicBlock
|
||||
from capa.features.extractors.helpers import MIN_STACKSTRING_LEN
|
||||
|
||||
@@ -37,7 +37,7 @@ def _bb_has_tight_loop(f, bb):
|
||||
"""
|
||||
if len(bb.instructions) > 0:
|
||||
for bva, bflags in bb.instructions[-1].getBranches():
|
||||
if bflags & vivisect.envi.BR_COND:
|
||||
if bflags & envi.BR_COND:
|
||||
if bva == bb.va:
|
||||
return True
|
||||
|
||||
@@ -45,7 +45,7 @@ def _bb_has_tight_loop(f, bb):
|
||||
|
||||
|
||||
def extract_bb_tight_loop(f, bb):
|
||||
""" check basic block for tight loop indicators """
|
||||
"""check basic block for tight loop indicators"""
|
||||
if _bb_has_tight_loop(f, bb):
|
||||
yield Characteristic("tight loop"), bb.va
|
||||
|
||||
@@ -68,12 +68,12 @@ def _bb_has_stackstring(f, bb):
|
||||
|
||||
|
||||
def extract_stackstring(f, bb):
|
||||
""" check basic block for stackstring indicators """
|
||||
"""check basic block for stackstring indicators"""
|
||||
if _bb_has_stackstring(f, bb):
|
||||
yield Characteristic("stack string"), bb.va
|
||||
|
||||
|
||||
def is_mov_imm_to_stack(instr):
|
||||
def is_mov_imm_to_stack(instr: envi.archs.i386.disasm.i386Opcode) -> bool:
|
||||
"""
|
||||
Return if instruction moves immediate onto stack
|
||||
"""
|
||||
@@ -105,7 +105,7 @@ def is_mov_imm_to_stack(instr):
|
||||
return True
|
||||
|
||||
|
||||
def get_printable_len(oper):
|
||||
def get_printable_len(oper: envi.archs.i386.disasm.i386ImmOper) -> int:
|
||||
"""
|
||||
Return string length if all operand bytes are ascii or utf16-le printable
|
||||
"""
|
||||
@@ -117,20 +117,30 @@ def get_printable_len(oper):
|
||||
chars = struct.pack("<I", oper.imm)
|
||||
elif oper.tsize == 8:
|
||||
chars = struct.pack("<Q", oper.imm)
|
||||
else:
|
||||
raise ValueError("unexpected oper.tsize: %d" % (oper.tsize))
|
||||
|
||||
if is_printable_ascii(chars):
|
||||
return oper.tsize
|
||||
if is_printable_utf16le(chars):
|
||||
elif is_printable_utf16le(chars):
|
||||
return oper.tsize / 2
|
||||
return 0
|
||||
else:
|
||||
return 0
|
||||
|
||||
|
||||
def is_printable_ascii(chars):
|
||||
return all(ord(c) < 127 and c in string.printable for c in chars)
|
||||
def is_printable_ascii(chars: bytes) -> bool:
|
||||
try:
|
||||
chars_str = chars.decode("ascii")
|
||||
except UnicodeDecodeError:
|
||||
return False
|
||||
else:
|
||||
return all(c in string.printable for c in chars_str)
|
||||
|
||||
|
||||
def is_printable_utf16le(chars):
|
||||
if all(c == "\x00" for c in chars[1::2]):
|
||||
def is_printable_utf16le(chars: bytes) -> bool:
|
||||
if all(c == b"\x00" for c in chars[1::2]):
|
||||
return is_printable_ascii(chars[::2])
|
||||
return False
|
||||
|
||||
|
||||
def extract_features(f, bb):
|
||||
@@ -142,7 +152,7 @@ def extract_features(f, bb):
|
||||
bb (viv_utils.BasicBlock): the basic block to process.
|
||||
|
||||
yields:
|
||||
Feature, set[VA]: the features and their location found in this basic block.
|
||||
Tuple[Feature, int]: the features and their location found in this basic block.
|
||||
"""
|
||||
yield BasicBlock(), bb.va
|
||||
for bb_handler in BASIC_BLOCK_HANDLERS:
|
||||
|
||||
76
capa/features/extractors/viv/extractor.py
Normal file
@@ -0,0 +1,76 @@
|
||||
# Copyright (C) 2020 FireEye, 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 logging
|
||||
|
||||
import viv_utils
|
||||
import viv_utils.flirt
|
||||
|
||||
import capa.features.extractors.viv.file
|
||||
import capa.features.extractors.viv.insn
|
||||
import capa.features.extractors.viv.function
|
||||
import capa.features.extractors.viv.basicblock
|
||||
from capa.features.extractors.base_extractor import FeatureExtractor
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class InstructionHandle:
|
||||
"""this acts like a vivisect.Opcode but with an __int__() method"""
|
||||
|
||||
def __init__(self, inner):
|
||||
self._inner = inner
|
||||
|
||||
def __int__(self):
|
||||
return self.va
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._inner, name)
|
||||
|
||||
|
||||
class VivisectFeatureExtractor(FeatureExtractor):
|
||||
def __init__(self, vw, path):
|
||||
super(VivisectFeatureExtractor, self).__init__()
|
||||
self.vw = vw
|
||||
self.path = path
|
||||
|
||||
def get_base_address(self):
|
||||
# assume there is only one file loaded into the vw
|
||||
return list(self.vw.filemeta.values())[0]["imagebase"]
|
||||
|
||||
def extract_file_features(self):
|
||||
for feature, va in capa.features.extractors.viv.file.extract_features(self.vw, self.path):
|
||||
yield feature, va
|
||||
|
||||
def get_functions(self):
|
||||
for va in sorted(self.vw.getFunctions()):
|
||||
yield viv_utils.Function(self.vw, va)
|
||||
|
||||
def extract_function_features(self, f):
|
||||
for feature, va in capa.features.extractors.viv.function.extract_features(f):
|
||||
yield feature, va
|
||||
|
||||
def get_basic_blocks(self, f):
|
||||
return f.basic_blocks
|
||||
|
||||
def extract_basic_block_features(self, f, bb):
|
||||
for feature, va in capa.features.extractors.viv.basicblock.extract_features(f, bb):
|
||||
yield feature, va
|
||||
|
||||
def get_instructions(self, f, bb):
|
||||
for insn in bb.instructions:
|
||||
yield InstructionHandle(insn)
|
||||
|
||||
def extract_insn_features(self, f, bb, insn):
|
||||
for feature, va in capa.features.extractors.viv.insn.extract_features(f, bb, insn):
|
||||
yield feature, va
|
||||
|
||||
def is_library_function(self, va):
|
||||
return viv_utils.flirt.is_library_function(self.vw, va)
|
||||
|
||||
def get_function_name(self, va):
|
||||
return viv_utils.get_function_name(self.vw, va)
|
||||
@@ -7,10 +7,14 @@
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import PE.carve as pe_carve # vivisect PE
|
||||
import viv_utils
|
||||
import viv_utils.flirt
|
||||
|
||||
import capa.features.insn
|
||||
import capa.features.extractors.helpers
|
||||
import capa.features.extractors.strings
|
||||
from capa.features import String, Characteristic
|
||||
from capa.features.file import Export, Import, Section
|
||||
from capa.features.file import Export, Import, Section, FunctionName
|
||||
from capa.features.common import String, Characteristic
|
||||
|
||||
|
||||
def extract_file_embedded_pe(vw, file_path):
|
||||
@@ -41,14 +45,12 @@ def extract_file_import_names(vw, file_path):
|
||||
if is_viv_ord_impname(impname):
|
||||
# replace ord prefix with #
|
||||
impname = "#%s" % impname[len("ord") :]
|
||||
tinfo = "%s.%s" % (modname, impname)
|
||||
yield Import(tinfo), va
|
||||
else:
|
||||
yield Import(tinfo), va
|
||||
yield Import(impname), va
|
||||
|
||||
for name in capa.features.extractors.helpers.generate_symbols(modname, impname):
|
||||
yield Import(name), va
|
||||
|
||||
|
||||
def is_viv_ord_impname(impname):
|
||||
def is_viv_ord_impname(impname: str) -> bool:
|
||||
"""
|
||||
return if import name matches vivisect's ordinal naming scheme `'ord%d' % ord`
|
||||
"""
|
||||
@@ -81,6 +83,16 @@ def extract_file_strings(vw, file_path):
|
||||
yield String(s.s), s.offset
|
||||
|
||||
|
||||
def extract_file_function_names(vw, file_path):
|
||||
"""
|
||||
extract the names of statically-linked library functions.
|
||||
"""
|
||||
for va in sorted(vw.getFunctions()):
|
||||
if viv_utils.flirt.is_library_function(vw, va):
|
||||
name = viv_utils.get_function_name(vw, va)
|
||||
yield FunctionName(name), va
|
||||
|
||||
|
||||
def extract_features(vw, file_path):
|
||||
"""
|
||||
extract file features from given workspace
|
||||
@@ -104,4 +116,5 @@ FILE_HANDLERS = (
|
||||
extract_file_import_names,
|
||||
extract_file_section_names,
|
||||
extract_file_strings,
|
||||
extract_file_function_names,
|
||||
)
|
||||
|
||||
@@ -6,9 +6,10 @@
|
||||
# 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 envi
|
||||
import vivisect.const
|
||||
|
||||
from capa.features import Characteristic
|
||||
from capa.features.common import Characteristic
|
||||
from capa.features.extractors import loops
|
||||
|
||||
|
||||
@@ -41,9 +42,9 @@ def extract_function_loop(f):
|
||||
for bva, bflags in bb.instructions[-1].getBranches():
|
||||
# vivisect does not set branch flags for non-conditional jmp so add explicit check
|
||||
if (
|
||||
bflags & vivisect.envi.BR_COND
|
||||
or bflags & vivisect.envi.BR_FALL
|
||||
or bflags & vivisect.envi.BR_TABLE
|
||||
bflags & envi.BR_COND
|
||||
or bflags & envi.BR_FALL
|
||||
or bflags & envi.BR_TABLE
|
||||
or bb.instructions[-1].mnem == "jmp"
|
||||
):
|
||||
edges.append((bb.va, bva))
|
||||
@@ -60,7 +61,7 @@ def extract_features(f):
|
||||
f (viv_utils.Function): the function from which to extract features
|
||||
|
||||
yields:
|
||||
Feature, set[VA]: the features and their location found in this function.
|
||||
Tuple[Feature, int]: the features and their location found in this function.
|
||||
"""
|
||||
for func_handler in FUNCTION_HANDLERS:
|
||||
for feature, va in func_handler(f):
|
||||
|
||||
23
capa/features/extractors/viv/helpers.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Copyright (C) 2020 FireEye, 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 Optional
|
||||
|
||||
from vivisect import VivWorkspace
|
||||
from vivisect.const import XR_TO, REF_CODE
|
||||
|
||||
|
||||
def get_coderef_from(vw: VivWorkspace, va: int) -> Optional[int]:
|
||||
"""
|
||||
return first code `tova` whose origin is the specified va
|
||||
return None if no code reference is found
|
||||
"""
|
||||
xrefs = vw.getXrefsFrom(va, REF_CODE)
|
||||
if len(xrefs) > 0:
|
||||
return xrefs[0][XR_TO]
|
||||
else:
|
||||
return None
|
||||
@@ -7,11 +7,16 @@
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import collections
|
||||
from typing import TYPE_CHECKING, Set, List, Deque, Tuple, Union, Optional
|
||||
|
||||
import envi
|
||||
import vivisect.const
|
||||
import envi.archs.i386.disasm
|
||||
import envi.archs.amd64.disasm
|
||||
from vivisect import VivWorkspace
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from capa.features.extractors.viv.extractor import InstructionHandle
|
||||
|
||||
# pull out consts for lookup performance
|
||||
i386RegOper = envi.archs.i386.disasm.i386RegOper
|
||||
@@ -26,7 +31,7 @@ FAR_BRANCH_MASK = envi.BR_PROC | envi.BR_DEREF | envi.BR_ARCH
|
||||
DESTRUCTIVE_MNEMONICS = ("mov", "lea", "pop", "xor")
|
||||
|
||||
|
||||
def get_previous_instructions(vw, va):
|
||||
def get_previous_instructions(vw: VivWorkspace, va: int) -> List[int]:
|
||||
"""
|
||||
collect the instructions that flow to the given address, local to the current function.
|
||||
|
||||
@@ -43,12 +48,14 @@ def get_previous_instructions(vw, va):
|
||||
# ensure that it fallsthrough to this one.
|
||||
loc = vw.getPrevLocation(va, adjacent=True)
|
||||
if loc is not None:
|
||||
# from vivisect.const:
|
||||
# location: (L_VA, L_SIZE, L_LTYPE, L_TINFO)
|
||||
(pva, _, ptype, pinfo) = vw.getPrevLocation(va, adjacent=True)
|
||||
ploc = vw.getPrevLocation(va, adjacent=True)
|
||||
if ploc is not None:
|
||||
# from vivisect.const:
|
||||
# location: (L_VA, L_SIZE, L_LTYPE, L_TINFO)
|
||||
(pva, _, ptype, pinfo) = ploc
|
||||
|
||||
if ptype == LOC_OP and not (pinfo & IF_NOFALL):
|
||||
ret.append(pva)
|
||||
if ptype == LOC_OP and not (pinfo & IF_NOFALL):
|
||||
ret.append(pva)
|
||||
|
||||
# find any code refs, e.g. jmp, to this location.
|
||||
# ignore any calls.
|
||||
@@ -67,7 +74,7 @@ class NotFoundError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def find_definition(vw, va, reg):
|
||||
def find_definition(vw: VivWorkspace, va: int, reg: int) -> Tuple[int, Union[int, None]]:
|
||||
"""
|
||||
scan backwards from the given address looking for assignments to the given register.
|
||||
if a constant, return that value.
|
||||
@@ -83,8 +90,8 @@ def find_definition(vw, va, reg):
|
||||
raises:
|
||||
NotFoundError: when the definition cannot be found.
|
||||
"""
|
||||
q = collections.deque()
|
||||
seen = set([])
|
||||
q = collections.deque() # type: Deque[int]
|
||||
seen = set([]) # type: Set[int]
|
||||
|
||||
q.extend(get_previous_instructions(vw, va))
|
||||
while q:
|
||||
@@ -128,14 +135,16 @@ def find_definition(vw, va, reg):
|
||||
raise NotFoundError()
|
||||
|
||||
|
||||
def is_indirect_call(vw, va, insn=None):
|
||||
def is_indirect_call(vw: VivWorkspace, va: int, insn: Optional["InstructionHandle"] = None) -> bool:
|
||||
if insn is None:
|
||||
insn = vw.parseOpcode(va)
|
||||
|
||||
return insn.mnem == "call" and isinstance(insn.opers[0], envi.archs.i386.disasm.i386RegOper)
|
||||
return insn.mnem in ("call", "jmp") and isinstance(insn.opers[0], envi.archs.i386.disasm.i386RegOper)
|
||||
|
||||
|
||||
def resolve_indirect_call(vw, va, insn=None):
|
||||
def resolve_indirect_call(
|
||||
vw: VivWorkspace, va: int, insn: Optional["InstructionHandle"] = None
|
||||
) -> Tuple[int, Optional[int]]:
|
||||
"""
|
||||
inspect the given indirect call instruction and attempt to resolve the target address.
|
||||
|
||||
|
||||
@@ -5,14 +5,28 @@
|
||||
# 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 envi
|
||||
import envi.exc
|
||||
import viv_utils
|
||||
import envi.memory
|
||||
import vivisect.const
|
||||
import viv_utils.flirt
|
||||
import envi.archs.i386.regs
|
||||
import envi.archs.amd64.regs
|
||||
import envi.archs.i386.disasm
|
||||
import envi.archs.amd64.disasm
|
||||
|
||||
import capa.features.extractors.helpers
|
||||
from capa.features import ARCH_X32, ARCH_X64, MAX_BYTES_FEATURE_SIZE, Bytes, String, Characteristic
|
||||
from capa.features.insn import Number, Offset, Mnemonic
|
||||
import capa.features.extractors.viv.helpers
|
||||
from capa.features.insn import API, Number, Offset, Mnemonic
|
||||
from capa.features.common import (
|
||||
ARCH_X32,
|
||||
ARCH_X64,
|
||||
MAX_BYTES_FEATURE_SIZE,
|
||||
THUNK_CHAIN_DEPTH_DELTA,
|
||||
Bytes,
|
||||
String,
|
||||
Characteristic,
|
||||
)
|
||||
from capa.features.extractors.viv.indirect_calls import NotFoundError, resolve_indirect_call
|
||||
|
||||
# security cookie checks may perform non-zeroing XORs, these are expected within a certain
|
||||
@@ -47,11 +61,15 @@ def get_imports(vw):
|
||||
"""
|
||||
caching accessor to vivisect workspace imports
|
||||
avoids performance issues in vivisect when collecting locations
|
||||
|
||||
returns: Dict[int, Tuple[str, str]]
|
||||
"""
|
||||
if "imports" in vw.metadata:
|
||||
return vw.metadata["imports"]
|
||||
else:
|
||||
imports = {p[0]: p[3] for p in vw.getImports()}
|
||||
imports = {
|
||||
p[0]: (p[3].rpartition(".")[0], p[3].replace(".ord", ".#").rpartition(".")[2]) for p in vw.getImports()
|
||||
}
|
||||
vw.metadata["imports"] = imports
|
||||
return imports
|
||||
|
||||
@@ -62,36 +80,56 @@ def extract_insn_api_features(f, bb, insn):
|
||||
# example:
|
||||
#
|
||||
# call dword [0x00473038]
|
||||
|
||||
if insn.mnem != "call":
|
||||
if insn.mnem not in ("call", "jmp"):
|
||||
return
|
||||
|
||||
if insn.mnem == "jmp":
|
||||
if f.vw.getFunctionMeta(f.va, "Thunk"):
|
||||
return
|
||||
|
||||
# traditional call via IAT
|
||||
if isinstance(insn.opers[0], envi.archs.i386.disasm.i386ImmMemOper):
|
||||
oper = insn.opers[0]
|
||||
target = oper.getOperAddr(insn)
|
||||
|
||||
imports = get_imports(f.vw)
|
||||
if target in imports.keys():
|
||||
for feature, va in capa.features.extractors.helpers.generate_api_features(imports[target], insn.va):
|
||||
yield feature, va
|
||||
if target in imports:
|
||||
dll, symbol = imports[target]
|
||||
for name in capa.features.extractors.helpers.generate_symbols(dll, symbol):
|
||||
yield API(name), insn.va
|
||||
|
||||
# call via thunk on x86,
|
||||
# see 9324d1a8ae37a36ae560c37448c9705a at 0x407985
|
||||
#
|
||||
# this is also how calls to internal functions may be decoded on x64.
|
||||
# this is also how calls to internal functions may be decoded on x32 and x64.
|
||||
# see Lab21-01.exe_:0x140001178
|
||||
elif isinstance(insn.opers[0], envi.archs.i386.disasm.i386PcRelOper):
|
||||
target = insn.opers[0].getOperValue(insn)
|
||||
#
|
||||
# follow chained thunks, e.g. in 82bf6347acf15e5d883715dc289d8a2b at 0x14005E0FF in
|
||||
# 0x140059342 (viv) / 0x14005E0C0 (IDA)
|
||||
# 14005E0FF call j_ElfClearEventLogFileW (14005AAF8)
|
||||
# 14005AAF8 jmp ElfClearEventLogFileW (14005E196)
|
||||
# 14005E196 jmp cs:__imp_ElfClearEventLogFileW
|
||||
|
||||
try:
|
||||
thunk = f.vw.getFunctionMeta(target, "Thunk")
|
||||
except vivisect.exc.InvalidFunction:
|
||||
elif isinstance(insn.opers[0], envi.archs.i386.disasm.i386PcRelOper):
|
||||
imports = get_imports(f.vw)
|
||||
target = capa.features.extractors.viv.helpers.get_coderef_from(f.vw, insn.va)
|
||||
if not target:
|
||||
return
|
||||
else:
|
||||
if thunk:
|
||||
for feature, va in capa.features.extractors.helpers.generate_api_features(thunk, insn.va):
|
||||
yield feature, va
|
||||
|
||||
if viv_utils.flirt.is_library_function(f.vw, target):
|
||||
name = viv_utils.get_function_name(f.vw, target)
|
||||
yield API(name), insn.va
|
||||
return
|
||||
|
||||
for _ in range(THUNK_CHAIN_DEPTH_DELTA):
|
||||
if target in imports:
|
||||
dll, symbol = imports[target]
|
||||
for name in capa.features.extractors.helpers.generate_symbols(dll, symbol):
|
||||
yield API(name), insn.va
|
||||
|
||||
target = capa.features.extractors.viv.helpers.get_coderef_from(f.vw, target)
|
||||
if not target:
|
||||
return
|
||||
|
||||
# call via import on x64
|
||||
# see Lab21-01.exe_:0x14000118C
|
||||
@@ -100,9 +138,10 @@ def extract_insn_api_features(f, bb, insn):
|
||||
target = op.getOperAddr(insn)
|
||||
|
||||
imports = get_imports(f.vw)
|
||||
if target in imports.keys():
|
||||
for feature, va in capa.features.extractors.helpers.generate_api_features(imports[target], insn.va):
|
||||
yield feature, va
|
||||
if target in imports:
|
||||
dll, symbol = imports[target]
|
||||
for name in capa.features.extractors.helpers.generate_symbols(dll, symbol):
|
||||
yield API(name), insn.va
|
||||
|
||||
elif isinstance(insn.opers[0], envi.archs.i386.disasm.i386RegOper):
|
||||
try:
|
||||
@@ -116,9 +155,10 @@ def extract_insn_api_features(f, bb, insn):
|
||||
return
|
||||
|
||||
imports = get_imports(f.vw)
|
||||
if target in imports.keys():
|
||||
for feature, va in capa.features.extractors.helpers.generate_api_features(imports[target], insn.va):
|
||||
yield feature, va
|
||||
if target in imports:
|
||||
dll, symbol = imports[target]
|
||||
for name in capa.features.extractors.helpers.generate_symbols(dll, symbol):
|
||||
yield API(name), insn.va
|
||||
|
||||
|
||||
def extract_insn_number_features(f, bb, insn):
|
||||
@@ -141,7 +181,7 @@ def extract_insn_number_features(f, bb, insn):
|
||||
# assume its not also a constant.
|
||||
continue
|
||||
|
||||
if insn.mnem == "add" and insn.opers[0].isReg() and insn.opers[0].reg == envi.archs.i386.disasm.REG_ESP:
|
||||
if insn.mnem == "add" and insn.opers[0].isReg() and insn.opers[0].reg == envi.archs.i386.regs.REG_ESP:
|
||||
# skip things like:
|
||||
#
|
||||
# .text:00401140 call sub_407E2B
|
||||
@@ -184,7 +224,7 @@ def derefs(vw, p):
|
||||
p = next
|
||||
|
||||
|
||||
def read_memory(vw, va, size):
|
||||
def read_memory(vw, va: int, size: int) -> bytes:
|
||||
# as documented in #176, vivisect will not readMemory() when the section is not marked readable.
|
||||
#
|
||||
# but here, we don't care about permissions.
|
||||
@@ -197,10 +237,10 @@ def read_memory(vw, va, size):
|
||||
mva, msize, mperms, mfname = mmap
|
||||
offset = va - mva
|
||||
return mbytes[offset : offset + size]
|
||||
raise envi.SegmentationViolation(va)
|
||||
raise envi.exc.SegmentationViolation(va)
|
||||
|
||||
|
||||
def read_bytes(vw, va):
|
||||
def read_bytes(vw, va: int) -> bytes:
|
||||
"""
|
||||
read up to MAX_BYTES_FEATURE_SIZE from the given address.
|
||||
|
||||
@@ -209,7 +249,7 @@ def read_bytes(vw, va):
|
||||
"""
|
||||
segm = vw.getSegment(va)
|
||||
if not segm:
|
||||
raise envi.SegmentationViolation()
|
||||
raise envi.exc.SegmentationViolation(va)
|
||||
|
||||
segm_end = segm[0] + segm[1]
|
||||
try:
|
||||
@@ -218,7 +258,7 @@ def read_bytes(vw, va):
|
||||
return read_memory(vw, va, segm_end - va)
|
||||
else:
|
||||
return read_memory(vw, va, MAX_BYTES_FEATURE_SIZE)
|
||||
except envi.SegmentationViolation:
|
||||
except envi.exc.SegmentationViolation:
|
||||
raise
|
||||
|
||||
|
||||
@@ -228,10 +268,10 @@ def extract_insn_bytes_features(f, bb, insn):
|
||||
example:
|
||||
# push offset iid_004118d4_IShellLinkA ; riid
|
||||
"""
|
||||
for oper in insn.opers:
|
||||
if insn.mnem == "call":
|
||||
continue
|
||||
if insn.mnem == "call":
|
||||
return
|
||||
|
||||
for oper in insn.opers:
|
||||
if isinstance(oper, envi.archs.i386.disasm.i386ImmOper):
|
||||
v = oper.getOperValue(oper)
|
||||
elif isinstance(oper, envi.archs.i386.disasm.i386RegMemOper):
|
||||
@@ -250,7 +290,7 @@ def extract_insn_bytes_features(f, bb, insn):
|
||||
for v in derefs(f.vw, v):
|
||||
try:
|
||||
buf = read_bytes(f.vw, v)
|
||||
except envi.SegmentationViolation:
|
||||
except envi.exc.SegmentationViolation:
|
||||
continue
|
||||
|
||||
if capa.features.extractors.helpers.all_zeros(buf):
|
||||
@@ -259,10 +299,10 @@ def extract_insn_bytes_features(f, bb, insn):
|
||||
yield Bytes(buf), insn.va
|
||||
|
||||
|
||||
def read_string(vw, offset):
|
||||
def read_string(vw, offset: int) -> str:
|
||||
try:
|
||||
alen = vw.detectString(offset)
|
||||
except envi.SegmentationViolation:
|
||||
except envi.exc.SegmentationViolation:
|
||||
pass
|
||||
else:
|
||||
if alen > 0:
|
||||
@@ -270,7 +310,7 @@ def read_string(vw, offset):
|
||||
|
||||
try:
|
||||
ulen = vw.detectUnicode(offset)
|
||||
except envi.SegmentationViolation:
|
||||
except envi.exc.SegmentationViolation:
|
||||
pass
|
||||
except IndexError:
|
||||
# potential vivisect bug detecting Unicode at segment end
|
||||
@@ -281,6 +321,10 @@ def read_string(vw, offset):
|
||||
# vivisect seems to mis-detect the end unicode strings
|
||||
# off by one, too short
|
||||
ulen += 1
|
||||
else:
|
||||
# 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")
|
||||
|
||||
raise ValueError("not a string", offset)
|
||||
@@ -295,6 +339,9 @@ def extract_insn_string_features(f, bb, insn):
|
||||
for oper in insn.opers:
|
||||
if isinstance(oper, envi.archs.i386.disasm.i386ImmOper):
|
||||
v = oper.getOperValue(oper)
|
||||
elif isinstance(oper, envi.archs.i386.disasm.i386ImmMemOper):
|
||||
# like 0x10056CB4 in `lea eax, dword [0x10056CB4]`
|
||||
v = oper.imm
|
||||
elif isinstance(oper, envi.archs.i386.disasm.i386SibOper):
|
||||
# like 0x401000 in `mov eax, 0x401000[2 * ebx]`
|
||||
v = oper.imm
|
||||
@@ -318,39 +365,52 @@ def extract_insn_offset_features(f, bb, insn):
|
||||
#
|
||||
# .text:0040112F cmp [esi+4], ebx
|
||||
for oper in insn.opers:
|
||||
|
||||
# this is for both x32 and x64
|
||||
if not isinstance(oper, envi.archs.i386.disasm.i386RegMemOper):
|
||||
continue
|
||||
# like [esi + 4]
|
||||
# reg ^
|
||||
# disp
|
||||
if isinstance(oper, envi.archs.i386.disasm.i386RegMemOper):
|
||||
if oper.reg == envi.archs.i386.regs.REG_ESP:
|
||||
continue
|
||||
|
||||
if oper.reg == envi.archs.i386.disasm.REG_ESP:
|
||||
continue
|
||||
if oper.reg == envi.archs.i386.regs.REG_EBP:
|
||||
continue
|
||||
|
||||
if oper.reg == envi.archs.i386.disasm.REG_EBP:
|
||||
continue
|
||||
# TODO: do x64 support for real.
|
||||
if oper.reg == envi.archs.amd64.regs.REG_RBP:
|
||||
continue
|
||||
|
||||
# TODO: do x64 support for real.
|
||||
if oper.reg == envi.archs.amd64.disasm.REG_RBP:
|
||||
continue
|
||||
# viv already decodes offsets as signed
|
||||
v = oper.disp
|
||||
|
||||
# viv already decodes offsets as signed
|
||||
v = oper.disp
|
||||
yield Offset(v), insn.va
|
||||
yield Offset(v, arch=get_arch(f.vw)), insn.va
|
||||
|
||||
yield Offset(v), insn.va
|
||||
yield Offset(v, arch=get_arch(f.vw)), insn.va
|
||||
# like: [esi + ecx + 16384]
|
||||
# reg ^ ^
|
||||
# index ^
|
||||
# disp
|
||||
elif isinstance(oper, envi.archs.i386.disasm.i386SibOper):
|
||||
# viv already decodes offsets as signed
|
||||
v = oper.disp
|
||||
|
||||
yield Offset(v), insn.va
|
||||
yield Offset(v, arch=get_arch(f.vw)), insn.va
|
||||
|
||||
|
||||
def is_security_cookie(f, bb, insn):
|
||||
def is_security_cookie(f, bb, insn) -> bool:
|
||||
"""
|
||||
check if an instruction is related to security cookie checks
|
||||
"""
|
||||
# security cookie check should use SP or BP
|
||||
oper = insn.opers[1]
|
||||
if oper.isReg() and oper.reg not in [
|
||||
envi.archs.i386.disasm.REG_ESP,
|
||||
envi.archs.i386.disasm.REG_EBP,
|
||||
envi.archs.i386.regs.REG_ESP,
|
||||
envi.archs.i386.regs.REG_EBP,
|
||||
# TODO: do x64 support for real.
|
||||
envi.archs.amd64.disasm.REG_RBP,
|
||||
envi.archs.amd64.disasm.REG_RSP,
|
||||
envi.archs.amd64.regs.REG_RBP,
|
||||
envi.archs.amd64.regs.REG_RSP,
|
||||
]:
|
||||
return False
|
||||
|
||||
@@ -372,7 +432,7 @@ def extract_insn_nzxor_characteristic_features(f, bb, insn):
|
||||
parse non-zeroing XOR instruction from the given instruction.
|
||||
ignore expected non-zeroing XORs, e.g. security cookies.
|
||||
"""
|
||||
if insn.mnem != "xor":
|
||||
if insn.mnem not in ("xor", "xorpd", "xorps", "pxor"):
|
||||
return
|
||||
|
||||
if insn.opers[0] == insn.opers[1]:
|
||||
@@ -426,7 +486,7 @@ def extract_insn_peb_access_characteristic_features(f, bb, insn):
|
||||
|
||||
|
||||
def extract_insn_segment_access_features(f, bb, insn):
|
||||
""" parse the instruction for access to fs or gs """
|
||||
"""parse the instruction for access to fs or gs"""
|
||||
prefix = insn.getPrefixName()
|
||||
|
||||
if prefix == "fs":
|
||||
@@ -436,7 +496,7 @@ def extract_insn_segment_access_features(f, bb, insn):
|
||||
yield Characteristic("gs access"), insn.va
|
||||
|
||||
|
||||
def get_section(vw, va):
|
||||
def get_section(vw, va: int):
|
||||
for start, length, _, __ in vw.getMemoryMaps():
|
||||
if start <= va < start + length:
|
||||
return start
|
||||
@@ -449,6 +509,10 @@ def extract_insn_cross_section_cflow(f, bb, insn):
|
||||
inspect the instruction for a CALL or JMP that crosses section boundaries.
|
||||
"""
|
||||
for va, flags in insn.getBranches():
|
||||
if va is None:
|
||||
# va may be none for dynamic branches that haven't been resolved, such as `jmp eax`.
|
||||
continue
|
||||
|
||||
if flags & envi.BR_FALL:
|
||||
continue
|
||||
|
||||
@@ -543,7 +607,7 @@ def extract_features(f, bb, insn):
|
||||
insn (vivisect...Instruction): the instruction to process.
|
||||
|
||||
yields:
|
||||
Feature, set[VA]: the features and their location found in this insn.
|
||||
Tuple[Feature, int]: the features and their location found in this insn.
|
||||
"""
|
||||
for insn_handler in INSTRUCTION_HANDLERS:
|
||||
for feature, va in insn_handler(f, bb, insn):
|
||||
|
||||
@@ -6,22 +6,33 @@
|
||||
# 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 capa.features import Feature
|
||||
from capa.features.common import Feature
|
||||
|
||||
|
||||
class Export(Feature):
|
||||
def __init__(self, value, description=None):
|
||||
def __init__(self, value: str, description=None):
|
||||
# value is export name
|
||||
super(Export, self).__init__(value, description=description)
|
||||
|
||||
|
||||
class Import(Feature):
|
||||
def __init__(self, value, description=None):
|
||||
def __init__(self, value: str, description=None):
|
||||
# value is import name
|
||||
super(Import, self).__init__(value, description=description)
|
||||
|
||||
|
||||
class Section(Feature):
|
||||
def __init__(self, value, description=None):
|
||||
def __init__(self, value: str, description=None):
|
||||
# value is section name
|
||||
super(Section, self).__init__(value, description=description)
|
||||
|
||||
|
||||
class FunctionName(Feature):
|
||||
"""recognized name for statically linked function"""
|
||||
|
||||
def __init__(self, name: str, description=None):
|
||||
# value is function name
|
||||
super(FunctionName, self).__init__(name, description=description)
|
||||
# override the name property set by `capa.features.Feature`
|
||||
# that would be `functionname` (note missing dash)
|
||||
self.name = "function-name"
|
||||
|
||||
@@ -5,6 +5,7 @@ json format:
|
||||
|
||||
{
|
||||
'version': 1,
|
||||
'base address': int(base address),
|
||||
'functions': {
|
||||
int(function va): {
|
||||
'basic blocks': {
|
||||
@@ -51,12 +52,13 @@ See the License for the specific language governing permissions and limitations
|
||||
import json
|
||||
import zlib
|
||||
import logging
|
||||
import os.path
|
||||
|
||||
import capa.features
|
||||
import capa.features.file
|
||||
import capa.features.insn
|
||||
import capa.features.common
|
||||
import capa.features.basicblock
|
||||
import capa.features.extractors
|
||||
import capa.features.extractors.base_extractor
|
||||
from capa.helpers import hex
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -66,7 +68,7 @@ def serialize_feature(feature):
|
||||
return feature.freeze_serialize()
|
||||
|
||||
|
||||
KNOWN_FEATURES = {F.__name__: F for F in capa.features.Feature.__subclasses__()}
|
||||
KNOWN_FEATURES = {F.__name__: F for F in capa.features.common.Feature.__subclasses__()}
|
||||
|
||||
|
||||
def deserialize_feature(doc):
|
||||
@@ -79,13 +81,14 @@ def dumps(extractor):
|
||||
serialize the given extractor to a string
|
||||
|
||||
args:
|
||||
extractor: capa.features.extractor.FeatureExtractor:
|
||||
extractor: capa.features.extractors.base_extractor.FeatureExtractor:
|
||||
|
||||
returns:
|
||||
str: the serialized features.
|
||||
"""
|
||||
ret = {
|
||||
"version": 1,
|
||||
"base address": extractor.get_base_address(),
|
||||
"functions": {},
|
||||
"scopes": {
|
||||
"file": [],
|
||||
@@ -120,7 +123,7 @@ def dumps(extractor):
|
||||
)
|
||||
|
||||
for insnva, insn in sorted(
|
||||
[(insn.__int__(), insn) for insn in extractor.get_instructions(f, bb)], key=lambda p: p[0]
|
||||
[(int(insn), insn) for insn in extractor.get_instructions(f, bb)], key=lambda p: p[0]
|
||||
):
|
||||
ret["functions"][hex(f)][hex(bb)].append(hex(insnva))
|
||||
|
||||
@@ -147,6 +150,7 @@ def loads(s):
|
||||
raise ValueError("unsupported freeze format version: %d" % (doc.get("version")))
|
||||
|
||||
features = {
|
||||
"base address": doc.get("base address"),
|
||||
"file features": [],
|
||||
"functions": {},
|
||||
}
|
||||
@@ -214,7 +218,7 @@ def loads(s):
|
||||
feature = deserialize_feature(feature[:2])
|
||||
features["functions"][loc[0]]["basic blocks"][loc[1]]["instructions"][loc[2]]["features"].append((va, feature))
|
||||
|
||||
return capa.features.extractors.NullFeatureExtractor(features)
|
||||
return capa.features.extractors.base_extractor.NullFeatureExtractor(features)
|
||||
|
||||
|
||||
MAGIC = "capa0000".encode("ascii")
|
||||
@@ -225,7 +229,7 @@ def dump(extractor):
|
||||
return MAGIC + zlib.compress(dumps(extractor).encode("utf-8"))
|
||||
|
||||
|
||||
def is_freeze(buf):
|
||||
def is_freeze(buf: bytes) -> bool:
|
||||
return buf[: len(MAGIC)] == MAGIC
|
||||
|
||||
|
||||
@@ -245,35 +249,16 @@ def main(argv=None):
|
||||
if argv is None:
|
||||
argv = sys.argv[1:]
|
||||
|
||||
formats = [
|
||||
("auto", "(default) detect file type automatically"),
|
||||
("pe", "Windows PE file"),
|
||||
("sc32", "32-bit shellcode"),
|
||||
("sc64", "64-bit shellcode"),
|
||||
]
|
||||
format_help = ", ".join(["%s: %s" % (f[0], f[1]) for f in formats])
|
||||
|
||||
parser = argparse.ArgumentParser(description="save capa features to a file")
|
||||
parser.add_argument("sample", type=str, help="Path to sample to analyze")
|
||||
capa.main.install_common_args(parser, {"sample", "format", "backend", "signatures"})
|
||||
parser.add_argument("output", type=str, help="Path to output file")
|
||||
parser.add_argument("-v", "--verbose", action="store_true", help="Enable verbose output")
|
||||
parser.add_argument("-q", "--quiet", action="store_true", help="Disable all output but errors")
|
||||
parser.add_argument(
|
||||
"-f", "--format", choices=[f[0] for f in formats], default="auto", help="Select sample format, %s" % format_help
|
||||
)
|
||||
args = parser.parse_args(args=argv)
|
||||
capa.main.handle_common_args(args)
|
||||
|
||||
if args.quiet:
|
||||
logging.basicConfig(level=logging.ERROR)
|
||||
logging.getLogger().setLevel(logging.ERROR)
|
||||
elif args.verbose:
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
else:
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logging.getLogger().setLevel(logging.INFO)
|
||||
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)
|
||||
with open(args.output, "wb") as f:
|
||||
f.write(dump(extractor))
|
||||
|
||||
|
||||
@@ -6,35 +6,36 @@
|
||||
# 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 capa.features import Feature
|
||||
import capa.render.utils
|
||||
from capa.features.common import Feature
|
||||
|
||||
|
||||
class API(Feature):
|
||||
def __init__(self, name, description=None):
|
||||
def __init__(self, name: str, description=None):
|
||||
# Downcase library name if given
|
||||
if "." in name:
|
||||
modname, impname = name.split(".")
|
||||
modname, _, impname = name.rpartition(".")
|
||||
name = modname.lower() + "." + impname
|
||||
|
||||
super(API, self).__init__(name, description)
|
||||
super(API, self).__init__(name, description=description)
|
||||
|
||||
|
||||
class Number(Feature):
|
||||
def __init__(self, value, arch=None, description=None):
|
||||
def __init__(self, value: int, arch=None, description=None):
|
||||
super(Number, self).__init__(value, arch=arch, description=description)
|
||||
|
||||
def get_value_str(self):
|
||||
return "0x%X" % self.value
|
||||
return capa.render.utils.hex(self.value)
|
||||
|
||||
|
||||
class Offset(Feature):
|
||||
def __init__(self, value, arch=None, description=None):
|
||||
def __init__(self, value: int, arch=None, description=None):
|
||||
super(Offset, self).__init__(value, arch=arch, description=description)
|
||||
|
||||
def get_value_str(self):
|
||||
return "0x%X" % self.value
|
||||
return capa.render.utils.hex(self.value)
|
||||
|
||||
|
||||
class Mnemonic(Feature):
|
||||
def __init__(self, value, description=None):
|
||||
def __init__(self, value: str, description=None):
|
||||
super(Mnemonic, self).__init__(value, description=description)
|
||||
|
||||
@@ -12,23 +12,10 @@ _hex = hex
|
||||
|
||||
|
||||
def hex(i):
|
||||
# under py2.7, long integers get formatted with a trailing `L`
|
||||
# and this is not pretty. so strip it out.
|
||||
return _hex(oint(i)).rstrip("L")
|
||||
return _hex(int(i))
|
||||
|
||||
|
||||
def oint(i):
|
||||
# there seems to be some trouble with using `int(viv_utils.Function)`
|
||||
# with the black magic we do with binding the `__int__()` routine.
|
||||
# i haven't had a chance to debug this yet (and i have no hotel wifi).
|
||||
# so in the meantime, detect this, and call the method directly.
|
||||
try:
|
||||
return int(i)
|
||||
except TypeError:
|
||||
return i.__int__()
|
||||
|
||||
|
||||
def get_file_taste(sample_path):
|
||||
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)
|
||||
with open(sample_path, "rb") as f:
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
# Copyright (C) 2020 FireEye, 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 PyQt5 import QtCore
|
||||
|
||||
from capa.ida.explorer.model import CapaExplorerDataModel
|
||||
|
||||
|
||||
class CapaExplorerSortFilterProxyModel(QtCore.QSortFilterProxyModel):
|
||||
def __init__(self, parent=None):
|
||||
""" """
|
||||
super(CapaExplorerSortFilterProxyModel, self).__init__(parent)
|
||||
|
||||
self.min_ea = None
|
||||
self.max_ea = None
|
||||
|
||||
def lessThan(self, left, right):
|
||||
"""true if the value of the left item is less than value of right item
|
||||
|
||||
@param left: QModelIndex*
|
||||
@param right: QModelIndex*
|
||||
|
||||
@retval True/False
|
||||
"""
|
||||
ldata = left.internalPointer().data(left.column())
|
||||
rdata = right.internalPointer().data(right.column())
|
||||
|
||||
if (
|
||||
ldata
|
||||
and rdata
|
||||
and left.column() == CapaExplorerDataModel.COLUMN_INDEX_VIRTUAL_ADDRESS
|
||||
and left.column() == right.column()
|
||||
):
|
||||
# convert virtual address before compare
|
||||
return int(ldata, 16) < int(rdata, 16)
|
||||
else:
|
||||
# compare as lowercase
|
||||
return ldata.lower() < rdata.lower()
|
||||
|
||||
def filterAcceptsRow(self, row, parent):
|
||||
"""true if the item in the row indicated by the given row and parent
|
||||
should be included in the model; otherwise returns false
|
||||
|
||||
@param row: int
|
||||
@param parent: QModelIndex*
|
||||
|
||||
@retval True/False
|
||||
"""
|
||||
if self.filter_accepts_row_self(row, parent):
|
||||
return True
|
||||
|
||||
alpha = parent
|
||||
while alpha.isValid():
|
||||
if self.filter_accepts_row_self(alpha.row(), alpha.parent()):
|
||||
return True
|
||||
alpha = alpha.parent()
|
||||
|
||||
if self.index_has_accepted_children(row, parent):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def index_has_accepted_children(self, row, parent):
|
||||
""" """
|
||||
model_index = self.sourceModel().index(row, 0, parent)
|
||||
|
||||
if model_index.isValid():
|
||||
for idx in range(self.sourceModel().rowCount(model_index)):
|
||||
if self.filter_accepts_row_self(idx, model_index):
|
||||
return True
|
||||
if self.index_has_accepted_children(idx, model_index):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def filter_accepts_row_self(self, row, parent):
|
||||
""" """
|
||||
# filter not set
|
||||
if self.min_ea is None and self.max_ea is None:
|
||||
return True
|
||||
|
||||
index = self.sourceModel().index(row, 0, parent)
|
||||
data = index.internalPointer().data(CapaExplorerDataModel.COLUMN_INDEX_VIRTUAL_ADDRESS)
|
||||
|
||||
if not data:
|
||||
return False
|
||||
|
||||
ea = int(data, 16)
|
||||
|
||||
if self.min_ea <= ea and ea < self.max_ea:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def add_address_range_filter(self, min_ea, max_ea):
|
||||
""" """
|
||||
self.min_ea = min_ea
|
||||
self.max_ea = max_ea
|
||||
|
||||
self.setFilterKeyColumn(CapaExplorerDataModel.COLUMN_INDEX_VIRTUAL_ADDRESS)
|
||||
self.invalidateFilter()
|
||||
|
||||
def reset_address_range_filter(self):
|
||||
""" """
|
||||
self.min_ea = None
|
||||
self.max_ea = None
|
||||
self.invalidateFilter()
|
||||
@@ -1,262 +0,0 @@
|
||||
# Copyright (C) 2020 FireEye, 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 idc
|
||||
import idaapi
|
||||
from PyQt5 import QtGui, QtCore, QtWidgets
|
||||
|
||||
from capa.ida.explorer.item import CapaExplorerRuleItem, CapaExplorerFunctionItem
|
||||
from capa.ida.explorer.model import CapaExplorerDataModel
|
||||
|
||||
|
||||
class CapaExplorerQtreeView(QtWidgets.QTreeView):
|
||||
"""capa explorer QTreeView implementation
|
||||
|
||||
view controls UI action responses and displays data from
|
||||
CapaExplorerDataModel
|
||||
|
||||
view does not modify CapaExplorerDataModel directly - data
|
||||
modifications should be implemented in CapaExplorerDataModel
|
||||
"""
|
||||
|
||||
def __init__(self, model, parent=None):
|
||||
""" initialize CapaExplorerQTreeView """
|
||||
super(CapaExplorerQtreeView, self).__init__(parent)
|
||||
|
||||
self.setModel(model)
|
||||
|
||||
self.model = model
|
||||
self.parent = parent
|
||||
|
||||
# configure custom UI controls
|
||||
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
self.setExpandsOnDoubleClick(False)
|
||||
self.setSortingEnabled(True)
|
||||
self.model.setDynamicSortFilter(False)
|
||||
|
||||
# configure view columns to auto-resize
|
||||
for idx in range(CapaExplorerDataModel.COLUMN_COUNT):
|
||||
self.header().setSectionResizeMode(idx, QtWidgets.QHeaderView.Interactive)
|
||||
|
||||
# connect slots to resize columns when expanded or collapsed
|
||||
self.expanded.connect(self.resize_columns_to_content)
|
||||
self.collapsed.connect(self.resize_columns_to_content)
|
||||
|
||||
# connect slots
|
||||
self.customContextMenuRequested.connect(self.slot_custom_context_menu_requested)
|
||||
self.doubleClicked.connect(self.slot_double_click)
|
||||
|
||||
self.setStyleSheet("QTreeView::item {padding-right: 15 px;padding-bottom: 2 px;}")
|
||||
|
||||
def reset(self):
|
||||
"""reset user interface changes
|
||||
|
||||
called when view should reset any user interface changes
|
||||
made since the last reset e.g. IDA window highlighting
|
||||
"""
|
||||
self.expandToDepth(0)
|
||||
self.resize_columns_to_content()
|
||||
|
||||
def resize_columns_to_content(self):
|
||||
""" reset view columns to contents """
|
||||
self.header().resizeSections(QtWidgets.QHeaderView.ResizeToContents)
|
||||
|
||||
def map_index_to_source_item(self, model_index):
|
||||
"""map proxy model index to source model item
|
||||
|
||||
@param model_index: QModelIndex*
|
||||
|
||||
@retval QObject*
|
||||
"""
|
||||
return self.model.mapToSource(model_index).internalPointer()
|
||||
|
||||
def send_data_to_clipboard(self, data):
|
||||
"""copy data to the clipboard
|
||||
|
||||
@param data: data to be copied
|
||||
"""
|
||||
clip = QtWidgets.QApplication.clipboard()
|
||||
clip.clear(mode=clip.Clipboard)
|
||||
clip.setText(data, mode=clip.Clipboard)
|
||||
|
||||
def new_action(self, display, data, slot):
|
||||
"""create action for context menu
|
||||
|
||||
@param display: text displayed to user in context menu
|
||||
@param data: data passed to slot
|
||||
@param slot: slot to connect
|
||||
|
||||
@retval QAction*
|
||||
"""
|
||||
action = QtWidgets.QAction(display, self.parent)
|
||||
action.setData(data)
|
||||
action.triggered.connect(lambda checked: slot(action))
|
||||
|
||||
return action
|
||||
|
||||
def load_default_context_menu_actions(self, data):
|
||||
"""yield actions specific to function custom context menu
|
||||
|
||||
@param data: tuple
|
||||
|
||||
@yield QAction*
|
||||
"""
|
||||
default_actions = (
|
||||
("Copy column", data, self.slot_copy_column),
|
||||
("Copy row", data, self.slot_copy_row),
|
||||
)
|
||||
|
||||
# add default actions
|
||||
for action in default_actions:
|
||||
yield self.new_action(*action)
|
||||
|
||||
def load_function_context_menu_actions(self, data):
|
||||
"""yield actions specific to function custom context menu
|
||||
|
||||
@param data: tuple
|
||||
|
||||
@yield QAction*
|
||||
"""
|
||||
function_actions = (("Rename function", data, self.slot_rename_function),)
|
||||
|
||||
# add function actions
|
||||
for action in function_actions:
|
||||
yield self.new_action(*action)
|
||||
|
||||
# add default actions
|
||||
for action in self.load_default_context_menu_actions(data):
|
||||
yield action
|
||||
|
||||
def load_default_context_menu(self, pos, item, model_index):
|
||||
"""create default custom context menu
|
||||
|
||||
creates custom context menu containing default actions
|
||||
|
||||
@param pos: TODO
|
||||
@param item: TODO
|
||||
@param model_index: TODO
|
||||
|
||||
@retval QMenu*
|
||||
"""
|
||||
menu = QtWidgets.QMenu()
|
||||
|
||||
for action in self.load_default_context_menu_actions((pos, item, model_index)):
|
||||
menu.addAction(action)
|
||||
|
||||
return menu
|
||||
|
||||
def load_function_item_context_menu(self, pos, item, model_index):
|
||||
"""create function custom context menu
|
||||
|
||||
creates custom context menu containing actions specific to functions
|
||||
and the default actions
|
||||
|
||||
@param pos: TODO
|
||||
@param item: TODO
|
||||
@param model_index: TODO
|
||||
|
||||
@retval QMenu*
|
||||
"""
|
||||
menu = QtWidgets.QMenu()
|
||||
|
||||
for action in self.load_function_context_menu_actions((pos, item, model_index)):
|
||||
menu.addAction(action)
|
||||
|
||||
return menu
|
||||
|
||||
def show_custom_context_menu(self, menu, pos):
|
||||
"""display custom context menu in view
|
||||
|
||||
@param menu: TODO
|
||||
@param pos: TODO
|
||||
"""
|
||||
if menu:
|
||||
menu.exec_(self.viewport().mapToGlobal(pos))
|
||||
|
||||
def slot_copy_column(self, action):
|
||||
"""slot connected to custom context menu
|
||||
|
||||
allows user to select a column and copy the data
|
||||
to clipboard
|
||||
|
||||
@param action: QAction*
|
||||
"""
|
||||
_, item, model_index = action.data()
|
||||
self.send_data_to_clipboard(item.data(model_index.column()))
|
||||
|
||||
def slot_copy_row(self, action):
|
||||
"""slot connected to custom context menu
|
||||
|
||||
allows user to select a row and copy the space-delimited
|
||||
data to clipboard
|
||||
|
||||
@param action: QAction*
|
||||
"""
|
||||
_, item, _ = action.data()
|
||||
self.send_data_to_clipboard(str(item))
|
||||
|
||||
def slot_rename_function(self, action):
|
||||
"""slot connected to custom context menu
|
||||
|
||||
allows user to select a edit a function name and push
|
||||
changes to IDA
|
||||
|
||||
@param action: QAction*
|
||||
"""
|
||||
_, item, model_index = action.data()
|
||||
|
||||
# make item temporary edit, reset after user is finished
|
||||
item.setIsEditable(True)
|
||||
self.edit(model_index)
|
||||
item.setIsEditable(False)
|
||||
|
||||
def slot_custom_context_menu_requested(self, pos):
|
||||
"""slot connected to custom context menu request
|
||||
|
||||
displays custom context menu to user containing action
|
||||
relevant to the data item selected
|
||||
|
||||
@param pos: TODO
|
||||
"""
|
||||
model_index = self.indexAt(pos)
|
||||
|
||||
if not model_index.isValid():
|
||||
return
|
||||
|
||||
item = self.map_index_to_source_item(model_index)
|
||||
column = model_index.column()
|
||||
menu = None
|
||||
|
||||
if CapaExplorerDataModel.COLUMN_INDEX_RULE_INFORMATION == column and isinstance(item, CapaExplorerFunctionItem):
|
||||
# user hovered function item
|
||||
menu = self.load_function_item_context_menu(pos, item, model_index)
|
||||
else:
|
||||
# user hovered default item
|
||||
menu = self.load_default_context_menu(pos, item, model_index)
|
||||
|
||||
# show custom context menu at view position
|
||||
self.show_custom_context_menu(menu, pos)
|
||||
|
||||
def slot_double_click(self, model_index):
|
||||
"""slot connected to double click event
|
||||
|
||||
@param model_index: QModelIndex*
|
||||
"""
|
||||
if not model_index.isValid():
|
||||
return
|
||||
|
||||
item = self.map_index_to_source_item(model_index)
|
||||
column = model_index.column()
|
||||
|
||||
if CapaExplorerDataModel.COLUMN_INDEX_VIRTUAL_ADDRESS == column and item.location:
|
||||
# user double-clicked virtual address column - navigate IDA to address
|
||||
idc.jumpto(item.location)
|
||||
|
||||
if CapaExplorerDataModel.COLUMN_INDEX_RULE_INFORMATION == column:
|
||||
# user double-clicked information column - un/expand
|
||||
self.collapse(model_index) if self.isExpanded(model_index) else self.expand(model_index)
|
||||
@@ -10,11 +10,12 @@ import logging
|
||||
import datetime
|
||||
|
||||
import idc
|
||||
import six
|
||||
import idaapi
|
||||
import idautils
|
||||
|
||||
import capa
|
||||
import capa.version
|
||||
import capa.features.common
|
||||
|
||||
logger = logging.getLogger("capa")
|
||||
|
||||
@@ -24,6 +25,7 @@ SUPPORTED_IDA_VERSIONS = [
|
||||
"7.3",
|
||||
"7.4",
|
||||
"7.5",
|
||||
"7.6",
|
||||
]
|
||||
|
||||
# file type names as returned by idaapi.get_file_type_name()
|
||||
@@ -46,7 +48,6 @@ def is_supported_ida_version():
|
||||
logger.warning(
|
||||
"Your IDA Pro version is: %s. Supported versions are: %s." % (version, ", ".join(SUPPORTED_IDA_VERSIONS))
|
||||
)
|
||||
capa.ida.helpers.inform_user_ida_ui(warning_msg)
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -62,7 +63,6 @@ def is_supported_file_type():
|
||||
)
|
||||
logger.error(" If you don't know the input file type, you can try using the `file` utility to guess it.")
|
||||
logger.error("-" * 80)
|
||||
inform_user_ida_ui("capa does not support the format of this file")
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -73,7 +73,7 @@ def get_disasm_line(va):
|
||||
|
||||
|
||||
def is_func_start(ea):
|
||||
""" check if function stat exists at virtual address """
|
||||
"""check if function stat exists at virtual address"""
|
||||
f = idaapi.get_func(ea)
|
||||
return f and f.start_ea == ea
|
||||
|
||||
@@ -84,14 +84,26 @@ def get_func_start_ea(ea):
|
||||
return f if f is None else f.start_ea
|
||||
|
||||
|
||||
def collect_metadata():
|
||||
def get_file_md5():
|
||||
""" """
|
||||
md5 = idautils.GetInputFileMD5()
|
||||
if not isinstance(md5, six.string_types):
|
||||
md5 = capa.features.bytes_to_str(md5)
|
||||
if not isinstance(md5, str):
|
||||
md5 = capa.features.common.bytes_to_str(md5)
|
||||
return md5
|
||||
|
||||
|
||||
def get_file_sha256():
|
||||
""" """
|
||||
sha256 = idaapi.retrieve_input_file_sha256()
|
||||
if not isinstance(sha256, six.string_types):
|
||||
sha256 = capa.features.bytes_to_str(sha256)
|
||||
if not isinstance(sha256, str):
|
||||
sha256 = capa.features.common.bytes_to_str(sha256)
|
||||
return sha256
|
||||
|
||||
|
||||
def collect_metadata():
|
||||
""" """
|
||||
md5 = get_file_md5()
|
||||
sha256 = get_file_sha256()
|
||||
|
||||
return {
|
||||
"timestamp": datetime.datetime.now().isoformat(),
|
||||
@@ -105,6 +117,7 @@ def collect_metadata():
|
||||
"analysis": {
|
||||
"format": idaapi.get_file_type_name(),
|
||||
"extractor": "ida",
|
||||
"base_address": idaapi.get_imagebase(),
|
||||
},
|
||||
"version": capa.version.__version__,
|
||||
}
|
||||
@@ -1,571 +0,0 @@
|
||||
# Copyright (C) 2020 FireEye, 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 json
|
||||
import logging
|
||||
import collections
|
||||
|
||||
import idaapi
|
||||
from PyQt5 import QtGui, QtCore, QtWidgets
|
||||
|
||||
import capa.main
|
||||
import capa.rules
|
||||
import capa.ida.helpers
|
||||
import capa.render.utils as rutils
|
||||
import capa.features.extractors.ida
|
||||
from capa.ida.explorer.view import CapaExplorerQtreeView
|
||||
from capa.ida.explorer.model import CapaExplorerDataModel
|
||||
from capa.ida.explorer.proxy import CapaExplorerSortFilterProxyModel
|
||||
|
||||
PLUGIN_NAME = "capa explorer"
|
||||
|
||||
logger = logging.getLogger("capa")
|
||||
|
||||
|
||||
class CapaExplorerIdaHooks(idaapi.UI_Hooks):
|
||||
def __init__(self, screen_ea_changed_hook, action_hooks):
|
||||
"""facilitate IDA 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__()
|
||||
|
||||
self.screen_ea_changed_hook = screen_ea_changed_hook
|
||||
self.process_action_hooks = action_hooks
|
||||
self.process_action_handle = None
|
||||
self.process_action_meta = {}
|
||||
|
||||
def preprocess_action(self, name):
|
||||
"""called prior to action completed
|
||||
|
||||
@param name: name of action defined by idagui.cfg
|
||||
|
||||
@retval must be 0
|
||||
"""
|
||||
self.process_action_handle = self.process_action_hooks.get(name, None)
|
||||
|
||||
if self.process_action_handle:
|
||||
self.process_action_handle(self.process_action_meta)
|
||||
|
||||
# must return 0 for IDA
|
||||
return 0
|
||||
|
||||
def postprocess_action(self):
|
||||
""" called after action completed """
|
||||
if not self.process_action_handle:
|
||||
return
|
||||
|
||||
self.process_action_handle(self.process_action_meta, post=True)
|
||||
self.reset()
|
||||
|
||||
def screen_ea_changed(self, curr_ea, prev_ea):
|
||||
"""called after screen location is changed
|
||||
|
||||
@param curr_ea: current location
|
||||
@param prev_ea: prev location
|
||||
"""
|
||||
self.screen_ea_changed_hook(idaapi.get_current_widget(), curr_ea, prev_ea)
|
||||
|
||||
def reset(self):
|
||||
""" reset internal state """
|
||||
self.process_action_handle = None
|
||||
self.process_action_meta.clear()
|
||||
|
||||
|
||||
class CapaExplorerForm(idaapi.PluginForm):
|
||||
def __init__(self):
|
||||
""" """
|
||||
super(CapaExplorerForm, self).__init__()
|
||||
|
||||
self.form_title = PLUGIN_NAME
|
||||
self.file_loc = __file__
|
||||
|
||||
self.parent = None
|
||||
self.ida_hooks = None
|
||||
self.doc = None
|
||||
|
||||
# models
|
||||
self.model_data = None
|
||||
self.model_proxy = None
|
||||
|
||||
# user interface elements
|
||||
self.view_limit_results_by_function = None
|
||||
self.view_tree = None
|
||||
self.view_summary = None
|
||||
self.view_attack = None
|
||||
self.view_tabs = None
|
||||
self.view_menu_bar = None
|
||||
|
||||
def OnCreate(self, form):
|
||||
""" """
|
||||
self.parent = self.FormToPyQtWidget(form)
|
||||
self.load_interface()
|
||||
self.load_capa_results()
|
||||
self.load_ida_hooks()
|
||||
|
||||
self.view_tree.reset()
|
||||
|
||||
logger.info("form created.")
|
||||
|
||||
def Show(self):
|
||||
""" """
|
||||
return idaapi.PluginForm.Show(
|
||||
self, self.form_title, options=(idaapi.PluginForm.WOPN_TAB | idaapi.PluginForm.WCLS_CLOSE_LATER)
|
||||
)
|
||||
|
||||
def OnClose(self, form):
|
||||
""" form is closed """
|
||||
self.unload_ida_hooks()
|
||||
self.ida_reset()
|
||||
|
||||
logger.info("form closed.")
|
||||
|
||||
def load_interface(self):
|
||||
""" load user interface """
|
||||
# load models
|
||||
self.model_data = CapaExplorerDataModel()
|
||||
self.model_proxy = CapaExplorerSortFilterProxyModel()
|
||||
self.model_proxy.setSourceModel(self.model_data)
|
||||
|
||||
# load tree
|
||||
self.view_tree = CapaExplorerQtreeView(self.model_proxy, self.parent)
|
||||
|
||||
# load summary table
|
||||
self.load_view_summary()
|
||||
self.load_view_attack()
|
||||
|
||||
# load parent tab and children tab views
|
||||
self.load_view_tabs()
|
||||
self.load_view_checkbox_limit_by()
|
||||
self.load_view_summary_tab()
|
||||
self.load_view_attack_tab()
|
||||
self.load_view_tree_tab()
|
||||
|
||||
# load menu bar and sub menus
|
||||
self.load_view_menu_bar()
|
||||
self.load_file_menu()
|
||||
|
||||
# load parent view
|
||||
self.load_view_parent()
|
||||
|
||||
def load_view_tabs(self):
|
||||
""" load tabs """
|
||||
tabs = QtWidgets.QTabWidget()
|
||||
self.view_tabs = tabs
|
||||
|
||||
def load_view_menu_bar(self):
|
||||
""" load menu bar """
|
||||
bar = QtWidgets.QMenuBar()
|
||||
self.view_menu_bar = bar
|
||||
|
||||
def load_view_summary(self):
|
||||
""" load capa summary table """
|
||||
table_headers = [
|
||||
"Capability",
|
||||
"Namespace",
|
||||
]
|
||||
|
||||
table = QtWidgets.QTableWidget()
|
||||
|
||||
table.setColumnCount(len(table_headers))
|
||||
table.verticalHeader().setVisible(False)
|
||||
table.setSortingEnabled(False)
|
||||
table.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
|
||||
table.setFocusPolicy(QtCore.Qt.NoFocus)
|
||||
table.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection)
|
||||
table.setHorizontalHeaderLabels(table_headers)
|
||||
table.horizontalHeader().setDefaultAlignment(QtCore.Qt.AlignLeft)
|
||||
table.setShowGrid(False)
|
||||
table.setStyleSheet("QTableWidget::item { padding: 25px; }")
|
||||
|
||||
self.view_summary = table
|
||||
|
||||
def load_view_attack(self):
|
||||
""" load MITRE ATT&CK table """
|
||||
table_headers = [
|
||||
"ATT&CK Tactic",
|
||||
"ATT&CK Technique ",
|
||||
]
|
||||
|
||||
table = QtWidgets.QTableWidget()
|
||||
|
||||
table.setColumnCount(len(table_headers))
|
||||
table.verticalHeader().setVisible(False)
|
||||
table.setSortingEnabled(False)
|
||||
table.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
|
||||
table.setFocusPolicy(QtCore.Qt.NoFocus)
|
||||
table.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection)
|
||||
table.setHorizontalHeaderLabels(table_headers)
|
||||
table.horizontalHeader().setDefaultAlignment(QtCore.Qt.AlignLeft)
|
||||
table.setShowGrid(False)
|
||||
table.setStyleSheet("QTableWidget::item { padding: 25px; }")
|
||||
|
||||
self.view_attack = table
|
||||
|
||||
def load_view_checkbox_limit_by(self):
|
||||
""" load limit results by function checkbox """
|
||||
check = QtWidgets.QCheckBox("Limit results to current function")
|
||||
check.setChecked(False)
|
||||
check.stateChanged.connect(self.slot_checkbox_limit_by_changed)
|
||||
|
||||
self.view_limit_results_by_function = check
|
||||
|
||||
def load_view_parent(self):
|
||||
""" load view parent """
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
|
||||
layout.addWidget(self.view_tabs)
|
||||
layout.setMenuBar(self.view_menu_bar)
|
||||
|
||||
self.parent.setLayout(layout)
|
||||
|
||||
def load_view_tree_tab(self):
|
||||
""" load capa tree tab view """
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.addWidget(self.view_limit_results_by_function)
|
||||
layout.addWidget(self.view_tree)
|
||||
|
||||
tab = QtWidgets.QWidget()
|
||||
tab.setLayout(layout)
|
||||
|
||||
self.view_tabs.addTab(tab, "Tree View")
|
||||
|
||||
def load_view_summary_tab(self):
|
||||
""" load capa summary tab view """
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.addWidget(self.view_summary)
|
||||
|
||||
tab = QtWidgets.QWidget()
|
||||
tab.setLayout(layout)
|
||||
|
||||
self.view_tabs.addTab(tab, "Summary")
|
||||
|
||||
def load_view_attack_tab(self):
|
||||
""" load MITRE ATT&CK tab view """
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.addWidget(self.view_attack)
|
||||
|
||||
tab = QtWidgets.QWidget()
|
||||
tab.setLayout(layout)
|
||||
|
||||
self.view_tabs.addTab(tab, "MITRE")
|
||||
|
||||
def load_file_menu(self):
|
||||
""" load file menu actions """
|
||||
actions = (
|
||||
("Reset view", "Reset plugin view", self.reset),
|
||||
("Run analysis", "Run capa analysis on current database", self.reload),
|
||||
("Export results...", "Export capa results as JSON file", self.export_json),
|
||||
)
|
||||
|
||||
menu = self.view_menu_bar.addMenu("File")
|
||||
for (name, _, handle) in actions:
|
||||
action = QtWidgets.QAction(name, self.parent)
|
||||
action.triggered.connect(handle)
|
||||
menu.addAction(action)
|
||||
|
||||
def export_json(self):
|
||||
""" export capa results as JSON file """
|
||||
if not self.doc:
|
||||
idaapi.info("No capa results to export.")
|
||||
return
|
||||
path = idaapi.ask_file(True, "*.json", "Choose file")
|
||||
if os.path.exists(path) and 1 != idaapi.ask_yn(1, "File already exists. Overwrite?"):
|
||||
return
|
||||
with open(path, "wb") as export_file:
|
||||
export_file.write(
|
||||
json.dumps(self.doc, sort_keys=True, cls=capa.render.CapaJsonObjectEncoder).encode("utf-8")
|
||||
)
|
||||
|
||||
def load_ida_hooks(self):
|
||||
""" load IDA Pro UI hooks """
|
||||
action_hooks = {
|
||||
"MakeName": self.ida_hook_rename,
|
||||
"EditFunction": self.ida_hook_rename,
|
||||
}
|
||||
|
||||
self.ida_hooks = CapaExplorerIdaHooks(self.ida_hook_screen_ea_changed, action_hooks)
|
||||
self.ida_hooks.hook()
|
||||
|
||||
def unload_ida_hooks(self):
|
||||
""" unload IDA Pro UI hooks """
|
||||
if self.ida_hooks:
|
||||
self.ida_hooks.unhook()
|
||||
|
||||
def ida_hook_rename(self, meta, post=False):
|
||||
"""hook for IDA rename action
|
||||
|
||||
called twice, once before action and once after
|
||||
action completes
|
||||
|
||||
@param meta: metadata cache
|
||||
@param post: indicates pre or post action
|
||||
"""
|
||||
location = idaapi.get_screen_ea()
|
||||
if not location or not capa.ida.helpers.is_func_start(location):
|
||||
return
|
||||
|
||||
curr_name = idaapi.get_name(location)
|
||||
|
||||
if post:
|
||||
# post action update data model w/ current name
|
||||
self.model_data.update_function_name(meta.get("prev_name", ""), curr_name)
|
||||
else:
|
||||
# pre action so save current name for replacement later
|
||||
meta["prev_name"] = curr_name
|
||||
|
||||
def ida_hook_screen_ea_changed(self, widget, new_ea, old_ea):
|
||||
"""hook for IDA screen ea changed
|
||||
|
||||
this hook is currently only relevant for limiting results displayed in the UI
|
||||
|
||||
@param widget: IDA widget type
|
||||
@param new_ea: destination ea
|
||||
@param old_ea: source ea
|
||||
"""
|
||||
if not self.view_limit_results_by_function.isChecked():
|
||||
# ignore if limit checkbox not selected
|
||||
return
|
||||
|
||||
if idaapi.get_widget_type(widget) != idaapi.BWN_DISASM:
|
||||
# ignore views not the assembly view
|
||||
return
|
||||
|
||||
if idaapi.get_func(new_ea) == idaapi.get_func(old_ea):
|
||||
# user navigated same function - ignore
|
||||
return
|
||||
|
||||
self.limit_results_to_function(idaapi.get_func(new_ea))
|
||||
self.view_tree.resize_columns_to_content()
|
||||
|
||||
def load_capa_results(self):
|
||||
""" run capa analysis and render results in UI """
|
||||
logger.info("-" * 80)
|
||||
logger.info(" Using default embedded rules.")
|
||||
logger.info(" ")
|
||||
logger.info(" You can see the current default rule set here:")
|
||||
logger.info(" https://github.com/fireeye/capa-rules")
|
||||
logger.info("-" * 80)
|
||||
|
||||
rules_path = os.path.join(os.path.dirname(self.file_loc), "../..", "rules")
|
||||
rules = capa.main.get_rules(rules_path)
|
||||
rules = capa.rules.RuleSet(rules)
|
||||
|
||||
meta = capa.ida.helpers.collect_metadata()
|
||||
|
||||
capabilities, counts = capa.main.find_capabilities(
|
||||
rules, capa.features.extractors.ida.IdaFeatureExtractor(), True
|
||||
)
|
||||
meta["analysis"].update(counts)
|
||||
|
||||
# support binary files specifically for x86/AMD64 shellcode
|
||||
# warn user binary file is loaded but still allow capa to process it
|
||||
# TODO: check specific architecture of binary files based on how user configured IDA processors
|
||||
if idaapi.get_file_type_name() == "Binary file":
|
||||
logger.warning("-" * 80)
|
||||
logger.warning(" Input file appears to be a binary file.")
|
||||
logger.warning(" ")
|
||||
logger.warning(
|
||||
" capa currently only supports analyzing binary files containing x86/AMD64 shellcode with IDA."
|
||||
)
|
||||
logger.warning(
|
||||
" This means the results may be misleading or incomplete if the binary file loaded in IDA is not x86/AMD64."
|
||||
)
|
||||
logger.warning(" If you don't know the input file type, you can try using the `file` utility to guess it.")
|
||||
logger.warning("-" * 80)
|
||||
|
||||
capa.ida.helpers.inform_user_ida_ui("capa encountered warnings during analysis")
|
||||
|
||||
if capa.main.has_file_limitation(rules, capabilities, is_standalone=False):
|
||||
capa.ida.helpers.inform_user_ida_ui("capa encountered warnings during analysis")
|
||||
|
||||
logger.info("analysis completed.")
|
||||
|
||||
self.doc = capa.render.convert_capabilities_to_result_document(meta, rules, capabilities)
|
||||
|
||||
self.model_data.render_capa_doc(self.doc)
|
||||
self.render_capa_doc_summary()
|
||||
self.render_capa_doc_mitre_summary()
|
||||
|
||||
self.set_view_tree_default_sort_order()
|
||||
|
||||
logger.info("render views completed.")
|
||||
|
||||
def set_view_tree_default_sort_order(self):
|
||||
""" set capa tree view default sort order """
|
||||
self.view_tree.sortByColumn(CapaExplorerDataModel.COLUMN_INDEX_RULE_INFORMATION, QtCore.Qt.AscendingOrder)
|
||||
|
||||
def render_capa_doc_summary(self):
|
||||
""" render capa summary results """
|
||||
for (row, rule) in enumerate(rutils.capability_rules(self.doc)):
|
||||
count = len(rule["matches"])
|
||||
|
||||
if count == 1:
|
||||
capability = rule["meta"]["name"]
|
||||
else:
|
||||
capability = "%s (%d matches)" % (rule["meta"]["name"], count)
|
||||
|
||||
self.view_summary.setRowCount(row + 1)
|
||||
|
||||
self.view_summary.setItem(row, 0, self.render_new_table_header_item(capability))
|
||||
self.view_summary.setItem(row, 1, QtWidgets.QTableWidgetItem(rule["meta"]["namespace"]))
|
||||
|
||||
# resize columns to content
|
||||
self.view_summary.resizeColumnsToContents()
|
||||
|
||||
def render_capa_doc_mitre_summary(self):
|
||||
""" render capa MITRE ATT&CK results """
|
||||
tactics = collections.defaultdict(set)
|
||||
|
||||
for rule in rutils.capability_rules(self.doc):
|
||||
if not rule["meta"].get("att&ck"):
|
||||
continue
|
||||
|
||||
for attack in rule["meta"]["att&ck"]:
|
||||
tactic, _, rest = attack.partition("::")
|
||||
if "::" in rest:
|
||||
technique, _, rest = rest.partition("::")
|
||||
subtechnique, _, id = rest.rpartition(" ")
|
||||
tactics[tactic].add((technique, subtechnique, id))
|
||||
else:
|
||||
technique, _, id = rest.rpartition(" ")
|
||||
tactics[tactic].add((technique, id))
|
||||
|
||||
column_one = []
|
||||
column_two = []
|
||||
|
||||
for (tactic, techniques) in sorted(tactics.items()):
|
||||
column_one.append(tactic.upper())
|
||||
# add extra space when more than one technique
|
||||
column_one.extend(["" for i in range(len(techniques) - 1)])
|
||||
|
||||
for spec in sorted(techniques):
|
||||
if len(spec) == 2:
|
||||
technique, id = spec
|
||||
column_two.append("%s %s" % (technique, id))
|
||||
elif len(spec) == 3:
|
||||
technique, subtechnique, id = spec
|
||||
column_two.append("%s::%s %s" % (technique, subtechnique, id))
|
||||
else:
|
||||
raise RuntimeError("unexpected ATT&CK spec format")
|
||||
|
||||
self.view_attack.setRowCount(max(len(column_one), len(column_two)))
|
||||
|
||||
for row, value in enumerate(column_one):
|
||||
self.view_attack.setItem(row, 0, self.render_new_table_header_item(value))
|
||||
|
||||
for row, value in enumerate(column_two):
|
||||
self.view_attack.setItem(row, 1, QtWidgets.QTableWidgetItem(value))
|
||||
|
||||
# resize columns to content
|
||||
self.view_attack.resizeColumnsToContents()
|
||||
|
||||
def render_new_table_header_item(self, text):
|
||||
""" create new table header item with default style """
|
||||
item = QtWidgets.QTableWidgetItem(text)
|
||||
item.setForeground(QtGui.QColor(88, 139, 174))
|
||||
|
||||
font = QtGui.QFont()
|
||||
font.setBold(True)
|
||||
|
||||
item.setFont(font)
|
||||
|
||||
return item
|
||||
|
||||
def ida_reset(self):
|
||||
""" reset IDA UI """
|
||||
self.model_data.reset()
|
||||
self.view_tree.reset()
|
||||
self.view_limit_results_by_function.setChecked(False)
|
||||
self.set_view_tree_default_sort_order()
|
||||
|
||||
def reload(self):
|
||||
""" reload views and re-run capa analysis """
|
||||
self.ida_reset()
|
||||
self.model_proxy.invalidate()
|
||||
self.model_data.clear()
|
||||
self.view_summary.setRowCount(0)
|
||||
self.load_capa_results()
|
||||
|
||||
logger.info("reload complete.")
|
||||
idaapi.info("%s reload completed." % PLUGIN_NAME)
|
||||
|
||||
def reset(self):
|
||||
"""reset UI elements
|
||||
|
||||
e.g. checkboxes and IDA highlighting
|
||||
"""
|
||||
self.ida_reset()
|
||||
|
||||
logger.info("reset completed.")
|
||||
idaapi.info("%s reset completed." % PLUGIN_NAME)
|
||||
|
||||
def slot_menu_bar_hovered(self, action):
|
||||
"""display menu action tooltip
|
||||
|
||||
@param action: QtWidgets.QAction*
|
||||
|
||||
@reference: https://stackoverflow.com/questions/21725119/why-wont-qtooltips-appear-on-qactions-within-a-qmenu
|
||||
"""
|
||||
QtWidgets.QToolTip.showText(
|
||||
QtGui.QCursor.pos(), action.toolTip(), self.view_menu_bar, self.view_menu_bar.actionGeometry(action)
|
||||
)
|
||||
|
||||
def slot_checkbox_limit_by_changed(self):
|
||||
"""slot activated if checkbox clicked
|
||||
|
||||
if checked, configure function filter if screen location is located
|
||||
in function, otherwise clear filter
|
||||
"""
|
||||
if self.view_limit_results_by_function.isChecked():
|
||||
self.limit_results_to_function(idaapi.get_func(idaapi.get_screen_ea()))
|
||||
else:
|
||||
self.model_proxy.reset_address_range_filter()
|
||||
|
||||
self.view_tree.reset()
|
||||
|
||||
def limit_results_to_function(self, f):
|
||||
"""add filter to limit results to current function
|
||||
|
||||
@param f: (IDA func_t)
|
||||
"""
|
||||
if f:
|
||||
self.model_proxy.add_address_range_filter(f.start_ea, f.end_ea)
|
||||
else:
|
||||
# if function not exists don't display any results (address should not be -1)
|
||||
self.model_proxy.add_address_range_filter(-1, -1)
|
||||
|
||||
|
||||
def main():
|
||||
""" TODO: move to idaapi.plugin_t class """
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
if not capa.ida.helpers.is_supported_ida_version():
|
||||
return -1
|
||||
|
||||
if not capa.ida.helpers.is_supported_file_type():
|
||||
return -1
|
||||
|
||||
global CAPA_EXPLORER_FORM
|
||||
|
||||
try:
|
||||
# there is an instance, reload it
|
||||
CAPA_EXPLORER_FORM
|
||||
CAPA_EXPLORER_FORM.Close()
|
||||
CAPA_EXPLORER_FORM = CapaExplorerForm()
|
||||
except Exception:
|
||||
# there is no instance yet
|
||||
CAPA_EXPLORER_FORM = CapaExplorerForm()
|
||||
|
||||
CAPA_EXPLORER_FORM.Show()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
125
capa/ida/plugin/README.md
Normal file
@@ -0,0 +1,125 @@
|
||||

|
||||
|
||||
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
|
||||
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
|
||||
to important addresses in the IDB and highlights key features in the Disassembly view so they stand out visually. To illustrate, we use capa explorer to
|
||||
analyze Lab 14-02 from [Practical Malware Analysis](https://nostarch.com/malware) (PMA) available [here](https://practicalmalwareanalysis.com/labs/). Our goal is to understand
|
||||
the program's functionality.
|
||||
|
||||
After loading Lab 14-02 into IDA and analyzing the database with capa explorer, we see that capa detected a rule match for `self delete via COMSPEC environment variable`:
|
||||
|
||||

|
||||
|
||||
We can use capa explorer to navigate our Disassembly view directly to the suspect function and get an assembly-level breakdown of why capa matched `self delete via COMSPEC environment variable`.
|
||||
|
||||

|
||||
|
||||
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,
|
||||
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`.
|
||||
|
||||

|
||||
|
||||
For more information on the FLARE team's open-source framework, capa, check out the overview in our first [blog](https://www.fireeye.com/blog/threat-research/2020/07/capa-automatically-identify-malware-capabilities.html).
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Requirements
|
||||
|
||||
capa explorer supports Python >= 3.6 and the following IDA Pro versions:
|
||||
|
||||
* IDA 7.4
|
||||
* IDA 7.5
|
||||
* IDA 7.6 (caveat below)
|
||||
|
||||
If you encounter issues with your specific setup, please open a new [Issue](https://github.com/fireeye/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)
|
||||
|
||||
|
||||
### Supported File Types
|
||||
|
||||
capa explorer is limited to the file types supported by capa, which include:
|
||||
|
||||
* Windows 32-bit and 64-bit PE files
|
||||
* Windows 32-bit 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/fireeye/capa-rules) (capa explorer needs capa rules to analyze a database)
|
||||
4. Copy [capa_explorer.py](https://raw.githubusercontent.com/fireeye/capa/master/capa/ida/plugin/capa_explorer.py) to your IDA plugins directory
|
||||
|
||||
### Usage
|
||||
|
||||
1. Open IDA and analyze a supported file type (select the `Manual Load` and `Load Resources` options in IDA for best results)
|
||||
2. Open capa explorer in IDA by navigating to `Edit > Plugins > FLARE capa explorer` or using the keyboard shortcut `Alt+F5`
|
||||
You can also use `ida_loader.load_and_run_plugin("capa_explorer", arg)`. `arg` is a bitflag for which setting the LSB enables automatic analysis. See `capa.ida.plugin.form.Options` for more details.
|
||||
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/fireeye/capa-rules) when getting started with the plugin.
|
||||
|
||||
#### Tips for Program Analysis
|
||||
|
||||
* Start analysis by clicking the `Analyze` button
|
||||
* 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`
|
||||
* 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
|
||||
|
||||
#### Tips for Rule Generator
|
||||
|
||||
* Navigate to a function in your Disassembly view and click`Analyze` to get started
|
||||
* Double-click or use multi-select + right-click to add features from the `Features` pane to the `Editor` pane
|
||||
* Right-click features in the `Editor` pane to make context-specific modifications
|
||||
* Drag-and-drop (single click + multi-select support) features in the `Editor` pane to construct your hierarchy of statements and features
|
||||
* Right-click anywhere in the `Editor` pane not on a feature to remove all features
|
||||
* Add descriptions or comments to a feature by editing the corresponding column in the `Editor` pane
|
||||
* 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`
|
||||
|
||||
## 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
|
||||
installation guide](https://github.com/fireeye/capa/blob/master/doc/installation.md#method-3-inspecting-the-capa-source-code). Once installed, copy [capa_explorer.py](https://raw.githubusercontent.com/fireeye/capa/master/capa/ida/plugin/capa_explorer.py)
|
||||
to your plugins directory to install capa explorer in IDA.
|
||||
|
||||
### Components
|
||||
|
||||
capa explorer consists of two main components:
|
||||
|
||||
* An [feature extractor](https://github.com/fireeye/capa/tree/master/capa/features/extractors/ida) built on top of IDA's binary analysis engine
|
||||
* This component uses IDAPython to extract [capa features](https://github.com/fireeye/capa-rules/blob/master/doc/format.md#extracted-features) from your IDBs such as strings,
|
||||
disassembly, and control flow; these extracted features are used by capa to find feature combinations that result in a rule match
|
||||
* An [interactive user interface](https://github.com/fireeye/capa/tree/master/capa/ida/plugin) for displaying and exploring capa rule matches
|
||||
* This component integrates the feature extractor and capa, providing an interactive user interface to dissect rule matches found by capa using features extracted directly from your IDBs
|
||||
124
capa/ida/plugin/__init__.py
Normal file
@@ -0,0 +1,124 @@
|
||||
# Copyright (C) 2020 FireEye, 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 logging
|
||||
|
||||
import idaapi
|
||||
import ida_kernwin
|
||||
|
||||
from capa.ida.plugin.form import CapaExplorerForm
|
||||
from capa.ida.plugin.icon import ICON
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CapaExplorerPlugin(idaapi.plugin_t):
|
||||
|
||||
# Mandatory definitions
|
||||
PLUGIN_NAME = "FLARE capa explorer"
|
||||
PLUGIN_VERSION = "1.0.0"
|
||||
PLUGIN_AUTHORS = "michael.hunhoff@mandiant.com, william.ballenthin@mandiant.com, moritz.raabe@mandiant.com"
|
||||
|
||||
wanted_name = PLUGIN_NAME
|
||||
wanted_hotkey = "ALT-F5"
|
||||
comment = "IDA Pro plugin for the FLARE team's capa tool to identify capabilities in executable files."
|
||||
website = "https://github.com/fireeye/capa"
|
||||
help = "See https://github.com/fireeye/capa/blob/master/doc/usage.md"
|
||||
version = ""
|
||||
flags = 0
|
||||
|
||||
def __init__(self):
|
||||
"""initialize plugin"""
|
||||
self.form = None
|
||||
|
||||
def init(self):
|
||||
"""called when IDA is loading the plugin"""
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
import capa.ida.helpers
|
||||
|
||||
# do not load plugin if IDA version/file type not supported
|
||||
if not capa.ida.helpers.is_supported_ida_version():
|
||||
return idaapi.PLUGIN_SKIP
|
||||
if not capa.ida.helpers.is_supported_file_type():
|
||||
return idaapi.PLUGIN_SKIP
|
||||
return idaapi.PLUGIN_OK
|
||||
|
||||
def term(self):
|
||||
"""called when IDA is unloading the plugin"""
|
||||
pass
|
||||
|
||||
def run(self, arg):
|
||||
"""
|
||||
called when IDA is running the plugin as a script
|
||||
|
||||
args:
|
||||
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)
|
||||
return True
|
||||
|
||||
|
||||
# set the capa plugin icon.
|
||||
#
|
||||
# TL;DR: temporarily install a UI hook set the icon.
|
||||
#
|
||||
# Long form:
|
||||
#
|
||||
# in the IDAPython `plugin_t` life cycle,
|
||||
# - `init` decides if a plugin should be registered
|
||||
# - `run` executes the main logic (shows the window)
|
||||
# - `term` cleans this up
|
||||
#
|
||||
# we want to associate an icon with the plugin action - which is created by IDA.
|
||||
# however, this action is created by IDA *after* `init` is called.
|
||||
# so, we can't do this in `plugin_t.init`.
|
||||
# we also can't spawn a thread and do it after a delay,
|
||||
# since `ida_kernwin.update_action_icon` must be called from the main thread.
|
||||
# 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.
|
||||
# so, create this hook, wait for capa plugin to load, set the icon, and unhook.
|
||||
|
||||
|
||||
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__()
|
||||
self.cb = cb
|
||||
|
||||
def updated_actions(self):
|
||||
if self.cb():
|
||||
# uninstall the callback once its run successfully
|
||||
self.unhook()
|
||||
|
||||
|
||||
def install_icon():
|
||||
plugin_name = CapaExplorerPlugin.PLUGIN_NAME
|
||||
action_name = "Edit/Plugins/" + plugin_name
|
||||
|
||||
if action_name not in ida_kernwin.get_registered_actions():
|
||||
# keep the hook registered
|
||||
return False
|
||||
|
||||
# resource leak here. need to call `ida_kernwin.free_custom_icon`?
|
||||
# however, since we're not cycling this icon a lot, its probably ok.
|
||||
# expect to leak exactly one icon per application load.
|
||||
icon = ida_kernwin.load_custom_icon(data=ICON)
|
||||
|
||||
ida_kernwin.update_action_icon(action_name, icon)
|
||||
|
||||
# uninstall the hook
|
||||
return True
|
||||
|
||||
|
||||
h = OnUpdatedActionsHook(install_icon)
|
||||
h.hook()
|
||||
17
capa/ida/plugin/capa_explorer.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# Copyright (C) 2020 FireEye, 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 capa.ida.plugin import CapaExplorerPlugin
|
||||
|
||||
|
||||
def PLUGIN_ENTRY():
|
||||
"""mandatory entry point for IDAPython plugins
|
||||
|
||||
copy this script to your IDA plugins directory and start the plugin by navigating to Edit > Plugins in IDA Pro
|
||||
"""
|
||||
return CapaExplorerPlugin()
|
||||
1227
capa/ida/plugin/form.py
Normal file
60
capa/ida/plugin/hooks.py
Normal file
@@ -0,0 +1,60 @@
|
||||
# Copyright (C) 2020 FireEye, 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 idaapi
|
||||
|
||||
|
||||
class CapaExplorerIdaHooks(idaapi.UI_Hooks):
|
||||
def __init__(self, screen_ea_changed_hook, action_hooks):
|
||||
"""facilitate IDA 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__()
|
||||
|
||||
self.screen_ea_changed_hook = screen_ea_changed_hook
|
||||
self.process_action_hooks = action_hooks
|
||||
self.process_action_handle = None
|
||||
self.process_action_meta = {}
|
||||
|
||||
def preprocess_action(self, name):
|
||||
"""called prior to action completed
|
||||
|
||||
@param name: name of action defined by idagui.cfg
|
||||
|
||||
@retval must be 0
|
||||
"""
|
||||
self.process_action_handle = self.process_action_hooks.get(name, None)
|
||||
|
||||
if self.process_action_handle:
|
||||
self.process_action_handle(self.process_action_meta)
|
||||
|
||||
# must return 0 for IDA
|
||||
return 0
|
||||
|
||||
def postprocess_action(self):
|
||||
"""called after action completed"""
|
||||
if not self.process_action_handle:
|
||||
return
|
||||
|
||||
self.process_action_handle(self.process_action_meta, post=True)
|
||||
self.reset()
|
||||
|
||||
def screen_ea_changed(self, curr_ea, prev_ea):
|
||||
"""called after screen location is changed
|
||||
|
||||
@param curr_ea: current location
|
||||
@param prev_ea: prev location
|
||||
"""
|
||||
self.screen_ea_changed_hook(idaapi.get_current_widget(), curr_ea, prev_ea)
|
||||
|
||||
def reset(self):
|
||||
"""reset internal state"""
|
||||
self.process_action_handle = None
|
||||
self.process_action_meta.clear()
|
||||
13
capa/ida/plugin/icon.py
Normal file
@@ -6,7 +6,6 @@
|
||||
# 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 codecs
|
||||
|
||||
import idc
|
||||
@@ -28,247 +27,324 @@ def info_to_name(display):
|
||||
|
||||
|
||||
def location_to_hex(location):
|
||||
""" convert location to hex for display """
|
||||
"""convert location to hex for display"""
|
||||
return "%08X" % location
|
||||
|
||||
|
||||
class CapaExplorerDataItem(object):
|
||||
""" store data for CapaExplorerDataModel """
|
||||
class CapaExplorerDataItem:
|
||||
"""store data for CapaExplorerDataModel"""
|
||||
|
||||
def __init__(self, parent, data):
|
||||
""" """
|
||||
def __init__(self, parent, data, can_check=True):
|
||||
"""initialize item"""
|
||||
self.pred = parent
|
||||
self._data = data
|
||||
self.children = []
|
||||
self._checked = False
|
||||
self._can_check = can_check
|
||||
|
||||
self.flags = (
|
||||
QtCore.Qt.ItemIsEnabled
|
||||
| QtCore.Qt.ItemIsSelectable
|
||||
| QtCore.Qt.ItemIsTristate
|
||||
| QtCore.Qt.ItemIsUserCheckable
|
||||
)
|
||||
# default state for item
|
||||
self.flags = QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
|
||||
|
||||
if self._can_check:
|
||||
self.flags = self.flags | QtCore.Qt.ItemIsUserCheckable | QtCore.Qt.ItemIsTristate
|
||||
|
||||
if self.pred:
|
||||
self.pred.appendChild(self)
|
||||
|
||||
def setIsEditable(self, isEditable=False):
|
||||
""" modify item flags to be editable or not """
|
||||
"""modify item editable flags
|
||||
|
||||
@param isEditable: True, can edit, False cannot edit
|
||||
"""
|
||||
if isEditable:
|
||||
self.flags |= QtCore.Qt.ItemIsEditable
|
||||
else:
|
||||
self.flags &= ~QtCore.Qt.ItemIsEditable
|
||||
|
||||
def setChecked(self, checked):
|
||||
""" set item as checked """
|
||||
"""set item as checked
|
||||
|
||||
@param checked: True, item checked, False item not checked
|
||||
"""
|
||||
self._checked = checked
|
||||
|
||||
def canCheck(self):
|
||||
""" """
|
||||
return self._can_check
|
||||
|
||||
def isChecked(self):
|
||||
""" get item is checked """
|
||||
"""get item is checked"""
|
||||
return self._checked
|
||||
|
||||
def appendChild(self, item):
|
||||
"""add child item
|
||||
"""add a new child to specified item
|
||||
|
||||
@param item: CapaExplorerDataItem*
|
||||
@param item: CapaExplorerDataItem
|
||||
"""
|
||||
self.children.append(item)
|
||||
|
||||
def child(self, row):
|
||||
"""get child row
|
||||
|
||||
@param row: TODO
|
||||
@param row: row number
|
||||
"""
|
||||
return self.children[row]
|
||||
|
||||
def childCount(self):
|
||||
""" get child count """
|
||||
"""get child count"""
|
||||
return len(self.children)
|
||||
|
||||
def columnCount(self):
|
||||
""" get column count """
|
||||
"""get column count"""
|
||||
return len(self._data)
|
||||
|
||||
def data(self, column):
|
||||
""" get data at column """
|
||||
"""get data at column
|
||||
|
||||
@param: column number
|
||||
"""
|
||||
try:
|
||||
return self._data[column]
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
def parent(self):
|
||||
""" get parent """
|
||||
"""get parent"""
|
||||
return self.pred
|
||||
|
||||
def row(self):
|
||||
""" get row location """
|
||||
"""get row location"""
|
||||
if self.pred:
|
||||
return self.pred.children.index(self)
|
||||
return 0
|
||||
|
||||
def setData(self, column, value):
|
||||
""" set data in column """
|
||||
"""set data in column
|
||||
|
||||
@param column: column number
|
||||
@value: value to set (assume str)
|
||||
"""
|
||||
self._data[column] = value
|
||||
|
||||
def children(self):
|
||||
""" yield children """
|
||||
"""yield children"""
|
||||
for child in self.children:
|
||||
yield child
|
||||
|
||||
def removeChildren(self):
|
||||
""" remove children from node """
|
||||
"""remove children"""
|
||||
del self.children[:]
|
||||
|
||||
def __str__(self):
|
||||
""" get string representation of columns """
|
||||
"""get string representation of columns
|
||||
|
||||
used for copy-n-paste operations
|
||||
"""
|
||||
return " ".join([data for data in self._data if data])
|
||||
|
||||
@property
|
||||
def info(self):
|
||||
""" return data stored in information column """
|
||||
"""return data stored in information column"""
|
||||
return self._data[0]
|
||||
|
||||
@property
|
||||
def location(self):
|
||||
""" return data stored in location column """
|
||||
"""return data stored in location column"""
|
||||
try:
|
||||
# address stored as str, convert to int before return
|
||||
return int(self._data[1], 16)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
@property
|
||||
def details(self):
|
||||
""" return data stored in details column """
|
||||
"""return data stored in details column"""
|
||||
return self._data[2]
|
||||
|
||||
|
||||
class CapaExplorerRuleItem(CapaExplorerDataItem):
|
||||
""" store data relevant to capa function result """
|
||||
"""store data for rule result"""
|
||||
|
||||
fmt = "%s (%d matches)"
|
||||
|
||||
def __init__(self, parent, display, count, source):
|
||||
""" """
|
||||
display = self.fmt % (display, count) if count > 1 else display
|
||||
super(CapaExplorerRuleItem, self).__init__(parent, [display, "", ""])
|
||||
def __init__(self, parent, name, namespace, count, source, can_check=True):
|
||||
"""initialize item
|
||||
|
||||
@param parent: parent node
|
||||
@param name: rule name
|
||||
@param namespace: rule namespace
|
||||
@param count: number of match for this rule
|
||||
@param source: rule source (tooltip)
|
||||
"""
|
||||
display = self.fmt % (name, count) if count > 1 else name
|
||||
super(CapaExplorerRuleItem, self).__init__(parent, [display, "", namespace], can_check)
|
||||
self._source = source
|
||||
|
||||
@property
|
||||
def source(self):
|
||||
""" return rule contents for display """
|
||||
"""return rule source to display (tooltip)"""
|
||||
return self._source
|
||||
|
||||
|
||||
class CapaExplorerRuleMatchItem(CapaExplorerDataItem):
|
||||
""" store data relevant to capa function match result """
|
||||
"""store data for rule match"""
|
||||
|
||||
def __init__(self, parent, display, source=""):
|
||||
""" """
|
||||
"""initialize item
|
||||
|
||||
@param parent: parent node
|
||||
@param display: text to display in UI
|
||||
@param source: rule match source to display (tooltip)
|
||||
"""
|
||||
super(CapaExplorerRuleMatchItem, self).__init__(parent, [display, "", ""])
|
||||
self._source = source
|
||||
|
||||
@property
|
||||
def source(self):
|
||||
""" return rule contents for display """
|
||||
"""return rule contents for display"""
|
||||
return self._source
|
||||
|
||||
|
||||
class CapaExplorerFunctionItem(CapaExplorerDataItem):
|
||||
""" store data relevant to capa function result """
|
||||
"""store data for function match"""
|
||||
|
||||
fmt = "function(%s)"
|
||||
|
||||
def __init__(self, parent, location):
|
||||
""" """
|
||||
def __init__(self, parent, location, can_check=True):
|
||||
"""initialize item
|
||||
|
||||
@param parent: parent node
|
||||
@param location: virtual address of function as seen by IDA
|
||||
"""
|
||||
super(CapaExplorerFunctionItem, self).__init__(
|
||||
parent, [self.fmt % idaapi.get_name(location), location_to_hex(location), ""]
|
||||
parent, [self.fmt % idaapi.get_name(location), location_to_hex(location), ""], can_check
|
||||
)
|
||||
|
||||
@property
|
||||
def info(self):
|
||||
""" """
|
||||
"""return function name"""
|
||||
info = super(CapaExplorerFunctionItem, self).info
|
||||
display = info_to_name(info)
|
||||
return display if display else info
|
||||
|
||||
@info.setter
|
||||
def info(self, display):
|
||||
""" """
|
||||
"""set function name
|
||||
|
||||
called when user changes function name in plugin UI
|
||||
|
||||
@param display: new function name to display
|
||||
"""
|
||||
self._data[0] = self.fmt % display
|
||||
|
||||
|
||||
class CapaExplorerSubscopeItem(CapaExplorerDataItem):
|
||||
""" store data relevant to subscope """
|
||||
"""store data for subscope match"""
|
||||
|
||||
fmt = "subscope(%s)"
|
||||
|
||||
def __init__(self, parent, scope):
|
||||
""" """
|
||||
"""initialize item
|
||||
|
||||
@param parent: parent node
|
||||
@param scope: subscope name
|
||||
"""
|
||||
super(CapaExplorerSubscopeItem, self).__init__(parent, [self.fmt % scope, "", ""])
|
||||
|
||||
|
||||
class CapaExplorerBlockItem(CapaExplorerDataItem):
|
||||
""" store data relevant to capa basic block result """
|
||||
"""store data for basic block match"""
|
||||
|
||||
fmt = "basic block(loc_%08X)"
|
||||
|
||||
def __init__(self, parent, location):
|
||||
""" """
|
||||
"""initialize item
|
||||
|
||||
@param parent: parent node
|
||||
@param location: virtual address of basic block as seen by IDA
|
||||
"""
|
||||
super(CapaExplorerBlockItem, self).__init__(parent, [self.fmt % location, location_to_hex(location), ""])
|
||||
|
||||
|
||||
class CapaExplorerDefaultItem(CapaExplorerDataItem):
|
||||
""" store data relevant to capa default result """
|
||||
"""store data for default match e.g. statement (and, or)"""
|
||||
|
||||
def __init__(self, parent, display, details="", location=None):
|
||||
""" """
|
||||
"""initialize item
|
||||
|
||||
@param parent: parent node
|
||||
@param display: text to display in UI
|
||||
@param details: text to display in details section of UI
|
||||
@param location: virtual address as seen by IDA
|
||||
"""
|
||||
location = location_to_hex(location) if location else ""
|
||||
super(CapaExplorerDefaultItem, self).__init__(parent, [display, location, details])
|
||||
|
||||
|
||||
class CapaExplorerFeatureItem(CapaExplorerDataItem):
|
||||
""" store data relevant to capa feature result """
|
||||
"""store data for feature match"""
|
||||
|
||||
def __init__(self, parent, display, location="", details=""):
|
||||
""" """
|
||||
"""initialize item
|
||||
|
||||
@param parent: parent node
|
||||
@param display: text to display in UI
|
||||
@param details: text to display in details section of UI
|
||||
@param location: virtual address as seen by IDA
|
||||
"""
|
||||
location = location_to_hex(location) if location else ""
|
||||
super(CapaExplorerFeatureItem, self).__init__(parent, [display, location, details])
|
||||
|
||||
|
||||
class CapaExplorerInstructionViewItem(CapaExplorerFeatureItem):
|
||||
""" store data relevant to an instruction preview """
|
||||
"""store data for instruction match"""
|
||||
|
||||
def __init__(self, parent, display, location):
|
||||
""" """
|
||||
"""initialize item
|
||||
|
||||
details section shows disassembly view for match
|
||||
|
||||
@param parent: parent node
|
||||
@param display: text to display in UI
|
||||
@param location: virtual address as seen by IDA
|
||||
"""
|
||||
details = capa.ida.helpers.get_disasm_line(location)
|
||||
super(CapaExplorerInstructionViewItem, self).__init__(parent, display, location=location, details=details)
|
||||
self.ida_highlight = idc.get_color(location, idc.CIC_ITEM)
|
||||
|
||||
|
||||
class CapaExplorerByteViewItem(CapaExplorerFeatureItem):
|
||||
""" store data relevant to byte preview """
|
||||
"""store data for byte match"""
|
||||
|
||||
def __init__(self, parent, display, location):
|
||||
""" """
|
||||
"""initialize item
|
||||
|
||||
details section shows byte preview for match
|
||||
|
||||
@param parent: parent node
|
||||
@param display: text to display in UI
|
||||
@param location: virtual address as seen by IDA
|
||||
"""
|
||||
byte_snap = idaapi.get_bytes(location, 32)
|
||||
|
||||
details = ""
|
||||
if byte_snap:
|
||||
byte_snap = codecs.encode(byte_snap, "hex").upper()
|
||||
if sys.version_info >= (3, 0):
|
||||
details = " ".join([byte_snap[i : i + 2].decode() for i in range(0, len(byte_snap), 2)])
|
||||
else:
|
||||
details = " ".join([byte_snap[i : i + 2] for i in range(0, len(byte_snap), 2)])
|
||||
else:
|
||||
details = ""
|
||||
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)
|
||||
self.ida_highlight = idc.get_color(location, idc.CIC_ITEM)
|
||||
|
||||
|
||||
class CapaExplorerStringViewItem(CapaExplorerFeatureItem):
|
||||
""" store data relevant to string preview """
|
||||
"""store data for string match"""
|
||||
|
||||
def __init__(self, parent, display, location):
|
||||
""" """
|
||||
super(CapaExplorerStringViewItem, self).__init__(parent, display, location=location)
|
||||
def __init__(self, parent, display, location, value):
|
||||
"""initialize item
|
||||
|
||||
@param parent: parent node
|
||||
@param display: text to display in UI
|
||||
@param location: virtual address as seen by IDA
|
||||
"""
|
||||
super(CapaExplorerStringViewItem, self).__init__(parent, display, location=location, details=value)
|
||||
self.ida_highlight = idc.get_color(location, idc.CIC_ITEM)
|
||||
@@ -6,16 +6,17 @@
|
||||
# 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 collections import deque
|
||||
from collections import deque, defaultdict
|
||||
|
||||
import idc
|
||||
import six
|
||||
import idaapi
|
||||
from PyQt5 import Qt, QtGui, QtCore
|
||||
from PyQt5 import QtGui, QtCore
|
||||
|
||||
import capa.rules
|
||||
import capa.ida.helpers
|
||||
import capa.render.utils as rutils
|
||||
from capa.ida.explorer.item import (
|
||||
import capa.features.common
|
||||
from capa.ida.plugin.item import (
|
||||
CapaExplorerDataItem,
|
||||
CapaExplorerRuleItem,
|
||||
CapaExplorerBlockItem,
|
||||
@@ -30,11 +31,11 @@ from capa.ida.explorer.item import (
|
||||
)
|
||||
|
||||
# default highlight color used in IDA window
|
||||
DEFAULT_HIGHLIGHT = 0xD096FF
|
||||
DEFAULT_HIGHLIGHT = 0xE6C700
|
||||
|
||||
|
||||
class CapaExplorerDataModel(QtCore.QAbstractItemModel):
|
||||
""" """
|
||||
"""model for displaying hierarchical results return by capa"""
|
||||
|
||||
COLUMN_INDEX_RULE_INFORMATION = 0
|
||||
COLUMN_INDEX_VIRTUAL_ADDRESS = 1
|
||||
@@ -43,14 +44,16 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
|
||||
COLUMN_COUNT = 3
|
||||
|
||||
def __init__(self, parent=None):
|
||||
""" """
|
||||
"""initialize model"""
|
||||
super(CapaExplorerDataModel, self).__init__(parent)
|
||||
# root node does not have parent, contains header columns
|
||||
self.root_node = CapaExplorerDataItem(None, ["Rule Information", "Address", "Details"])
|
||||
|
||||
def reset(self):
|
||||
""" """
|
||||
# reset checkboxes and color highlights
|
||||
# TODO: make less hacky
|
||||
"""reset UI elements (e.g. checkboxes, IDA color highlights)
|
||||
|
||||
called when view wants to reset UI display
|
||||
"""
|
||||
for idx in range(self.root_node.childCount()):
|
||||
root_index = self.index(idx, 0, QtCore.QModelIndex())
|
||||
for model_index in self.iterateChildrenIndexFromRootIndex(root_index, ignore_root=False):
|
||||
@@ -59,15 +62,18 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
|
||||
self.dataChanged.emit(model_index, model_index)
|
||||
|
||||
def clear(self):
|
||||
""" """
|
||||
"""clear model data
|
||||
|
||||
called when view wants to clear UI display
|
||||
"""
|
||||
self.beginResetModel()
|
||||
self.root_node.removeChildren()
|
||||
self.endResetModel()
|
||||
|
||||
def columnCount(self, model_index):
|
||||
"""get the number of columns for the children of the given parent
|
||||
"""return number of columns for the children of the given parent
|
||||
|
||||
@param model_index: QModelIndex*
|
||||
@param model_index: QModelIndex
|
||||
|
||||
@retval column count
|
||||
"""
|
||||
@@ -77,9 +83,11 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
|
||||
return self.root_node.columnCount()
|
||||
|
||||
def data(self, model_index, role):
|
||||
"""get data stored under the given role for the item referred to by the index
|
||||
"""return data stored at given index by display role
|
||||
|
||||
@param model_index: QModelIndex*
|
||||
this function is used to control UI elements (e.g. text font, color, etc.) based on column, item type, etc.
|
||||
|
||||
@param model_index: QModelIndex
|
||||
@param role: QtCore.Qt.*
|
||||
|
||||
@retval data to be displayed
|
||||
@@ -104,6 +112,8 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
|
||||
|
||||
if role == QtCore.Qt.CheckStateRole and column == CapaExplorerDataModel.COLUMN_INDEX_RULE_INFORMATION:
|
||||
# inform view how to display content of checkbox - un/checked
|
||||
if not item.canCheck():
|
||||
return None
|
||||
return QtCore.Qt.Checked if item.isChecked() else QtCore.Qt.Unchecked
|
||||
|
||||
if role == QtCore.Qt.FontRole and column in (
|
||||
@@ -131,14 +141,14 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
|
||||
)
|
||||
and column == CapaExplorerDataModel.COLUMN_INDEX_RULE_INFORMATION
|
||||
):
|
||||
# set bold font for top-level rules
|
||||
# set bold font for important items
|
||||
font = QtGui.QFont()
|
||||
font.setBold(True)
|
||||
return font
|
||||
|
||||
if role == QtCore.Qt.ForegroundRole and column == CapaExplorerDataModel.COLUMN_INDEX_VIRTUAL_ADDRESS:
|
||||
# set color for virtual address column
|
||||
return QtGui.QColor(88, 139, 174)
|
||||
return QtGui.QColor(37, 147, 215)
|
||||
|
||||
if (
|
||||
role == QtCore.Qt.ForegroundRole
|
||||
@@ -151,9 +161,9 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
|
||||
return None
|
||||
|
||||
def flags(self, model_index):
|
||||
"""get item flags for given index
|
||||
"""return item flags for given index
|
||||
|
||||
@param model_index: QModelIndex*
|
||||
@param model_index: QModelIndex
|
||||
|
||||
@retval QtCore.Qt.ItemFlags
|
||||
"""
|
||||
@@ -163,13 +173,13 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
|
||||
return model_index.internalPointer().flags
|
||||
|
||||
def headerData(self, section, orientation, role):
|
||||
"""get data for the given role and section in the header with the specified orientation
|
||||
"""return data for the given role and section in the header with the specified orientation
|
||||
|
||||
@param section: int
|
||||
@param orientation: QtCore.Qt.Orientation
|
||||
@param role: QtCore.Qt.DisplayRole
|
||||
|
||||
@retval header data list()
|
||||
@retval header data
|
||||
"""
|
||||
if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
|
||||
return self.root_node.data(section)
|
||||
@@ -177,13 +187,13 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
|
||||
return None
|
||||
|
||||
def index(self, row, column, parent):
|
||||
"""get index of the item in the model specified by the given row, column and parent index
|
||||
"""return index of the item by row, column, and parent index
|
||||
|
||||
@param row: int
|
||||
@param column: int
|
||||
@param parent: QModelIndex*
|
||||
@param row: item row
|
||||
@param column: item column
|
||||
@param parent: QModelIndex of parent
|
||||
|
||||
@retval QModelIndex*
|
||||
@retval QModelIndex of item
|
||||
"""
|
||||
if not self.hasIndex(row, column, parent):
|
||||
return QtCore.QModelIndex()
|
||||
@@ -201,13 +211,13 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
|
||||
return QtCore.QModelIndex()
|
||||
|
||||
def parent(self, model_index):
|
||||
"""get parent of the model item with the given index
|
||||
"""return parent index by child index
|
||||
|
||||
if the item has no parent, an invalid QModelIndex* is returned
|
||||
if the item has no parent, an invalid QModelIndex is returned
|
||||
|
||||
@param model_index: QModelIndex*
|
||||
@param model_index: QModelIndex of child
|
||||
|
||||
@retval QModelIndex*
|
||||
@retval QModelIndex of parent
|
||||
"""
|
||||
if not model_index.isValid():
|
||||
return QtCore.QModelIndex()
|
||||
@@ -223,10 +233,10 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
|
||||
def iterateChildrenIndexFromRootIndex(self, model_index, ignore_root=True):
|
||||
"""depth-first traversal of child nodes
|
||||
|
||||
@param model_index: QModelIndex*
|
||||
@param ignore_root: if set, do not return root index
|
||||
@param model_index: QModelIndex of starting item
|
||||
@param ignore_root: True, do not yield root index, False yield root index
|
||||
|
||||
@retval yield QModelIndex*
|
||||
@retval yield QModelIndex
|
||||
"""
|
||||
visited = set()
|
||||
stack = deque((model_index,))
|
||||
@@ -248,10 +258,10 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
|
||||
stack.append(child_index.child(idx, 0))
|
||||
|
||||
def reset_ida_highlighting(self, item, checked):
|
||||
"""reset IDA highlight for an item
|
||||
"""reset IDA highlight for item
|
||||
|
||||
@param item: capa explorer item
|
||||
@param checked: indicates item is or not checked
|
||||
@param item: CapaExplorerDataItem
|
||||
@param checked: True, item checked, False item not checked
|
||||
"""
|
||||
if not isinstance(
|
||||
item, (CapaExplorerStringViewItem, CapaExplorerInstructionViewItem, CapaExplorerByteViewItem)
|
||||
@@ -275,13 +285,11 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
|
||||
idc.set_color(item.location, idc.CIC_ITEM, item.ida_highlight)
|
||||
|
||||
def setData(self, model_index, value, role):
|
||||
"""set the role data for the item at index to value
|
||||
"""set data at index by role
|
||||
|
||||
@param model_index: QModelIndex*
|
||||
@param value: QVariant*
|
||||
@param model_index: QModelIndex of item
|
||||
@param value: value to set
|
||||
@param role: QtCore.Qt.EditRole
|
||||
|
||||
@retval True/False
|
||||
"""
|
||||
if not model_index.isValid():
|
||||
return False
|
||||
@@ -316,12 +324,11 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
|
||||
return False
|
||||
|
||||
def rowCount(self, model_index):
|
||||
"""get the number of rows under the given parent
|
||||
"""return number of rows under item by index
|
||||
|
||||
when the parent is valid it means that is returning the number of
|
||||
children of parent
|
||||
when the parent is valid it means that is returning the number of children of parent
|
||||
|
||||
@param model_index: QModelIndex*
|
||||
@param model_index: QModelIndex
|
||||
|
||||
@retval row count
|
||||
"""
|
||||
@@ -341,11 +348,7 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
|
||||
@param parent: parent to which new child is assigned
|
||||
@param statement: statement read from doc
|
||||
@param locations: locations of children (applies to range only?)
|
||||
@param doc: capa result doc
|
||||
|
||||
"statement": {
|
||||
"type": "or"
|
||||
},
|
||||
@param doc: result doc
|
||||
"""
|
||||
if statement["type"] in ("and", "or", "optional"):
|
||||
display = statement["type"]
|
||||
@@ -399,24 +402,7 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
|
||||
|
||||
@param parent: parent node to which new child is assigned
|
||||
@param match: match read from doc
|
||||
@param doc: capa result doc
|
||||
|
||||
"matches": {
|
||||
"0": {
|
||||
"children": [],
|
||||
"locations": [
|
||||
4317184
|
||||
],
|
||||
"node": {
|
||||
"feature": {
|
||||
"section": ".rsrc",
|
||||
"type": "section"
|
||||
},
|
||||
"type": "feature"
|
||||
},
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
@param doc: result doc
|
||||
"""
|
||||
if not match["success"]:
|
||||
# TODO: display failed branches at some point? Help with debugging rules?
|
||||
@@ -442,16 +428,34 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
|
||||
for child in match.get("children", []):
|
||||
self.render_capa_doc_match(parent2, child, doc)
|
||||
|
||||
def render_capa_doc(self, doc):
|
||||
"""render capa features specified in doc
|
||||
|
||||
@param doc: capa result doc
|
||||
"""
|
||||
# inform model that changes are about to occur
|
||||
self.beginResetModel()
|
||||
|
||||
def render_capa_doc_by_function(self, doc):
|
||||
""" """
|
||||
matches_by_function = {}
|
||||
for rule in rutils.capability_rules(doc):
|
||||
parent = CapaExplorerRuleItem(self.root_node, rule["meta"]["name"], len(rule["matches"]), rule["source"])
|
||||
for ea in rule["matches"].keys():
|
||||
ea = capa.ida.helpers.get_func_start_ea(ea)
|
||||
if ea is None:
|
||||
# file scope, skip for rendering in this mode
|
||||
continue
|
||||
if None is matches_by_function.get(ea, None):
|
||||
matches_by_function[ea] = CapaExplorerFunctionItem(self.root_node, ea, can_check=False)
|
||||
CapaExplorerRuleItem(
|
||||
matches_by_function[ea],
|
||||
rule["meta"]["name"],
|
||||
rule["meta"].get("namespace"),
|
||||
len(rule["matches"]),
|
||||
rule["source"],
|
||||
can_check=False,
|
||||
)
|
||||
|
||||
def render_capa_doc_by_program(self, doc):
|
||||
""" """
|
||||
for rule in rutils.capability_rules(doc):
|
||||
rule_name = rule["meta"]["name"]
|
||||
rule_namespace = rule["meta"].get("namespace")
|
||||
parent = CapaExplorerRuleItem(
|
||||
self.root_node, rule_name, rule_namespace, len(rule["matches"]), rule["source"]
|
||||
)
|
||||
|
||||
for (location, match) in doc["rules"][rule["meta"]["name"]]["matches"].items():
|
||||
if rule["meta"]["scope"] == capa.rules.FILE_SCOPE:
|
||||
@@ -465,6 +469,19 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
|
||||
|
||||
self.render_capa_doc_match(parent2, match, doc)
|
||||
|
||||
def render_capa_doc(self, doc, by_function):
|
||||
"""render capa features specified in doc
|
||||
|
||||
@param doc: capa result doc
|
||||
"""
|
||||
# inform model that changes are about to occur
|
||||
self.beginResetModel()
|
||||
|
||||
if by_function:
|
||||
self.render_capa_doc_by_function(doc)
|
||||
else:
|
||||
self.render_capa_doc_by_program(doc)
|
||||
|
||||
# inform model changes have ended
|
||||
self.endResetModel()
|
||||
|
||||
@@ -472,23 +489,18 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
|
||||
"""convert capa doc feature type string to display string for ui
|
||||
|
||||
@param feature: capa feature read from doc
|
||||
|
||||
Example:
|
||||
"feature": {
|
||||
"bytes": "01 14 02 00 00 00 00 00 C0 00 00 00 00 00 00 46",
|
||||
"description": "CLSID_ShellLink",
|
||||
"type": "bytes"
|
||||
}
|
||||
|
||||
bytes(01 14 02 00 00 00 00 00 C0 00 00 00 00 00 00 46 = CLSID_ShellLink)
|
||||
"""
|
||||
if feature[feature["type"]]:
|
||||
key = feature["type"]
|
||||
value = feature[feature["type"]]
|
||||
if value:
|
||||
if key == "string":
|
||||
value = '"%s"' % capa.features.common.escape_string(value)
|
||||
if feature.get("description", ""):
|
||||
return "%s(%s = %s)" % (feature["type"], feature[feature["type"]], feature["description"])
|
||||
return "%s(%s = %s)" % (key, value, feature["description"])
|
||||
else:
|
||||
return "%s(%s)" % (feature["type"], feature[feature["type"]])
|
||||
return "%s(%s)" % (key, value)
|
||||
else:
|
||||
return "%s" % feature["type"]
|
||||
return "%s" % key
|
||||
|
||||
def render_capa_doc_feature_node(self, parent, feature, locations, doc):
|
||||
"""process capa doc feature node
|
||||
@@ -497,13 +509,6 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
|
||||
@param feature: capa doc feature node
|
||||
@param locations: locations identified for feature
|
||||
@param doc: capa doc
|
||||
|
||||
Example:
|
||||
"feature": {
|
||||
"description": "FILE_WRITE_DATA",
|
||||
"number": "0x2",
|
||||
"type": "number"
|
||||
}
|
||||
"""
|
||||
display = self.capa_doc_feature_to_display(feature)
|
||||
|
||||
@@ -532,14 +537,7 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
|
||||
@param feature: feature read from doc
|
||||
@param doc: capa feature doc
|
||||
@param location: address of feature
|
||||
@param display: text to display in plugin ui
|
||||
|
||||
Example:
|
||||
"feature": {
|
||||
"description": "FILE_WRITE_DATA",
|
||||
"number": "0x2",
|
||||
"type": "number"
|
||||
}
|
||||
@param display: text to display in plugin UI
|
||||
"""
|
||||
# special handling for characteristic pending type
|
||||
if feature["type"] == "characteristic":
|
||||
@@ -559,7 +557,14 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
|
||||
)
|
||||
|
||||
if feature["type"] == "regex":
|
||||
return CapaExplorerFeatureItem(parent, display, location, details=feature["match"])
|
||||
for s, locations in feature["matches"].items():
|
||||
if location in locations:
|
||||
return CapaExplorerStringViewItem(
|
||||
parent, display, location, '"' + capa.features.common.escape_string(s) + '"'
|
||||
)
|
||||
|
||||
# programming error: the given location should always be found in the regex matches
|
||||
raise ValueError("regex match at location not found")
|
||||
|
||||
if feature["type"] == "basicblock":
|
||||
return CapaExplorerBlockItem(parent, location)
|
||||
@@ -584,18 +589,22 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
|
||||
|
||||
if feature["type"] in ("string",):
|
||||
# display string preview
|
||||
return CapaExplorerStringViewItem(parent, display, location)
|
||||
return CapaExplorerStringViewItem(
|
||||
parent, display, location, '"%s"' % capa.features.common.escape_string(feature[feature["type"]])
|
||||
)
|
||||
|
||||
if feature["type"] in ("import", "export"):
|
||||
if feature["type"] in ("import", "export", "function-name"):
|
||||
# display no preview
|
||||
return CapaExplorerFeatureItem(parent, display=display)
|
||||
return CapaExplorerFeatureItem(parent, location=location, display=display)
|
||||
|
||||
raise RuntimeError("unexpected feature type: " + str(feature["type"]))
|
||||
|
||||
def update_function_name(self, old_name, new_name):
|
||||
"""update all instances of old function name with new function name
|
||||
|
||||
@param old_name: previous function name
|
||||
called when user updates function name using plugin UI
|
||||
|
||||
@param old_name: old function name
|
||||
@param new_name: new function name
|
||||
"""
|
||||
# create empty root index for search
|
||||
225
capa/ida/plugin/proxy.py
Normal file
@@ -0,0 +1,225 @@
|
||||
# Copyright (C) 2020 FireEye, 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 PyQt5 import QtCore
|
||||
from PyQt5.QtCore import Qt
|
||||
|
||||
from capa.ida.plugin.model import CapaExplorerDataModel
|
||||
|
||||
|
||||
class CapaExplorerRangeProxyModel(QtCore.QSortFilterProxyModel):
|
||||
"""filter results based on virtual address range as seen by IDA
|
||||
|
||||
implements filtering for "limit results by current function" checkbox in plugin UI
|
||||
|
||||
minimum and maximum virtual addresses are used to filter results to a specific address range. this allows
|
||||
basic blocks to be included when limiting results to a specific function
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
"""initialize proxy filter"""
|
||||
super(CapaExplorerRangeProxyModel, self).__init__(parent)
|
||||
self.min_ea = None
|
||||
self.max_ea = None
|
||||
|
||||
def lessThan(self, left, right):
|
||||
"""return True if left item is less than right item, else False
|
||||
|
||||
@param left: QModelIndex of left
|
||||
@param right: QModelIndex of right
|
||||
"""
|
||||
ldata = left.internalPointer().data(left.column())
|
||||
rdata = right.internalPointer().data(right.column())
|
||||
|
||||
if (
|
||||
ldata
|
||||
and rdata
|
||||
and left.column() == CapaExplorerDataModel.COLUMN_INDEX_VIRTUAL_ADDRESS
|
||||
and left.column() == right.column()
|
||||
):
|
||||
# convert virtual address before compare
|
||||
return int(ldata, 16) < int(rdata, 16)
|
||||
else:
|
||||
# compare as lowercase
|
||||
return ldata.lower() < rdata.lower()
|
||||
|
||||
def filterAcceptsRow(self, row, parent):
|
||||
"""return true if the item in the row indicated by the given row and parent should be included in the model;
|
||||
otherwise return false
|
||||
|
||||
@param row: row number
|
||||
@param parent: QModelIndex of parent
|
||||
"""
|
||||
if self.filter_accepts_row_self(row, parent):
|
||||
return True
|
||||
|
||||
alpha = parent
|
||||
while alpha.isValid():
|
||||
if self.filter_accepts_row_self(alpha.row(), alpha.parent()):
|
||||
return True
|
||||
alpha = alpha.parent()
|
||||
|
||||
if self.index_has_accepted_children(row, parent):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def index_has_accepted_children(self, row, parent):
|
||||
"""return True if parent has one or more children that match filter, else False
|
||||
|
||||
@param row: row number
|
||||
@param parent: QModelIndex of parent
|
||||
"""
|
||||
model_index = self.sourceModel().index(row, 0, parent)
|
||||
|
||||
if model_index.isValid():
|
||||
for idx in range(self.sourceModel().rowCount(model_index)):
|
||||
if self.filter_accepts_row_self(idx, model_index):
|
||||
return True
|
||||
if self.index_has_accepted_children(idx, model_index):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def filter_accepts_row_self(self, row, parent):
|
||||
"""return True if filter accepts row, else False
|
||||
|
||||
@param row: row number
|
||||
@param parent: QModelIndex of parent
|
||||
"""
|
||||
# filter not set
|
||||
if self.min_ea is None and self.max_ea is None:
|
||||
return True
|
||||
|
||||
index = self.sourceModel().index(row, 0, parent)
|
||||
data = index.internalPointer().data(CapaExplorerDataModel.COLUMN_INDEX_VIRTUAL_ADDRESS)
|
||||
|
||||
# virtual address may be empty
|
||||
if not data:
|
||||
return False
|
||||
|
||||
# convert virtual address str to int
|
||||
ea = int(data, 16)
|
||||
|
||||
if self.min_ea <= ea and ea < self.max_ea:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def add_address_range_filter(self, min_ea, max_ea):
|
||||
"""add new address range filter
|
||||
|
||||
called when user checks "limit results by current function" in plugin UI
|
||||
|
||||
@param min_ea: minimum virtual address as seen by IDA
|
||||
@param max_ea: maximum virtual address as seen by IDA
|
||||
"""
|
||||
self.min_ea = min_ea
|
||||
self.max_ea = max_ea
|
||||
|
||||
self.setFilterKeyColumn(CapaExplorerDataModel.COLUMN_INDEX_VIRTUAL_ADDRESS)
|
||||
self.invalidateFilter()
|
||||
|
||||
def reset_address_range_filter(self):
|
||||
"""remove address range filter (accept all results)
|
||||
|
||||
called when user un-checks "limit results by current function" in plugin UI
|
||||
"""
|
||||
self.min_ea = None
|
||||
self.max_ea = None
|
||||
self.invalidateFilter()
|
||||
|
||||
|
||||
class CapaExplorerSearchProxyModel(QtCore.QSortFilterProxyModel):
|
||||
"""A SortFilterProxyModel that accepts rows with a substring match for a configurable query.
|
||||
|
||||
Looks for matches in the text of all rows.
|
||||
Displays the entire tree row if any of the tree branches,
|
||||
that is, you can filter by rule name, or also
|
||||
filter by "characteristic(nzxor)" to filter matches with some feature.
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
""" """
|
||||
super(CapaExplorerSearchProxyModel, self).__init__(parent)
|
||||
self.query = ""
|
||||
self.setFilterKeyColumn(-1) # all columns
|
||||
|
||||
def filterAcceptsRow(self, row, parent):
|
||||
"""true if the item in the row indicated by the given row and parent
|
||||
should be included in the model; otherwise returns false
|
||||
|
||||
@param row: int
|
||||
@param parent: QModelIndex*
|
||||
|
||||
@retval True/False
|
||||
"""
|
||||
# this row matches, accept it
|
||||
if self.filter_accepts_row_self(row, parent):
|
||||
return True
|
||||
|
||||
# the parent of this row matches, accept it
|
||||
alpha = parent
|
||||
while alpha.isValid():
|
||||
if self.filter_accepts_row_self(alpha.row(), alpha.parent()):
|
||||
return True
|
||||
alpha = alpha.parent()
|
||||
|
||||
# this row is a parent, and a child matches, accept it
|
||||
if self.index_has_accepted_children(row, parent):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def index_has_accepted_children(self, row, parent):
|
||||
"""returns True if the given row or its children should be accepted"""
|
||||
source_model = self.sourceModel()
|
||||
model_index = source_model.index(row, 0, parent)
|
||||
|
||||
if model_index.isValid():
|
||||
for idx in range(source_model.rowCount(model_index)):
|
||||
if self.filter_accepts_row_self(idx, model_index):
|
||||
return True
|
||||
if self.index_has_accepted_children(idx, model_index):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def filter_accepts_row_self(self, row, parent):
|
||||
"""returns True if the given row should be accepted"""
|
||||
if self.query == "":
|
||||
return True
|
||||
|
||||
source_model = self.sourceModel()
|
||||
|
||||
for column in (
|
||||
CapaExplorerDataModel.COLUMN_INDEX_RULE_INFORMATION,
|
||||
CapaExplorerDataModel.COLUMN_INDEX_VIRTUAL_ADDRESS,
|
||||
CapaExplorerDataModel.COLUMN_INDEX_DETAILS,
|
||||
):
|
||||
index = source_model.index(row, column, parent)
|
||||
data = source_model.data(index, Qt.DisplayRole)
|
||||
|
||||
if not data:
|
||||
continue
|
||||
|
||||
if not isinstance(data, str):
|
||||
# sanity check: should already be a string, but double check
|
||||
continue
|
||||
|
||||
# case in-sensitive matching
|
||||
if self.query.lower() in data.lower():
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def set_query(self, query):
|
||||
self.query = query
|
||||
self.invalidateFilter()
|
||||
|
||||
def reset_query(self):
|
||||
self.set_query("")
|
||||
1218
capa/ida/plugin/view.py
Normal file
@@ -1,99 +0,0 @@
|
||||
# Copyright (C) 2020 FireEye, 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 logging
|
||||
|
||||
import idc
|
||||
import idaapi
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import QTreeWidgetItem, QTreeWidgetItemIterator
|
||||
|
||||
CAPA_EXTENSION = ".capas"
|
||||
|
||||
|
||||
logger = logging.getLogger("capa_ida")
|
||||
|
||||
|
||||
def get_input_file(freeze=True):
|
||||
"""
|
||||
get input file path
|
||||
|
||||
freeze (bool): if True, get freeze file if it exists
|
||||
"""
|
||||
# try original file in same directory as idb/i64 without idb/i64 file extension
|
||||
input_file = idc.get_idb_path()[:-4]
|
||||
|
||||
if freeze:
|
||||
# use frozen file if it exists
|
||||
freeze_file_cand = "%s%s" % (input_file, CAPA_EXTENSION)
|
||||
if os.path.isfile(freeze_file_cand):
|
||||
return freeze_file_cand
|
||||
|
||||
if not os.path.isfile(input_file):
|
||||
# TM naming
|
||||
input_file = "%s.mal_" % idc.get_idb_path()[:-4]
|
||||
if not os.path.isfile(input_file):
|
||||
input_file = idaapi.ask_file(0, "*.*", "Please specify input file.")
|
||||
if not input_file:
|
||||
raise ValueError("could not find input file")
|
||||
return input_file
|
||||
|
||||
|
||||
def get_orig_color_feature_vas(vas):
|
||||
orig_colors = {}
|
||||
for va in vas:
|
||||
orig_colors[va] = idc.get_color(va, idc.CIC_ITEM)
|
||||
return orig_colors
|
||||
|
||||
|
||||
def reset_colors(orig_colors):
|
||||
if orig_colors:
|
||||
for va, color in orig_colors.iteritems():
|
||||
idc.set_color(va, idc.CIC_ITEM, orig_colors[va])
|
||||
|
||||
|
||||
def reset_selection(tree):
|
||||
iterator = QTreeWidgetItemIterator(tree, QTreeWidgetItemIterator.Checked)
|
||||
while iterator.value():
|
||||
item = iterator.value()
|
||||
item.setCheckState(0, Qt.Unchecked) # column, state
|
||||
iterator += 1
|
||||
|
||||
|
||||
def get_disasm_line(va):
|
||||
return idc.generate_disasm_line(va, idc.GENDSM_FORCE_CODE)
|
||||
|
||||
|
||||
def get_selected_items(tree, skip_level_1=False):
|
||||
selected = []
|
||||
iterator = QTreeWidgetItemIterator(tree, QTreeWidgetItemIterator.Checked)
|
||||
while iterator.value():
|
||||
item = iterator.value()
|
||||
if skip_level_1:
|
||||
# hacky way to check if item is at level 1, if so, skip
|
||||
# alternative, check if text in disasm column
|
||||
if item.parent() and item.parent().parent() is None:
|
||||
iterator += 1
|
||||
continue
|
||||
if item.text(1):
|
||||
# logger.debug('selected %s, %s', item.text(0), item.text(1))
|
||||
selected.append(int(item.text(1), 0x10))
|
||||
iterator += 1
|
||||
return selected
|
||||
|
||||
|
||||
def add_child_item(parent, values, feature=None):
|
||||
child = QTreeWidgetItem(parent)
|
||||
child.setFlags(child.flags() | Qt.ItemIsTristate | Qt.ItemIsUserCheckable)
|
||||
for i, v in enumerate(values):
|
||||
child.setText(i, v)
|
||||
if feature:
|
||||
child.setData(0, 0x100, feature)
|
||||
child.setCheckState(0, Qt.Unchecked)
|
||||
return child
|
||||
819
capa/main.py
@@ -1,266 +0,0 @@
|
||||
# Copyright (C) 2020 FireEye, 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 json
|
||||
|
||||
import six
|
||||
|
||||
import capa.rules
|
||||
import capa.engine
|
||||
|
||||
|
||||
def convert_statement_to_result_document(statement):
|
||||
"""
|
||||
"statement": {
|
||||
"type": "or"
|
||||
},
|
||||
|
||||
"statement": {
|
||||
"max": 9223372036854775808,
|
||||
"min": 2,
|
||||
"type": "range"
|
||||
},
|
||||
"""
|
||||
statement_type = statement.name.lower()
|
||||
result = {"type": statement_type}
|
||||
if statement.description:
|
||||
result["description"] = statement.description
|
||||
|
||||
if statement_type == "some" and statement.count == 0:
|
||||
result["type"] = "optional"
|
||||
elif statement_type == "some":
|
||||
result["count"] = statement.count
|
||||
elif statement_type == "range":
|
||||
result["min"] = statement.min
|
||||
result["max"] = statement.max
|
||||
result["child"] = convert_feature_to_result_document(statement.child)
|
||||
elif statement_type == "subscope":
|
||||
result["subscope"] = statement.scope
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def convert_feature_to_result_document(feature):
|
||||
"""
|
||||
"feature": {
|
||||
"number": 6,
|
||||
"type": "number"
|
||||
},
|
||||
|
||||
"feature": {
|
||||
"api": "ws2_32.WSASocket",
|
||||
"type": "api"
|
||||
},
|
||||
|
||||
"feature": {
|
||||
"match": "create TCP socket",
|
||||
"type": "match"
|
||||
},
|
||||
|
||||
"feature": {
|
||||
"characteristic": [
|
||||
"loop",
|
||||
true
|
||||
],
|
||||
"type": "characteristic"
|
||||
},
|
||||
"""
|
||||
result = {"type": feature.name, feature.name: feature.get_value_str()}
|
||||
if feature.description:
|
||||
result["description"] = feature.description
|
||||
if feature.name == "regex":
|
||||
result["match"] = feature.match
|
||||
return result
|
||||
|
||||
|
||||
def convert_node_to_result_document(node):
|
||||
"""
|
||||
"node": {
|
||||
"type": "statement",
|
||||
"statement": { ... }
|
||||
},
|
||||
|
||||
"node": {
|
||||
"type": "feature",
|
||||
"feature": { ... }
|
||||
},
|
||||
"""
|
||||
|
||||
if isinstance(node, capa.engine.Statement):
|
||||
return {
|
||||
"type": "statement",
|
||||
"statement": convert_statement_to_result_document(node),
|
||||
}
|
||||
elif isinstance(node, capa.features.Feature):
|
||||
return {
|
||||
"type": "feature",
|
||||
"feature": convert_feature_to_result_document(node),
|
||||
}
|
||||
else:
|
||||
raise RuntimeError("unexpected match node type")
|
||||
|
||||
|
||||
def convert_match_to_result_document(rules, capabilities, result):
|
||||
"""
|
||||
convert the given Result instance into a common, Python-native data structure.
|
||||
this will become part of the "result document" format that can be emitted to JSON.
|
||||
"""
|
||||
doc = {
|
||||
"success": bool(result.success),
|
||||
"node": convert_node_to_result_document(result.statement),
|
||||
"children": [convert_match_to_result_document(rules, capabilities, child) for child in result.children],
|
||||
}
|
||||
|
||||
# logic expression, like `and`, don't have locations - their children do.
|
||||
# so only add `locations` to feature nodes.
|
||||
if isinstance(result.statement, capa.features.Feature):
|
||||
if bool(result.success):
|
||||
doc["locations"] = result.locations
|
||||
elif isinstance(result.statement, capa.rules.Range):
|
||||
if bool(result.success):
|
||||
doc["locations"] = result.locations
|
||||
|
||||
# if we have a `match` statement, then we're referencing another rule.
|
||||
# this could an external rule (written by a human), or
|
||||
# rule generated to support a subscope (basic block, etc.)
|
||||
# we still want to include the matching logic in this tree.
|
||||
#
|
||||
# so, we need to lookup the other rule results
|
||||
# and then filter those down to the address used here.
|
||||
# finally, splice that logic into this tree.
|
||||
if (
|
||||
doc["node"]["type"] == "feature"
|
||||
and doc["node"]["feature"]["type"] == "match"
|
||||
# only add subtree on success,
|
||||
# because there won't be results for the other rule on failure.
|
||||
and doc["success"]
|
||||
):
|
||||
|
||||
rule_name = doc["node"]["feature"]["match"]
|
||||
rule = rules[rule_name]
|
||||
rule_matches = {address: result for (address, result) in capabilities[rule_name]}
|
||||
|
||||
if rule.meta.get("capa/subscope-rule"):
|
||||
# for a subscope rule, fixup the node to be a scope node, rather than a match feature node.
|
||||
#
|
||||
# e.g. `contain loop/30c4c78e29bf4d54894fc74f664c62e8` -> `basic block`
|
||||
scope = rule.meta["scope"]
|
||||
doc["node"] = {
|
||||
"type": "statement",
|
||||
"statement": {
|
||||
"type": "subscope",
|
||||
"subscope": scope,
|
||||
},
|
||||
}
|
||||
|
||||
for location in doc["locations"]:
|
||||
doc["children"].append(convert_match_to_result_document(rules, capabilities, rule_matches[location]))
|
||||
|
||||
return doc
|
||||
|
||||
|
||||
def convert_capabilities_to_result_document(meta, rules, capabilities):
|
||||
"""
|
||||
convert the given rule set and capabilities result to a common, Python-native data structure.
|
||||
this format can be directly emitted to JSON, or passed to the other `render_*` routines
|
||||
to render as text.
|
||||
|
||||
see examples of substructures in above routines.
|
||||
|
||||
schema:
|
||||
|
||||
```json
|
||||
{
|
||||
"meta": {...},
|
||||
"rules: {
|
||||
$rule-name: {
|
||||
"meta": {...copied from rule.meta...},
|
||||
"matches: {
|
||||
$address: {...match details...},
|
||||
...
|
||||
}
|
||||
},
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Args:
|
||||
meta (Dict[str, Any]):
|
||||
rules (RuleSet):
|
||||
capabilities (Dict[str, List[Tuple[int, Result]]]):
|
||||
"""
|
||||
doc = {
|
||||
"meta": meta,
|
||||
"rules": {},
|
||||
}
|
||||
|
||||
for rule_name, matches in capabilities.items():
|
||||
rule = rules[rule_name]
|
||||
|
||||
if rule.meta.get("capa/subscope-rule"):
|
||||
continue
|
||||
|
||||
doc["rules"][rule_name] = {
|
||||
"meta": dict(rule.meta),
|
||||
"source": rule.definition,
|
||||
"matches": {
|
||||
addr: convert_match_to_result_document(rules, capabilities, match) for (addr, match) in matches
|
||||
},
|
||||
}
|
||||
|
||||
return doc
|
||||
|
||||
|
||||
def render_vverbose(meta, rules, capabilities):
|
||||
# there's an import loop here
|
||||
# if capa.render imports capa.render.vverbose
|
||||
# and capa.render.vverbose import capa.render (implicitly, as a submodule)
|
||||
# so, defer the import until routine is called, breaking the import loop.
|
||||
import capa.render.vverbose
|
||||
|
||||
doc = convert_capabilities_to_result_document(meta, rules, capabilities)
|
||||
return capa.render.vverbose.render_vverbose(doc)
|
||||
|
||||
|
||||
def render_verbose(meta, rules, capabilities):
|
||||
# break import loop
|
||||
import capa.render.verbose
|
||||
|
||||
doc = convert_capabilities_to_result_document(meta, rules, capabilities)
|
||||
return capa.render.verbose.render_verbose(doc)
|
||||
|
||||
|
||||
def render_default(meta, rules, capabilities):
|
||||
# break import loop
|
||||
import capa.render.default
|
||||
import capa.render.verbose
|
||||
|
||||
doc = convert_capabilities_to_result_document(meta, rules, capabilities)
|
||||
return capa.render.default.render_default(doc)
|
||||
|
||||
|
||||
class CapaJsonObjectEncoder(json.JSONEncoder):
|
||||
"""JSON encoder that emits Python sets as sorted lists"""
|
||||
|
||||
def default(self, obj):
|
||||
if isinstance(obj, (list, dict, int, float, bool, type(None))) or isinstance(obj, six.string_types):
|
||||
return json.JSONEncoder.default(self, obj)
|
||||
elif isinstance(obj, set):
|
||||
return list(sorted(obj))
|
||||
else:
|
||||
# probably will TypeError
|
||||
return json.JSONEncoder.default(self, obj)
|
||||
|
||||
|
||||
def render_json(meta, rules, capabilities):
|
||||
return json.dumps(
|
||||
convert_capabilities_to_result_document(meta, rules, capabilities),
|
||||
cls=CapaJsonObjectEncoder,
|
||||
sort_keys=True,
|
||||
)
|
||||
|
||||
@@ -7,16 +7,20 @@
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import collections
|
||||
from typing import Dict, List
|
||||
|
||||
import six
|
||||
import tabulate
|
||||
|
||||
import capa.render.utils as rutils
|
||||
import capa.render.result_document
|
||||
from capa.rules import RuleSet
|
||||
from capa.engine import MatchResults
|
||||
from capa.render.utils import StringIO
|
||||
|
||||
tabulate.PRESERVE_WHITESPACE = True
|
||||
|
||||
|
||||
def width(s, character_count):
|
||||
def width(s: str, character_count: int) -> str:
|
||||
"""pad the given string to at least `character_count`"""
|
||||
if len(s) < character_count:
|
||||
return s + " " * (character_count - len(s))
|
||||
@@ -24,7 +28,7 @@ def width(s, character_count):
|
||||
return s
|
||||
|
||||
|
||||
def render_meta(doc, ostream):
|
||||
def render_meta(doc, ostream: StringIO):
|
||||
rows = [
|
||||
(width("md5", 22), width(doc["meta"]["sample"]["md5"], 82)),
|
||||
("sha1", doc["meta"]["sample"]["sha1"]),
|
||||
@@ -36,7 +40,35 @@ def render_meta(doc, ostream):
|
||||
ostream.write("\n")
|
||||
|
||||
|
||||
def render_capabilities(doc, ostream):
|
||||
def find_subrule_matches(doc):
|
||||
"""
|
||||
collect the rule names that have been matched as a subrule match.
|
||||
this way we can avoid displaying entries for things that are too specific.
|
||||
"""
|
||||
matches = set([])
|
||||
|
||||
def rec(node):
|
||||
if not node["success"]:
|
||||
# there's probably a bug here for rules that do `not: match: ...`
|
||||
# but we don't have any examples of this yet
|
||||
return
|
||||
|
||||
elif node["node"]["type"] == "statement":
|
||||
for child in node["children"]:
|
||||
rec(child)
|
||||
|
||||
elif node["node"]["type"] == "feature":
|
||||
if node["node"]["feature"]["type"] == "match":
|
||||
matches.add(node["node"]["feature"]["match"])
|
||||
|
||||
for rule in rutils.capability_rules(doc):
|
||||
for node in rule["matches"].values():
|
||||
rec(node)
|
||||
|
||||
return matches
|
||||
|
||||
|
||||
def render_capabilities(doc, ostream: StringIO):
|
||||
"""
|
||||
example::
|
||||
|
||||
@@ -48,8 +80,16 @@ def render_capabilities(doc, ostream):
|
||||
| ... | ... |
|
||||
+-------------------------------------------------------+-------------------------------------------------+
|
||||
"""
|
||||
subrule_matches = find_subrule_matches(doc)
|
||||
|
||||
rows = []
|
||||
for rule in rutils.capability_rules(doc):
|
||||
if rule["meta"]["name"] in subrule_matches:
|
||||
# rules that are also matched by other rules should not get rendered by default.
|
||||
# this cuts down on the amount of output while giving approx the same detail.
|
||||
# see #224
|
||||
continue
|
||||
|
||||
count = len(rule["matches"])
|
||||
if count == 1:
|
||||
capability = rutils.bold(rule["meta"]["name"])
|
||||
@@ -66,7 +106,7 @@ def render_capabilities(doc, ostream):
|
||||
ostream.writeln(rutils.bold("no capabilities found"))
|
||||
|
||||
|
||||
def render_attack(doc, ostream):
|
||||
def render_attack(doc, ostream: StringIO):
|
||||
"""
|
||||
example::
|
||||
|
||||
@@ -88,27 +128,16 @@ def render_attack(doc, ostream):
|
||||
continue
|
||||
|
||||
for attack in rule["meta"]["att&ck"]:
|
||||
tactic, _, rest = attack.partition("::")
|
||||
if "::" in rest:
|
||||
technique, _, rest = rest.partition("::")
|
||||
subtechnique, _, id = rest.rpartition(" ")
|
||||
tactics[tactic].add((technique, subtechnique, id))
|
||||
else:
|
||||
technique, _, id = rest.rpartition(" ")
|
||||
tactics[tactic].add((technique, id))
|
||||
tactics[attack["tactic"]].add((attack["technique"], attack.get("subtechnique"), attack["id"]))
|
||||
|
||||
rows = []
|
||||
for tactic, techniques in sorted(tactics.items()):
|
||||
inner_rows = []
|
||||
for spec in sorted(techniques):
|
||||
if len(spec) == 2:
|
||||
technique, id = spec
|
||||
for (technique, subtechnique, id) in sorted(techniques):
|
||||
if subtechnique is None:
|
||||
inner_rows.append("%s %s" % (rutils.bold(technique), id))
|
||||
elif len(spec) == 3:
|
||||
technique, subtechnique, id = spec
|
||||
inner_rows.append("%s::%s %s" % (rutils.bold(technique), subtechnique, id))
|
||||
else:
|
||||
raise RuntimeError("unexpected ATT&CK spec format")
|
||||
inner_rows.append("%s::%s %s" % (rutils.bold(technique), subtechnique, id))
|
||||
rows.append(
|
||||
(
|
||||
rutils.bold(tactic.upper()),
|
||||
@@ -125,6 +154,50 @@ def render_attack(doc, ostream):
|
||||
ostream.write("\n")
|
||||
|
||||
|
||||
def render_mbc(doc, ostream: StringIO):
|
||||
"""
|
||||
example::
|
||||
|
||||
+--------------------------+------------------------------------------------------------+
|
||||
| MBC Objective | MBC Behavior |
|
||||
|--------------------------+------------------------------------------------------------|
|
||||
| ANTI-BEHAVIORAL ANALYSIS | Virtual Machine Detection::Instruction Testing [B0009.029] |
|
||||
| COLLECTION | Keylogging::Polling [F0002.002] |
|
||||
| COMMUNICATION | Interprocess Communication::Create Pipe [C0003.001] |
|
||||
| | Interprocess Communication::Write Pipe [C0003.004] |
|
||||
| IMPACT | Remote Access::Reverse Shell [B0022.001] |
|
||||
+--------------------------+------------------------------------------------------------+
|
||||
"""
|
||||
objectives = collections.defaultdict(set)
|
||||
for rule in rutils.capability_rules(doc):
|
||||
if not rule["meta"].get("mbc"):
|
||||
continue
|
||||
|
||||
for mbc in rule["meta"]["mbc"]:
|
||||
objectives[mbc["objective"]].add((mbc["behavior"], mbc.get("method"), mbc["id"]))
|
||||
|
||||
rows = []
|
||||
for objective, behaviors in sorted(objectives.items()):
|
||||
inner_rows = []
|
||||
for (behavior, method, id) in sorted(behaviors):
|
||||
if method is None:
|
||||
inner_rows.append("%s [%s]" % (rutils.bold(behavior), id))
|
||||
else:
|
||||
inner_rows.append("%s::%s [%s]" % (rutils.bold(behavior), method, id))
|
||||
rows.append(
|
||||
(
|
||||
rutils.bold(objective.upper()),
|
||||
"\n".join(inner_rows),
|
||||
)
|
||||
)
|
||||
|
||||
if rows:
|
||||
ostream.write(
|
||||
tabulate.tabulate(rows, headers=[width("MBC Objective", 25), width("MBC Behavior", 75)], tablefmt="psql")
|
||||
)
|
||||
ostream.write("\n")
|
||||
|
||||
|
||||
def render_default(doc):
|
||||
ostream = rutils.StringIO()
|
||||
|
||||
@@ -132,6 +205,13 @@ def render_default(doc):
|
||||
ostream.write("\n")
|
||||
render_attack(doc, ostream)
|
||||
ostream.write("\n")
|
||||
render_mbc(doc, ostream)
|
||||
ostream.write("\n")
|
||||
render_capabilities(doc, ostream)
|
||||
|
||||
return ostream.getvalue()
|
||||
|
||||
|
||||
def render(meta, rules: RuleSet, capabilities: MatchResults) -> str:
|
||||
doc = capa.render.result_document.convert_capabilities_to_result_document(meta, rules, capabilities)
|
||||
return render_default(doc)
|
||||
|
||||
33
capa/render/json.py
Normal file
@@ -0,0 +1,33 @@
|
||||
# Copyright (C) 2020 FireEye, 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 json
|
||||
|
||||
import capa.render.result_document
|
||||
from capa.rules import RuleSet
|
||||
from capa.engine import MatchResults
|
||||
|
||||
|
||||
class CapaJsonObjectEncoder(json.JSONEncoder):
|
||||
"""JSON encoder that emits Python sets as sorted lists"""
|
||||
|
||||
def default(self, obj):
|
||||
if isinstance(obj, (list, dict, int, float, bool, type(None))) or isinstance(obj, str):
|
||||
return json.JSONEncoder.default(self, obj)
|
||||
elif isinstance(obj, set):
|
||||
return list(sorted(obj))
|
||||
else:
|
||||
# probably will TypeError
|
||||
return json.JSONEncoder.default(self, obj)
|
||||
|
||||
|
||||
def render(meta, rules: RuleSet, capabilities: MatchResults) -> str:
|
||||
return json.dumps(
|
||||
capa.render.result_document.convert_capabilities_to_result_document(meta, rules, capabilities),
|
||||
cls=CapaJsonObjectEncoder,
|
||||
sort_keys=True,
|
||||
)
|
||||
319
capa/render/result_document.py
Normal file
@@ -0,0 +1,319 @@
|
||||
# Copyright (C) 2020 FireEye, 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 copy
|
||||
|
||||
import capa.rules
|
||||
import capa.engine
|
||||
import capa.render.utils
|
||||
import capa.features.common
|
||||
from capa.rules import RuleSet
|
||||
from capa.engine import MatchResults
|
||||
|
||||
|
||||
def convert_statement_to_result_document(statement):
|
||||
"""
|
||||
"statement": {
|
||||
"type": "or"
|
||||
},
|
||||
|
||||
"statement": {
|
||||
"max": 9223372036854775808,
|
||||
"min": 2,
|
||||
"type": "range"
|
||||
},
|
||||
"""
|
||||
statement_type = statement.name.lower()
|
||||
result = {"type": statement_type}
|
||||
if statement.description:
|
||||
result["description"] = statement.description
|
||||
|
||||
if statement_type == "some" and statement.count == 0:
|
||||
result["type"] = "optional"
|
||||
elif statement_type == "some":
|
||||
result["count"] = statement.count
|
||||
elif statement_type == "range":
|
||||
result["min"] = statement.min
|
||||
result["max"] = statement.max
|
||||
result["child"] = convert_feature_to_result_document(statement.child)
|
||||
elif statement_type == "subscope":
|
||||
result["subscope"] = statement.scope
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def convert_feature_to_result_document(feature):
|
||||
"""
|
||||
"feature": {
|
||||
"number": 6,
|
||||
"type": "number"
|
||||
},
|
||||
|
||||
"feature": {
|
||||
"api": "ws2_32.WSASocket",
|
||||
"type": "api"
|
||||
},
|
||||
|
||||
"feature": {
|
||||
"match": "create TCP socket",
|
||||
"type": "match"
|
||||
},
|
||||
|
||||
"feature": {
|
||||
"characteristic": [
|
||||
"loop",
|
||||
true
|
||||
],
|
||||
"type": "characteristic"
|
||||
},
|
||||
"""
|
||||
result = {"type": feature.name, feature.name: feature.get_value_str()}
|
||||
if feature.description:
|
||||
result["description"] = feature.description
|
||||
if feature.name == "regex":
|
||||
result["matches"] = feature.matches
|
||||
return result
|
||||
|
||||
|
||||
def convert_node_to_result_document(node):
|
||||
"""
|
||||
"node": {
|
||||
"type": "statement",
|
||||
"statement": { ... }
|
||||
},
|
||||
|
||||
"node": {
|
||||
"type": "feature",
|
||||
"feature": { ... }
|
||||
},
|
||||
"""
|
||||
|
||||
if isinstance(node, capa.engine.Statement):
|
||||
return {
|
||||
"type": "statement",
|
||||
"statement": convert_statement_to_result_document(node),
|
||||
}
|
||||
elif isinstance(node, capa.features.common.Feature):
|
||||
return {
|
||||
"type": "feature",
|
||||
"feature": convert_feature_to_result_document(node),
|
||||
}
|
||||
else:
|
||||
raise RuntimeError("unexpected match node type")
|
||||
|
||||
|
||||
def convert_match_to_result_document(rules, capabilities, result):
|
||||
"""
|
||||
convert the given Result instance into a common, Python-native data structure.
|
||||
this will become part of the "result document" format that can be emitted to JSON.
|
||||
"""
|
||||
doc = {
|
||||
"success": bool(result.success),
|
||||
"node": convert_node_to_result_document(result.statement),
|
||||
"children": [convert_match_to_result_document(rules, capabilities, child) for child in result.children],
|
||||
}
|
||||
|
||||
# logic expression, like `and`, don't have locations - their children do.
|
||||
# so only add `locations` to feature nodes.
|
||||
if isinstance(result.statement, capa.features.common.Feature):
|
||||
if bool(result.success):
|
||||
doc["locations"] = result.locations
|
||||
elif isinstance(result.statement, capa.engine.Range):
|
||||
if bool(result.success):
|
||||
doc["locations"] = result.locations
|
||||
|
||||
# if we have a `match` statement, then we're referencing another rule or namespace.
|
||||
# this could an external rule (written by a human), or
|
||||
# rule generated to support a subscope (basic block, etc.)
|
||||
# we still want to include the matching logic in this tree.
|
||||
#
|
||||
# so, we need to lookup the other rule results
|
||||
# and then filter those down to the address used here.
|
||||
# finally, splice that logic into this tree.
|
||||
if (
|
||||
doc["node"]["type"] == "feature"
|
||||
and doc["node"]["feature"]["type"] == "match"
|
||||
# only add subtree on success,
|
||||
# because there won't be results for the other rule on failure.
|
||||
and doc["success"]
|
||||
):
|
||||
|
||||
name = doc["node"]["feature"]["match"]
|
||||
|
||||
if name in rules:
|
||||
# this is a rule that we're matching
|
||||
#
|
||||
# pull matches from the referenced rule into our tree here.
|
||||
rule_name = doc["node"]["feature"]["match"]
|
||||
rule = rules[rule_name]
|
||||
rule_matches = {address: result for (address, result) in capabilities[rule_name]}
|
||||
|
||||
if rule.meta.get("capa/subscope-rule"):
|
||||
# for a subscope rule, fixup the node to be a scope node, rather than a match feature node.
|
||||
#
|
||||
# e.g. `contain loop/30c4c78e29bf4d54894fc74f664c62e8` -> `basic block`
|
||||
scope = rule.meta["scope"]
|
||||
doc["node"] = {
|
||||
"type": "statement",
|
||||
"statement": {
|
||||
"type": "subscope",
|
||||
"subscope": scope,
|
||||
},
|
||||
}
|
||||
|
||||
for location in doc["locations"]:
|
||||
doc["children"].append(convert_match_to_result_document(rules, capabilities, rule_matches[location]))
|
||||
else:
|
||||
# this is a namespace that we're matching
|
||||
#
|
||||
# check for all rules in the namespace,
|
||||
# seeing if they matched.
|
||||
# if so, pull their matches into our match tree here.
|
||||
ns_name = doc["node"]["feature"]["match"]
|
||||
ns_rules = rules.rules_by_namespace[ns_name]
|
||||
|
||||
for rule in ns_rules:
|
||||
if rule.name in capabilities:
|
||||
# the rule matched, so splice results into our tree here.
|
||||
#
|
||||
# note, there's a shortcoming in our result document schema here:
|
||||
# we lose the name of the rule that matched in a namespace.
|
||||
# for example, if we have a statement: `match: runtime/dotnet`
|
||||
# and we get matches, we can say the following:
|
||||
#
|
||||
# match: runtime/dotnet @ 0x0
|
||||
# or:
|
||||
# import: mscoree._CorExeMain @ 0x402000
|
||||
#
|
||||
# however, we lose the fact that it was rule
|
||||
# "compiled to the .NET platform"
|
||||
# that contained this logic and did the match.
|
||||
#
|
||||
# we could introduce an intermediate node here.
|
||||
# this would be a breaking change and require updates to the renderers.
|
||||
# in the meantime, the above might be sufficient.
|
||||
rule_matches = {address: result for (address, result) in capabilities[rule.name]}
|
||||
for location in doc["locations"]:
|
||||
doc["children"].append(
|
||||
convert_match_to_result_document(rules, capabilities, rule_matches[location])
|
||||
)
|
||||
|
||||
return doc
|
||||
|
||||
|
||||
def convert_meta_to_result_document(meta):
|
||||
# make a copy so that we don't modify the given parameter
|
||||
meta = copy.deepcopy(meta)
|
||||
|
||||
attacks = meta.get("att&ck", [])
|
||||
meta["att&ck"] = [parse_canonical_attack(attack) for attack in attacks]
|
||||
mbcs = meta.get("mbc", [])
|
||||
meta["mbc"] = [parse_canonical_mbc(mbc) for mbc in mbcs]
|
||||
return meta
|
||||
|
||||
|
||||
def parse_canonical_attack(attack: str):
|
||||
"""
|
||||
parse capa's canonical ATT&CK representation: `Tactic::Technique::Subtechnique [Identifier]`
|
||||
"""
|
||||
tactic = ""
|
||||
technique = ""
|
||||
subtechnique = ""
|
||||
parts, id = capa.render.utils.parse_parts_id(attack)
|
||||
if len(parts) > 0:
|
||||
tactic = parts[0]
|
||||
if len(parts) > 1:
|
||||
technique = parts[1]
|
||||
if len(parts) > 2:
|
||||
subtechnique = parts[2]
|
||||
|
||||
return {
|
||||
"parts": parts,
|
||||
"id": id,
|
||||
"tactic": tactic,
|
||||
"technique": technique,
|
||||
"subtechnique": subtechnique,
|
||||
}
|
||||
|
||||
|
||||
def parse_canonical_mbc(mbc: str):
|
||||
"""
|
||||
parse capa's canonical MBC representation: `Objective::Behavior::Method [Identifier]`
|
||||
"""
|
||||
objective = ""
|
||||
behavior = ""
|
||||
method = ""
|
||||
parts, id = capa.render.utils.parse_parts_id(mbc)
|
||||
if len(parts) > 0:
|
||||
objective = parts[0]
|
||||
if len(parts) > 1:
|
||||
behavior = parts[1]
|
||||
if len(parts) > 2:
|
||||
method = parts[2]
|
||||
|
||||
return {
|
||||
"parts": parts,
|
||||
"id": id,
|
||||
"objective": objective,
|
||||
"behavior": behavior,
|
||||
"method": method,
|
||||
}
|
||||
|
||||
|
||||
def convert_capabilities_to_result_document(meta, rules: RuleSet, capabilities: MatchResults):
|
||||
"""
|
||||
convert the given rule set and capabilities result to a common, Python-native data structure.
|
||||
this format can be directly emitted to JSON, or passed to the other `capa.render.*.render()` routines
|
||||
to render as text.
|
||||
|
||||
see examples of substructures in above routines.
|
||||
|
||||
schema:
|
||||
|
||||
```json
|
||||
{
|
||||
"meta": {...},
|
||||
"rules: {
|
||||
$rule-name: {
|
||||
"meta": {...copied from rule.meta...},
|
||||
"matches: {
|
||||
$address: {...match details...},
|
||||
...
|
||||
}
|
||||
},
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Args:
|
||||
meta (Dict[str, Any]):
|
||||
rules (RuleSet):
|
||||
capabilities (Dict[str, List[Tuple[int, Result]]]):
|
||||
"""
|
||||
doc = {
|
||||
"meta": meta,
|
||||
"rules": {},
|
||||
}
|
||||
|
||||
for rule_name, matches in capabilities.items():
|
||||
rule = rules[rule_name]
|
||||
|
||||
if rule.meta.get("capa/subscope-rule"):
|
||||
continue
|
||||
|
||||
rule_meta = convert_meta_to_result_document(rule.meta)
|
||||
|
||||
doc["rules"][rule_name] = {
|
||||
"meta": rule_meta,
|
||||
"source": rule.definition,
|
||||
"matches": {
|
||||
addr: convert_match_to_result_document(rules, capabilities, match) for (addr, match) in matches
|
||||
},
|
||||
}
|
||||
|
||||
return doc
|
||||
@@ -6,21 +6,22 @@
|
||||
# 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 six
|
||||
import io
|
||||
|
||||
import termcolor
|
||||
|
||||
|
||||
def bold(s):
|
||||
def bold(s: str) -> str:
|
||||
"""draw attention to the given string"""
|
||||
return termcolor.colored(s, "blue")
|
||||
|
||||
|
||||
def bold2(s):
|
||||
def bold2(s: str) -> str:
|
||||
"""draw attention to the given string, within a `bold` section"""
|
||||
return termcolor.colored(s, "green")
|
||||
|
||||
|
||||
def hex(n):
|
||||
def hex(n: int) -> str:
|
||||
"""render the given number using upper case hex, like: 0x123ABC"""
|
||||
if n < 0:
|
||||
return "-0x%X" % (-n)
|
||||
@@ -28,6 +29,24 @@ def hex(n):
|
||||
return "0x%X" % n
|
||||
|
||||
|
||||
def parse_parts_id(s: str):
|
||||
id = ""
|
||||
parts = s.split("::")
|
||||
if len(parts) > 0:
|
||||
last = parts.pop()
|
||||
last, _, id = last.rpartition(" ")
|
||||
id = id.lstrip("[").rstrip("]")
|
||||
parts.append(last)
|
||||
return parts, id
|
||||
|
||||
|
||||
def format_parts_id(data):
|
||||
"""
|
||||
format canonical representation of ATT&CK/MBC parts and ID
|
||||
"""
|
||||
return "%s [%s]" % ("::".join(data["parts"]), data["id"])
|
||||
|
||||
|
||||
def capability_rules(doc):
|
||||
"""enumerate the rules in (namespace, name) order that are 'capability' rules (not lib/subscope/disposition/etc)."""
|
||||
for (_, _, rule) in sorted(
|
||||
@@ -49,7 +68,7 @@ def capability_rules(doc):
|
||||
yield rule
|
||||
|
||||
|
||||
class StringIO(six.StringIO):
|
||||
class StringIO(io.StringIO):
|
||||
def writeln(self, s):
|
||||
self.write(s)
|
||||
self.write("\n")
|
||||
|
||||
@@ -26,6 +26,9 @@ import tabulate
|
||||
|
||||
import capa.rules
|
||||
import capa.render.utils as rutils
|
||||
import capa.render.result_document
|
||||
from capa.rules import RuleSet
|
||||
from capa.engine import MatchResults
|
||||
|
||||
|
||||
def render_meta(ostream, doc):
|
||||
@@ -57,6 +60,7 @@ def render_meta(ostream, doc):
|
||||
("base address", hex(doc["meta"]["analysis"]["base_address"])),
|
||||
("rules", doc["meta"]["analysis"]["rules"]),
|
||||
("function count", len(doc["meta"]["analysis"]["feature_counts"]["functions"])),
|
||||
("library function count", len(doc["meta"]["analysis"]["library_functions"])),
|
||||
(
|
||||
"total feature count",
|
||||
doc["meta"]["analysis"]["feature_counts"]["file"]
|
||||
@@ -119,3 +123,8 @@ def render_verbose(doc):
|
||||
ostream.write("\n")
|
||||
|
||||
return ostream.getvalue()
|
||||
|
||||
|
||||
def render(meta, rules: RuleSet, capabilities: MatchResults) -> str:
|
||||
doc = capa.render.result_document.convert_capabilities_to_result_document(meta, rules, capabilities)
|
||||
return render_verbose(doc)
|
||||
|
||||
@@ -6,13 +6,15 @@
|
||||
# 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 collections
|
||||
|
||||
import tabulate
|
||||
|
||||
import capa.rules
|
||||
import capa.render.utils as rutils
|
||||
import capa.render.verbose
|
||||
import capa.features.common
|
||||
import capa.render.result_document
|
||||
from capa.rules import RuleSet
|
||||
from capa.engine import MatchResults
|
||||
|
||||
|
||||
def render_locations(ostream, match):
|
||||
@@ -56,7 +58,11 @@ def render_statement(ostream, match, statement, indent=0):
|
||||
child = statement["child"]
|
||||
|
||||
if child[child["type"]]:
|
||||
value = rutils.bold2(child[child["type"]])
|
||||
if child["type"] == "string":
|
||||
value = '"%s"' % capa.features.common.escape_string(child[child["type"]])
|
||||
else:
|
||||
value = child[child["type"]]
|
||||
value = rutils.bold2(value)
|
||||
if child.get("description"):
|
||||
ostream.write("count(%s(%s = %s)): " % (child["type"], value, child["description"]))
|
||||
else:
|
||||
@@ -81,27 +87,50 @@ def render_statement(ostream, match, statement, indent=0):
|
||||
raise RuntimeError("unexpected match statement type: " + str(statement))
|
||||
|
||||
|
||||
def render_string_value(s):
|
||||
return '"%s"' % capa.features.common.escape_string(s)
|
||||
|
||||
|
||||
def render_feature(ostream, match, feature, indent=0):
|
||||
ostream.write(" " * indent)
|
||||
|
||||
key = feature["type"]
|
||||
value = feature[feature["type"]]
|
||||
if key == "regex":
|
||||
key = "string" # render string for regex to mirror the rule source
|
||||
value = feature["match"] # the match provides more information than the value for regex
|
||||
|
||||
ostream.write(key)
|
||||
ostream.write(": ")
|
||||
if key != "regex":
|
||||
# like:
|
||||
# number: 10 = SOME_CONSTANT @ 0x401000
|
||||
if key == "string":
|
||||
value = render_string_value(value)
|
||||
|
||||
if value:
|
||||
ostream.write(rutils.bold2(value))
|
||||
ostream.write(key)
|
||||
ostream.write(": ")
|
||||
|
||||
if "description" in feature:
|
||||
ostream.write(capa.rules.DESCRIPTION_SEPARATOR)
|
||||
ostream.write(feature["description"])
|
||||
if value:
|
||||
ostream.write(rutils.bold2(value))
|
||||
|
||||
render_locations(ostream, match)
|
||||
ostream.write("\n")
|
||||
if "description" in feature:
|
||||
ostream.write(capa.rules.DESCRIPTION_SEPARATOR)
|
||||
ostream.write(feature["description"])
|
||||
|
||||
render_locations(ostream, match)
|
||||
ostream.write("\n")
|
||||
else:
|
||||
# like:
|
||||
# regex: /blah/ = SOME_CONSTANT
|
||||
# - "foo blah baz" @ 0x401000
|
||||
# - "aaa blah bbb" @ 0x402000, 0x403400
|
||||
ostream.write(key)
|
||||
ostream.write(": ")
|
||||
ostream.write(value)
|
||||
ostream.write("\n")
|
||||
|
||||
for match, locations in sorted(feature["matches"].items(), key=lambda p: p[0]):
|
||||
ostream.write(" " * (indent + 1))
|
||||
ostream.write("- ")
|
||||
ostream.write(rutils.bold2(render_string_value(match)))
|
||||
render_locations(ostream, {"locations": locations})
|
||||
ostream.write("\n")
|
||||
|
||||
|
||||
def render_node(ostream, match, node, indent=0):
|
||||
@@ -190,6 +219,12 @@ def render_rules(ostream, doc):
|
||||
continue
|
||||
|
||||
v = rule["meta"][key]
|
||||
if not v:
|
||||
continue
|
||||
|
||||
if key in ("att&ck", "mbc"):
|
||||
v = [rutils.format_parts_id(vv) for vv in v]
|
||||
|
||||
if isinstance(v, list) and len(v) == 1:
|
||||
v = v[0]
|
||||
elif isinstance(v, list) and len(v) > 1:
|
||||
@@ -229,3 +264,8 @@ def render_vverbose(doc):
|
||||
ostream.write("\n")
|
||||
|
||||
return ostream.getvalue()
|
||||
|
||||
|
||||
def render(meta, rules: RuleSet, capabilities: MatchResults) -> str:
|
||||
doc = capa.render.result_document.convert_capabilities_to_result_document(meta, rules, capabilities)
|
||||
return render_vverbose(doc)
|
||||
|
||||
539
capa/rules.py
@@ -6,26 +6,38 @@
|
||||
# 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 io
|
||||
import re
|
||||
import uuid
|
||||
import codecs
|
||||
import logging
|
||||
import binascii
|
||||
import functools
|
||||
import collections
|
||||
|
||||
import six
|
||||
try:
|
||||
from functools import lru_cache
|
||||
except ImportError:
|
||||
# need to type ignore this due to mypy bug here (duplicate name):
|
||||
# https://github.com/python/mypy/issues/1153
|
||||
from backports.functools_lru_cache import lru_cache # type: ignore
|
||||
|
||||
from typing import Any, Set, Dict, List, Union, Iterator
|
||||
|
||||
import yaml
|
||||
import ruamel.yaml
|
||||
|
||||
import capa.engine
|
||||
import capa.engine as ceng
|
||||
import capa.features
|
||||
import capa.features.file
|
||||
import capa.features.insn
|
||||
import capa.features.common
|
||||
import capa.features.basicblock
|
||||
from capa.engine import *
|
||||
from capa.features import MAX_BYTES_FEATURE_SIZE
|
||||
from capa.engine import Statement, FeatureSet
|
||||
from capa.features.common import MAX_BYTES_FEATURE_SIZE, Feature
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# these are the standard metadata fields, in the preferred order.
|
||||
# when reformatted, any custom keys will come after these.
|
||||
META_KEYS = (
|
||||
@@ -59,37 +71,38 @@ BASIC_BLOCK_SCOPE = "basic block"
|
||||
|
||||
SUPPORTED_FEATURES = {
|
||||
FILE_SCOPE: {
|
||||
capa.features.MatchedRule,
|
||||
capa.features.common.MatchedRule,
|
||||
capa.features.file.Export,
|
||||
capa.features.file.Import,
|
||||
capa.features.file.Section,
|
||||
capa.features.Characteristic("embedded pe"),
|
||||
capa.features.String,
|
||||
capa.features.file.FunctionName,
|
||||
capa.features.common.Characteristic("embedded pe"),
|
||||
capa.features.common.String,
|
||||
},
|
||||
FUNCTION_SCOPE: {
|
||||
# plus basic block scope features, see below
|
||||
capa.features.basicblock.BasicBlock,
|
||||
capa.features.Characteristic("calls from"),
|
||||
capa.features.Characteristic("calls to"),
|
||||
capa.features.Characteristic("loop"),
|
||||
capa.features.Characteristic("recursive call"),
|
||||
capa.features.common.Characteristic("calls from"),
|
||||
capa.features.common.Characteristic("calls to"),
|
||||
capa.features.common.Characteristic("loop"),
|
||||
capa.features.common.Characteristic("recursive call"),
|
||||
},
|
||||
BASIC_BLOCK_SCOPE: {
|
||||
capa.features.MatchedRule,
|
||||
capa.features.common.MatchedRule,
|
||||
capa.features.insn.API,
|
||||
capa.features.insn.Number,
|
||||
capa.features.String,
|
||||
capa.features.Bytes,
|
||||
capa.features.common.String,
|
||||
capa.features.common.Bytes,
|
||||
capa.features.insn.Offset,
|
||||
capa.features.insn.Mnemonic,
|
||||
capa.features.Characteristic("nzxor"),
|
||||
capa.features.Characteristic("peb access"),
|
||||
capa.features.Characteristic("fs access"),
|
||||
capa.features.Characteristic("gs access"),
|
||||
capa.features.Characteristic("cross section flow"),
|
||||
capa.features.Characteristic("tight loop"),
|
||||
capa.features.Characteristic("stack string"),
|
||||
capa.features.Characteristic("indirect call"),
|
||||
capa.features.common.Characteristic("nzxor"),
|
||||
capa.features.common.Characteristic("peb access"),
|
||||
capa.features.common.Characteristic("fs access"),
|
||||
capa.features.common.Characteristic("gs access"),
|
||||
capa.features.common.Characteristic("cross section flow"),
|
||||
capa.features.common.Characteristic("tight loop"),
|
||||
capa.features.common.Characteristic("stack string"),
|
||||
capa.features.common.Characteristic("indirect call"),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -132,22 +145,32 @@ class InvalidRuleSet(ValueError):
|
||||
return str(self)
|
||||
|
||||
|
||||
def ensure_feature_valid_for_scope(scope, feature):
|
||||
if isinstance(feature, capa.features.Characteristic):
|
||||
if capa.features.Characteristic(feature.value) not in SUPPORTED_FEATURES[scope]:
|
||||
raise InvalidRule("feature %s not support for scope %s" % (feature, scope))
|
||||
elif not isinstance(feature, tuple(filter(lambda t: isinstance(t, type), SUPPORTED_FEATURES[scope]))):
|
||||
def ensure_feature_valid_for_scope(scope: str, feature: Union[Feature, Statement]):
|
||||
# if the given feature is a characteristic,
|
||||
# check that is a valid characteristic for the given scope.
|
||||
if (
|
||||
isinstance(feature, capa.features.common.Characteristic)
|
||||
and isinstance(feature.value, str)
|
||||
and capa.features.common.Characteristic(feature.value) not in SUPPORTED_FEATURES[scope]
|
||||
):
|
||||
raise InvalidRule("feature %s not support for scope %s" % (feature, 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 support for scope %s" % (feature, scope))
|
||||
|
||||
def parse_int(s):
|
||||
|
||||
def parse_int(s: str) -> int:
|
||||
if s.startswith("0x"):
|
||||
return int(s, 0x10)
|
||||
else:
|
||||
return int(s, 10)
|
||||
|
||||
|
||||
def parse_range(s):
|
||||
def parse_range(s: str):
|
||||
"""
|
||||
parse a string "(0, 1)" into a range (min, max).
|
||||
min and/or max may by None to indicate an unbound range.
|
||||
@@ -160,23 +183,21 @@ def parse_range(s):
|
||||
raise InvalidRule("invalid range: %s" % (s))
|
||||
|
||||
s = s[len("(") : -len(")")]
|
||||
min, _, max = s.partition(",")
|
||||
min = min.strip()
|
||||
max = max.strip()
|
||||
min_spec, _, max_spec = s.partition(",")
|
||||
min_spec = min_spec.strip()
|
||||
max_spec = max_spec.strip()
|
||||
|
||||
if min:
|
||||
min = parse_int(min.strip())
|
||||
min = None
|
||||
if min_spec:
|
||||
min = parse_int(min_spec)
|
||||
if min < 0:
|
||||
raise InvalidRule("range min less than zero")
|
||||
else:
|
||||
min = None
|
||||
|
||||
if max:
|
||||
max = parse_int(max.strip())
|
||||
max = None
|
||||
if max_spec:
|
||||
max = parse_int(max_spec)
|
||||
if max < 0:
|
||||
raise InvalidRule("range max less than zero")
|
||||
else:
|
||||
max = None
|
||||
|
||||
if min is not None and max is not None:
|
||||
if max < min:
|
||||
@@ -185,14 +206,14 @@ def parse_range(s):
|
||||
return min, max
|
||||
|
||||
|
||||
def parse_feature(key):
|
||||
def parse_feature(key: str):
|
||||
# keep this in sync with supported features
|
||||
if key == "api":
|
||||
return capa.features.insn.API
|
||||
elif key == "string":
|
||||
return capa.features.StringFactory
|
||||
return capa.features.common.StringFactory
|
||||
elif key == "bytes":
|
||||
return capa.features.Bytes
|
||||
return capa.features.common.Bytes
|
||||
elif key == "number":
|
||||
return capa.features.insn.Number
|
||||
elif key.startswith("number/"):
|
||||
@@ -214,7 +235,7 @@ def parse_feature(key):
|
||||
elif key == "basic blocks":
|
||||
return capa.features.basicblock.BasicBlock
|
||||
elif key == "characteristic":
|
||||
return capa.features.Characteristic
|
||||
return capa.features.common.Characteristic
|
||||
elif key == "export":
|
||||
return capa.features.file.Export
|
||||
elif key == "import":
|
||||
@@ -222,7 +243,9 @@ def parse_feature(key):
|
||||
elif key == "section":
|
||||
return capa.features.file.Section
|
||||
elif key == "match":
|
||||
return capa.features.MatchedRule
|
||||
return capa.features.common.MatchedRule
|
||||
elif key == "function-name":
|
||||
return capa.features.file.FunctionName
|
||||
else:
|
||||
raise InvalidRule("unexpected statement: %s" % key)
|
||||
|
||||
@@ -234,64 +257,131 @@ def parse_feature(key):
|
||||
DESCRIPTION_SEPARATOR = " = "
|
||||
|
||||
|
||||
def parse_description(s, value_type, description=None):
|
||||
"""
|
||||
s can be an int or a string
|
||||
"""
|
||||
if value_type != "string" and isinstance(s, six.string_types) and DESCRIPTION_SEPARATOR in s:
|
||||
if description:
|
||||
raise InvalidRule(
|
||||
'unexpected value: "%s", only one description allowed (inline description with `%s`)'
|
||||
% (s, DESCRIPTION_SEPARATOR)
|
||||
)
|
||||
value, _, description = s.partition(DESCRIPTION_SEPARATOR)
|
||||
if description == "":
|
||||
raise InvalidRule('unexpected value: "%s", description cannot be empty' % s)
|
||||
else:
|
||||
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)
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
return b
|
||||
|
||||
|
||||
def parse_description(s: Union[str, int, bytes], value_type: str, description=None):
|
||||
if value_type == "string":
|
||||
# string features cannot have inline descriptions,
|
||||
# so we assume the entire value is the string,
|
||||
# like: `string: foo = bar` -> "foo = bar"
|
||||
value = s
|
||||
else:
|
||||
# other features can have inline descriptions, like `number: 10 = CONST_FOO`.
|
||||
# in this case, the RHS will be like `10 = CONST_FOO` or some other string
|
||||
if isinstance(s, str):
|
||||
if DESCRIPTION_SEPARATOR in s:
|
||||
if description:
|
||||
# there is already a description passed in as a sub node, like:
|
||||
#
|
||||
# - number: 10 = CONST_FOO
|
||||
# description: CONST_FOO
|
||||
raise InvalidRule(
|
||||
'unexpected value: "%s", only one description allowed (inline description with `%s`)'
|
||||
% (s, DESCRIPTION_SEPARATOR)
|
||||
)
|
||||
|
||||
if isinstance(value, six.string_types):
|
||||
if value_type == "bytes":
|
||||
try:
|
||||
value = codecs.decode(value.replace(" ", ""), "hex")
|
||||
# TODO: Remove TypeError when Python2 is not used anymore
|
||||
except (TypeError, binascii.Error):
|
||||
raise InvalidRule('unexpected bytes value: "%s", must be a valid hex sequence' % value)
|
||||
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)
|
||||
else:
|
||||
# this is a string, but there is no description,
|
||||
# like: `api: CreateFileA`
|
||||
value = s
|
||||
|
||||
if len(value) > MAX_BYTES_FEATURE_SIZE:
|
||||
raise InvalidRule(
|
||||
"unexpected bytes value: byte sequences must be no larger than %s bytes" % MAX_BYTES_FEATURE_SIZE
|
||||
)
|
||||
elif value_type in ("number", "offset") or value_type.startswith(("number/", "offset/")):
|
||||
try:
|
||||
value = parse_int(value)
|
||||
except ValueError:
|
||||
raise InvalidRule('unexpected value: "%s", must begin with numerical value' % value)
|
||||
# cast from the received string value to the appropriate type.
|
||||
#
|
||||
# without a description, this type would already be correct,
|
||||
# but since we parsed the description from a string,
|
||||
# we need to convert the value to the expected type.
|
||||
#
|
||||
# for example, from `number: 10 = CONST_FOO` we have
|
||||
# the string "10" that needs to become the number 10.
|
||||
if value_type == "bytes":
|
||||
value = parse_bytes(value)
|
||||
elif value_type in ("number", "offset") or value_type.startswith(("number/", "offset/")):
|
||||
try:
|
||||
value = parse_int(value)
|
||||
except ValueError:
|
||||
raise InvalidRule('unexpected value: "%s", must begin with numerical value' % value)
|
||||
|
||||
else:
|
||||
# the value might be a number, like: `number: 10`
|
||||
value = s
|
||||
|
||||
return value, description
|
||||
|
||||
|
||||
def build_statements(d, scope):
|
||||
def pop_statement_description_entry(d):
|
||||
"""
|
||||
extracts the description for statements and removes the description entry from the document
|
||||
a statement can only have one description
|
||||
|
||||
example:
|
||||
the features definition
|
||||
- or:
|
||||
- description: statement description
|
||||
- number: 1
|
||||
description: feature description
|
||||
|
||||
becomes
|
||||
<statement>: [
|
||||
{ "description": "statement description" }, <-- extracted here
|
||||
{ "number": 1, "description": "feature description" }
|
||||
]
|
||||
"""
|
||||
if not isinstance(d, list):
|
||||
return None
|
||||
|
||||
# identify child of form '{ "description": <description> }'
|
||||
descriptions = list(filter(lambda c: isinstance(c, dict) and len(c) == 1 and "description" in c, d))
|
||||
if len(descriptions) > 1:
|
||||
raise InvalidRule("statements can only have one description")
|
||||
|
||||
if not descriptions:
|
||||
return None
|
||||
|
||||
description = descriptions[0]
|
||||
d.remove(description)
|
||||
|
||||
return description["description"]
|
||||
|
||||
|
||||
def build_statements(d, scope: str):
|
||||
if len(d.keys()) > 2:
|
||||
raise InvalidRule("too many statements")
|
||||
|
||||
key = list(d.keys())[0]
|
||||
description = pop_statement_description_entry(d[key])
|
||||
if key == "and":
|
||||
return And([build_statements(dd, scope) for dd in d[key]], description=d.get("description"))
|
||||
return ceng.And([build_statements(dd, scope) for dd in d[key]], description=description)
|
||||
elif key == "or":
|
||||
return Or([build_statements(dd, scope) for dd in d[key]], description=d.get("description"))
|
||||
return ceng.Or([build_statements(dd, scope) for dd in d[key]], description=description)
|
||||
elif key == "not":
|
||||
if len(d[key]) != 1:
|
||||
raise InvalidRule("not statement must have exactly one child statement")
|
||||
return Not(build_statements(d[key][0], scope), description=d.get("description"))
|
||||
return ceng.Not(build_statements(d[key][0], scope), description=description)
|
||||
elif key.endswith(" or more"):
|
||||
count = int(key[: -len("or more")])
|
||||
return Some(count, [build_statements(dd, scope) for dd in d[key]], description=d.get("description"))
|
||||
return ceng.Some(count, [build_statements(dd, scope) for dd in d[key]], description=description)
|
||||
elif key == "optional":
|
||||
# `optional` is an alias for `0 or more`
|
||||
# which is useful for documenting behaviors,
|
||||
# like with `write file`, we might say that `WriteFile` is optionally found alongside `CreateFileA`.
|
||||
return Some(0, [build_statements(dd, scope) for dd in d[key]], description=d.get("description"))
|
||||
return ceng.Some(0, [build_statements(dd, scope) for dd in d[key]], description=description)
|
||||
|
||||
elif key == "function":
|
||||
if scope != FILE_SCOPE:
|
||||
@@ -300,7 +390,7 @@ def build_statements(d, scope):
|
||||
if len(d[key]) != 1:
|
||||
raise InvalidRule("subscope must have exactly one child statement")
|
||||
|
||||
return Subscope(FUNCTION_SCOPE, build_statements(d[key][0], FUNCTION_SCOPE))
|
||||
return ceng.Subscope(FUNCTION_SCOPE, build_statements(d[key][0], FUNCTION_SCOPE))
|
||||
|
||||
elif key == "basic block":
|
||||
if scope != FUNCTION_SCOPE:
|
||||
@@ -309,7 +399,7 @@ def build_statements(d, scope):
|
||||
if len(d[key]) != 1:
|
||||
raise InvalidRule("subscope must have exactly one child statement")
|
||||
|
||||
return Subscope(BASIC_BLOCK_SCOPE, build_statements(d[key][0], BASIC_BLOCK_SCOPE))
|
||||
return ceng.Subscope(BASIC_BLOCK_SCOPE, build_statements(d[key][0], BASIC_BLOCK_SCOPE))
|
||||
|
||||
elif key.startswith("count(") and key.endswith(")"):
|
||||
# e.g.:
|
||||
@@ -350,21 +440,21 @@ def build_statements(d, scope):
|
||||
|
||||
count = d[key]
|
||||
if isinstance(count, int):
|
||||
return Range(feature, min=count, max=count, description=d.get("description"))
|
||||
return ceng.Range(feature, min=count, max=count, description=description)
|
||||
elif count.endswith(" or more"):
|
||||
min = parse_int(count[: -len(" or more")])
|
||||
max = None
|
||||
return Range(feature, min=min, max=max, description=d.get("description"))
|
||||
return ceng.Range(feature, min=min, max=max, description=description)
|
||||
elif count.endswith(" or fewer"):
|
||||
min = None
|
||||
max = parse_int(count[: -len(" or fewer")])
|
||||
return Range(feature, min=min, max=max, description=d.get("description"))
|
||||
return ceng.Range(feature, min=min, max=max, description=description)
|
||||
elif count.startswith("("):
|
||||
min, max = parse_range(count)
|
||||
return Range(feature, min=min, max=max, description=d.get("description"))
|
||||
return ceng.Range(feature, min=min, max=max, description=description)
|
||||
else:
|
||||
raise InvalidRule("unexpected range: %s" % (count))
|
||||
elif key == "string" and not isinstance(d[key], six.string_types):
|
||||
elif key == "string" and not isinstance(d[key], str):
|
||||
raise InvalidRule("ambiguous string value %s, must be defined as explicit string" % d[key])
|
||||
else:
|
||||
Feature = parse_feature(key)
|
||||
@@ -377,36 +467,16 @@ def build_statements(d, scope):
|
||||
return feature
|
||||
|
||||
|
||||
def first(s):
|
||||
def first(s: List[Any]) -> Any:
|
||||
return s[0]
|
||||
|
||||
|
||||
def second(s):
|
||||
def second(s: List[Any]) -> Any:
|
||||
return s[1]
|
||||
|
||||
|
||||
# we use the ruamel.yaml parser because it supports roundtripping of documents with comments.
|
||||
yaml = ruamel.yaml.YAML(typ="rt")
|
||||
|
||||
|
||||
# use block mode, not inline json-like mode
|
||||
yaml.default_flow_style = False
|
||||
|
||||
|
||||
# indent lists by two spaces below their parent
|
||||
#
|
||||
# features:
|
||||
# - or:
|
||||
# - mnemonic: aesdec
|
||||
# - mnemonic: vaesdec
|
||||
yaml.indent(sequence=2, offset=2)
|
||||
|
||||
# avoid word wrapping
|
||||
yaml.width = 4096
|
||||
|
||||
|
||||
class Rule(object):
|
||||
def __init__(self, name, scope, statement, meta, definition=""):
|
||||
class Rule:
|
||||
def __init__(self, name: str, scope: str, statement: Statement, meta, definition=""):
|
||||
super(Rule, self).__init__()
|
||||
self.name = name
|
||||
self.scope = scope
|
||||
@@ -436,7 +506,7 @@ class Rule(object):
|
||||
deps = set([])
|
||||
|
||||
def rec(statement):
|
||||
if isinstance(statement, capa.features.MatchedRule):
|
||||
if isinstance(statement, capa.features.common.MatchedRule):
|
||||
# we're not sure at this point if the `statement.value` is
|
||||
# really a rule name or a namespace name (we use `MatchedRule` for both cases).
|
||||
# we'll give precedence to namespaces, and then assume if that does work,
|
||||
@@ -452,7 +522,7 @@ class Rule(object):
|
||||
# not a namespace, assume its a rule name.
|
||||
deps.add(statement.value)
|
||||
|
||||
elif isinstance(statement, Statement):
|
||||
elif isinstance(statement, ceng.Statement):
|
||||
for child in statement.get_children():
|
||||
rec(child)
|
||||
|
||||
@@ -463,11 +533,9 @@ class Rule(object):
|
||||
return deps
|
||||
|
||||
def _extract_subscope_rules_rec(self, statement):
|
||||
if isinstance(statement, Statement):
|
||||
if isinstance(statement, ceng.Statement):
|
||||
# for each child that is a subscope,
|
||||
for subscope in filter(
|
||||
lambda statement: isinstance(statement, capa.engine.Subscope), statement.get_children()
|
||||
):
|
||||
for subscope in filter(lambda statement: isinstance(statement, ceng.Subscope), statement.get_children()):
|
||||
|
||||
# create a new rule from it.
|
||||
# the name is a randomly generated, hopefully unique value.
|
||||
@@ -492,7 +560,7 @@ class Rule(object):
|
||||
)
|
||||
|
||||
# update the existing statement to `match` the new rule
|
||||
new_node = capa.features.MatchedRule(name)
|
||||
new_node = capa.features.common.MatchedRule(name)
|
||||
statement.replace_child(subscope, new_node)
|
||||
|
||||
# and yield the new rule to our caller
|
||||
@@ -529,15 +597,16 @@ class Rule(object):
|
||||
for new_rule in self._extract_subscope_rules_rec(self.statement):
|
||||
yield new_rule
|
||||
|
||||
def evaluate(self, features):
|
||||
def evaluate(self, features: FeatureSet):
|
||||
return self.statement.evaluate(features)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d, s):
|
||||
name = d["rule"]["meta"]["name"]
|
||||
def from_dict(cls, d, definition):
|
||||
meta = d["rule"]["meta"]
|
||||
name = meta["name"]
|
||||
# if scope is not specified, default to function scope.
|
||||
# this is probably the mode that rule authors will start with.
|
||||
scope = d["rule"]["meta"].get("scope", FUNCTION_SCOPE)
|
||||
scope = meta.get("scope", FUNCTION_SCOPE)
|
||||
statements = d["rule"]["features"]
|
||||
|
||||
# the rule must start with a single logic node.
|
||||
@@ -545,23 +614,77 @@ class Rule(object):
|
||||
if len(statements) != 1:
|
||||
raise InvalidRule("rule must begin with a single top level statement")
|
||||
|
||||
if isinstance(statements[0], capa.engine.Subscope):
|
||||
if isinstance(statements[0], ceng.Subscope):
|
||||
raise InvalidRule("top level statement may not be a subscope")
|
||||
|
||||
if scope not in SUPPORTED_FEATURES.keys():
|
||||
raise InvalidRule("{:s} is not a supported scope".format(scope))
|
||||
|
||||
return cls(name, scope, build_statements(statements[0], scope), d["rule"]["meta"], s)
|
||||
meta = d["rule"]["meta"]
|
||||
if not isinstance(meta.get("att&ck", []), list):
|
||||
raise InvalidRule("ATT&CK mapping must be a list")
|
||||
if not isinstance(meta.get("mbc", []), list):
|
||||
raise InvalidRule("MBC mapping must be a list")
|
||||
|
||||
return cls(name, scope, build_statements(statements[0], scope), meta, definition)
|
||||
|
||||
@staticmethod
|
||||
@lru_cache()
|
||||
def _get_yaml_loader():
|
||||
try:
|
||||
# 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.")
|
||||
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
|
||||
|
||||
@staticmethod
|
||||
def _get_ruamel_yaml_parser():
|
||||
# use ruamel to enable nice formatting
|
||||
|
||||
# we use the ruamel.yaml parser because it supports roundtripping of documents with comments.
|
||||
y = ruamel.yaml.YAML(typ="rt")
|
||||
|
||||
# use block mode, not inline json-like mode
|
||||
y.default_flow_style = False
|
||||
|
||||
# leave quotes unchanged
|
||||
y.preserve_quotes = True
|
||||
|
||||
# indent lists by two spaces below their parent
|
||||
#
|
||||
# features:
|
||||
# - or:
|
||||
# - mnemonic: aesdec
|
||||
# - mnemonic: vaesdec
|
||||
y.indent(sequence=2, offset=2)
|
||||
|
||||
# avoid word wrapping
|
||||
y.width = 4096
|
||||
|
||||
return y
|
||||
|
||||
@classmethod
|
||||
def from_yaml(cls, s):
|
||||
return cls.from_dict(yaml.load(s), s)
|
||||
def from_yaml(cls, s, use_ruamel=False):
|
||||
if use_ruamel:
|
||||
# ruamel enables nice formatting and doc roundtripping with comments
|
||||
doc = cls._get_ruamel_yaml_parser().load(s)
|
||||
else:
|
||||
# use pyyaml because it can be much faster than ruamel (pure python)
|
||||
doc = yaml.load(s, Loader=cls._get_yaml_loader())
|
||||
return cls.from_dict(doc, s)
|
||||
|
||||
@classmethod
|
||||
def from_yaml_file(cls, path):
|
||||
def from_yaml_file(cls, path, use_ruamel=False):
|
||||
with open(path, "rb") as f:
|
||||
try:
|
||||
return cls.from_yaml(f.read().decode("utf-8"))
|
||||
return cls.from_yaml(f.read().decode("utf-8"), use_ruamel=use_ruamel)
|
||||
except InvalidRule as e:
|
||||
raise InvalidRuleWithPath(path, str(e))
|
||||
|
||||
@@ -575,12 +698,25 @@ class Rule(object):
|
||||
# but not for rule logic.
|
||||
# programmatic generation of rules is not yet supported.
|
||||
|
||||
definition = yaml.load(self.definition)
|
||||
# definition retains a reference to `meta`,
|
||||
# so we're updating that in place.
|
||||
definition["rule"]["meta"] = self.meta
|
||||
meta = self.meta
|
||||
# use ruamel because it supports round tripping.
|
||||
# pyyaml will lose the existing ordering of rule statements.
|
||||
definition = self._get_ruamel_yaml_parser().load(self.definition)
|
||||
|
||||
# we want to apply any updates that have been made to `meta`.
|
||||
# so we would like to assigned it like this:
|
||||
#
|
||||
# definition["rule"]["meta"] = self.meta
|
||||
#
|
||||
# however, `self.meta` is not ordered, its just a dict, so subsequent formatting doesn't work.
|
||||
# so, we'll manually copy the keys over, re-using the existing ordereddict/CommentedMap
|
||||
meta = definition["rule"]["meta"]
|
||||
for k in meta.keys():
|
||||
if k not in self.meta:
|
||||
del meta[k]
|
||||
for k, v in self.meta.items():
|
||||
meta[k] = v
|
||||
|
||||
# the name and scope of the rule instance overrides anything in meta.
|
||||
meta["name"] = self.name
|
||||
meta["scope"] = self.scope
|
||||
|
||||
@@ -616,61 +752,79 @@ class Rule(object):
|
||||
for key in hidden_meta.keys():
|
||||
del meta[key]
|
||||
|
||||
ostream = six.BytesIO()
|
||||
yaml.dump(definition, ostream)
|
||||
ostream = io.BytesIO()
|
||||
self._get_ruamel_yaml_parser().dump(definition, ostream)
|
||||
|
||||
for key, value in hidden_meta.items():
|
||||
if value is None:
|
||||
continue
|
||||
meta[key] = value
|
||||
|
||||
return ostream.getvalue().decode("utf-8").rstrip("\n") + "\n"
|
||||
doc = ostream.getvalue().decode("utf-8").rstrip("\n") + "\n"
|
||||
# when we have something like:
|
||||
#
|
||||
# and:
|
||||
# - string: foo
|
||||
# description: bar
|
||||
#
|
||||
# we want the `description` horizontally aligned with the start of the `string` (like above).
|
||||
# however, ruamel will give us (which I don't think is even valid yaml):
|
||||
#
|
||||
# and:
|
||||
# - string: foo
|
||||
# description: bar
|
||||
#
|
||||
# tweaking `ruamel.indent()` doesn't quite give us the control we want.
|
||||
# so, add the two extra spaces that we've determined we need through experimentation.
|
||||
# see #263
|
||||
# only do this for the features section, so the meta description doesn't get reformatted
|
||||
# assumes features section always exists
|
||||
features_offset = doc.find("features")
|
||||
doc = doc[:features_offset] + doc[features_offset:].replace(" description:", " description:")
|
||||
|
||||
# for negative hex numbers, yaml dump outputs:
|
||||
# - offset: !!int '0x-30'
|
||||
# we prefer:
|
||||
# - offset: -0x30
|
||||
# the below regex makes these adjustments and while ugly, we don't have to explore the ruamel.yaml insides
|
||||
doc = re.sub(r"!!int '0x-([0-9a-fA-F]+)'", r"-0x\1", doc)
|
||||
|
||||
# normalize CRLF to LF
|
||||
doc = doc.replace("\r\n", "\n")
|
||||
return doc
|
||||
|
||||
|
||||
def get_rules_with_scope(rules, scope):
|
||||
def get_rules_with_scope(rules, scope) -> List[Rule]:
|
||||
"""
|
||||
from the given collection of rules, select those with the given scope.
|
||||
|
||||
args:
|
||||
rules (List[capa.rules.Rule]):
|
||||
scope (str): one of the capa.rules.*_SCOPE constants.
|
||||
|
||||
returns:
|
||||
List[capa.rules.Rule]:
|
||||
`scope` is one of the capa.rules.*_SCOPE constants.
|
||||
"""
|
||||
return list(rule for rule in rules if rule.scope == scope)
|
||||
|
||||
|
||||
def get_rules_and_dependencies(rules, rule_name):
|
||||
def get_rules_and_dependencies(rules: List[Rule], rule_name: str) -> Iterator[Rule]:
|
||||
"""
|
||||
from the given collection of rules, select a rule and its dependencies (transitively).
|
||||
|
||||
args:
|
||||
rules (List[Rule]):
|
||||
rule_name (str):
|
||||
|
||||
yields:
|
||||
Rule:
|
||||
"""
|
||||
# we evaluate `rules` multiple times, so if its a generator, realize it into a list.
|
||||
rules = list(rules)
|
||||
namespaces = index_rules_by_namespace(rules)
|
||||
rules = {rule.name: rule for rule in rules}
|
||||
rules_by_name = {rule.name: rule for rule in rules}
|
||||
wanted = set([rule_name])
|
||||
|
||||
def rec(rule):
|
||||
wanted.add(rule.name)
|
||||
for dep in rule.get_dependencies(namespaces):
|
||||
rec(rules[dep])
|
||||
rec(rules_by_name[dep])
|
||||
|
||||
rec(rules[rule_name])
|
||||
rec(rules_by_name[rule_name])
|
||||
|
||||
for rule in rules.values():
|
||||
for rule in rules_by_name.values():
|
||||
if rule.name in wanted:
|
||||
yield rule
|
||||
|
||||
|
||||
def ensure_rules_are_unique(rules):
|
||||
def ensure_rules_are_unique(rules: List[Rule]) -> None:
|
||||
seen = set([])
|
||||
for rule in rules:
|
||||
if rule.name in seen:
|
||||
@@ -678,7 +832,7 @@ def ensure_rules_are_unique(rules):
|
||||
seen.add(rule.name)
|
||||
|
||||
|
||||
def ensure_rule_dependencies_are_met(rules):
|
||||
def ensure_rule_dependencies_are_met(rules: List[Rule]) -> None:
|
||||
"""
|
||||
raise an exception if a rule dependency does not exist.
|
||||
|
||||
@@ -688,14 +842,14 @@ def ensure_rule_dependencies_are_met(rules):
|
||||
# we evaluate `rules` multiple times, so if its a generator, realize it into a list.
|
||||
rules = list(rules)
|
||||
namespaces = index_rules_by_namespace(rules)
|
||||
rules = {rule.name: rule for rule in rules}
|
||||
for rule in rules.values():
|
||||
rules_by_name = {rule.name: rule for rule in rules}
|
||||
for rule in rules_by_name.values():
|
||||
for dep in rule.get_dependencies(namespaces):
|
||||
if dep not in rules:
|
||||
if dep not in rules_by_name:
|
||||
raise InvalidRule('rule "%s" depends on missing rule "%s"' % (rule.name, dep))
|
||||
|
||||
|
||||
def index_rules_by_namespace(rules):
|
||||
def index_rules_by_namespace(rules: List[Rule]) -> Dict[str, List[Rule]]:
|
||||
"""
|
||||
compute the rules that fit into each namespace found within the given rules.
|
||||
|
||||
@@ -709,11 +863,6 @@ def index_rules_by_namespace(rules):
|
||||
c2/shell: [create reverse shell]
|
||||
c2/file-transfer: [download and write a file]
|
||||
c2: [create reverse shell, download and write a file]
|
||||
|
||||
Args:
|
||||
rules (List[Rule]):
|
||||
|
||||
Returns: Dict[str, List[Rule]]
|
||||
"""
|
||||
namespaces = collections.defaultdict(list)
|
||||
|
||||
@@ -729,7 +878,38 @@ def index_rules_by_namespace(rules):
|
||||
return dict(namespaces)
|
||||
|
||||
|
||||
class RuleSet(object):
|
||||
def topologically_order_rules(rules: List[Rule]) -> List[Rule]:
|
||||
"""
|
||||
order the given rules such that dependencies show up before dependents.
|
||||
this means that as we match rules, we can add features for the matches, and these
|
||||
will be matched by subsequent rules if they follow this order.
|
||||
|
||||
assumes that the rule dependency graph is a DAG.
|
||||
"""
|
||||
# we evaluate `rules` multiple times, so if its a generator, realize it into a list.
|
||||
rules = list(rules)
|
||||
namespaces = index_rules_by_namespace(rules)
|
||||
rules_by_name = {rule.name: rule for rule in rules}
|
||||
seen = set([])
|
||||
ret = []
|
||||
|
||||
def rec(rule):
|
||||
if rule.name in seen:
|
||||
return
|
||||
|
||||
for dep in rule.get_dependencies(namespaces):
|
||||
rec(rules_by_name[dep])
|
||||
|
||||
ret.append(rule)
|
||||
seen.add(rule.name)
|
||||
|
||||
for rule in rules_by_name.values():
|
||||
rec(rule)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
class RuleSet:
|
||||
"""
|
||||
a ruleset is initialized with a collection of rules, which it verifies and sorts into scopes.
|
||||
each set of scoped rules is sorted topologically, which enables rules to match on past rule matches.
|
||||
@@ -744,7 +924,7 @@ class RuleSet(object):
|
||||
capa.engine.match(ruleset.file_rules, ...)
|
||||
"""
|
||||
|
||||
def __init__(self, rules):
|
||||
def __init__(self, rules: List[Rule]):
|
||||
super(RuleSet, self).__init__()
|
||||
|
||||
ensure_rules_are_unique(rules)
|
||||
@@ -760,6 +940,7 @@ class RuleSet(object):
|
||||
self.function_rules = self._get_rules_for_scope(rules, FUNCTION_SCOPE)
|
||||
self.basic_block_rules = self._get_rules_for_scope(rules, BASIC_BLOCK_SCOPE)
|
||||
self.rules = {rule.name: rule for rule in rules}
|
||||
self.rules_by_namespace = index_rules_by_namespace(rules)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.rules)
|
||||
@@ -767,13 +948,17 @@ class RuleSet(object):
|
||||
def __getitem__(self, rulename):
|
||||
return self.rules[rulename]
|
||||
|
||||
def __contains__(self, rulename):
|
||||
return rulename in self.rules
|
||||
|
||||
@staticmethod
|
||||
def _get_rules_for_scope(rules, scope):
|
||||
"""
|
||||
given a collection of rules, collect the rules that are needed at the given scope.
|
||||
these rules are ordered topologically.
|
||||
|
||||
don't include "lib" rules, unless they are dependencies of other rules.
|
||||
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([])
|
||||
|
||||
@@ -782,11 +967,11 @@ class RuleSet(object):
|
||||
# at lower scope, e.g. function scope.
|
||||
# so, we find all dependencies of all rules, and later will filter them down.
|
||||
for rule in rules:
|
||||
if rule.meta.get("lib", False):
|
||||
if rule.meta.get("capa/subscope-rule", False):
|
||||
continue
|
||||
|
||||
scope_rules.update(get_rules_and_dependencies(rules, rule.name))
|
||||
return get_rules_with_scope(capa.engine.topologically_order_rules(scope_rules), scope)
|
||||
return get_rules_with_scope(topologically_order_rules(list(scope_rules)), scope)
|
||||
|
||||
@staticmethod
|
||||
def _extract_subscope_rules(rules):
|
||||
@@ -810,7 +995,7 @@ class RuleSet(object):
|
||||
|
||||
return done
|
||||
|
||||
def filter_rules_by_meta(self, tag):
|
||||
def filter_rules_by_meta(self, tag: str) -> "RuleSet":
|
||||
"""
|
||||
return new rule set with rules filtered based on all meta field values, adds all dependency rules
|
||||
apply tag-based rule filter assuming that all required rules are loaded
|
||||
@@ -819,11 +1004,11 @@ class RuleSet(object):
|
||||
TODO handle circular dependencies?
|
||||
TODO support -t=metafield <k>
|
||||
"""
|
||||
rules = self.rules.values()
|
||||
rules = list(self.rules.values())
|
||||
rules_filtered = set([])
|
||||
for rule in rules:
|
||||
for k, v in rule.meta.items():
|
||||
if isinstance(v, six.string_types) and tag in v:
|
||||
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)))
|
||||
break
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "1.2.0"
|
||||
__version__ = "2.0.0"
|
||||
|
||||
|
Before Width: | Height: | Size: 83 KiB |
BIN
doc/img/changelog/flirt-ignore.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
doc/img/changelog/tab.gif
Normal file
|
After Width: | Height: | Size: 136 KiB |
BIN
doc/img/explorer_condensed.png
Normal file
|
After Width: | Height: | Size: 141 KiB |
BIN
doc/img/explorer_expanded.png
Normal file
|
After Width: | Height: | Size: 322 KiB |
BIN
doc/img/rulegen_expanded.png
Normal file
|
After Width: | Height: | Size: 135 KiB |
@@ -22,12 +22,15 @@ 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, you'll need to install a few dependencies, and then use `pip` to fetch the capa module.
|
||||
Note: this technique doesn't pull the default rule set, so you should check it out separately from [capa-rules](https://github.com/fireeye/capa-rules/) and pass the directory to the entrypoint using `-r`.
|
||||
To install capa as a Python library use `pip` to fetch the `flare-capa` module.
|
||||
|
||||
#### *Note*:
|
||||
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/fireeye/capa-rules/) and pass the directory to the entrypoint using `-r` or set the rules path in the IDA Pro plugin.
|
||||
Alternatively, see Method 3 below.
|
||||
|
||||
### 1. Install capa module
|
||||
Second, 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 https://github.com/fireeye/capa/archive/master.zip`
|
||||
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.
|
||||
@@ -39,6 +42,7 @@ If you'd like to review and modify the capa source code, you'll need to check it
|
||||
Next, clone the capa git repository.
|
||||
We use submodules to separate [code](https://github.com/fireeye/capa), [rules](https://github.com/fireeye/capa-rules), and [test data](https://github.com/fireeye/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.
|
||||
- `$ git clone --recurse-submodules https://github.com/fireeye/capa.git /local/path/to/src` (HTTPS)
|
||||
- `$ git clone --recurse-submodules git@github.com:fireeye/capa.git /local/path/to/src` (SSH)
|
||||
|
||||
@@ -56,6 +60,25 @@ Use `pip` to install the source code in "editable" mode. This means that Python
|
||||
|
||||
You'll find that the `capa.exe` (Windows) or `capa` (Linux/MacOS) executables in your path now invoke the capa binary from this directory.
|
||||
|
||||
#### Development
|
||||
|
||||
##### venv [optional]
|
||||
|
||||
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 activate `capa-env` in Linux or MacOS, run:
|
||||
`$ source ../capa-env/bin/activate`
|
||||
|
||||
To activate `capa-env` in Windows, run:
|
||||
`$ ..\capa-env\Scripts\activate.bat`
|
||||
|
||||
For more details about creating and using virtual environments, check out the [venv documentation](https://docs.python.org/3/tutorial/venv.html).
|
||||
|
||||
##### 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`
|
||||
@@ -66,19 +89,28 @@ To install these development dependencies, run:
|
||||
|
||||
`$ pip install -e /local/path/to/src[dev]`
|
||||
|
||||
Note that some development dependencies (including the black code formatter) require Python 3.
|
||||
|
||||
To check the code style, formatting and run the tests you can run the script `scripts/ci.sh`.
|
||||
You can run it with the argument `no_tests` to skip the tests and only run the code style and formatting: `scripts/ci.sh no_tests`
|
||||
|
||||
### 3. Setup hooks [optional]
|
||||
##### Setup hooks [optional]
|
||||
|
||||
If you plan to contribute to capa, you may want to setup the 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.
|
||||
You can skip this check by using the `--no-verify` git option.
|
||||
- The `pre-push` hook runs checks before every `git push`.
|
||||
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.
|
||||
|
||||
### 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.
|
||||
|
||||
#### Install PyInstaller:
|
||||
`$ pip install pyinstaller` (Python 3)
|
||||
|
||||
#### Run Pyinstaller
|
||||
`$ pyinstaller .github/pyinstaller/pyinstaller.spec`
|
||||
|
||||
You can find the compiled binary in the created directory `dist/`.
|
||||
|
||||
43
doc/release.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# Release checklist
|
||||
|
||||
- [ ] Ensure all [milestoned issues/PRs](https://github.com/fireeye/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/fireeye/capa/pulls) and [capa-rules](https://github.com/fireeye/capa-rules/pulls).
|
||||
- [ ] Ensure the [CI workflow succeeds in master](https://github.com/fireeye/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).
|
||||
- [ ] Review changes
|
||||
- capa https://github.com/fireeye/capa/compare/\<last-release\>...master
|
||||
- capa-rules https://github.com/fireeye/capa-rules/compare/\<last-release>\...master
|
||||
- [ ] Update [CHANGELOG.md](https://github.com/fireeye/capa/blob/master/CHANGELOG.md)
|
||||
- Do not forget to add a nice introduction thanking contributors
|
||||
- Remember that we need a major release if we introduce breaking changes
|
||||
- Sections: see template below
|
||||
- Update `Raw diffs` links
|
||||
- Create placeholder for `master (unreleased)` section
|
||||
```
|
||||
## master (unreleased)
|
||||
|
||||
### New Features
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
### New Rules (0)
|
||||
|
||||
-
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
### capa explorer IDA Pro plugin
|
||||
|
||||
### Development
|
||||
|
||||
### Raw diffs
|
||||
- [capa <release>...master](https://github.com/fireeye/capa/compare/<release>...master)
|
||||
- [capa-rules <release>...master](https://github.com/fireeye/capa-rules/compare/<release>...master)
|
||||
```
|
||||
- [ ] Update [capa/version.py](https://github.com/fireeye/capa/blob/master/capa/version.py)
|
||||
- [ ] Create a PR with the updated [CHANGELOG.md](https://github.com/fireeye/capa/blob/master/CHANGELOG.md) and [capa/version.py](https://github.com/fireeye/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/fireeye/capa/releases/new) using text from the [CHANGELOG.md](https://github.com/fireeye/capa/blob/master/CHANGELOG.md).
|
||||
- [ ] Verify GH actions [upload artifacts](https://github.com/fireeye/capa/releases), [publish to PyPI](https://pypi.org/project/flare-capa) and [create a tag in capa rules](https://github.com/fireeye/capa-rules/tags) upon completion.
|
||||
- [ ] [Spread the word](https://twitter.com)
|
||||
- [ ] Update internal service
|
||||
|
||||
35
doc/usage.md
@@ -4,37 +4,16 @@ See `capa -h` for all supported arguments and usage examples.
|
||||
|
||||
## tips and tricks
|
||||
|
||||
- [match only rules by given author or namespace](#only-run-selected-rules)
|
||||
- [IDA Pro capa explorer](#capa-explorer)
|
||||
- [IDA Pro rule generator](#rule-generator)
|
||||
|
||||
### only run selected rules
|
||||
Use the `-t` option to run rules with the given metadata value (see the rule fields `rule.meta.*`).
|
||||
For example, `capa -t william.ballenthin@mandiant.com` runs rules that reference Willi's email address (probably as the author), or
|
||||
`capa -t communication` runs rules with the namespace `communication`.
|
||||
|
||||
### IDA Pro integrations
|
||||
You can run capa from within IDA Pro. Run `capa/main.py` via `File - Script file...` (or ALT + F7).
|
||||
When running in IDA, capa uses IDA's disassembly and file analysis as its backend.
|
||||
These results may vary from the standalone version that uses vivisect.
|
||||
IDA's analysis is generally a bit faster and more thorough than vivisect's, so you might prefer this mode.
|
||||
### IDA Pro plugin: capa explorer
|
||||
Please check out the [capa explorer documentation](/capa/ida/plugin/README.md).
|
||||
|
||||
When run under IDA, capa supports both Python 2 and Python 3 interpreters.
|
||||
If you encounter issues with your specific setup, please open a new [Issue](https://github.com/fireeye/capa/issues).
|
||||
|
||||
Additionally, capa comes with an IDA Pro plugin located in the `capa/ida` directory: the explorer.
|
||||
|
||||
#### capa explorer
|
||||
The capa explorer allows you to interactively display and browse capabilities capa identified in a binary.
|
||||
As you select rules or logic, capa will highlight the addresses that support its analysis conclusions.
|
||||
We like to use capa to help find the most interesting parts of a program, such as where the C2 mechanism might be.
|
||||
|
||||

|
||||
|
||||
To install the plugin, you'll need to be running IDA Pro 7.4 or 7.5 with either Python 2 or Python 3.
|
||||
Next make sure pip commands are run using the Python install that is configured for your IDA install:
|
||||
|
||||
1. Only if running Python 2.7, run command `$ pip install https://github.com/williballenthin/vivisect/zipball/master`
|
||||
2. Run `$ pip install .` from capa root directory
|
||||
3. Open IDA and navigate to `File > Script file…` or `Alt+F7`
|
||||
4. Navigate to `<capa_install_dir>\capa\ida\` and choose `ida_capa_explorer.py`
|
||||
### save time by reusing .viv files
|
||||
Set the environment variable `CAPA_SAVE_WORKSPACE` to instruct the underlying analysis engine to
|
||||
cache its intermediate results to the file system. For example, vivisect will create `.viv` files.
|
||||
Subsequently, capa may run faster when reprocessing the same input file.
|
||||
This is particularly useful during rule development as you repeatedly test a rule against a known sample.
|
||||
2
rules
223
scripts/bulk-process.py
Normal file
@@ -0,0 +1,223 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
bulk-process
|
||||
|
||||
Invoke capa recursively against a directory of samples
|
||||
and emit a JSON document mapping the file paths to their results.
|
||||
|
||||
By default, this will use subprocesses for parallelism.
|
||||
Use `-n/--parallelism` to change the subprocess count from
|
||||
the default of current CPU count.
|
||||
Use `--no-mp` to use threads instead of processes,
|
||||
which is probably not useful unless you set `--parallelism=1`.
|
||||
|
||||
example:
|
||||
|
||||
$ python scripts/bulk-process /tmp/suspicious
|
||||
{
|
||||
"/tmp/suspicious/suspicious.dll_": {
|
||||
"rules": {
|
||||
"encode data using XOR": {
|
||||
"matches": {
|
||||
"268440358": {
|
||||
[...]
|
||||
"/tmp/suspicious/1.dll_": { ... }
|
||||
"/tmp/suspicious/2.dll_": { ... }
|
||||
}
|
||||
|
||||
|
||||
usage:
|
||||
|
||||
usage: bulk-process.py [-h] [-r RULES] [-d] [-q] [-n PARALLELISM] [--no-mp]
|
||||
input
|
||||
|
||||
detect capabilities in programs.
|
||||
|
||||
positional arguments:
|
||||
input Path to directory of files to recursively analyze
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-r RULES, --rules RULES
|
||||
Path to rule file or directory, use embedded rules by
|
||||
default
|
||||
-d, --debug Enable debugging output on STDERR
|
||||
-q, --quiet Disable all output but errors
|
||||
-n PARALLELISM, --parallelism PARALLELISM
|
||||
parallelism factor
|
||||
--no-mp disable subprocesses
|
||||
|
||||
Copyright (C) 2020 FireEye, 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 json
|
||||
import logging
|
||||
import os.path
|
||||
import argparse
|
||||
import multiprocessing
|
||||
import multiprocessing.pool
|
||||
|
||||
import capa
|
||||
import capa.main
|
||||
import capa.rules
|
||||
import capa.render.json
|
||||
|
||||
logger = logging.getLogger("capa")
|
||||
|
||||
|
||||
def get_capa_results(args):
|
||||
"""
|
||||
run capa against the file at the given path, using the given rules.
|
||||
|
||||
args is a tuple, containing:
|
||||
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
|
||||
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`.
|
||||
|
||||
returns an dict with two required keys:
|
||||
path (str): the file system path of the sample to process
|
||||
status (str): either "error" or "ok"
|
||||
|
||||
when status == "error", then a human readable message is found in property "error".
|
||||
when status == "ok", then the capa results are found in the property "ok".
|
||||
|
||||
the capa results are a dictionary with the following keys:
|
||||
meta (dict): the meta analysis results
|
||||
capabilities (dict): the matched capabilities and their result objects
|
||||
"""
|
||||
rules, sigpaths, format, 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
|
||||
)
|
||||
except capa.main.UnsupportedFormatError:
|
||||
# i'm 100% sure if multiprocessing will reliably raise exceptions across process boundaries.
|
||||
# so instead, return an object with explicit success/failure status.
|
||||
#
|
||||
# if success, then status=ok, and results found in property "ok"
|
||||
# if error, then status=error, and human readable message in property "error"
|
||||
return {
|
||||
"path": path,
|
||||
"status": "error",
|
||||
"error": "input file does not appear to be a PE file: %s" % path,
|
||||
}
|
||||
except capa.main.UnsupportedRuntimeError:
|
||||
return {
|
||||
"path": path,
|
||||
"status": "error",
|
||||
"error": "unsupported runtime or Python interpreter",
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"path": path,
|
||||
"status": "error",
|
||||
"error": "unexpected error: %s" % (e),
|
||||
}
|
||||
|
||||
meta = capa.main.collect_metadata("", path, "", format, extractor)
|
||||
capabilities, counts = capa.main.find_capabilities(rules, extractor, disable_progress=True)
|
||||
meta["analysis"].update(counts)
|
||||
|
||||
return {
|
||||
"path": path,
|
||||
"status": "ok",
|
||||
"ok": {
|
||||
"meta": meta,
|
||||
"capabilities": capabilities,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
if argv is None:
|
||||
argv = sys.argv[1:]
|
||||
|
||||
parser = argparse.ArgumentParser(description="detect capabilities in programs.")
|
||||
capa.main.install_common_args(parser, wanted={"rules", "signatures"})
|
||||
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"
|
||||
)
|
||||
parser.add_argument("--no-mp", action="store_true", help="disable subprocesses")
|
||||
args = parser.parse_args(args=argv)
|
||||
capa.main.handle_common_args(args)
|
||||
|
||||
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))
|
||||
return -1
|
||||
|
||||
try:
|
||||
sig_paths = capa.main.get_signatures(args.signatures)
|
||||
except (IOError) as e:
|
||||
logger.error("%s", str(e))
|
||||
return -1
|
||||
|
||||
samples = []
|
||||
for (base, directories, files) in os.walk(args.input):
|
||||
for file in files:
|
||||
samples.append(os.path.join(base, file))
|
||||
|
||||
def pmap(f, args, parallelism=multiprocessing.cpu_count()):
|
||||
"""apply the given function f to the given args using subprocesses"""
|
||||
return multiprocessing.Pool(parallelism).imap(f, args)
|
||||
|
||||
def tmap(f, args, parallelism=multiprocessing.cpu_count()):
|
||||
"""apply the given function f to the given args using threads"""
|
||||
return multiprocessing.pool.ThreadPool(parallelism).imap(f, args)
|
||||
|
||||
def map(f, args, parallelism=None):
|
||||
"""apply the given function f to the given args in the current thread"""
|
||||
for arg in args:
|
||||
yield f(arg)
|
||||
|
||||
if args.no_mp:
|
||||
if args.parallelism == 1:
|
||||
logger.debug("using current thread mapper")
|
||||
mapper = map
|
||||
else:
|
||||
logger.debug("using threading mapper")
|
||||
mapper = tmap
|
||||
else:
|
||||
logger.debug("using process mapper")
|
||||
mapper = pmap
|
||||
|
||||
results = {}
|
||||
for result in mapper(
|
||||
get_capa_results, [(rules, sig_paths, "pe", sample) for sample in samples], parallelism=args.parallelism
|
||||
):
|
||||
if result["status"] == "error":
|
||||
logger.warning(result["error"])
|
||||
elif result["status"] == "ok":
|
||||
meta = result["ok"]["meta"]
|
||||
capabilities = result["ok"]["capabilities"]
|
||||
# our renderer expects to emit a json document for a single sample
|
||||
# so we deserialize the json document, store it in a larger dict, and we'll subsequently re-encode.
|
||||
results[result["path"]] = json.loads(capa.render.json.render(meta, rules, capabilities))
|
||||
else:
|
||||
raise ValueError("unexpected status: %s" % (result["status"]))
|
||||
|
||||
print(json.dumps(results))
|
||||
|
||||
logger.info("done.")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
760
scripts/capa2yara.py
Normal file
@@ -0,0 +1,760 @@
|
||||
"""
|
||||
Convert capa rules to YARA rules (where this is possible)
|
||||
|
||||
usage: capa2yara.py [-h] [--private] [--version] [-v] [-vv] [-d] [-q] [--color {auto,always,never}] [-t TAG] rules
|
||||
|
||||
Capa to YARA rule converter
|
||||
|
||||
positional arguments:
|
||||
rules Path to rules
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
--private, -p Create private rules
|
||||
--version show program's version number and exit
|
||||
-v, --verbose enable verbose result document (no effect with --json)
|
||||
-vv, --vverbose enable very verbose result document (no effect with --json)
|
||||
-d, --debug enable debugging output on STDERR
|
||||
-q, --quiet disable all output but errors
|
||||
--color {auto,always,never}
|
||||
enable ANSI color codes in results, default: only during interactive session
|
||||
-t TAG, --tag TAG filter on rule meta field values
|
||||
|
||||
|
||||
Copyright (C) 2020, 2021 Arnim Rupp (@ruppde) and FireEye, 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
|
||||
import sys
|
||||
import string
|
||||
import logging
|
||||
import argparse
|
||||
import datetime
|
||||
import itertools
|
||||
|
||||
import capa.main
|
||||
import capa.rules
|
||||
import capa.engine
|
||||
import capa.features
|
||||
import capa.features.insn
|
||||
from capa.features.common import ARCH_X32, ARCH_X64, String
|
||||
|
||||
logger = logging.getLogger("capa2yara")
|
||||
|
||||
today = str(datetime.date.today())
|
||||
|
||||
# create unique variable names for each rule in case somebody wants to move/copy stuff around later
|
||||
var_names = ["".join(letters) for letters in itertools.product(string.ascii_lowercase, repeat=3)]
|
||||
|
||||
|
||||
# this have to be the internal names used by capa.py which are sometimes different to the ones written out in the rules, e.g. "2 or more" is "Some", count is Range
|
||||
unsupported = ["characteristic", "mnemonic", "offset", "subscope", "Range"]
|
||||
# TODO shorten this list, possible stuff:
|
||||
# - 2 or more strings: e.g.
|
||||
# -- https://github.com/fireeye/capa-rules/blob/master/collection/file-managers/gather-direct-ftp-information.yml
|
||||
# -- https://github.com/fireeye/capa-rules/blob/master/collection/browser/gather-firefox-profile-information.yml
|
||||
# - count(string (1 rule: /executable/subfile/pe/contain-an-embedded-pe-file.yml)
|
||||
# - count(match( could be done by creating the referenced rule a 2nd time with the condition, that it hits x times (only 1 rule: ./anti-analysis/anti-disasm/contain-anti-disasm-techniques.yml)
|
||||
# - it would be technically possible to get the "basic blocks" working, but the rules contain mostly other non supported statements in there => not worth the effort.
|
||||
|
||||
# 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 "
|
||||
|
||||
# minimum number of rounds to do be able to convert rules which depend on referenced rules in several levels of depth
|
||||
min_rounds = 5
|
||||
|
||||
unsupported_capa_rules = open("unsupported_capa_rules.yml", "wb")
|
||||
unsupported_capa_rules_names = open("unsupported_capa_rules.txt", "wb")
|
||||
unsupported_capa_rules_list = []
|
||||
|
||||
condition_header = """
|
||||
capa_pe_file and
|
||||
"""
|
||||
|
||||
condition_rule = """
|
||||
private rule capa_pe_file : CAPA {
|
||||
meta:
|
||||
description = "match in PE files. used by all further CAPA rules"
|
||||
author = "Arnim Rupp"
|
||||
condition:
|
||||
uint16be(0) == 0x4d5a
|
||||
or uint16be(0) == 0x558b
|
||||
or uint16be(0) == 0x5649
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def check_feature(statement, rulename):
|
||||
if statement in unsupported:
|
||||
logger.info("unsupported: " + statement + " in rule: " + rulename)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def get_rule_url(path):
|
||||
path = re.sub(r"\.\.\/", "", path)
|
||||
path = re.sub(r"capa-rules\/", "", path)
|
||||
return "https://github.com/fireeye/capa-rules/blob/master/" + path
|
||||
|
||||
|
||||
def convert_capa_number_to_yara_bytes(number):
|
||||
if not number.startswith("0x"):
|
||||
print("TODO: fix decimal")
|
||||
sys.exit()
|
||||
|
||||
number = re.sub(r"^0[xX]", "", number)
|
||||
logger.info("number ok: " + repr(number))
|
||||
|
||||
# include spaces every 2 hex
|
||||
bytesv = re.sub(r"(..)", r"\1 ", number)
|
||||
|
||||
# reverse order
|
||||
bytesl = bytesv.split(" ")
|
||||
bytesl.reverse()
|
||||
bytesv = " ".join(bytesl)
|
||||
|
||||
# fix spaces
|
||||
bytesv = bytesv[1:] + " "
|
||||
|
||||
return bytesv
|
||||
|
||||
|
||||
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 _
|
||||
rule_name = re.sub(r"\W", "_", rule_name)
|
||||
rule_name = "capa_" + rule_name
|
||||
|
||||
return rule_name
|
||||
|
||||
|
||||
def convert_description(statement):
|
||||
try:
|
||||
desc = statement.description
|
||||
if desc:
|
||||
yara_desc = " // " + desc
|
||||
logger.info("using desc: " + repr(yara_desc))
|
||||
return yara_desc
|
||||
except:
|
||||
# no description
|
||||
pass
|
||||
|
||||
return ""
|
||||
|
||||
|
||||
def convert_rule(rule, rulename, cround, depth):
|
||||
|
||||
depth += 1
|
||||
logger.info("recursion depth: " + str(depth))
|
||||
|
||||
global var_names
|
||||
|
||||
def do_statement(s_type, kid):
|
||||
yara_strings = ""
|
||||
yara_condition = ""
|
||||
if check_feature(s_type, rulename):
|
||||
return "BREAK", s_type
|
||||
elif s_type == "string":
|
||||
string = kid.value
|
||||
logger.info("doing string: " + repr(string))
|
||||
string = string.replace("\\", "\\\\")
|
||||
string = string.replace("\n", "\\n")
|
||||
string = string.replace("\t", "\\t")
|
||||
var_name = "str_" + var_names.pop(0)
|
||||
yara_strings += "\t$" + var_name + ' = "' + string + '" ascii wide' + convert_description(kid) + "\n"
|
||||
yara_condition += "\t$" + var_name + " "
|
||||
elif s_type == "api" or s_type == "import":
|
||||
# TODO: is it possible in YARA to make a difference between api & import?
|
||||
|
||||
# https://github.com/fireeye/capa-rules/blob/master/doc/format.md#api
|
||||
api = kid.value
|
||||
logger.info("doing api: " + repr(api))
|
||||
|
||||
# e.g. kernel32.CreateNamedPipe => look for kernel32.dll and CreateNamedPipe
|
||||
if "." in api:
|
||||
dll, api = api.split(".")
|
||||
|
||||
# usage of regex is needed and /i because string search for "CreateMutex" in imports() doesn't look for e.g. CreateMutexA
|
||||
yara_condition += "\tpe.imports(/" + dll + "/i, /" + api + "/) "
|
||||
|
||||
else:
|
||||
# e.g. - api: 'CallNextHookEx'
|
||||
# (from user32.dll)
|
||||
|
||||
# even looking for empty string in dll_regex doesn't work for some files (list below) with pe.imports so do just a string search
|
||||
# yara_condition += '\tpe.imports(/.{0,30}/i, /' + api + '/) '
|
||||
# 5fbbfeed28b258c42e0cfeb16718b31c, 2D3EDC218A90F03089CC01715A9F047F, 7EFF498DE13CC734262F87E6B3EF38AB, C91887D861D9BD4A5872249B641BC9F9, a70052c45e907820187c7e6bcdc7ecca, 0596C4EA5AA8DEF47F22C85D75AACA95
|
||||
var_name = "api_" + var_names.pop(0)
|
||||
|
||||
# limit regex with word boundary \b but also search for appended A and W
|
||||
# TODO: better use something like /(\\x00|\\x01|\\x02|\\x03|\\x04)' + api + '(A|W)?\\x00/ ???
|
||||
yara_strings += "\t$" + var_name + " = /\\b" + api + "(A|W)?\\b/ ascii wide\n"
|
||||
yara_condition += "\t$" + var_name + " "
|
||||
|
||||
elif s_type == "export":
|
||||
export = kid.value
|
||||
logger.info("doing export: " + repr(export))
|
||||
|
||||
yara_condition += '\tpe.exports("' + export + '") '
|
||||
|
||||
elif s_type == "section":
|
||||
# https://github.com/fireeye/capa-rules/blob/master/doc/format.md#section
|
||||
section = kid.value
|
||||
logger.info("doing section: " + repr(section))
|
||||
|
||||
# e.g. - section: .rsrc
|
||||
var_name_sec = var_names.pop(0)
|
||||
# yeah, it would be better to make one loop out of multiple sections but we're in POC-land (and I guess it's not much of a performance hit, loop over short array?)
|
||||
yara_condition += (
|
||||
"\tfor any " + var_name_sec + " in pe.sections : ( " + var_name_sec + '.name == "' + section + '" ) '
|
||||
)
|
||||
|
||||
elif s_type == "match":
|
||||
# https://github.com/fireeye/capa-rules/blob/master/doc/format.md#matching-prior-rule-matches-and-namespaces
|
||||
match = kid.value
|
||||
logger.info("doing match: " + repr(match))
|
||||
|
||||
# e.g. - match: create process
|
||||
# - match: host-interaction/file-system/write
|
||||
match_rule_name = convert_rule_name(match)
|
||||
|
||||
if match.startswith(rulename + "/"):
|
||||
logger.info("Depending on myself = basic block: " + match)
|
||||
return "BREAK", "Depending on myself = basic block"
|
||||
|
||||
if match_rule_name in converted_rules:
|
||||
yara_condition += "\t" + match_rule_name + "\n"
|
||||
else:
|
||||
# don't complain in the early rounds as there should be 3+ rounds (if all rules are converted)
|
||||
if cround > min_rounds - 2:
|
||||
logger.info("needed sub-rule not converted (yet, maybe in next round): " + repr(match))
|
||||
return "BREAK", "needed sub-rule not converted"
|
||||
else:
|
||||
return "BREAK", "NOLOG"
|
||||
|
||||
elif s_type == "bytes":
|
||||
bytesv = kid.get_value_str()
|
||||
logger.info("doing bytes: " + repr(bytesv))
|
||||
var_name = var_names.pop(0)
|
||||
|
||||
yara_strings += "\t$" + var_name + " = { " + bytesv + " }" + convert_description(kid) + "\n"
|
||||
yara_condition += "\t$" + var_name + " "
|
||||
|
||||
elif s_type == "number":
|
||||
number = kid.get_value_str()
|
||||
logger.info("doing number: " + repr(number))
|
||||
|
||||
if len(number) < 10:
|
||||
logger.info("too short for byte search (until I figure out how to do it properly)" + repr(number))
|
||||
return "BREAK", "Number too short"
|
||||
|
||||
# there's just one rule which contains 0xFFFFFFF but yara gives a warning if if used
|
||||
if number == "0xFFFFFFFF":
|
||||
return "BREAK", "slow byte pattern for YARA search"
|
||||
|
||||
logger.info("number ok: " + repr(number))
|
||||
number = convert_capa_number_to_yara_bytes(number)
|
||||
logger.info("number ok: " + repr(number))
|
||||
|
||||
var_name = "num_" + var_names.pop(0)
|
||||
yara_strings += "\t$" + var_name + " = { " + number + "}" + convert_description(kid) + "\n"
|
||||
yara_condition += "$" + var_name + " "
|
||||
|
||||
elif s_type == "regex":
|
||||
regex = kid.get_value_str()
|
||||
logger.info("doing regex: " + repr(regex))
|
||||
|
||||
# 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
|
||||
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:
|
||||
# would assume that get_value_str() gives the raw string
|
||||
regex = re.sub(r"(?<!\\)/", r"\/", regex)
|
||||
|
||||
# capa uses python regex which accepts /reg(|.exe)/ but yaras regex engine doesn't not => fix it
|
||||
# /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)
|
||||
regex = re.sub(r"^\^", r"\\x00", regex)
|
||||
|
||||
# regex = re.sub(r"^\^", r"\\b", regex)
|
||||
|
||||
regex = "/" + regex + "/"
|
||||
if count:
|
||||
regex += " nocase"
|
||||
|
||||
# strange: if statement.name == "string", the string is as it is, if statement.name == "regex", the string has // around it, e.g. /regex/
|
||||
var_name = "re_" + var_names.pop(0)
|
||||
yara_strings += "\t" + "$" + var_name + " = " + regex + " ascii wide " + convert_description(kid) + "\n"
|
||||
yara_condition += "\t" + "$" + var_name + " "
|
||||
elif s_type == "Not" or s_type == "And" or s_type == "Or":
|
||||
pass
|
||||
else:
|
||||
logger.info("something unhandled: " + repr(s_type))
|
||||
sys.exit()
|
||||
|
||||
return yara_strings, yara_condition
|
||||
|
||||
############################## end def do_statement
|
||||
|
||||
yara_strings_list = []
|
||||
yara_condition_list = []
|
||||
rule_comment = ""
|
||||
incomplete = 0
|
||||
|
||||
statement = rule.name
|
||||
|
||||
logger.info("doing statement: " + statement)
|
||||
|
||||
if check_feature(statement, rulename):
|
||||
return "BREAK", statement, rule_comment, incomplete
|
||||
|
||||
if statement == "And" or statement == "Or":
|
||||
desc = convert_description(rule)
|
||||
if desc:
|
||||
logger.info("description of bool statement: " + repr(desc))
|
||||
yara_strings_list.append("\t" * depth + desc + "\n")
|
||||
elif statement == "Not":
|
||||
logger.info("one of those seldom nots: " + rule.name)
|
||||
|
||||
# check for nested statements
|
||||
try:
|
||||
kids = rule.children
|
||||
num_kids = len(kids)
|
||||
logger.info("kids: " + kids)
|
||||
except:
|
||||
logger.info("no kids in rule: " + rule.name)
|
||||
|
||||
try:
|
||||
# maybe it's "Not" = only one child:
|
||||
kid = rule.child
|
||||
kids = [kid]
|
||||
num_kids = 1
|
||||
logger.info("kid: %s", kids)
|
||||
except:
|
||||
logger.info("no kid in rule: %s", rule.name)
|
||||
|
||||
# just a single statement without 'and' or 'or' before it in this rule
|
||||
if "kids" not in locals().keys():
|
||||
logger.info("no kids: " + rule.name)
|
||||
|
||||
yara_strings_sub, yara_condition_sub = do_statement(statement, rule)
|
||||
|
||||
if yara_strings_sub == "BREAK":
|
||||
logger.info("Unknown feature at1: " + rule.name)
|
||||
return "BREAK", yara_condition_sub, rule_comment, incomplete
|
||||
yara_strings_list.append(yara_strings_sub)
|
||||
yara_condition_list.append(yara_condition_sub)
|
||||
|
||||
else:
|
||||
x = 0
|
||||
logger.info("doing kids: %r - len: %s", kids, num_kids)
|
||||
for kid in kids:
|
||||
s_type = kid.name
|
||||
logger.info("doing type: " + s_type + " kidnum: " + str(x))
|
||||
|
||||
if s_type == "Some":
|
||||
cmin = kid.count
|
||||
logger.info("Some type with mininum: " + str(cmin))
|
||||
|
||||
if not cmin:
|
||||
logger.info("this is optional: which means, we can just ignore it")
|
||||
x += 1
|
||||
continue
|
||||
elif statement == "Or":
|
||||
logger.info("we're inside an OR, we can just ignore it")
|
||||
x += 1
|
||||
continue
|
||||
else:
|
||||
# this is "x or more". could be coded for strings TODO
|
||||
return "BREAK", "Some aka x or more (TODO)", rule_comment, incomplete
|
||||
|
||||
if s_type == "And" or s_type == "Or" or s_type == "Not" and not kid.name == "Some":
|
||||
logger.info("doing bool with recursion: " + repr(kid))
|
||||
logger.info("kid coming: " + repr(kid.name))
|
||||
# logger.info("grandchildren: " + repr(kid.children))
|
||||
|
||||
##### here we go into RECURSION ##################################################################################
|
||||
yara_strings_sub, yara_condition_sub, rule_comment_sub, incomplete_sub = convert_rule(
|
||||
kid, rulename, cround, depth
|
||||
)
|
||||
|
||||
logger.info("coming out of this recursion, depth: " + repr(depth) + " s_type: " + s_type)
|
||||
|
||||
if yara_strings_sub == "BREAK":
|
||||
logger.info(
|
||||
"Unknown feature at2: " + rule.name + " - s_type: " + s_type + " - depth: " + str(depth)
|
||||
)
|
||||
|
||||
# luckily this is only a killer, if we're inside an 'And', inside 'Or' we're just missing some coverage
|
||||
# only accept incomplete rules in rounds > 3 because the reason might be a reference to another rule not converted yet because of missing dependencies
|
||||
logger.info("rule.name, depth, cround: " + rule.name + ", " + str(depth) + ", " + str(cround))
|
||||
if rule.name == "Or" and depth == 1 and cround > min_rounds - 1:
|
||||
logger.info(
|
||||
"Unknown feature, just ignore this branch and keep the rest bec we're in Or (1): "
|
||||
+ s_type
|
||||
+ " - depth: "
|
||||
+ str(depth)
|
||||
)
|
||||
# remove last 'or'
|
||||
# yara_condition = re.sub(r'\sor $', ' ', yara_condition)
|
||||
rule_comment += "This rule is incomplete because a branch inside an Or-statement had an unsupported feature and was skipped => coverage is reduced compared to the original capa rule. "
|
||||
x += 1
|
||||
incomplete = 1
|
||||
continue
|
||||
else:
|
||||
return "BREAK", yara_condition_sub, rule_comment, incomplete
|
||||
|
||||
rule_comment += rule_comment_sub
|
||||
yara_strings_list.append(yara_strings_sub)
|
||||
yara_condition_list.append(yara_condition_sub)
|
||||
|
||||
incomplete = incomplete or incomplete_sub
|
||||
|
||||
yara_strings_sub, yara_condition_sub = do_statement(s_type, kid)
|
||||
|
||||
if yara_strings_sub == "BREAK":
|
||||
logger.info("Unknown feature at3: " + rule.name)
|
||||
logger.info("rule.name, depth, cround: " + rule.name + ", " + str(depth) + ", " + str(cround))
|
||||
if rule.name == "Or" and depth == 1 and cround > min_rounds - 1:
|
||||
logger.info(
|
||||
"Unknown feature, just ignore this branch and keep the rest bec we're in Or (2): "
|
||||
+ s_type
|
||||
+ " - depth: "
|
||||
+ str(depth)
|
||||
)
|
||||
|
||||
rule_comment += "This rule is incomplete because a branch inside an Or-statement had an unsupported feature and was skipped => coverage is reduced compared to the original capa rule. "
|
||||
x += 1
|
||||
incomplete = 1
|
||||
continue
|
||||
else:
|
||||
return "BREAK", yara_condition_sub, rule_comment, incomplete
|
||||
|
||||
# don't append And or Or if we got no condition back from this kid from e.g. match in myself or unsupported feature inside 'Or'
|
||||
if not yara_condition_sub:
|
||||
continue
|
||||
|
||||
yara_strings_list.append(yara_strings_sub)
|
||||
yara_condition_list.append(yara_condition_sub)
|
||||
x += 1
|
||||
|
||||
# this might happen, if all conditions are inside "or" and none of them was supported
|
||||
if not yara_condition_list:
|
||||
return (
|
||||
"BREAK",
|
||||
'Multiple statements inside "- or:" where all unsupported, the last one was "' + s_type + '"',
|
||||
rule_comment,
|
||||
incomplete,
|
||||
)
|
||||
|
||||
if statement == "And" or statement == "Or":
|
||||
if yara_strings_list:
|
||||
yara_strings = "".join(yara_strings_list)
|
||||
else:
|
||||
yara_strings = ""
|
||||
|
||||
yara_condition = " (\n\t\t" + ("\n\t\t" + statement.lower() + " ").join(yara_condition_list) + " \n\t) "
|
||||
|
||||
elif statement == "Some":
|
||||
cmin = rule.count
|
||||
logger.info("Some type with mininum at2: " + str(cmin))
|
||||
|
||||
if not cmin:
|
||||
logger.info("this is optional: which means, we can just ignore it")
|
||||
else:
|
||||
# this is "x or more". could be coded for strings TODO
|
||||
return "BREAK", "Some aka x or more (TODO)", rule_comment, incomplete
|
||||
elif statement == "Not":
|
||||
logger.info("Not")
|
||||
yara_strings = "".join(yara_strings_list)
|
||||
yara_condition = "not " + "".join(yara_condition_list) + " "
|
||||
else:
|
||||
if len(yara_condition_list) != 1:
|
||||
logger.info("something wrong around here" + repr(yara_condition_list) + " - " + statement)
|
||||
sys.exit()
|
||||
|
||||
# strings might be empty with only conditions
|
||||
if yara_strings_list:
|
||||
yara_strings = "\n\t" + yara_strings_list[0]
|
||||
|
||||
yara_condition = "\n\t" + yara_condition_list[0]
|
||||
|
||||
logger.info(
|
||||
f"################# end of convert_rule() #strings: {len(yara_strings_list)} #conditions: {len(yara_condition_list)}"
|
||||
)
|
||||
logger.info(f"strings: {yara_strings} conditions: {yara_condition}")
|
||||
|
||||
return yara_strings, yara_condition, rule_comment, incomplete
|
||||
|
||||
|
||||
def output_yar(yara):
|
||||
print(yara + "\n")
|
||||
|
||||
|
||||
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)
|
||||
|
||||
unsupported_capa_rules_list.append(capa_rulename)
|
||||
unsupported_capa_rules.write(yaml.encode("utf-8") + b"\n")
|
||||
unsupported_capa_rules.write(
|
||||
(
|
||||
"Reason: "
|
||||
+ reason
|
||||
+ " (there might be multiple unsupported things in this rule, this is the 1st one encountered)"
|
||||
).encode("utf-8")
|
||||
+ b"\n"
|
||||
)
|
||||
unsupported_capa_rules.write(url.encode("utf-8") + b"\n----------------------------------------------\n")
|
||||
unsupported_capa_rules_names.write(capa_rulename.encode("utf-8") + b":")
|
||||
unsupported_capa_rules_names.write(reason.encode("utf-8") + b":")
|
||||
unsupported_capa_rules_names.write(url.encode("utf-8") + b"\n")
|
||||
|
||||
|
||||
def convert_rules(rules, namespaces, cround):
|
||||
for rule in rules.rules.values():
|
||||
|
||||
rule_name = convert_rule_name(rule.name)
|
||||
|
||||
if rule.meta.get("capa/subscope-rule", False):
|
||||
logger.info("skipping sub scope rule capa: " + rule.name)
|
||||
continue
|
||||
|
||||
if rule_name in converted_rules:
|
||||
logger.info("skipping already converted rule capa: " + rule.name + " - yara rule: " + rule_name)
|
||||
continue
|
||||
|
||||
logger.info("-------------------------- DOING RULE CAPA: " + rule.name + " - yara rule: " + rule_name)
|
||||
if "capa/path" in rule.meta:
|
||||
url = get_rule_url(rule.meta["capa/path"])
|
||||
else:
|
||||
url = "no url"
|
||||
|
||||
logger.info("URL: " + url)
|
||||
logger.info("statements: " + repr(rule.statement))
|
||||
|
||||
# don't really know what that passed empty string is good for :)
|
||||
dependencies = rule.get_dependencies(namespaces)
|
||||
|
||||
if len(dependencies):
|
||||
logger.info("Dependencies at4: " + rule.name + " - dep: " + str(dependencies))
|
||||
|
||||
for dep in dependencies:
|
||||
logger.info("Dependencies at44: " + dep)
|
||||
if not dep.startswith(rule.name + "/"):
|
||||
logger.info("Depending on another rule: " + dep)
|
||||
continue
|
||||
|
||||
yara_strings, yara_condition, rule_comment, incomplete = convert_rule(rule.statement, rule.name, cround, 0)
|
||||
|
||||
if yara_strings == "BREAK":
|
||||
# only give up if in final extra round #9000
|
||||
if cround == 9000:
|
||||
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 = ""
|
||||
|
||||
for meta in metas:
|
||||
meta_name = meta
|
||||
# e.g. 'examples:' can be a list
|
||||
seen_hashes = []
|
||||
if isinstance(metas[meta], list):
|
||||
if meta_name == "examples":
|
||||
meta_name = "hash"
|
||||
if meta_name == "att&ck":
|
||||
meta_name = "attack"
|
||||
for attack in list(metas[meta]):
|
||||
logger.info("attack:" + attack)
|
||||
# cut out tag in square brackets, e.g. Defense Evasion::Obfuscated Files or Information [T1027] => T1027
|
||||
r = re.search(r"\[(T[^\]]*)", attack)
|
||||
if r:
|
||||
tag = r.group(1)
|
||||
logger.info("attack tag:" + tag)
|
||||
tag = re.sub(r"\W", "_", tag)
|
||||
rule_tags += tag + " "
|
||||
# also add a line "attack = ..." to yaras 'meta:' to keep the long description:
|
||||
yara_meta += '\tattack = "' + attack + '"\n'
|
||||
elif meta_name == "mbc":
|
||||
for mbc in list(metas[meta]):
|
||||
logger.info("mbc:" + mbc)
|
||||
# cut out tag in square brackets, e.g. Cryptography::Encrypt Data::RC6 [C0027.010] => C0027.010
|
||||
r = re.search(r"\[(.[^\]]*)", mbc)
|
||||
if r:
|
||||
tag = r.group(1)
|
||||
logger.info("mbc tag:" + tag)
|
||||
tag = re.sub(r"\W", "_", tag)
|
||||
rule_tags += tag + " "
|
||||
|
||||
# also add a line "mbc = ..." to yaras 'meta:' to keep the long description:
|
||||
yara_meta += '\tmbc = "' + mbc + '"\n'
|
||||
|
||||
for value in metas[meta]:
|
||||
if meta_name == "hash":
|
||||
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
|
||||
if not value in seen_hashes:
|
||||
yara_meta += "\t" + meta_name + ' = "' + value + '"\n'
|
||||
seen_hashes.append(value)
|
||||
|
||||
else:
|
||||
# no list:
|
||||
if meta == "capa/path":
|
||||
url = get_rule_url(metas[meta])
|
||||
meta_name = "reference"
|
||||
meta_value = "This YARA rule converted from capa rule: " + url
|
||||
else:
|
||||
meta_value = metas[meta]
|
||||
|
||||
if meta_name == "name":
|
||||
meta_name = "description"
|
||||
meta_value += " (converted from capa rule)"
|
||||
elif meta_name == "lib":
|
||||
meta_value = str(meta_value)
|
||||
elif meta_name == "capa/nursery":
|
||||
meta_name = "capa_nursery"
|
||||
meta_value = str(meta_value)
|
||||
|
||||
# for the rest of the maec/malware-category names:
|
||||
meta_name = re.sub(r"\W", "_", meta_name)
|
||||
|
||||
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'
|
||||
yara_meta += '\tminimum_yara = "3.8"\n'
|
||||
yara_meta += '\tlicense = "Apache-2.0 License"\n'
|
||||
|
||||
# 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 "
|
||||
|
||||
# put yara rule tags here:
|
||||
rule_tags = default_tags + rule_tags
|
||||
yara += "rule " + rule_name + " : " + rule_tags + " { \n meta: \n " + yara_meta + "\n"
|
||||
|
||||
if "$" in yara_strings:
|
||||
yara += " strings: \n " + yara_strings + " \n"
|
||||
|
||||
yara += " condition:" + condition_header + yara_condition + "\n}"
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
if argv is None:
|
||||
argv = sys.argv[1:]
|
||||
|
||||
parser = argparse.ArgumentParser(description="Capa to YARA rule converter")
|
||||
parser.add_argument("rules", type=str, help="Path to rules")
|
||||
parser.add_argument("--private", "-p", action="store_true", help="Create private rules", default=False)
|
||||
capa.main.install_common_args(parser, wanted={"tag"})
|
||||
|
||||
args = parser.parse_args(args=argv)
|
||||
global make_priv
|
||||
make_priv = args.private
|
||||
|
||||
if args.verbose:
|
||||
level = logging.DEBUG
|
||||
elif args.quiet:
|
||||
level = logging.ERROR
|
||||
else:
|
||||
level = logging.INFO
|
||||
|
||||
logging.basicConfig(level=level)
|
||||
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)
|
||||
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)
|
||||
logger.debug("selected %s rules", len(rules))
|
||||
for i, r in enumerate(rules.rules, 1):
|
||||
logger.debug(" %d. %s", i, r)
|
||||
except (IOError, capa.rules.InvalidRule, capa.rules.InvalidRuleSet) as e:
|
||||
logger.error("%s", str(e))
|
||||
return -1
|
||||
|
||||
output_yar(
|
||||
"// Rules from FireEye's https://github.com/fireeye/capa-rules converted to YARA using https://github.com/fireeye/capa/blob/master/scripts/capa2yara.py by Arnim Rupp"
|
||||
)
|
||||
output_yar(
|
||||
"// Beware: These are less rules than capa (because not all fit into YARA, stats at EOF) and is less precise because e.g. capas function scopes are applied to the whole file"
|
||||
)
|
||||
output_yar(
|
||||
'// Beware: Some rules are incomplete because an optional branch was not supported by YARA. These rules are marked in a comment in meta: (search for "incomplete")'
|
||||
)
|
||||
output_yar("// Rule authors and license stay the same")
|
||||
output_yar(
|
||||
'// att&ck and MBC tags are put into YARA rule tags. All rules are tagged with "CAPA" for easy filtering'
|
||||
)
|
||||
output_yar("// The date = in meta: is the date of converting (there is no date in capa rules)")
|
||||
output_yar("// Minimum YARA version is 3.8.0 plus PE module")
|
||||
output_yar('\nimport "pe"')
|
||||
|
||||
output_yar(condition_rule)
|
||||
|
||||
# do several rounds of converting rules because some rules for match: might not be converted in the 1st run
|
||||
num_rules = 9999999
|
||||
cround = 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)
|
||||
|
||||
# one last round to collect all unconverted rules
|
||||
convert_rules(rules, namespaces, 9000)
|
||||
|
||||
stats = "\n// converted rules : " + str(len(converted_rules))
|
||||
stats += "\n// among those are incomplete : " + str(count_incomplete)
|
||||
stats += "\n// unconverted rules : " + str(len(unsupported_capa_rules_list)) + "\n"
|
||||
logger.info(stats)
|
||||
output_yar(stats)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
215
scripts/capa_as_library.py
Normal file
@@ -0,0 +1,215 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import json
|
||||
import collections
|
||||
|
||||
import capa.main
|
||||
import capa.rules
|
||||
import capa.engine
|
||||
import capa.features
|
||||
import capa.render.json
|
||||
import capa.render.utils as rutils
|
||||
import capa.render.default
|
||||
import capa.render.result_document
|
||||
from capa.engine import *
|
||||
|
||||
# edit this to set the path for file to analyze and rule directory
|
||||
RULES_PATH = "/tmp/capa/rules/"
|
||||
|
||||
# load rules from disk
|
||||
rules = capa.rules.RuleSet(capa.main.get_rules(RULES_PATH, disable_progress=True))
|
||||
|
||||
# == Render ddictionary helpers
|
||||
def render_meta(doc, ostream):
|
||||
ostream["md5"] = doc["meta"]["sample"]["md5"]
|
||||
ostream["sha1"] = doc["meta"]["sample"]["sha1"]
|
||||
ostream["sha256"] = doc["meta"]["sample"]["sha256"]
|
||||
ostream["path"] = doc["meta"]["sample"]["path"]
|
||||
|
||||
|
||||
def find_subrule_matches(doc):
|
||||
"""
|
||||
collect the rule names that have been matched as a subrule match.
|
||||
this way we can avoid displaying entries for things that are too specific.
|
||||
"""
|
||||
matches = set([])
|
||||
|
||||
def rec(node):
|
||||
if not node["success"]:
|
||||
# there's probably a bug here for rules that do `not: match: ...`
|
||||
# but we don't have any examples of this yet
|
||||
return
|
||||
|
||||
elif node["node"]["type"] == "statement":
|
||||
for child in node["children"]:
|
||||
rec(child)
|
||||
|
||||
elif node["node"]["type"] == "feature":
|
||||
if node["node"]["feature"]["type"] == "match":
|
||||
matches.add(node["node"]["feature"]["match"])
|
||||
|
||||
for rule in rutils.capability_rules(doc):
|
||||
for node in rule["matches"].values():
|
||||
rec(node)
|
||||
|
||||
return matches
|
||||
|
||||
|
||||
def render_capabilities(doc, ostream):
|
||||
"""
|
||||
example::
|
||||
{'CAPABILITY': {'accept command line arguments': 'host-interaction/cli',
|
||||
'allocate thread local storage (2 matches)': 'host-interaction/process',
|
||||
'check for time delay via GetTickCount': 'anti-analysis/anti-debugging/debugger-detection',
|
||||
'check if process is running under wine': 'anti-analysis/anti-emulation/wine',
|
||||
'contain a resource (.rsrc) section': 'executable/pe/section/rsrc',
|
||||
'write file (3 matches)': 'host-interaction/file-system/write'}
|
||||
}
|
||||
"""
|
||||
subrule_matches = find_subrule_matches(doc)
|
||||
|
||||
ostream["CAPABILITY"] = dict()
|
||||
for rule in rutils.capability_rules(doc):
|
||||
if rule["meta"]["name"] in subrule_matches:
|
||||
# rules that are also matched by other rules should not get rendered by default.
|
||||
# this cuts down on the amount of output while giving approx the same detail.
|
||||
# see #224
|
||||
continue
|
||||
|
||||
count = len(rule["matches"])
|
||||
if count == 1:
|
||||
capability = rule["meta"]["name"]
|
||||
else:
|
||||
capability = "%s (%d matches)" % (rule["meta"]["name"], count)
|
||||
|
||||
ostream["CAPABILITY"].setdefault(rule["meta"]["namespace"], list())
|
||||
ostream["CAPABILITY"][rule["meta"]["namespace"]].append(capability)
|
||||
|
||||
|
||||
def render_attack(doc, ostream):
|
||||
"""
|
||||
example::
|
||||
{'ATT&CK': {'COLLECTION': ['Input Capture::Keylogging [T1056.001]'],
|
||||
'DEFENSE EVASION': ['Obfuscated Files or Information [T1027]',
|
||||
'Virtualization/Sandbox Evasion::System Checks '
|
||||
'[T1497.001]'],
|
||||
'DISCOVERY': ['File and Directory Discovery [T1083]',
|
||||
'Query Registry [T1012]',
|
||||
'System Information Discovery [T1082]'],
|
||||
'EXECUTION': ['Shared Modules [T1129]']}
|
||||
}
|
||||
"""
|
||||
ostream["ATTCK"] = dict()
|
||||
tactics = collections.defaultdict(set)
|
||||
for rule in rutils.capability_rules(doc):
|
||||
if not rule["meta"].get("att&ck"):
|
||||
continue
|
||||
|
||||
for attack in rule["meta"]["att&ck"]:
|
||||
tactic, _, rest = attack.partition("::")
|
||||
if "::" in rest:
|
||||
technique, _, rest = rest.partition("::")
|
||||
subtechnique, _, id = rest.rpartition(" ")
|
||||
tactics[tactic].add((technique, subtechnique, id))
|
||||
else:
|
||||
technique, _, id = rest.rpartition(" ")
|
||||
tactics[tactic].add((technique, id))
|
||||
|
||||
for tactic, techniques in sorted(tactics.items()):
|
||||
inner_rows = []
|
||||
for spec in sorted(techniques):
|
||||
if len(spec) == 2:
|
||||
technique, id = spec
|
||||
inner_rows.append("%s %s" % (technique, id))
|
||||
elif len(spec) == 3:
|
||||
technique, subtechnique, id = spec
|
||||
inner_rows.append("%s::%s %s" % (technique, subtechnique, id))
|
||||
else:
|
||||
raise RuntimeError("unexpected ATT&CK spec format")
|
||||
ostream["ATTCK"].setdefault(tactic.upper(), inner_rows)
|
||||
|
||||
|
||||
def render_mbc(doc, ostream):
|
||||
"""
|
||||
example::
|
||||
{'MBC': {'ANTI-BEHAVIORAL ANALYSIS': ['Debugger Detection::Timing/Delay Check '
|
||||
'GetTickCount [B0001.032]',
|
||||
'Emulator Detection [B0004]',
|
||||
'Virtual Machine Detection::Instruction '
|
||||
'Testing [B0009.029]',
|
||||
'Virtual Machine Detection [B0009]'],
|
||||
'COLLECTION': ['Keylogging::Polling [F0002.002]'],
|
||||
'CRYPTOGRAPHY': ['Encrypt Data::RC4 [C0027.009]',
|
||||
'Generate Pseudo-random Sequence::RC4 PRGA '
|
||||
'[C0021.004]']}
|
||||
}
|
||||
"""
|
||||
ostream["MBC"] = dict()
|
||||
objectives = collections.defaultdict(set)
|
||||
for rule in rutils.capability_rules(doc):
|
||||
if not rule["meta"].get("mbc"):
|
||||
continue
|
||||
|
||||
mbcs = rule["meta"]["mbc"]
|
||||
if not isinstance(mbcs, list):
|
||||
raise ValueError("invalid rule: MBC mapping is not a list")
|
||||
|
||||
for mbc in mbcs:
|
||||
objective, _, rest = mbc.partition("::")
|
||||
if "::" in rest:
|
||||
behavior, _, rest = rest.partition("::")
|
||||
method, _, id = rest.rpartition(" ")
|
||||
objectives[objective].add((behavior, method, id))
|
||||
else:
|
||||
behavior, _, id = rest.rpartition(" ")
|
||||
objectives[objective].add((behavior, id))
|
||||
|
||||
for objective, behaviors in sorted(objectives.items()):
|
||||
inner_rows = []
|
||||
for spec in sorted(behaviors):
|
||||
if len(spec) == 2:
|
||||
behavior, id = spec
|
||||
inner_rows.append("%s %s" % (behavior, id))
|
||||
elif len(spec) == 3:
|
||||
behavior, method, id = spec
|
||||
inner_rows.append("%s::%s %s" % (behavior, method, id))
|
||||
else:
|
||||
raise RuntimeError("unexpected MBC spec format")
|
||||
ostream["MBC"].setdefault(objective.upper(), inner_rows)
|
||||
|
||||
|
||||
def render_dictionary(doc):
|
||||
ostream = dict()
|
||||
render_meta(doc, ostream)
|
||||
render_attack(doc, ostream)
|
||||
render_mbc(doc, ostream)
|
||||
render_capabilities(doc, ostream)
|
||||
|
||||
return ostream
|
||||
|
||||
|
||||
# ==== render dictionary helpers
|
||||
def capa_details(file_path, output_format="dictionary"):
|
||||
|
||||
# extract features and find capabilities
|
||||
extractor = capa.main.get_extractor(file_path, "auto", capa.main.BACKEND_VIV, [], False, disable_progress=True)
|
||||
capabilities, counts = capa.main.find_capabilities(rules, extractor, disable_progress=True)
|
||||
|
||||
# collect metadata (used only to make rendering more complete)
|
||||
meta = capa.main.collect_metadata("", file_path, RULES_PATH, "auto", extractor)
|
||||
meta["analysis"].update(counts)
|
||||
|
||||
capa_output = False
|
||||
if output_format == "dictionary":
|
||||
# ...as python dictionary, simplified as textable but in dictionary
|
||||
doc = capa.render.result_document.convert_capabilities_to_result_document(meta, rules, capabilities)
|
||||
capa_output = render_dictionary(doc)
|
||||
elif output_format == "json":
|
||||
# render results
|
||||
# ...as json
|
||||
capa_output = json.loads(capa.render.json.render(meta, rules, capabilities))
|
||||
elif output_format == "texttable":
|
||||
# ...as human readable text table
|
||||
capa_output = capa.render.default.render(meta, rules, capabilities)
|
||||
|
||||
return capa_output
|
||||
@@ -38,6 +38,12 @@ def main(argv=None):
|
||||
)
|
||||
parser.add_argument("-v", "--verbose", action="store_true", help="Enable debug logging")
|
||||
parser.add_argument("-q", "--quiet", action="store_true", help="Disable all output but errors")
|
||||
parser.add_argument(
|
||||
"-c",
|
||||
"--check",
|
||||
action="store_true",
|
||||
help="Don't output (reformatted) rule, only return status. 0 = no changes, 1 = would reformat",
|
||||
)
|
||||
args = parser.parse_args(args=argv)
|
||||
|
||||
if args.verbose:
|
||||
@@ -50,12 +56,24 @@ def main(argv=None):
|
||||
logging.basicConfig(level=level)
|
||||
logging.getLogger("capafmt").setLevel(level)
|
||||
|
||||
rule = capa.rules.Rule.from_yaml_file(args.path)
|
||||
rule = capa.rules.Rule.from_yaml_file(args.path, use_ruamel=True)
|
||||
reformatted_rule = rule.to_yaml()
|
||||
|
||||
if args.check:
|
||||
if rule.definition == reformatted_rule:
|
||||
logger.info("rule is formatted correctly, nice! (%s)", rule.name)
|
||||
return 0
|
||||
else:
|
||||
logger.info("rule requires reformatting (%s)", rule.name)
|
||||
if "\r\n" in rule.definition:
|
||||
logger.info("please make sure that the file uses LF (\\n) line endings only")
|
||||
return 1
|
||||
|
||||
if args.in_place:
|
||||
with open(args.path, "wb") as f:
|
||||
f.write(rule.to_yaml().encode("utf-8"))
|
||||
f.write(reformatted_rule.encode("utf-8"))
|
||||
else:
|
||||
print(rule.to_yaml().rstrip("\n"))
|
||||
print(reformatted_rule)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
# Use a console with emojis support for a better experience
|
||||
# Use venv to ensure that `python` calls the correct python version
|
||||
|
||||
# Stash uncommited changes
|
||||
MSG="pre-push-$(date +%s)";
|
||||
@@ -25,17 +26,8 @@ restore_stashed() {
|
||||
fi
|
||||
}
|
||||
|
||||
python_3() {
|
||||
case "$(uname -s)" in
|
||||
CYGWIN*|MINGW32*|MSYS*|MINGW*)
|
||||
py -3 -m $1 > $2 2>&1;;
|
||||
*)
|
||||
python3 -m $1 > $2 2>&1;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Run isort and print state
|
||||
python_3 'isort --profile black --length-sort --line-width 120 -c .' 'isort-output.log';
|
||||
python -m isort --profile black --length-sort --line-width 120 -c . > isort-output.log 2>&1;
|
||||
if [ $? == 0 ]; then
|
||||
echo 'isort succeeded!! 💖';
|
||||
else
|
||||
@@ -46,7 +38,7 @@ else
|
||||
fi
|
||||
|
||||
# Run black and print state
|
||||
python_3 'black -l 120 --check .' 'black-output.log';
|
||||
python -m black -l 120 --check . > black-output.log 2>&1;
|
||||
if [ $? == 0 ]; then
|
||||
echo 'black succeeded!! 💝';
|
||||
else
|
||||
@@ -70,7 +62,7 @@ fi
|
||||
# Run tests except if first argument is no_tests
|
||||
if [ "$1" != 'no_tests' ]; then
|
||||
echo 'Running tests, please wait ⌛';
|
||||
pytest tests/ --maxfail=1;
|
||||
python -m pytest tests/ --maxfail=1;
|
||||
if [ $? == 0 ]; then
|
||||
echo 'Tests succeed!! 🎉';
|
||||
else
|
||||
|
||||
@@ -25,7 +25,8 @@ Derived from: https://github.com/fireeye/capa/blob/master/scripts/import-to-ida.
|
||||
import os
|
||||
import json
|
||||
|
||||
from binaryninja import *
|
||||
import binaryninja
|
||||
import binaryninja.interaction
|
||||
|
||||
|
||||
def append_func_cmt(bv, va, cmt):
|
||||
@@ -46,31 +47,31 @@ def append_func_cmt(bv, va, cmt):
|
||||
def load_analysis(bv):
|
||||
shortname = os.path.splitext(os.path.basename(bv.file.filename))[0]
|
||||
dirname = os.path.dirname(bv.file.filename)
|
||||
log_info(f"dirname: {dirname}\nshortname: {shortname}\n")
|
||||
binaryninja.log_info(f"dirname: {dirname}\nshortname: {shortname}\n")
|
||||
if os.access(os.path.join(dirname, shortname + ".js"), os.R_OK):
|
||||
path = os.path.join(dirname, shortname + ".js")
|
||||
elif os.access(os.path.join(dirname, shortname + ".json"), os.R_OK):
|
||||
path = os.path.join(dirname, shortname + ".json")
|
||||
else:
|
||||
path = interaction.get_open_filename_input("capa report:", "JSON (*.js *.json);;All Files (*)")
|
||||
path = binaryninja.interaction.get_open_filename_input("capa report:", "JSON (*.js *.json);;All Files (*)")
|
||||
if not path or not os.access(path, os.R_OK):
|
||||
log_error("Invalid filename.")
|
||||
binaryninja.log_error("Invalid filename.")
|
||||
return 0
|
||||
log_info("Using capa file %s" % path)
|
||||
binaryninja.log_info("Using capa file %s" % path)
|
||||
|
||||
with open(path, "rb") as f:
|
||||
doc = json.loads(f.read().decode("utf-8"))
|
||||
|
||||
if "meta" not in doc or "rules" not in doc:
|
||||
log_error("doesn't appear to be a capa report")
|
||||
binaryninja.log_error("doesn't appear to be a capa report")
|
||||
return -1
|
||||
|
||||
a = doc["meta"]["sample"]["md5"].lower()
|
||||
md5 = Transform["MD5"]
|
||||
rawhex = Transform["RawHex"]
|
||||
md5 = binaryninja.Transform["MD5"]
|
||||
rawhex = binaryninja.Transform["RawHex"]
|
||||
b = rawhex.encode(md5.encode(bv.parent_view.read(bv.parent_view.start, bv.parent_view.end))).decode("utf-8")
|
||||
if not a == b:
|
||||
log_error("sample mismatch")
|
||||
binaryninja.log_error("sample mismatch")
|
||||
return -2
|
||||
|
||||
rows = []
|
||||
@@ -96,7 +97,7 @@ def load_analysis(bv):
|
||||
else:
|
||||
cmt = "%s" % (name,)
|
||||
|
||||
log_info("0x%x: %s" % (va, cmt))
|
||||
binaryninja.log_info("0x%x: %s" % (va, cmt))
|
||||
try:
|
||||
# message will look something like:
|
||||
#
|
||||
@@ -105,7 +106,7 @@ def load_analysis(bv):
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
log_info("ok")
|
||||
binaryninja.log_info("ok")
|
||||
|
||||
|
||||
PluginCommand.register("Load capa file", "Loads an analysis file from capa", load_analysis)
|
||||
binaryninja.PluginCommand.register("Load capa file", "Loads an analysis file from capa", load_analysis)
|
||||
|
||||
@@ -31,9 +31,8 @@ See the License for the specific language governing permissions and limitations
|
||||
import json
|
||||
import logging
|
||||
|
||||
import idc
|
||||
import idautils
|
||||
import ida_idaapi
|
||||
import ida_funcs
|
||||
import ida_kernwin
|
||||
|
||||
logger = logging.getLogger("capa")
|
||||
|
||||
322
scripts/lint.py
@@ -15,7 +15,9 @@ See the License for the specific language governing permissions and limitations
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import string
|
||||
import difflib
|
||||
import hashlib
|
||||
import logging
|
||||
import os.path
|
||||
@@ -23,16 +25,36 @@ import argparse
|
||||
import itertools
|
||||
import posixpath
|
||||
|
||||
import termcolor
|
||||
import ruamel.yaml
|
||||
|
||||
import capa.main
|
||||
import capa.rules
|
||||
import capa.engine
|
||||
import capa.features
|
||||
import capa.features.insn
|
||||
import capa.features.common
|
||||
|
||||
logger = logging.getLogger("capa.lint")
|
||||
logger = logging.getLogger("lint")
|
||||
|
||||
|
||||
class Lint(object):
|
||||
def red(s):
|
||||
return termcolor.colored(s, "red")
|
||||
|
||||
|
||||
def orange(s):
|
||||
return termcolor.colored(s, "yellow")
|
||||
|
||||
|
||||
def green(s):
|
||||
return termcolor.colored(s, "green")
|
||||
|
||||
|
||||
class Lint:
|
||||
WARN = orange("WARN")
|
||||
FAIL = red("FAIL")
|
||||
|
||||
name = "lint"
|
||||
level = FAIL
|
||||
recommendation = ""
|
||||
|
||||
def check_rule(self, ctx, rule):
|
||||
@@ -172,6 +194,9 @@ class ExampleFileDNE(Lint):
|
||||
return not found
|
||||
|
||||
|
||||
DEFAULT_SIGNATURES = capa.main.get_default_signatures()
|
||||
|
||||
|
||||
class DoesntMatchExample(Lint):
|
||||
name = "doesn't match on referenced example"
|
||||
recommendation = "Fix the rule logic or provide a different example"
|
||||
@@ -194,7 +219,9 @@ class DoesntMatchExample(Lint):
|
||||
continue
|
||||
|
||||
try:
|
||||
extractor = capa.main.get_extractor(path, "auto")
|
||||
extractor = capa.main.get_extractor(
|
||||
path, "auto", capa.main.BACKEND_VIV, DEFAULT_SIGNATURES, False, disable_progress=True
|
||||
)
|
||||
capabilities, meta = capa.main.find_capabilities(ctx["rules"], extractor, disable_progress=True)
|
||||
except Exception as e:
|
||||
logger.error("failed to extract capabilities: %s %s %s", rule.name, path, e)
|
||||
@@ -204,6 +231,53 @@ class DoesntMatchExample(Lint):
|
||||
return True
|
||||
|
||||
|
||||
class StatementWithSingleChildStatement(Lint):
|
||||
name = "rule contains one or more statements with a single child statement"
|
||||
recommendation = "remove the superfluous parent statement"
|
||||
recommendation_template = "remove the superfluous parent statement: {:s}"
|
||||
violation = False
|
||||
|
||||
def check_rule(self, ctx, rule):
|
||||
self.violation = False
|
||||
|
||||
def rec(statement, is_root=False):
|
||||
if isinstance(statement, (capa.engine.And, capa.engine.Or)):
|
||||
children = list(statement.get_children())
|
||||
if not is_root and len(children) == 1 and isinstance(children[0], capa.engine.Statement):
|
||||
self.recommendation = self.recommendation_template.format(str(statement))
|
||||
self.violation = True
|
||||
for child in children:
|
||||
rec(child)
|
||||
|
||||
rec(rule.statement, is_root=True)
|
||||
|
||||
return self.violation
|
||||
|
||||
|
||||
class OrStatementWithAlwaysTrueChild(Lint):
|
||||
name = "rule contains an `or` statement that's always True because of an `optional` or other child statement that's always True"
|
||||
recommendation = "clarify the rule logic, e.g. by moving the always True child statement"
|
||||
recommendation_template = "clarify the rule logic, e.g. by moving the always True child statement: {:s}"
|
||||
violation = False
|
||||
|
||||
def check_rule(self, ctx, rule):
|
||||
self.violation = False
|
||||
|
||||
def rec(statement):
|
||||
if isinstance(statement, capa.engine.Or):
|
||||
children = list(statement.get_children())
|
||||
for child in children:
|
||||
# `Some` implements `optional` which is an alias for `0 or more`
|
||||
if isinstance(child, capa.engine.Some) and child.count == 0:
|
||||
self.recommendation = self.recommendation_template.format(str(child))
|
||||
self.violation = True
|
||||
rec(child)
|
||||
|
||||
rec(rule.statement)
|
||||
|
||||
return self.violation
|
||||
|
||||
|
||||
class UnusualMetaField(Lint):
|
||||
name = "unusual meta field"
|
||||
recommendation = "Remove the meta field"
|
||||
@@ -232,7 +306,7 @@ class LibRuleNotInLibDirectory(Lint):
|
||||
if "lib" not in rule.meta:
|
||||
return False
|
||||
|
||||
return "/lib/" not in get_normpath(rule.meta["capa/path"])
|
||||
return "lib/" not in get_normpath(rule.meta["capa/path"])
|
||||
|
||||
|
||||
class LibRuleHasNamespace(Lint):
|
||||
@@ -252,7 +326,7 @@ class FeatureStringTooShort(Lint):
|
||||
|
||||
def check_features(self, ctx, features):
|
||||
for feature in features:
|
||||
if isinstance(feature, capa.features.String):
|
||||
if isinstance(feature, capa.features.common.String):
|
||||
if len(feature.value) < 4:
|
||||
self.recommendation = self.recommendation.format(feature.value)
|
||||
return True
|
||||
@@ -276,6 +350,91 @@ class FeatureNegativeNumber(Lint):
|
||||
return False
|
||||
|
||||
|
||||
class FeatureNtdllNtoskrnlApi(Lint):
|
||||
name = "feature api may overlap with ntdll and ntoskrnl"
|
||||
level = Lint.WARN
|
||||
recommendation = (
|
||||
"check if {:s} is exported by both ntdll and ntoskrnl; if true, consider removing {:s} "
|
||||
"module requirement to improve detection"
|
||||
)
|
||||
|
||||
def check_features(self, ctx, features):
|
||||
for feature in features:
|
||||
if isinstance(feature, capa.features.insn.API):
|
||||
modname, _, impname = feature.value.rpartition(".")
|
||||
if modname in ("ntdll", "ntoskrnl"):
|
||||
self.recommendation = self.recommendation.format(impname, modname)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class FormatLineFeedEOL(Lint):
|
||||
name = "line(s) end with CRLF (\\r\\n)"
|
||||
recommendation = "convert line endings to LF (\\n) for example using dos2unix"
|
||||
|
||||
def check_rule(self, ctx, rule):
|
||||
if len(rule.definition.split("\r\n")) > 0:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class FormatSingleEmptyLineEOF(Lint):
|
||||
name = "EOF format"
|
||||
recommendation = "end file with a single empty line"
|
||||
|
||||
def check_rule(self, ctx, rule):
|
||||
if rule.definition.endswith("\n") and not rule.definition.endswith("\n\n"):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class FormatIncorrect(Lint):
|
||||
name = "rule format incorrect"
|
||||
recommendation_template = "use scripts/capafmt.py or adjust as follows\n{:s}"
|
||||
|
||||
def check_rule(self, ctx, rule):
|
||||
actual = rule.definition
|
||||
expected = capa.rules.Rule.from_yaml(rule.definition, use_ruamel=True).to_yaml()
|
||||
|
||||
if actual != expected:
|
||||
diff = difflib.ndiff(actual.splitlines(1), expected.splitlines(True))
|
||||
recommendation_template = self.recommendation_template
|
||||
if "\r\n" in actual:
|
||||
recommendation_template = (
|
||||
self.recommendation_template + "\nplease make sure that the file uses LF (\\n) line endings only"
|
||||
)
|
||||
self.recommendation = recommendation_template.format("".join(diff))
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class FormatStringQuotesIncorrect(Lint):
|
||||
name = "rule string quotes incorrect"
|
||||
|
||||
def check_rule(self, ctx, rule):
|
||||
events = capa.rules.Rule._get_ruamel_yaml_parser().parse(rule.definition)
|
||||
for key in events:
|
||||
if not (isinstance(key, ruamel.yaml.ScalarEvent) and key.value == "string"):
|
||||
continue
|
||||
value = next(events) # assume value is next event
|
||||
if not isinstance(value, ruamel.yaml.ScalarEvent):
|
||||
# ignore non-scalar
|
||||
continue
|
||||
if value.value.startswith("/") and value.value.endswith(("/", "/i")):
|
||||
# ignore regex for now
|
||||
continue
|
||||
if value.style is None:
|
||||
# no quotes
|
||||
self.recommendation = 'add double quotes to "%s"' % value.value
|
||||
return True
|
||||
if value.style == "'":
|
||||
# single quote
|
||||
self.recommendation = 'change single quotes to double quotes for "%s"' % value.value
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def run_lints(lints, ctx, rule):
|
||||
for lint in lints:
|
||||
if lint.check_rule(ctx, rule):
|
||||
@@ -325,14 +484,7 @@ def lint_meta(ctx, rule):
|
||||
return run_lints(META_LINTS, ctx, rule)
|
||||
|
||||
|
||||
FEATURE_LINTS = (
|
||||
FeatureStringTooShort(),
|
||||
FeatureNegativeNumber(),
|
||||
)
|
||||
|
||||
|
||||
def get_normpath(path):
|
||||
return posixpath.normpath(path).replace(os.sep, "/")
|
||||
FEATURE_LINTS = (FeatureStringTooShort(), FeatureNegativeNumber(), FeatureNtdllNtoskrnlApi())
|
||||
|
||||
|
||||
def lint_features(ctx, rule):
|
||||
@@ -340,10 +492,26 @@ def lint_features(ctx, rule):
|
||||
return run_feature_lints(FEATURE_LINTS, ctx, features)
|
||||
|
||||
|
||||
FORMAT_LINTS = (
|
||||
FormatLineFeedEOL(),
|
||||
FormatSingleEmptyLineEOF(),
|
||||
FormatStringQuotesIncorrect(),
|
||||
FormatIncorrect(),
|
||||
)
|
||||
|
||||
|
||||
def lint_format(ctx, rule):
|
||||
return run_lints(FORMAT_LINTS, ctx, rule)
|
||||
|
||||
|
||||
def get_normpath(path):
|
||||
return posixpath.normpath(path).replace(os.sep, "/")
|
||||
|
||||
|
||||
def get_features(ctx, rule):
|
||||
# get features from rule and all dependencies including subscopes and matched rules
|
||||
features = []
|
||||
namespaces = capa.rules.index_rules_by_namespace([rule])
|
||||
namespaces = ctx["rules"].rules_by_namespace
|
||||
deps = [ctx["rules"].rules[dep] for dep in rule.get_dependencies(namespaces)]
|
||||
for r in [rule] + deps:
|
||||
features.extend(get_rule_features(r))
|
||||
@@ -364,7 +532,11 @@ def get_rule_features(rule):
|
||||
return features
|
||||
|
||||
|
||||
LOGIC_LINTS = (DoesntMatchExample(),)
|
||||
LOGIC_LINTS = (
|
||||
DoesntMatchExample(),
|
||||
StatementWithSingleChildStatement(),
|
||||
OrStatementWithAlwaysTrueChild(),
|
||||
)
|
||||
|
||||
|
||||
def lint_logic(ctx, rule):
|
||||
@@ -390,6 +562,7 @@ def lint_rule(ctx, rule):
|
||||
lint_meta(ctx, rule),
|
||||
lint_logic(ctx, rule),
|
||||
lint_features(ctx, rule),
|
||||
lint_format(ctx, rule),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -406,25 +579,50 @@ def lint_rule(ctx, rule):
|
||||
)
|
||||
)
|
||||
|
||||
level = "WARN" if is_nursery_rule(rule) else "FAIL"
|
||||
|
||||
for violation in violations:
|
||||
print(
|
||||
"%s %s: %s: %s"
|
||||
% (
|
||||
" " if is_nursery_rule(rule) else "",
|
||||
level,
|
||||
Lint.WARN if is_nursery_rule(rule) else violation.level,
|
||||
violation.name,
|
||||
violation.recommendation,
|
||||
)
|
||||
)
|
||||
|
||||
elif len(violations) == 0 and is_nursery_rule(rule):
|
||||
print("")
|
||||
print("%s%s" % (" (nursery) ", rule.name))
|
||||
print("%s %s: %s: %s" % (" ", "WARN", "no violations", "Graduate the rule"))
|
||||
|
||||
return len(violations) > 0 and not is_nursery_rule(rule)
|
||||
if is_nursery_rule(rule):
|
||||
has_examples = not any(map(lambda v: v.level == Lint.FAIL and v.name == "missing examples", violations))
|
||||
lints_failed = len(
|
||||
tuple(
|
||||
filter(
|
||||
lambda v: v.level == Lint.FAIL
|
||||
and not (v.name == "missing examples" or v.name == "referenced example doesn't exist"),
|
||||
violations,
|
||||
)
|
||||
)
|
||||
)
|
||||
lints_warned = len(
|
||||
tuple(
|
||||
filter(
|
||||
lambda v: v.level == Lint.WARN
|
||||
or (v.level == Lint.FAIL and v.name == "referenced example doesn't exist"),
|
||||
violations,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if (not lints_failed) and (not lints_warned) and has_examples:
|
||||
print("")
|
||||
print("%s%s" % (" (nursery) ", rule.name))
|
||||
print("%s %s: %s: %s" % (" ", Lint.WARN, green("no lint failures"), "Graduate the rule"))
|
||||
print("")
|
||||
else:
|
||||
lints_failed = len(tuple(filter(lambda v: v.level == Lint.FAIL, violations)))
|
||||
lints_warned = len(tuple(filter(lambda v: v.level == Lint.WARN, violations)))
|
||||
|
||||
return (lints_failed, lints_warned)
|
||||
|
||||
|
||||
def lint(ctx, rules):
|
||||
@@ -434,15 +632,20 @@ def lint(ctx, rules):
|
||||
for each sample, record sample id of sha256, md5, and filename.
|
||||
see `collect_samples(path)`.
|
||||
rules (List[Rule]): the rules to lint.
|
||||
|
||||
Returns: Dict[string, Tuple(int, int)]
|
||||
- # lints failed
|
||||
- # lints warned
|
||||
"""
|
||||
did_suggest_fix = False
|
||||
for rule in rules.rules.values():
|
||||
ret = {}
|
||||
|
||||
for name, rule in rules.rules.items():
|
||||
if rule.meta.get("capa/subscope-rule", False):
|
||||
continue
|
||||
|
||||
did_suggest_fix = lint_rule(ctx, rule) or did_suggest_fix
|
||||
ret[name] = lint_rule(ctx, rule)
|
||||
|
||||
return did_suggest_fix
|
||||
return ret
|
||||
|
||||
|
||||
def collect_samples(path):
|
||||
@@ -492,7 +695,8 @@ def main(argv=None):
|
||||
|
||||
samples_path = os.path.join(os.path.dirname(__file__), "..", "tests", "data")
|
||||
|
||||
parser = argparse.ArgumentParser(description="A program.")
|
||||
parser = argparse.ArgumentParser(description="Lint capa rules.")
|
||||
capa.main.install_common_args(parser, wanted={"tag"})
|
||||
parser.add_argument("rules", type=str, help="Path to rules")
|
||||
parser.add_argument("--samples", type=str, default=samples_path, help="Path to samples")
|
||||
parser.add_argument(
|
||||
@@ -500,31 +704,28 @@ def main(argv=None):
|
||||
action="store_true",
|
||||
help="Enable thorough linting - takes more time, but does a better job",
|
||||
)
|
||||
parser.add_argument("-v", "--verbose", action="store_true", help="Enable debug logging")
|
||||
parser.add_argument("-q", "--quiet", action="store_true", help="Disable all output but errors")
|
||||
args = parser.parse_args(args=argv)
|
||||
capa.main.handle_common_args(args)
|
||||
|
||||
if args.verbose:
|
||||
level = logging.DEBUG
|
||||
elif args.quiet:
|
||||
level = logging.ERROR
|
||||
if args.debug:
|
||||
logging.getLogger("capa").setLevel(logging.DEBUG)
|
||||
logging.getLogger("viv_utils").setLevel(logging.DEBUG)
|
||||
else:
|
||||
level = logging.INFO
|
||||
logging.getLogger("capa").setLevel(logging.ERROR)
|
||||
logging.getLogger("viv_utils").setLevel(logging.ERROR)
|
||||
|
||||
logging.basicConfig(level=level)
|
||||
logging.getLogger("capa.lint").setLevel(level)
|
||||
|
||||
capa.main.set_vivisect_log_level(logging.CRITICAL)
|
||||
logging.getLogger("capa").setLevel(logging.CRITICAL)
|
||||
time0 = time.time()
|
||||
|
||||
try:
|
||||
rules = capa.main.get_rules(args.rules)
|
||||
rules = capa.main.get_rules(args.rules, disable_progress=True)
|
||||
rules = capa.rules.RuleSet(rules)
|
||||
logger.info("successfully loaded %s rules", len(rules))
|
||||
except IOError as e:
|
||||
logger.error("%s", str(e))
|
||||
return -1
|
||||
except capa.rules.InvalidRule as e:
|
||||
if args.tag:
|
||||
rules = rules.filter_rules_by_meta(args.tag)
|
||||
logger.debug("selected %s rules", len(rules))
|
||||
for i, r in enumerate(rules.rules, 1):
|
||||
logger.debug(" %d. %s", i, r)
|
||||
except (IOError, capa.rules.InvalidRule, capa.rules.InvalidRuleSet) as e:
|
||||
logger.error("%s", str(e))
|
||||
return -1
|
||||
|
||||
@@ -541,12 +742,33 @@ def main(argv=None):
|
||||
"is_thorough": args.thorough,
|
||||
}
|
||||
|
||||
did_violate = lint(ctx, rules)
|
||||
if not did_violate:
|
||||
logger.info("no suggestions, nice!")
|
||||
return 0
|
||||
else:
|
||||
results_by_name = lint(ctx, rules)
|
||||
failed_rules = []
|
||||
warned_rules = []
|
||||
for name, (fail_count, warn_count) in results_by_name.items():
|
||||
if fail_count > 0:
|
||||
failed_rules.append(name)
|
||||
|
||||
if warn_count > 0:
|
||||
warned_rules.append(name)
|
||||
|
||||
min, sec = divmod(time.time() - time0, 60)
|
||||
logger.debug("lints ran for ~ %02d:%02dm", min, sec)
|
||||
|
||||
if warned_rules:
|
||||
print(orange("rules with WARN:"))
|
||||
for warned_rule in sorted(warned_rules):
|
||||
print(" - " + warned_rule)
|
||||
print()
|
||||
|
||||
if failed_rules:
|
||||
print(red("rules with FAIL:"))
|
||||
for failed_rule in sorted(failed_rules):
|
||||
print(" - " + failed_rule)
|
||||
return 1
|
||||
else:
|
||||
logger.info(green("no lints failed, nice!"))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
134
scripts/match-function-id.py
Normal file
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Copyright (C) 2021 FireEye, 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.
|
||||
|
||||
match-function-id
|
||||
|
||||
Show the names of functions as recognized by the function identification subsystem.
|
||||
This can help identify library functions statically linked into a program,
|
||||
such as when triaging false positive matches in capa rules.
|
||||
|
||||
Example::
|
||||
|
||||
$ python scripts/match-function-id.py --signature sigs/vc6.pat.gz /tmp/suspicious.dll_
|
||||
0x44cf30: ?GetPdbDll@@YAPAUHINSTANCE__@@XZ
|
||||
0x44bb20: ?_strlen_priv@@YAIPBD@Z
|
||||
0x44b6b0: ?invoke_main@@YAHXZ
|
||||
0x44a5d0: ?find_pe_section@@YAPAU_IMAGE_SECTION_HEADER@@QAEI@Z
|
||||
0x44a690: ?is_potentially_valid_image_base@@YA_NQAX@Z
|
||||
0x44cbe0: ___get_entropy
|
||||
0x44a4a0: __except_handler4
|
||||
0x44b3d0: ?pre_cpp_initialization@@YAXXZ
|
||||
0x44b2e0: ?pre_c_initialization@@YAHXZ
|
||||
0x44b3c0: ?post_pgo_initialization@@YAHXZ
|
||||
0x420156: ?
|
||||
0x420270: ?
|
||||
0x430dcd: ?
|
||||
0x44d930: __except_handler4_noexcept
|
||||
0x41e960: ?
|
||||
0x44a1e0: @_RTC_AllocaHelper@12
|
||||
0x44ba90: ?_getMemBlockDataString@@YAXPAD0PBDI@Z
|
||||
0x44a220: @_RTC_CheckStackVars2@12
|
||||
0x44a790: ___scrt_dllmain_after_initialize_c
|
||||
0x44a7d0: ___scrt_dllmain_before_initialize_c
|
||||
0x44a800: ___scrt_dllmain_crt_thread_attach
|
||||
0x44a860: ___scrt_dllmain_exception_filter
|
||||
0x44a900: ___scrt_dllmain_uninitialize_critical
|
||||
0x44ad10: _at_quick_exit
|
||||
0x44b940: ?_RTC_Failure@@YAXPAXH@Z
|
||||
0x44be60: __RTC_UninitUse
|
||||
0x44bfd0: __RTC_GetErrDesc
|
||||
0x44c060: __RTC_SetErrorType
|
||||
0x44cb60: ?
|
||||
0x44cba0: __guard_icall_checks_enforced
|
||||
"""
|
||||
import sys
|
||||
import logging
|
||||
import argparse
|
||||
|
||||
import flirt
|
||||
import viv_utils
|
||||
import viv_utils.flirt
|
||||
|
||||
import capa.main
|
||||
import capa.rules
|
||||
import capa.engine
|
||||
import capa.helpers
|
||||
import capa.features
|
||||
import capa.features.freeze
|
||||
|
||||
logger = logging.getLogger("capa.match-function-id")
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
if argv is None:
|
||||
argv = sys.argv[1:]
|
||||
|
||||
parser = argparse.ArgumentParser(description="FLIRT match each function")
|
||||
parser.add_argument("sample", type=str, help="Path to sample to analyze")
|
||||
parser.add_argument(
|
||||
"-F",
|
||||
"--function",
|
||||
type=lambda x: int(x, 0x10),
|
||||
help="match a specific function by VA, rather than add functions",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--signature",
|
||||
action="append",
|
||||
dest="signatures",
|
||||
type=str,
|
||||
default=[],
|
||||
help="use the given signatures to identify library functions, file system paths to .sig/.pat files.",
|
||||
)
|
||||
parser.add_argument("-d", "--debug", action="store_true", help="Enable debugging output on STDERR")
|
||||
parser.add_argument("-q", "--quiet", action="store_true", help="Disable all output but errors")
|
||||
args = parser.parse_args(args=argv)
|
||||
|
||||
if args.quiet:
|
||||
logging.basicConfig(level=logging.ERROR)
|
||||
logging.getLogger().setLevel(logging.ERROR)
|
||||
elif args.debug:
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
else:
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logging.getLogger().setLevel(logging.INFO)
|
||||
|
||||
# disable vivisect-related logging, it's verbose and not relevant for capa users
|
||||
capa.main.set_vivisect_log_level(logging.CRITICAL)
|
||||
|
||||
analyzers = []
|
||||
for sigpath in args.signatures:
|
||||
sigs = capa.main.load_flirt_signature(sigpath)
|
||||
|
||||
with capa.main.timing("flirt: compiling sigs"):
|
||||
matcher = flirt.compile(sigs)
|
||||
|
||||
analyzer = viv_utils.flirt.FlirtFunctionAnalyzer(matcher, sigpath)
|
||||
logger.debug("registering viv function analyzer: %s", repr(analyzer))
|
||||
analyzers.append(analyzer)
|
||||
|
||||
vw = viv_utils.getWorkspace(args.sample, analyze=True, should_save=False)
|
||||
|
||||
functions = vw.getFunctions()
|
||||
if args.function:
|
||||
functions = [args.function]
|
||||
|
||||
for function in functions:
|
||||
logger.debug("matching function: 0x%04x", function)
|
||||
for analyzer in analyzers:
|
||||
name = viv_utils.flirt.match_function_flirt_signatures(analyzer.matcher, vw, function)
|
||||
if name:
|
||||
print("0x%04x: %s" % (function, name))
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,167 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
migrate rules and their namespaces.
|
||||
|
||||
example:
|
||||
|
||||
$ python scripts/migrate-rules.py migration.csv ./rules ./new-rules
|
||||
|
||||
Copyright (C) 2020 FireEye, 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 csv
|
||||
import sys
|
||||
import logging
|
||||
import os.path
|
||||
import argparse
|
||||
import collections
|
||||
|
||||
import capa.rules
|
||||
|
||||
logger = logging.getLogger("migrate-rules")
|
||||
|
||||
|
||||
def read_plan(plan_path):
|
||||
with open(plan_path, "rb") as f:
|
||||
return list(
|
||||
csv.DictReader(
|
||||
f,
|
||||
restkey="other",
|
||||
fieldnames=(
|
||||
"existing path",
|
||||
"existing name",
|
||||
"existing rule-category",
|
||||
"proposed name",
|
||||
"proposed namespace",
|
||||
"ATT&CK",
|
||||
"MBC",
|
||||
"comment1",
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def read_rules(rule_directory):
|
||||
rules = {}
|
||||
for root, dirs, files in os.walk(rule_directory):
|
||||
for file in files:
|
||||
path = os.path.join(root, file)
|
||||
if not path.endswith(".yml"):
|
||||
logger.info("skipping file: %s", path)
|
||||
continue
|
||||
|
||||
rule = capa.rules.Rule.from_yaml_file(path)
|
||||
rules[rule.name] = rule
|
||||
|
||||
if "nursery" in path:
|
||||
rule.meta["capa/nursery"] = True
|
||||
return rules
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
if argv is None:
|
||||
argv = sys.argv[1:]
|
||||
|
||||
parser = argparse.ArgumentParser(description="migrate rules.")
|
||||
parser.add_argument("plan", type=str, help="Path to CSV describing migration")
|
||||
parser.add_argument("source", type=str, help="Source directory of rules")
|
||||
parser.add_argument("destination", type=str, help="Destination directory of rules")
|
||||
args = parser.parse_args(args=argv)
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logging.getLogger().setLevel(logging.INFO)
|
||||
|
||||
plan = read_plan(args.plan)
|
||||
logger.info("read %d plan entries", len(plan))
|
||||
|
||||
rules = read_rules(args.source)
|
||||
logger.info("read %d rules", len(rules))
|
||||
|
||||
planned_rules = set([row["existing name"] for row in plan])
|
||||
unplanned_rules = [rule for (name, rule) in rules.items() if name not in planned_rules]
|
||||
|
||||
if unplanned_rules:
|
||||
logger.error("plan does not account for %d rules:" % (len(unplanned_rules)))
|
||||
for rule in unplanned_rules:
|
||||
logger.error(" " + rule.name)
|
||||
return -1
|
||||
|
||||
# pairs of strings (needle, replacement)
|
||||
match_translations = []
|
||||
|
||||
for row in plan:
|
||||
if not row["existing name"]:
|
||||
continue
|
||||
|
||||
rule = rules[row["existing name"]]
|
||||
|
||||
if rule.meta["name"] != row["proposed name"]:
|
||||
logger.info("renaming rule '%s' -> '%s'", rule.meta["name"], row["proposed name"])
|
||||
|
||||
# assume the yaml is formatted like `- match: $rule-name`.
|
||||
# but since its been linted, this should be ok.
|
||||
match_translations.append(("- match: " + rule.meta["name"], "- match: " + row["proposed name"]))
|
||||
|
||||
rule.meta["name"] = row["proposed name"]
|
||||
rule.name = row["proposed name"]
|
||||
|
||||
if "rule-category" in rule.meta:
|
||||
logger.info("deleting rule category '%s'", rule.meta["rule-category"])
|
||||
del rule.meta["rule-category"]
|
||||
|
||||
rule.meta["namespace"] = row["proposed namespace"]
|
||||
|
||||
if row["ATT&CK"] != "n/a" and row["ATT&CK"] != "":
|
||||
tag = row["ATT&CK"]
|
||||
name, _, id = tag.rpartition(" ")
|
||||
tag = "%s [%s]" % (name, id)
|
||||
rule.meta["att&ck"] = [tag]
|
||||
|
||||
if row["MBC"] != "n/a" and row["MBC"] != "":
|
||||
tag = row["MBC"]
|
||||
rule.meta["mbc"] = [tag]
|
||||
|
||||
for rule in rules.values():
|
||||
filename = rule.name
|
||||
filename = filename.lower()
|
||||
filename = filename.replace(" ", "-")
|
||||
filename = filename.replace("(", "")
|
||||
filename = filename.replace(")", "")
|
||||
filename = filename.replace("+", "")
|
||||
filename = filename.replace("/", "")
|
||||
filename = filename + ".yml"
|
||||
|
||||
try:
|
||||
if rule.meta.get("capa/nursery"):
|
||||
directory = os.path.join(args.destination, "nursery")
|
||||
elif rule.meta.get("lib"):
|
||||
directory = os.path.join(args.destination, "lib")
|
||||
else:
|
||||
directory = os.path.join(args.destination, rule.meta.get("namespace"))
|
||||
os.makedirs(directory)
|
||||
except OSError:
|
||||
pass
|
||||
else:
|
||||
logger.info("created namespace: %s", directory)
|
||||
|
||||
path = os.path.join(directory, filename)
|
||||
logger.info("writing rule %s", path)
|
||||
|
||||
doc = rule.to_yaml().decode("utf-8")
|
||||
for (needle, replacement) in match_translations:
|
||||
doc = doc.replace(needle, replacement)
|
||||
|
||||
with open(path, "wb") as f:
|
||||
f.write(doc.encode("utf-8"))
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -59,11 +59,10 @@ import colorama
|
||||
import capa.main
|
||||
import capa.rules
|
||||
import capa.engine
|
||||
import capa.render
|
||||
import capa.features
|
||||
import capa.render.utils as rutils
|
||||
import capa.features.freeze
|
||||
import capa.features.extractors.viv
|
||||
import capa.render.result_document
|
||||
from capa.helpers import get_file_taste
|
||||
|
||||
logger = logging.getLogger("capa.show-capabilities-by-function")
|
||||
@@ -111,143 +110,89 @@ def main(argv=None):
|
||||
if argv is None:
|
||||
argv = sys.argv[1:]
|
||||
|
||||
formats = [
|
||||
("auto", "(default) detect file type automatically"),
|
||||
("pe", "Windows PE file"),
|
||||
("sc32", "32-bit shellcode"),
|
||||
("sc64", "64-bit shellcode"),
|
||||
("freeze", "features previously frozen by capa"),
|
||||
]
|
||||
format_help = ", ".join(["%s: %s" % (f[0], f[1]) for f in formats])
|
||||
parser = argparse.ArgumentParser(description="detect capabilities in programs.")
|
||||
capa.main.install_common_args(parser, wanted={"format", "backend", "sample", "signatures", "rules", "tag"})
|
||||
args = parser.parse_args(args=argv)
|
||||
capa.main.handle_common_args(args)
|
||||
|
||||
parser = argparse.ArgumentParser(description="detect capabilities in programs.")
|
||||
parser.add_argument("sample", type=str, help="Path to sample to analyze")
|
||||
parser.add_argument(
|
||||
"-r",
|
||||
"--rules",
|
||||
type=str,
|
||||
default="(embedded rules)",
|
||||
help="Path to rule file or directory, use embedded rules by default",
|
||||
)
|
||||
parser.add_argument("-t", "--tag", type=str, help="Filter on rule meta field values")
|
||||
parser.add_argument("-d", "--debug", action="store_true", help="Enable debugging output on STDERR")
|
||||
parser.add_argument("-q", "--quiet", action="store_true", help="Disable all output but errors")
|
||||
parser.add_argument(
|
||||
"-f",
|
||||
"--format",
|
||||
choices=[f[0] for f in formats],
|
||||
default="auto",
|
||||
help="Select sample format, %s" % format_help,
|
||||
)
|
||||
args = parser.parse_args(args=argv)
|
||||
try:
|
||||
taste = get_file_taste(args.sample)
|
||||
except IOError as e:
|
||||
logger.error("%s", str(e))
|
||||
return -1
|
||||
|
||||
if args.quiet:
|
||||
logging.basicConfig(level=logging.ERROR)
|
||||
logging.getLogger().setLevel(logging.ERROR)
|
||||
elif args.debug:
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
else:
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logging.getLogger().setLevel(logging.INFO)
|
||||
try:
|
||||
rules = capa.main.get_rules(args.rules)
|
||||
rules = capa.rules.RuleSet(rules)
|
||||
logger.info("successfully loaded %s rules", len(rules))
|
||||
if args.tag:
|
||||
rules = rules.filter_rules_by_meta(args.tag)
|
||||
logger.info("selected %s rules", len(rules))
|
||||
except (IOError, capa.rules.InvalidRule, capa.rules.InvalidRuleSet) as e:
|
||||
logger.error("%s", str(e))
|
||||
return -1
|
||||
|
||||
# disable vivisect-related logging, it's verbose and not relevant for capa users
|
||||
capa.main.set_vivisect_log_level(logging.CRITICAL)
|
||||
try:
|
||||
sig_paths = capa.main.get_signatures(args.signatures)
|
||||
except (IOError) as e:
|
||||
logger.error("%s", str(e))
|
||||
return -1
|
||||
|
||||
if (args.format == "freeze") or (args.format == "auto" and capa.features.freeze.is_freeze(taste)):
|
||||
format = "freeze"
|
||||
with open(args.sample, "rb") as f:
|
||||
extractor = capa.features.freeze.load(f.read())
|
||||
else:
|
||||
format = args.format
|
||||
should_save_workspace = os.environ.get("CAPA_SAVE_WORKSPACE") not in ("0", "no", "NO", "n", None)
|
||||
|
||||
try:
|
||||
taste = get_file_taste(args.sample)
|
||||
except IOError as e:
|
||||
logger.error("%s", str(e))
|
||||
extractor = capa.main.get_extractor(
|
||||
args.sample, args.format, args.backend, sig_paths, should_save_workspace
|
||||
)
|
||||
except capa.main.UnsupportedFormatError:
|
||||
logger.error("-" * 80)
|
||||
logger.error(" Input file does not appear to be a PE file.")
|
||||
logger.error(" ")
|
||||
logger.error(
|
||||
" capa currently only supports analyzing PE files (or shellcode, when using --format sc32|sc64)."
|
||||
)
|
||||
logger.error(" If you don't know the input file type, you can try using the `file` utility to guess it.")
|
||||
logger.error("-" * 80)
|
||||
return -1
|
||||
except capa.main.UnsupportedRuntimeError:
|
||||
logger.error("-" * 80)
|
||||
logger.error(" Unsupported runtime or Python interpreter.")
|
||||
logger.error(" ")
|
||||
logger.error(" capa supports running under Python 2.7 using Vivisect for binary analysis.")
|
||||
logger.error(" It can also run within IDA Pro, using either Python 2.7 or 3.5+.")
|
||||
logger.error(" ")
|
||||
logger.error(" If you're seeing this message on the command line, please ensure you're running Python 2.7.")
|
||||
logger.error("-" * 80)
|
||||
return -1
|
||||
|
||||
# py2 doesn't know about cp65001, which is a variant of utf-8 on windows
|
||||
# tqdm bails when trying to render the progress bar in this setup.
|
||||
# because cp65001 is utf-8, we just map that codepage to the utf-8 codec.
|
||||
# see #380 and: https://stackoverflow.com/a/3259271/87207
|
||||
import codecs
|
||||
meta = capa.main.collect_metadata(argv, args.sample, args.rules, format, extractor)
|
||||
capabilities, counts = capa.main.find_capabilities(rules, extractor)
|
||||
meta["analysis"].update(counts)
|
||||
|
||||
codecs.register(lambda name: codecs.lookup("utf-8") if name == "cp65001" else None)
|
||||
|
||||
if args.rules == "(embedded rules)":
|
||||
logger.info("-" * 80)
|
||||
logger.info(" Using default embedded rules.")
|
||||
logger.info(" To provide your own rules, use the form `capa.exe -r ./path/to/rules/ /path/to/mal.exe`.")
|
||||
logger.info(" You can see the current default rule set here:")
|
||||
logger.info(" https://github.com/fireeye/capa-rules")
|
||||
logger.info("-" * 80)
|
||||
|
||||
logger.debug("detected running from source")
|
||||
args.rules = os.path.join(os.path.dirname(__file__), "..", "rules")
|
||||
logger.debug("default rule path (source method): %s", args.rules)
|
||||
else:
|
||||
logger.info("using rules path: %s", args.rules)
|
||||
|
||||
try:
|
||||
rules = capa.main.get_rules(args.rules)
|
||||
rules = capa.rules.RuleSet(rules)
|
||||
logger.info("successfully loaded %s rules", len(rules))
|
||||
if args.tag:
|
||||
rules = rules.filter_rules_by_meta(args.tag)
|
||||
logger.info("selected %s rules", len(rules))
|
||||
except (IOError, capa.rules.InvalidRule, capa.rules.InvalidRuleSet) as e:
|
||||
logger.error("%s", str(e))
|
||||
if capa.main.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 -1
|
||||
|
||||
if (args.format == "freeze") or (args.format == "auto" and capa.features.freeze.is_freeze(taste)):
|
||||
format = "freeze"
|
||||
with open(args.sample, "rb") as f:
|
||||
extractor = capa.features.freeze.load(f.read())
|
||||
else:
|
||||
format = args.format
|
||||
try:
|
||||
extractor = capa.main.get_extractor(args.sample, args.format)
|
||||
except capa.main.UnsupportedFormatError:
|
||||
logger.error("-" * 80)
|
||||
logger.error(" Input file does not appear to be a PE file.")
|
||||
logger.error(" ")
|
||||
logger.error(
|
||||
" capa currently only supports analyzing PE files (or shellcode, when using --format sc32|sc64)."
|
||||
)
|
||||
logger.error(
|
||||
" If you don't know the input file type, you can try using the `file` utility to guess it."
|
||||
)
|
||||
logger.error("-" * 80)
|
||||
return -1
|
||||
except capa.main.UnsupportedRuntimeError:
|
||||
logger.error("-" * 80)
|
||||
logger.error(" Unsupported runtime or Python interpreter.")
|
||||
logger.error(" ")
|
||||
logger.error(" capa supports running under Python 2.7 using Vivisect for binary analysis.")
|
||||
logger.error(" It can also run within IDA Pro, using either Python 2.7 or 3.5+.")
|
||||
logger.error(" ")
|
||||
logger.error(
|
||||
" If you're seeing this message on the command line, please ensure you're running Python 2.7."
|
||||
)
|
||||
logger.error("-" * 80)
|
||||
return -1
|
||||
# colorama will detect:
|
||||
# - when on Windows console, and fixup coloring, and
|
||||
# - when not an interactive session, and disable coloring
|
||||
# renderers should use coloring and assume it will be stripped out if necessary.
|
||||
colorama.init()
|
||||
doc = capa.render.result_document.convert_capabilities_to_result_document(meta, rules, capabilities)
|
||||
print(render_matches_by_function(doc))
|
||||
colorama.deinit()
|
||||
|
||||
meta = capa.main.collect_metadata(argv, args.sample, args.rules, format, extractor)
|
||||
capabilities, counts = capa.main.find_capabilities(rules, extractor)
|
||||
meta["analysis"].update(counts)
|
||||
logger.info("done.")
|
||||
|
||||
if capa.main.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 -1
|
||||
|
||||
# colorama will detect:
|
||||
# - when on Windows console, and fixup coloring, and
|
||||
# - when not an interactive session, and disable coloring
|
||||
# renderers should use coloring and assume it will be stripped out if necessary.
|
||||
colorama.init()
|
||||
doc = capa.render.convert_capabilities_to_result_document(meta, rules, capabilities)
|
||||
print(render_matches_by_function(doc))
|
||||
colorama.deinit()
|
||||
|
||||
logger.info("done.")
|
||||
|
||||
return 0
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
#!/usr/bin/env python2
|
||||
"""
|
||||
Copyright (C) 2020 FireEye, 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.
|
||||
|
||||
show-features
|
||||
|
||||
Show the features that capa extracts from the given sample,
|
||||
@@ -55,57 +63,75 @@ Example::
|
||||
insn: 0x10001027: number(0x1)
|
||||
insn: 0x10001027: mnemonic(shl)
|
||||
...
|
||||
|
||||
Copyright (C) 2020 FireEye, 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 logging
|
||||
import os.path
|
||||
import argparse
|
||||
|
||||
import capa.main
|
||||
import capa.rules
|
||||
import capa.engine
|
||||
import capa.helpers
|
||||
import capa.features
|
||||
import capa.features.freeze
|
||||
import capa.features.extractors.viv
|
||||
|
||||
logger = logging.getLogger("capa.show-features")
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
if argv is None:
|
||||
argv = sys.argv[1:]
|
||||
|
||||
formats = [
|
||||
("auto", "(default) detect file type automatically"),
|
||||
("pe", "Windows PE file"),
|
||||
("sc32", "32-bit shellcode"),
|
||||
("sc64", "64-bit shellcode"),
|
||||
("freeze", "features previously frozen by capa"),
|
||||
]
|
||||
format_help = ", ".join(["%s: %s" % (f[0], f[1]) for f in formats])
|
||||
parser = argparse.ArgumentParser(description="Show the features that capa extracts from the given sample")
|
||||
capa.main.install_common_args(parser, wanted={"format", "sample", "signatures"})
|
||||
|
||||
parser = argparse.ArgumentParser(description="detect capabilities in programs.")
|
||||
parser.add_argument("sample", type=str, help="Path to sample to analyze")
|
||||
parser.add_argument(
|
||||
"-f", "--format", choices=[f[0] for f in formats], default="auto", help="Select sample format, %s" % format_help
|
||||
)
|
||||
parser.add_argument("-F", "--function", type=lambda x: int(x, 0), help="Show features for specific function")
|
||||
parser.add_argument("-F", "--function", type=lambda x: int(x, 0x10), help="Show features for specific function")
|
||||
args = parser.parse_args(args=argv)
|
||||
capa.main.handle_common_args(args)
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logging.getLogger().setLevel(logging.INFO)
|
||||
try:
|
||||
taste = capa.helpers.get_file_taste(args.sample)
|
||||
except IOError as e:
|
||||
logger.error("%s", str(e))
|
||||
return -1
|
||||
|
||||
if args.format == "freeze":
|
||||
try:
|
||||
sig_paths = capa.main.get_signatures(args.signatures)
|
||||
except (IOError) as e:
|
||||
logger.error("%s", str(e))
|
||||
return -1
|
||||
|
||||
if (args.format == "freeze") or (args.format == "auto" and capa.features.freeze.is_freeze(taste)):
|
||||
with open(args.sample, "rb") as f:
|
||||
extractor = capa.features.freeze.load(f.read())
|
||||
else:
|
||||
vw = capa.main.get_workspace(args.sample, args.format)
|
||||
extractor = capa.features.extractors.viv.VivisectFeatureExtractor(vw, args.sample)
|
||||
should_save_workspace = os.environ.get("CAPA_SAVE_WORKSPACE") not in ("0", "no", "NO", "n", None)
|
||||
try:
|
||||
extractor = capa.main.get_extractor(
|
||||
args.sample, args.format, capa.main.BACKEND_VIV, sig_paths, should_save_workspace
|
||||
)
|
||||
except capa.main.UnsupportedFormatError:
|
||||
logger.error("-" * 80)
|
||||
logger.error(" Input file does not appear to be a PE file.")
|
||||
logger.error(" ")
|
||||
logger.error(
|
||||
" capa currently only supports analyzing PE files (or shellcode, when using --format sc32|sc64)."
|
||||
)
|
||||
logger.error(" If you don't know the input file type, you can try using the `file` utility to guess it.")
|
||||
logger.error("-" * 80)
|
||||
return -1
|
||||
except capa.main.UnsupportedRuntimeError:
|
||||
logger.error("-" * 80)
|
||||
logger.error(" Unsupported runtime or Python interpreter.")
|
||||
logger.error(" ")
|
||||
logger.error(" capa supports running under Python 2.7 using Vivisect for binary analysis.")
|
||||
logger.error(" It can also run within IDA Pro, using either Python 2.7 or 3.5+.")
|
||||
logger.error(" ")
|
||||
logger.error(" If you're seeing this message on the command line, please ensure you're running Python 2.7.")
|
||||
logger.error("-" * 80)
|
||||
return -1
|
||||
|
||||
if not args.function:
|
||||
for feature, va in extractor.extract_file_features():
|
||||
@@ -118,11 +144,64 @@ def main(argv=None):
|
||||
|
||||
if args.function:
|
||||
if args.format == "freeze":
|
||||
functions = filter(lambda f: f == args.function, functions)
|
||||
functions = tuple(filter(lambda f: f == args.function, functions))
|
||||
else:
|
||||
functions = filter(lambda f: f.va == args.function, functions)
|
||||
functions = tuple(filter(lambda f: int(f) == args.function, functions))
|
||||
|
||||
if args.function not in [int(f) for f in functions]:
|
||||
print("0x%X not a function" % args.function)
|
||||
return -1
|
||||
|
||||
if len(functions) == 0:
|
||||
print("0x%X not a function")
|
||||
return -1
|
||||
|
||||
print_features(functions, extractor)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def ida_main():
|
||||
import idc
|
||||
|
||||
import capa.features.extractors.ida.extractor
|
||||
|
||||
function = idc.get_func_attr(idc.here(), idc.FUNCATTR_START)
|
||||
print("getting features for current function 0x%X" % function)
|
||||
|
||||
extractor = capa.features.extractors.ida.extractor.IdaFeatureExtractor()
|
||||
|
||||
if not function:
|
||||
for feature, va in extractor.extract_file_features():
|
||||
if va:
|
||||
print("file: 0x%08x: %s" % (va, feature))
|
||||
else:
|
||||
print("file: 0x00000000: %s" % (feature))
|
||||
return
|
||||
|
||||
functions = extractor.get_functions()
|
||||
|
||||
if function:
|
||||
functions = tuple(filter(lambda f: f.start_ea == function, functions))
|
||||
|
||||
if len(functions) == 0:
|
||||
print("0x%X not a function" % function)
|
||||
return -1
|
||||
|
||||
print_features(functions, extractor)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def print_features(functions, extractor):
|
||||
for f in functions:
|
||||
function_address = int(f)
|
||||
|
||||
if extractor.is_library_function(function_address):
|
||||
function_name = extractor.get_function_name(function_address)
|
||||
logger.debug("skipping library function 0x%x (%s)", function_address, function_name)
|
||||
continue
|
||||
|
||||
for feature, va in extractor.extract_function_features(f):
|
||||
print("func: 0x%08x: %s" % (va, feature))
|
||||
|
||||
@@ -138,8 +217,9 @@ def main(argv=None):
|
||||
# may be an issue while piping to less and encountering non-ascii characters
|
||||
continue
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
if capa.main.is_runtime_ida():
|
||||
ida_main()
|
||||
else:
|
||||
sys.exit(main())
|
||||
|
||||