mirror of
https://github.com/mandiant/capa.git
synced 2025-12-06 12:51:03 -08:00
Compare commits
4059 Commits
v1.2.0
...
dex-suppor
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec1ddb506c | ||
|
|
f229c8ecb8 | ||
|
|
e3da2d88d0 | ||
|
|
e4eb4340b1 | ||
|
|
a8e7611252 | ||
|
|
8531acd7c5 | ||
|
|
e2f655428e | ||
|
|
b5a4d766d9 | ||
|
|
b77103a646 | ||
|
|
036f147df8 | ||
|
|
52d20d2f46 | ||
|
|
e90be5a9bb | ||
|
|
d6f7d2180f | ||
|
|
d1b213aaac | ||
|
|
51ddadbc87 | ||
|
|
cd52b1937b | ||
|
|
ca14dab804 | ||
|
|
fbe0440361 | ||
|
|
4c3586b5e9 | ||
|
|
47019e4d7c | ||
|
|
a236a952bc | ||
|
|
73ea822123 | ||
|
|
3c159a1f52 | ||
|
|
7db40c3af8 | ||
|
|
9a996d07c7 | ||
|
|
93cfb6ef8c | ||
|
|
a29c320f95 | ||
|
|
277d7e0687 | ||
|
|
e66c2efcf5 | ||
|
|
583f8b5688 | ||
|
|
b4c6bf859e | ||
|
|
ba9da0dd82 | ||
|
|
92770dd5c7 | ||
|
|
8946cb633e | ||
|
|
8f0eb5676e | ||
|
|
cb1a037502 | ||
|
|
c8d0071443 | ||
|
|
e6b8a3e505 | ||
|
|
f328df1bc4 | ||
|
|
d1aa1557b2 | ||
|
|
a0929124ec | ||
|
|
84ed6c8d24 | ||
|
|
61c8e30f65 | ||
|
|
6a4994f1ef | ||
|
|
fce105060d | ||
|
|
d84457eac7 | ||
|
|
890c879e7c | ||
|
|
f201ef1d22 | ||
|
|
f763d14266 | ||
|
|
6f0be06f86 | ||
|
|
347687579c | ||
|
|
d61d1dc591 | ||
|
|
235a3bede0 | ||
|
|
cf35d2c497 | ||
|
|
f6048b9e99 | ||
|
|
9d1e60d4a2 | ||
|
|
fb1235d26f | ||
|
|
3fe2328bd2 | ||
|
|
647abb669f | ||
|
|
a5e1eca8cc | ||
|
|
fdb96709ae | ||
|
|
490271e50b | ||
|
|
a870c92a2f | ||
|
|
de5f08871e | ||
|
|
2f60ec03af | ||
|
|
987eb2d358 | ||
|
|
6e3fff4bae | ||
|
|
a705bf9eab | ||
|
|
c68c68d5cb | ||
|
|
82013f0e24 | ||
|
|
210a13d94e | ||
|
|
0d5ff45c76 | ||
|
|
11b98cb0b1 | ||
|
|
3c9ab63521 | ||
|
|
a2fde921aa | ||
|
|
d4f7c77be8 | ||
|
|
f0f95824ac | ||
|
|
0ba5c23847 | ||
|
|
dee0aa73eb | ||
|
|
41a397661f | ||
|
|
52997e70a0 | ||
|
|
1acc2d1959 | ||
|
|
74f70856a6 | ||
|
|
e5b7ee96fc | ||
|
|
92d43f5327 | ||
|
|
48abd297a8 | ||
|
|
d64a10a287 | ||
|
|
abf83fe8cf | ||
|
|
6380d936ae | ||
|
|
18ab8d28d9 | ||
|
|
a52af3895a | ||
|
|
5d31bc462b | ||
|
|
7678897334 | ||
|
|
75ff58edaa | ||
|
|
eb12ec43f0 | ||
|
|
f7c72cd1c3 | ||
|
|
0da614aa4f | ||
|
|
9c81ccf88a | ||
|
|
c141f7ec6e | ||
|
|
274a710bb1 | ||
|
|
4a7e488e4c | ||
|
|
348120dea9 | ||
|
|
435eea1b80 | ||
|
|
621d42a093 | ||
|
|
15701c6d12 | ||
|
|
ec7fc86dc5 | ||
|
|
8d55c2f249 | ||
|
|
66607f1412 | ||
|
|
0097822e51 | ||
|
|
e559cc27d5 | ||
|
|
a0cec3f07d | ||
|
|
874faf0901 | ||
|
|
4750913fad | ||
|
|
e7198b2aaf | ||
|
|
426931c392 | ||
|
|
fec1e6a947 | ||
|
|
db53424548 | ||
|
|
8029fed31c | ||
|
|
3572b512d9 | ||
|
|
ab06c94d80 | ||
|
|
9e6919f33c | ||
|
|
99042f232d | ||
|
|
393b0e63f0 | ||
|
|
ee4f02908c | ||
|
|
c9df78252a | ||
|
|
788251ba2b | ||
|
|
62d4b008c5 | ||
|
|
be6f87318e | ||
|
|
aae72667a3 | ||
|
|
d6c5d98b0d | ||
|
|
d5ae2ffd91 | ||
|
|
96fb204d9d | ||
|
|
20604c4b41 | ||
|
|
423d942bd0 | ||
|
|
f9b87417e6 | ||
|
|
fc4618e234 | ||
|
|
1143f2ba56 | ||
|
|
10dc4b92b1 | ||
|
|
bfecf414fb | ||
|
|
0231ceef87 | ||
|
|
0ae8f34aff | ||
|
|
b8b55f4e19 | ||
|
|
d42829d7e7 | ||
|
|
c724a4b311 | ||
|
|
84e22b187d | ||
|
|
b6a0d6e1f3 | ||
|
|
1cb3ca61cd | ||
|
|
288313a300 | ||
|
|
2cc6a37713 | ||
|
|
fbeb33a91f | ||
|
|
3519125e03 | ||
|
|
98360328f9 | ||
|
|
3d4facd9a3 | ||
|
|
8b0ba1e656 | ||
|
|
7bc3fba7b0 | ||
|
|
d5e187bc70 | ||
|
|
85610a82c5 | ||
|
|
f2011c162c | ||
|
|
37caeb2736 | ||
|
|
5c48f38208 | ||
|
|
8687c740d5 | ||
|
|
9609d63f8a | ||
|
|
772f806eb6 | ||
|
|
5eaba611d1 | ||
|
|
b6f13f3489 | ||
|
|
178cfce456 | ||
|
|
94cf53a1e3 | ||
|
|
2cfd45022a | ||
|
|
26a2d1b4d1 | ||
|
|
6dbd3768ce | ||
|
|
21f9e0736d | ||
|
|
7cd5aa1c40 | ||
|
|
55e4fddc51 | ||
|
|
1aac4a1a69 | ||
|
|
92daf3a530 | ||
|
|
547502051f | ||
|
|
884b714be2 | ||
|
|
7205bc26ef | ||
|
|
e1b3a3f6b4 | ||
|
|
cb5fa36fc8 | ||
|
|
8ee97acf2a | ||
|
|
44d05f9498 | ||
|
|
bf233c1c7a | ||
|
|
182a9868ca | ||
|
|
40d9587fa4 | ||
|
|
430fdb074b | ||
|
|
0324d24490 | ||
|
|
41c286d1a3 | ||
|
|
187cf40d6f | ||
|
|
c37a0e525c | ||
|
|
de0c35b6ad | ||
|
|
d99b454c0e | ||
|
|
44f156925a | ||
|
|
599c115767 | ||
|
|
6ecc9b77b9 | ||
|
|
412d296d6b | ||
|
|
db32d90480 | ||
|
|
9a66c265db | ||
|
|
a1aca3aeb3 | ||
|
|
ffe6ab6842 | ||
|
|
d1b7afbe13 | ||
|
|
77de088ac9 | ||
|
|
40ba6679f0 | ||
|
|
8b6fa35e9f | ||
|
|
f85ea915bf | ||
|
|
312ad48041 | ||
|
|
65b80d4d13 | ||
|
|
fb098fde5f | ||
|
|
eedec933c2 | ||
|
|
559f2fd162 | ||
|
|
953b2e82d2 | ||
|
|
cd268d6327 | ||
|
|
23ecb248a5 | ||
|
|
bc165331db | ||
|
|
5d66a389d3 | ||
|
|
248a51c15f | ||
|
|
8a0628f357 | ||
|
|
2ec87f717a | ||
|
|
4430fce314 | ||
|
|
174c8121ca | ||
|
|
fa1371cfa8 | ||
|
|
a0a2b07b85 | ||
|
|
a9daa92c9a | ||
|
|
b315aacd73 | ||
|
|
3dd051582a | ||
|
|
5f7b4fbf74 | ||
|
|
8b287c1704 | ||
|
|
28a722d4c3 | ||
|
|
35f64f37bb | ||
|
|
7d9ae57692 | ||
|
|
b1175ab16a | ||
|
|
838205b375 | ||
|
|
0fbec49708 | ||
|
|
0bdc727dce | ||
|
|
8ea7708a38 | ||
|
|
9b5c906c2a | ||
|
|
240376153a | ||
|
|
321ef100c5 | ||
|
|
d8eebf524e | ||
|
|
c6c54c316f | ||
|
|
b1e00150f4 | ||
|
|
83a7ce0b82 | ||
|
|
303170f45d | ||
|
|
8a019aa360 | ||
|
|
3dffa8145f | ||
|
|
782a5b3aa7 | ||
|
|
b0af78569c | ||
|
|
79cef0e783 | ||
|
|
09b54a86f0 | ||
|
|
57106701c4 | ||
|
|
55af6f052f | ||
|
|
d2d32f88ef | ||
|
|
7abcf3de9a | ||
|
|
b3dccb3841 | ||
|
|
bc71c94171 | ||
|
|
59d03b3ba3 | ||
|
|
3a5c8ec3b8 | ||
|
|
fd3678904a | ||
|
|
d04ae5294e | ||
|
|
6bae9d757d | ||
|
|
b9c05cf44a | ||
|
|
dc32289aab | ||
|
|
3c1a8f4461 | ||
|
|
8331ed6ea0 | ||
|
|
b0d55143a4 | ||
|
|
e006702245 | ||
|
|
72e836166f | ||
|
|
d64ab41dfd | ||
|
|
5b4c167489 | ||
|
|
2a757b0cbb | ||
|
|
69836a0f13 | ||
|
|
866c7c5ce4 | ||
|
|
3725618d50 | ||
|
|
766b05e5c3 | ||
|
|
1224b7e514 | ||
|
|
46e3ed1100 | ||
|
|
dd0eadb438 | ||
|
|
f905ed611b | ||
|
|
cfa703eaae | ||
|
|
9ec1bf3e42 | ||
|
|
d83c0e70de | ||
|
|
1d8e650d7b | ||
|
|
99caa87a3d | ||
|
|
7b08f2d55a | ||
|
|
d17db614b9 | ||
|
|
6317153ef0 | ||
|
|
24dad6bcc4 | ||
|
|
73c158ad68 | ||
|
|
47330e69d4 | ||
|
|
0987673bf3 | ||
|
|
2c75f786c3 | ||
|
|
09afcfbac1 | ||
|
|
ab3747e448 | ||
|
|
72ed4d1165 | ||
|
|
0ec682a464 | ||
|
|
37917b6181 | ||
|
|
a6e61ed6f1 | ||
|
|
1fddf800c6 | ||
|
|
0ffd631606 | ||
|
|
7cc10401d5 | ||
|
|
3929164fc2 | ||
|
|
f3a2a5958d | ||
|
|
6d3f649a0c | ||
|
|
e00608e298 | ||
|
|
995014afc2 | ||
|
|
a522ae20f1 | ||
|
|
203fc36865 | ||
|
|
7bd2467074 | ||
|
|
f339bbf68c | ||
|
|
8ed4062cf1 | ||
|
|
807792f879 | ||
|
|
9dc457e61e | ||
|
|
9eb88e6ca7 | ||
|
|
214a355b9c | ||
|
|
9cea7346b2 | ||
|
|
4d538b939e | ||
|
|
8c9e676868 | ||
|
|
b0133f0aa1 | ||
|
|
49adecb25c | ||
|
|
e9a9b3a6b6 | ||
|
|
d7c9ae26bc | ||
|
|
fddec33d04 | ||
|
|
65179805a7 | ||
|
|
d5daa79547 | ||
|
|
90df85b332 | ||
|
|
88ee6e661e | ||
|
|
08c9bbcc91 | ||
|
|
f96b9e6a6e | ||
|
|
9bbd3184b0 | ||
|
|
e4c1361d42 | ||
|
|
17e4765728 | ||
|
|
7e258a91ec | ||
|
|
b88853f327 | ||
|
|
a60401fc7e | ||
|
|
a734358377 | ||
|
|
ebcbad3ae3 | ||
|
|
8ff74d4a04 | ||
|
|
bd0d8eb403 | ||
|
|
9b79aa1983 | ||
|
|
172968c77e | ||
|
|
f1a7049ab5 | ||
|
|
155a2904fb | ||
|
|
4c2e8fd718 | ||
|
|
95e279a03b | ||
|
|
f2909c82f3 | ||
|
|
164b08276c | ||
|
|
b930523d44 | ||
|
|
9d21addc6b | ||
|
|
9accb60eff | ||
|
|
61202913a6 | ||
|
|
2b59fef1b2 | ||
|
|
ddff8634de | ||
|
|
1905f1bfbd | ||
|
|
f34b0355e7 | ||
|
|
7a70bc9b2a | ||
|
|
3ee56e3bee | ||
|
|
49bf2eb6d4 | ||
|
|
707dee4c3f | ||
|
|
0ded827290 | ||
|
|
f74107d960 | ||
|
|
448b122ef0 | ||
|
|
bd2f7bc1f4 | ||
|
|
acd3a30d27 | ||
|
|
b636f23e3c | ||
|
|
70eae1a6f0 | ||
|
|
3574bd49bd | ||
|
|
46217a3acb | ||
|
|
9eb1255b29 | ||
|
|
d66f834e54 | ||
|
|
7c101f01e5 | ||
|
|
42689ef1da | ||
|
|
70d36ab640 | ||
|
|
19b8000c00 | ||
|
|
06f48063d0 | ||
|
|
5ba7325646 | ||
|
|
86effec1a2 | ||
|
|
cdb469eca0 | ||
|
|
39c8fd8286 | ||
|
|
5730e5515f | ||
|
|
901ba551bc | ||
|
|
77b3fadf79 | ||
|
|
44fc3357d1 | ||
|
|
25414044ef | ||
|
|
d1068991e3 | ||
|
|
4ab240e990 | ||
|
|
9489927bed | ||
|
|
c160f45849 | ||
|
|
5b585c0e39 | ||
|
|
c6ee919619 | ||
|
|
675ad364ac | ||
|
|
21cefa0932 | ||
|
|
934d0f969b | ||
|
|
b7b79b565b | ||
|
|
979aab3098 | ||
|
|
89c8c6d212 | ||
|
|
e5af7165ea | ||
|
|
ee936f9257 | ||
|
|
058c1fefd2 | ||
|
|
8ed00a2847 | ||
|
|
6482848fa4 | ||
|
|
7c2a736c4b | ||
|
|
918ec22667 | ||
|
|
1027da9be0 | ||
|
|
5787e41dd2 | ||
|
|
0265657937 | ||
|
|
73477b6495 | ||
|
|
521bd25d31 | ||
|
|
e7c0bea6e5 | ||
|
|
a8bd5b1119 | ||
|
|
9144d12e51 | ||
|
|
d741544514 | ||
|
|
5e31f0df23 | ||
|
|
18dff9d664 | ||
|
|
350094759a | ||
|
|
b10275e851 | ||
|
|
05cf7201ad | ||
|
|
8cd5e03e87 | ||
|
|
120917e0b5 | ||
|
|
a2a2949675 | ||
|
|
b3cf1129e3 | ||
|
|
264958ebfe | ||
|
|
3614ce1409 | ||
|
|
c80542ded3 | ||
|
|
3350a936b7 | ||
|
|
724db83920 | ||
|
|
8788a40d12 | ||
|
|
6f7bf96776 | ||
|
|
e943a71dff | ||
|
|
4be1c89c5b | ||
|
|
2eda053c79 | ||
|
|
26539e68d9 | ||
|
|
046427cf55 | ||
|
|
25aabcd7e4 | ||
|
|
d8bea816dd | ||
|
|
bb2b1824a9 | ||
|
|
7e78133925 | ||
|
|
59a129d6d6 | ||
|
|
db40d9bc7a | ||
|
|
d71ecc7a79 | ||
|
|
a5a1a0bfee | ||
|
|
827b4b29b4 | ||
|
|
2a31b16567 | ||
|
|
8118a3f353 | ||
|
|
e6d64ef561 | ||
|
|
408c5076c6 | ||
|
|
c001c883f7 | ||
|
|
476c7ff749 | ||
|
|
4978aa74e7 | ||
|
|
4411911664 | ||
|
|
0e1ce21488 | ||
|
|
88aa17fa7b | ||
|
|
3169ee28e9 | ||
|
|
d648fdf6c0 | ||
|
|
3b9f5114ce | ||
|
|
623fc270c1 | ||
|
|
1199fb94d4 | ||
|
|
26fdbbd442 | ||
|
|
737fab7969 | ||
|
|
f6ee465a0a | ||
|
|
82f352f719 | ||
|
|
846bd62817 | ||
|
|
84cddc70fd | ||
|
|
2dc5295c0c | ||
|
|
8479bc2f1f | ||
|
|
7c1522d84d | ||
|
|
9afe19a096 | ||
|
|
bd5c65d22c | ||
|
|
e6cb3d3b3b | ||
|
|
18058beb0a | ||
|
|
8003547414 | ||
|
|
2a83f1fc23 | ||
|
|
751231b730 | ||
|
|
c6d400bcf3 | ||
|
|
fd1cd05b99 | ||
|
|
8202e9e921 | ||
|
|
3c069a6784 | ||
|
|
e100a63cc8 | ||
|
|
3057b5fb9d | ||
|
|
c91dc71e75 | ||
|
|
f48e4a8ad8 | ||
|
|
dafbefb325 | ||
|
|
6de23a9748 | ||
|
|
1cf33e4343 | ||
|
|
34db63171f | ||
|
|
ec93ca5b21 | ||
|
|
2de6dc7cb8 | ||
|
|
19495f69d7 | ||
|
|
c1fbb27d73 | ||
|
|
3cf748a135 | ||
|
|
85b58d041b | ||
|
|
ae9d773e04 | ||
|
|
582bb7c897 | ||
|
|
e5efc158b7 | ||
|
|
9f436763f7 | ||
|
|
a383022cff | ||
|
|
57486733e7 | ||
|
|
df9828dd7f | ||
|
|
d81f3a461e | ||
|
|
f1e737ac92 | ||
|
|
448aa9cd21 | ||
|
|
f2c0509f81 | ||
|
|
6287fbb958 | ||
|
|
c497ad8253 | ||
|
|
9c1aa2fc5d | ||
|
|
f5a254f21f | ||
|
|
fb3ae0267e | ||
|
|
5400576d4e | ||
|
|
dabd9d0810 | ||
|
|
2bd777dbe4 | ||
|
|
959c64b484 | ||
|
|
232c9ce35c | ||
|
|
b3a9763a32 | ||
|
|
0fdc1dd3f5 | ||
|
|
80e224ec7c | ||
|
|
75a4f309b4 | ||
|
|
358888178a | ||
|
|
57e393bf7a | ||
|
|
eb7aa63be6 | ||
|
|
298a07dc07 | ||
|
|
f50a5e8efc | ||
|
|
d06b33e7ea | ||
|
|
9660f1e5ab | ||
|
|
74d9b06835 | ||
|
|
681d4fb007 | ||
|
|
a185341a4d | ||
|
|
aacd9f51b3 | ||
|
|
95148d445a | ||
|
|
65ac422e36 | ||
|
|
5ffb6ca0cd | ||
|
|
85f151303a | ||
|
|
216cd01b3c | ||
|
|
23bd2e7cd4 | ||
|
|
5de055e2af | ||
|
|
dd870a5cbd | ||
|
|
a2254852b0 | ||
|
|
17aad56800 | ||
|
|
f461f65a86 | ||
|
|
2c8f99143a | ||
|
|
ee68031d19 | ||
|
|
8dc4adbb5e | ||
|
|
8b36cd1e35 | ||
|
|
851da25560 | ||
|
|
a4b00b9064 | ||
|
|
fd61456164 | ||
|
|
261baca683 | ||
|
|
c7dde262ed | ||
|
|
cd700a1782 | ||
|
|
60e94adeb1 | ||
|
|
eafed0f1d4 | ||
|
|
7c14c51012 | ||
|
|
2bed3468f6 | ||
|
|
4f9d24598f | ||
|
|
4277b4bef8 | ||
|
|
bab6c978fb | ||
|
|
3c3205adf1 | ||
|
|
4e1527df95 | ||
|
|
ca2760fb46 | ||
|
|
6647ecb6d4 | ||
|
|
13533074ea | ||
|
|
a538a7bbab | ||
|
|
b2789f0df6 | ||
|
|
ab5c8b1129 | ||
|
|
149983dced | ||
|
|
04fbcbbbd3 | ||
|
|
727ece499a | ||
|
|
62f50265bc | ||
|
|
95ffdf19ff | ||
|
|
d18224eac6 | ||
|
|
26935ee6e6 | ||
|
|
f8c499fb43 | ||
|
|
61924672e2 | ||
|
|
7fdd988e4f | ||
|
|
a85e0523f8 | ||
|
|
3bb5754b66 | ||
|
|
dd2eef52c3 | ||
|
|
da45fb4bea | ||
|
|
7ed517a8f3 | ||
|
|
f00e7426c5 | ||
|
|
3f29c61038 | ||
|
|
647ce67f7e | ||
|
|
224923b8bd | ||
|
|
8a08a93b1c | ||
|
|
ed98bb3a57 | ||
|
|
d12185d851 | ||
|
|
5f8280eb09 | ||
|
|
462024ad03 | ||
|
|
f0d09899a1 | ||
|
|
30abe40999 | ||
|
|
b8212b3da7 | ||
|
|
3d812edc4d | ||
|
|
2efb7f2975 | ||
|
|
44c5e96cf0 | ||
|
|
97c878db22 | ||
|
|
16e32f8441 | ||
|
|
d6aced5ec7 | ||
|
|
0e58ec5176 | ||
|
|
b843382065 | ||
|
|
dd53349aea | ||
|
|
d598faf145 | ||
|
|
c265b1ca96 | ||
|
|
b554eaf563 | ||
|
|
3d51b84bd1 | ||
|
|
684b2ded38 | ||
|
|
557e83b1dc | ||
|
|
8f826cb92d | ||
|
|
78a9909ec6 | ||
|
|
f4bdff0824 | ||
|
|
d8c28e80eb | ||
|
|
344b3e9931 | ||
|
|
c32ac19c0d | ||
|
|
d13114e907 | ||
|
|
90298fe2c8 | ||
|
|
3d1a1fb9fa | ||
|
|
830bad54bd | ||
|
|
c4ba5afe6b | ||
|
|
4ec39d49aa | ||
|
|
ab585ef951 | ||
|
|
674122999f | ||
|
|
8085caef35 | ||
|
|
3ab3c61d5e | ||
|
|
736b2cd689 | ||
|
|
bd8331678c | ||
|
|
6f3fb42385 | ||
|
|
da4e887aee | ||
|
|
b1e468dae4 | ||
|
|
6d1a885864 | ||
|
|
24b3abd706 | ||
|
|
806bc1853d | ||
|
|
6ee1dfd656 | ||
|
|
ab092cb536 | ||
|
|
b4cf50fb6e | ||
|
|
2b2b2b6545 | ||
|
|
fd7b926a33 | ||
|
|
482e0d386b | ||
|
|
d99b16ed5e | ||
|
|
0a4fe58ac6 | ||
|
|
8ac9caf45c | ||
|
|
1029b369f2 | ||
|
|
5ae588deaa | ||
|
|
a2f31ab8ae | ||
|
|
666c9c21a1 | ||
|
|
a675c4c7a1 | ||
|
|
16eab6b5e5 | ||
|
|
d520bfc753 | ||
|
|
301b10d261 | ||
|
|
e38e56ccf6 | ||
|
|
c0e126f812 | ||
|
|
7de223f116 | ||
|
|
4eabee7329 | ||
|
|
0719273cee | ||
|
|
de6bdf0621 | ||
|
|
c5d08ec0d1 | ||
|
|
1790dab1ab | ||
|
|
4e4b1235c3 | ||
|
|
e5d7903475 | ||
|
|
781c33d13c | ||
|
|
70a1e66020 | ||
|
|
91b65d1d7f | ||
|
|
a22dd65032 | ||
|
|
3899662cbd | ||
|
|
b73e1e3d7f | ||
|
|
25624a1b46 | ||
|
|
e3c8cb74df | ||
|
|
f99824d996 | ||
|
|
33cb81449c | ||
|
|
c49385e681 | ||
|
|
5277f3b640 | ||
|
|
dbfcbaa98e | ||
|
|
a2d70a12a9 | ||
|
|
be58f65ae5 | ||
|
|
15caa9ee6e | ||
|
|
0398baa752 | ||
|
|
b1214df621 | ||
|
|
c0ed955362 | ||
|
|
bc46bf3202 | ||
|
|
1c6434a380 | ||
|
|
fff1248ec4 | ||
|
|
14f0589194 | ||
|
|
d47703fada | ||
|
|
faf3ca53f7 | ||
|
|
18e0408577 | ||
|
|
972fbe7290 | ||
|
|
40793eeefb | ||
|
|
221a5a9f03 | ||
|
|
d1f5a6e76b | ||
|
|
d2567692a8 | ||
|
|
6fa7f24818 | ||
|
|
4af84e53d5 | ||
|
|
e3f60ea0fb | ||
|
|
68caece2fa | ||
|
|
94aaaa297d | ||
|
|
6ce897e39b | ||
|
|
7c67fae52a | ||
|
|
ebae5e5ca0 | ||
|
|
244d56e32a | ||
|
|
5f2b92de40 | ||
|
|
1065ff9779 | ||
|
|
5253ad7014 | ||
|
|
82223dcdc9 | ||
|
|
724f9e4b81 | ||
|
|
c4da4bcfe7 | ||
|
|
fd36946c4b | ||
|
|
8c9853ad12 | ||
|
|
562a61930d | ||
|
|
f9d210367e | ||
|
|
bb6557ea0a | ||
|
|
cb8133467b | ||
|
|
718813bc1c | ||
|
|
394c3807c1 | ||
|
|
74924990a2 | ||
|
|
330f2a6b9b | ||
|
|
6b81c77d22 | ||
|
|
9e9f120c80 | ||
|
|
546789fea6 | ||
|
|
76901ced19 | ||
|
|
c29d0a4f56 | ||
|
|
6b6d7eb494 | ||
|
|
21b2aac8b5 | ||
|
|
7898ac24d5 | ||
|
|
5a3775455b | ||
|
|
892cd48713 | ||
|
|
c062115366 | ||
|
|
ff7a006ba1 | ||
|
|
7665d56f93 | ||
|
|
280e253286 | ||
|
|
7edf126a63 | ||
|
|
ad6b475dfe | ||
|
|
f897f00227 | ||
|
|
ea3090a066 | ||
|
|
b9090b86ce | ||
|
|
5088f45b6a | ||
|
|
ea51801806 | ||
|
|
04db034895 | ||
|
|
b547987b33 | ||
|
|
0511ef7093 | ||
|
|
e9ccc5276a | ||
|
|
36a840cb2c | ||
|
|
797021874b | ||
|
|
2370c5b50d | ||
|
|
b285985a79 | ||
|
|
59bd930881 | ||
|
|
c86ab51210 | ||
|
|
e987fc2034 | ||
|
|
7550cc8466 | ||
|
|
acaf6c1272 | ||
|
|
a28000b41a | ||
|
|
560dc358fa | ||
|
|
a32f2cc0f8 | ||
|
|
eeb0f78564 | ||
|
|
ce15a2b01e | ||
|
|
97c2005661 | ||
|
|
9c878458b8 | ||
|
|
53d897da09 | ||
|
|
17030395c6 | ||
|
|
34d3d6c1f9 | ||
|
|
87a6459278 | ||
|
|
4e02e36d2c | ||
|
|
a35bf4c807 | ||
|
|
a106953fec | ||
|
|
65e8300145 | ||
|
|
7526ff876f | ||
|
|
78a6d9a511 | ||
|
|
e335c9f977 | ||
|
|
2343e73f41 | ||
|
|
aae2e51688 | ||
|
|
fe57016abd | ||
|
|
de8bba41dc | ||
|
|
90a2fd936c | ||
|
|
deb6114530 | ||
|
|
4ee38cbe29 | ||
|
|
12c9154f55 | ||
|
|
0e312d6dfe | ||
|
|
7e18eeddba | ||
|
|
0db7141e33 | ||
|
|
1ef0b16f11 | ||
|
|
37c1bf98eb | ||
|
|
85d4c00096 | ||
|
|
078978a5b5 | ||
|
|
841d393f8b | ||
|
|
740d1f6d4e | ||
|
|
b615c103ef | ||
|
|
f879f53a6b | ||
|
|
42baa10bcb | ||
|
|
d438b90879 | ||
|
|
6feb9f540f | ||
|
|
f86ecfe446 | ||
|
|
c1cd272865 | ||
|
|
fdb53d97ce | ||
|
|
db5e735928 | ||
|
|
785825d77e | ||
|
|
1baa7a5e4b | ||
|
|
ef39bc3c3a | ||
|
|
8e346cb411 | ||
|
|
d1a1c6875b | ||
|
|
b84af6a205 | ||
|
|
64a16314ab | ||
|
|
dccebaeff8 | ||
|
|
d2e5dea3e2 | ||
|
|
ec59886031 | ||
|
|
917dd8b0db | ||
|
|
160c662e7c | ||
|
|
63e273efd4 | ||
|
|
015056c54a | ||
|
|
babf99ea48 | ||
|
|
c8f5496008 | ||
|
|
9394194031 | ||
|
|
af256bc0e9 | ||
|
|
37e4b913b0 | ||
|
|
aa8055229d | ||
|
|
454b6d1aca | ||
|
|
722ee2f3d0 | ||
|
|
e5f5d542d0 | ||
|
|
1373fabf02 | ||
|
|
320539bd26 | ||
|
|
ac12d5a7e2 | ||
|
|
1ac64aca10 | ||
|
|
78054eea5a | ||
|
|
ff63b0ff1a | ||
|
|
e2e367f091 | ||
|
|
5aa1a1afc7 | ||
|
|
506d677684 | ||
|
|
f983307c97 | ||
|
|
a712bf3389 | ||
|
|
a2d6bd693b | ||
|
|
7f57fccefb | ||
|
|
72e123e319 | ||
|
|
d29e7140b6 | ||
|
|
dc1f2e728d | ||
|
|
1f8aa7cfe1 | ||
|
|
81b964386f | ||
|
|
cb289e3fc5 | ||
|
|
fb176196eb | ||
|
|
dd2bbc9a48 | ||
|
|
118b955e10 | ||
|
|
d89dd499b6 | ||
|
|
430f9da449 | ||
|
|
ae10a2ea34 | ||
|
|
4a49543d12 | ||
|
|
106b12e2a4 | ||
|
|
7fe738e28f | ||
|
|
54203f3be9 | ||
|
|
a949698b86 | ||
|
|
673af45c55 | ||
|
|
e0ed8c6e04 | ||
|
|
fc1dd401d2 | ||
|
|
d452fdeca5 | ||
|
|
b6580f99db | ||
|
|
605fbaf803 | ||
|
|
03b0493d29 | ||
|
|
5e295f59a4 | ||
|
|
f3135630d1 | ||
|
|
4a2902512e | ||
|
|
e140fba5df | ||
|
|
fa7a7c294e | ||
|
|
9dd65bfcb9 | ||
|
|
a8f1067f8a | ||
|
|
ef9b0737a8 | ||
|
|
6218f31ea2 | ||
|
|
14924174c5 | ||
|
|
edeb458b33 | ||
|
|
b8f277b3c6 | ||
|
|
5bc85f39a6 | ||
|
|
51ffb1d75c | ||
|
|
1f631b3ed1 | ||
|
|
1ea91d60ac | ||
|
|
13a8e252f0 | ||
|
|
ff47270681 | ||
|
|
3ad4de70bf | ||
|
|
9f6165f65c | ||
|
|
982dc46623 | ||
|
|
a8f722c4de | ||
|
|
a43d2c115f | ||
|
|
0c56291e4a | ||
|
|
c916e3b07f | ||
|
|
32f936ce8c | ||
|
|
e675bef062 | ||
|
|
511aa0fb51 | ||
|
|
90e607fe9a | ||
|
|
9441da4887 | ||
|
|
47074fd129 | ||
|
|
adbfb8db06 | ||
|
|
8c8601197b | ||
|
|
3ca233e0bd | ||
|
|
f17edb3151 | ||
|
|
691ef1c72f | ||
|
|
75a76b47be | ||
|
|
6f0d1f7518 | ||
|
|
25a6d78b88 | ||
|
|
65e309450d | ||
|
|
51292880fd | ||
|
|
26998efead | ||
|
|
cf9421aabf | ||
|
|
e53fd8d6c8 | ||
|
|
b62c011823 | ||
|
|
f9248262f5 | ||
|
|
bbafedc992 | ||
|
|
46ff798fae | ||
|
|
c5f51e03f4 | ||
|
|
b57188e98c | ||
|
|
49ffbdd54d | ||
|
|
855463b319 | ||
|
|
62db346b49 | ||
|
|
47aebcbdd4 | ||
|
|
20e7acaa1a | ||
|
|
c0d712acea | ||
|
|
4649c9a61d | ||
|
|
9300e68225 | ||
|
|
19e40a3383 | ||
|
|
66e2a225d2 | ||
|
|
2e27745b5f | ||
|
|
b5a063b0d9 | ||
|
|
ba8040ace5 | ||
|
|
9bcd7678a4 | ||
|
|
23ed0a5d9d | ||
|
|
2b6cc6fee2 | ||
|
|
6a76760033 | ||
|
|
dd2d5431a9 | ||
|
|
5d1e26a95e | ||
|
|
bf5b2612c8 | ||
|
|
694143ce6b | ||
|
|
19a5ef8a64 | ||
|
|
169b3d60a8 | ||
|
|
bb053561ef | ||
|
|
9ffe85fd9c | ||
|
|
8ba86e9cea | ||
|
|
b1eda6c24d | ||
|
|
1a2e034ee0 | ||
|
|
a6763d8882 | ||
|
|
16ce6a5ef2 | ||
|
|
0a74eb671f | ||
|
|
0c3c5e42ff | ||
|
|
1e258c3bc2 | ||
|
|
2d55976cb4 | ||
|
|
9a7ce0b048 | ||
|
|
446114acc3 | ||
|
|
30950f129e | ||
|
|
c042a28af1 | ||
|
|
066e42e271 | ||
|
|
301d8425c1 | ||
|
|
165fe87aca | ||
|
|
1b59efc79a | ||
|
|
06dd6f45c0 | ||
|
|
f1d7ac36eb | ||
|
|
21cecb2aec | ||
|
|
8a93a06b71 | ||
|
|
d2ff0af34a | ||
|
|
ae5f2ec104 | ||
|
|
6f0566581e | ||
|
|
e726c7894c | ||
|
|
c4bb4d9508 | ||
|
|
cfad228d3c | ||
|
|
2cd6b8bdac | ||
|
|
7ab2a9b163 | ||
|
|
670faf1d1d | ||
|
|
659163a93c | ||
|
|
2b163edc0e | ||
|
|
0d38f85db7 | ||
|
|
1dc2825a75 | ||
|
|
630e2d23c9 | ||
|
|
c73187e7d4 | ||
|
|
4548303a0c | ||
|
|
e18afe5d1e | ||
|
|
7534e3f739 | ||
|
|
0e01d91cec | ||
|
|
4ceff605bf | ||
|
|
06aea6b97c | ||
|
|
a99ff813cb | ||
|
|
92734416a6 | ||
|
|
2f32d4fe49 | ||
|
|
81d35eb645 | ||
|
|
ac24ac2507 | ||
|
|
39bb4ed842 | ||
|
|
8edeb0e6e8 | ||
|
|
e3b58eac67 | ||
|
|
8b23a86d2e | ||
|
|
d95acc9734 | ||
|
|
b172f9a354 | ||
|
|
63e4d3d5eb | ||
|
|
c74c8871f8 | ||
|
|
3f5d08aedb | ||
|
|
ddcb299834 | ||
|
|
a9f70dd1e5 | ||
|
|
7c72b56a4e | ||
|
|
8429d6b8e2 | ||
|
|
aff0c6b49b | ||
|
|
417bb42ac8 | ||
|
|
040ed4fa57 | ||
|
|
94fc7b4e9a | ||
|
|
172e7a7649 | ||
|
|
37ed138dcf | ||
|
|
842f76c8bd | ||
|
|
157dfac527 | ||
|
|
5f6aade92b | ||
|
|
0c62a5736e | ||
|
|
a92d91e82a | ||
|
|
f1406c1ffd | ||
|
|
1cdc3e5232 | ||
|
|
bd9870254e | ||
|
|
0442b8c1e1 | ||
|
|
585876d6af | ||
|
|
902d726ea6 | ||
|
|
3f35b426dd | ||
|
|
761d861888 | ||
|
|
9f185ed5c0 | ||
|
|
63b2077335 | ||
|
|
12d5beec6e | ||
|
|
b77e68df19 | ||
|
|
fcdd4fa410 | ||
|
|
07c48bca68 | ||
|
|
79ff76d124 | ||
|
|
de2ba1ca94 | ||
|
|
45002bd51d | ||
|
|
be7ebad956 | ||
|
|
64189a4d08 | ||
|
|
33a3170bc4 | ||
|
|
708cb28ed0 | ||
|
|
6712801b01 | ||
|
|
f29db693c8 | ||
|
|
0502bfd95d | ||
|
|
78a3901c61 | ||
|
|
0a4e3008af | ||
|
|
2ce4f8769d | ||
|
|
4dedc24f9f | ||
|
|
d03ba5394f | ||
|
|
2262e6c7d0 | ||
|
|
31a349b13b | ||
|
|
1ba143ef26 | ||
|
|
1532ce1bab | ||
|
|
fa9b920b71 | ||
|
|
40b2d5f724 | ||
|
|
0623a5a8de | ||
|
|
cfa1d08e7e | ||
|
|
6196814672 | ||
|
|
f5af2bf393 | ||
|
|
374fb033c1 | ||
|
|
4db80e75a4 | ||
|
|
8547277958 | ||
|
|
ec3366b0e5 | ||
|
|
48bd04b387 | ||
|
|
41a481252c | ||
|
|
a7cf3b5b10 | ||
|
|
ba63188f27 | ||
|
|
9cc34cb70f | ||
|
|
b9a4d72b42 | ||
|
|
8eef210547 | ||
|
|
ef999ed954 | ||
|
|
33de609560 | ||
|
|
624151c3f7 | ||
|
|
c88f859dae | ||
|
|
49b77d5477 | ||
|
|
d4c4a17eb7 | ||
|
|
3c8abab574 | ||
|
|
38596f8d0e | ||
|
|
4acdca090d | ||
|
|
f02178852b | ||
|
|
98e7acddf4 | ||
|
|
9458e851c0 | ||
|
|
a04512d7b8 | ||
|
|
1bc0174f6f | ||
|
|
90842f313a | ||
|
|
6aa2f6457c | ||
|
|
b7c600e60b | ||
|
|
d397b46b63 | ||
|
|
7a6b7c5ef0 | ||
|
|
d6fa832d83 | ||
|
|
dbad921fa5 | ||
|
|
e1535dd574 | ||
|
|
22640eb900 | ||
|
|
7e51e03043 | ||
|
|
865616284f | ||
|
|
0cf728b7e1 | ||
|
|
a2d563b081 | ||
|
|
8119aa6933 | ||
|
|
6b953363d1 | ||
|
|
139b240250 | ||
|
|
36b5dff1f0 | ||
|
|
7ae07d4de5 | ||
|
|
59ef52a271 | ||
|
|
34a1b22a38 | ||
|
|
b4f01fa6c2 | ||
|
|
2d6d16dcd0 | ||
|
|
1ccae4fef2 | ||
|
|
ee30acab32 | ||
|
|
5189bef325 | ||
|
|
17597580f4 | ||
|
|
f97f9e8646 | ||
|
|
91f1d41324 | ||
|
|
d9d9d98ea0 | ||
|
|
e7115c7316 | ||
|
|
6c58e26f14 | ||
|
|
dc371580a5 | ||
|
|
2a047073e9 | ||
|
|
6e3b1bc240 | ||
|
|
51faaae1d0 | ||
|
|
f55804ef06 | ||
|
|
e671e1c87c | ||
|
|
a7aa817dce | ||
|
|
dcce4db6d5 | ||
|
|
64c4f0f1aa | ||
|
|
a8f928200b | ||
|
|
58d42b09d9 | ||
|
|
0cd481b149 | ||
|
|
a66c55ca14 | ||
|
|
18715dbe2e | ||
|
|
23dee61389 | ||
|
|
23dc3f29cd | ||
|
|
4c701f4b6c | ||
|
|
7a94f524b4 | ||
|
|
23deb41436 | ||
|
|
7198ebefc9 | ||
|
|
32cb57532e | ||
|
|
edcfece993 | ||
|
|
baf209f3cc | ||
|
|
ece47c9ed5 | ||
|
|
3d40ed968a | ||
|
|
10f56de5e8 | ||
|
|
5ee4fc2cd5 | ||
|
|
a7917a0f3d | ||
|
|
0274cf3ec7 | ||
|
|
3aa7c96902 | ||
|
|
7ef78fdbce | ||
|
|
ffa1851bbf | ||
|
|
45c3345bbc | ||
|
|
a6ca3aaa66 | ||
|
|
366c55231e | ||
|
|
43b2ee3c52 | ||
|
|
85a7c87830 | ||
|
|
2d7e20f532 | ||
|
|
cc993b67a3 | ||
|
|
5a10b612a1 | ||
|
|
632b3ff07c | ||
|
|
efe1d1c0ac | ||
|
|
86e2f83a7d | ||
|
|
a2b3a38f86 | ||
|
|
f243749d38 | ||
|
|
dac103c621 | ||
|
|
a74911e926 | ||
|
|
8cc16e8de9 | ||
|
|
35e53e9691 | ||
|
|
0559e61af1 | ||
|
|
3da233dcad | ||
|
|
2fe0713faa | ||
|
|
28629b352c | ||
|
|
e5f79c9f5c | ||
|
|
c6815ef126 | ||
|
|
28b2cd5117 | ||
|
|
28c24c9d48 | ||
|
|
b2080cdfbc | ||
|
|
57095175d2 | ||
|
|
5b260c00f4 | ||
|
|
9b0fb74d94 | ||
|
|
103b384c09 | ||
|
|
65f18aecc8 | ||
|
|
e971bc4044 | ||
|
|
b4870b120e | ||
|
|
a7988a6e78 | ||
|
|
de19c9300d | ||
|
|
a7639d33b9 | ||
|
|
c3f9c27e34 | ||
|
|
b849cfd4a5 | ||
|
|
7dff76b122 | ||
|
|
be5ada26ea | ||
|
|
5b903ca4f3 | ||
|
|
6b2710ac7e | ||
|
|
764fda8e7b | ||
|
|
151ef95b79 | ||
|
|
4976375d74 | ||
|
|
0b834a1623 | ||
|
|
41c512624b | ||
|
|
9467ee6f10 | ||
|
|
dde76e301d | ||
|
|
5ded85f46e | ||
|
|
0cbe4618e1 | ||
|
|
f03ad2d208 | ||
|
|
8b867836e9 | ||
|
|
236c1c9d17 | ||
|
|
64dca7d801 | ||
|
|
3834314c2a | ||
|
|
144723be3c | ||
|
|
0f54a6f67e | ||
|
|
1cec768521 | ||
|
|
d85d01eea1 | ||
|
|
8d1e1cc54c | ||
|
|
0d9e74028e | ||
|
|
445214b23b | ||
|
|
16444fe5ed | ||
|
|
994edf66fe | ||
|
|
f9291d4e50 | ||
|
|
ab089c024d | ||
|
|
ffb1cb3128 | ||
|
|
57386812f9 | ||
|
|
ce8e15a220 | ||
|
|
0d42ac3912 | ||
|
|
f10a43abe6 | ||
|
|
64ef2c8a65 | ||
|
|
d3c44a8263 | ||
|
|
8d016de217 | ||
|
|
ee3d3a964e | ||
|
|
d6e145936d | ||
|
|
9caea57cde | ||
|
|
99e81e1d8f | ||
|
|
1696a9ad2d | ||
|
|
6c2a83dda8 | ||
|
|
5af1a42bf1 | ||
|
|
73183e9c19 | ||
|
|
b35cfdaf6a | ||
|
|
8c40e82796 | ||
|
|
c113a3b5b8 | ||
|
|
a07b47c845 | ||
|
|
f789e144fd | ||
|
|
78bd5e1e3b | ||
|
|
2e534a4128 | ||
|
|
50afc2f9b2 | ||
|
|
e068ce7bc9 | ||
|
|
2daf880e39 | ||
|
|
7897fa9f29 | ||
|
|
456d4272ab | ||
|
|
52c3ea733b | ||
|
|
acdaeb26d3 | ||
|
|
ffe089d444 | ||
|
|
1f09c92306 | ||
|
|
14b0c5fdbf | ||
|
|
932066bc0e | ||
|
|
66ea0451e9 | ||
|
|
bc05118ee7 | ||
|
|
275386806d | ||
|
|
0afc16fd02 | ||
|
|
6cafe14060 | ||
|
|
ad611c2058 | ||
|
|
b876adbc27 | ||
|
|
e428b74657 | ||
|
|
7ab083f19a | ||
|
|
931dcb1dc5 | ||
|
|
12c191582f | ||
|
|
d861b0798e | ||
|
|
b6e85b878e | ||
|
|
807efec40f | ||
|
|
41ff457d65 | ||
|
|
e605dfb483 | ||
|
|
2511f40ab8 | ||
|
|
61554dbaf0 | ||
|
|
ce56ab71d4 | ||
|
|
21c2705827 | ||
|
|
916db6c197 | ||
|
|
562e03d2d2 | ||
|
|
eca86470c6 | ||
|
|
a90eda50a7 | ||
|
|
187a4712cb | ||
|
|
58bbb8e3a4 | ||
|
|
d57ed97f9d | ||
|
|
b7b451dace | ||
|
|
d91070c116 | ||
|
|
39d2a70679 | ||
|
|
ec6b6a2266 | ||
|
|
9eacf72366 | ||
|
|
30516c33b7 | ||
|
|
615628805c | ||
|
|
8bac455bc9 | ||
|
|
0945d9aea2 | ||
|
|
45c6e74945 | ||
|
|
b32ab87bb7 | ||
|
|
8d2a186b1a | ||
|
|
a62996420f | ||
|
|
7dc4c44393 | ||
|
|
6ffcbfef3d | ||
|
|
1c558a203d | ||
|
|
ed5dabe432 | ||
|
|
ce28d60edf | ||
|
|
afa9410209 | ||
|
|
09865ccd9b | ||
|
|
256611bef5 | ||
|
|
7b0fac27dc | ||
|
|
c7b65cfe8a | ||
|
|
f811b6b803 | ||
|
|
ba43513172 | ||
|
|
f3bb2169c0 | ||
|
|
68b58f979b | ||
|
|
8e80bc844d | ||
|
|
a45cab06d3 | ||
|
|
695508aa4c | ||
|
|
957083d805 | ||
|
|
2aac99b037 | ||
|
|
2401dc785c | ||
|
|
f902add0ce | ||
|
|
2faae5d022 | ||
|
|
2a2878bba0 | ||
|
|
2bb6f924cd | ||
|
|
ee881ab82f | ||
|
|
b32a8ca510 | ||
|
|
b766d957b0 | ||
|
|
e7ccea44e7 | ||
|
|
861e96d33e | ||
|
|
07e6407115 | ||
|
|
69d44cdc16 | ||
|
|
97c8fd0525 | ||
|
|
259dfaed11 | ||
|
|
bf02b2ecb4 | ||
|
|
88c78bb411 | ||
|
|
2c73f08364 | ||
|
|
467c19be97 | ||
|
|
96d7f20980 | ||
|
|
8965fc8a79 | ||
|
|
f4968bc1f1 | ||
|
|
fe0702a06b | ||
|
|
44254bfffe | ||
|
|
c85050ac1a | ||
|
|
21f2cb6e6f | ||
|
|
c71cb55051 | ||
|
|
6ba5b2b72b | ||
|
|
dd207fb238 | ||
|
|
e9e06bb571 | ||
|
|
ae0e0a03a3 | ||
|
|
526fc15082 | ||
|
|
271107436b | ||
|
|
eaa4e15439 | ||
|
|
7cfeebfff7 | ||
|
|
6f3bffe689 | ||
|
|
7c4a46b7b4 | ||
|
|
efb07fafb3 | ||
|
|
eedd885683 | ||
|
|
e6248cd9ed | ||
|
|
3d1ef51863 | ||
|
|
068ac0ca2c | ||
|
|
8fe88f601f | ||
|
|
eef1548baa | ||
|
|
6eaa46ea9a | ||
|
|
6641c8c9c9 | ||
|
|
a40126aeff | ||
|
|
ccc51dab35 | ||
|
|
89c6c235f7 | ||
|
|
a260b35c9d | ||
|
|
c04774b4b1 | ||
|
|
d46cf5b519 | ||
|
|
29682cf767 | ||
|
|
42df936336 | ||
|
|
fe6117e87a | ||
|
|
04ca770545 | ||
|
|
43f3f31d69 | ||
|
|
acd0020413 | ||
|
|
0002b05418 | ||
|
|
545e198257 | ||
|
|
d4b83e3f8a | ||
|
|
efcc2e0dd4 | ||
|
|
5e0d6176a1 | ||
|
|
e240372a90 | ||
|
|
a64a88981f | ||
|
|
bc8df09be5 | ||
|
|
b09e3e69f2 | ||
|
|
43128404be | ||
|
|
28e85aa548 | ||
|
|
30c14210ed | ||
|
|
d2fc740278 | ||
|
|
cbe30199ff | ||
|
|
3f5d9c79f9 | ||
|
|
59332c2e94 | ||
|
|
d230780443 | ||
|
|
7387c073fb | ||
|
|
535ba622ae | ||
|
|
c6b634f3ae | ||
|
|
386baec3c5 | ||
|
|
b2ead45ad4 | ||
|
|
74284e9dad | ||
|
|
270077bc73 | ||
|
|
367a0c483c | ||
|
|
8a272e92c7 | ||
|
|
2d1105dba9 | ||
|
|
c798996f6e | ||
|
|
ef0e4bd4fd | ||
|
|
bfaee2c402 | ||
|
|
1f6cd807a4 | ||
|
|
6f416dfefb | ||
|
|
06c71a7f2b | ||
|
|
270350f8d1 | ||
|
|
c603b92bc5 | ||
|
|
59be399dac | ||
|
|
7f39cb1bc3 | ||
|
|
d09e1c8ee2 | ||
|
|
c1735b6033 | ||
|
|
1921961cff | ||
|
|
3cd766630f | ||
|
|
fac548a76e | ||
|
|
24f4ebef23 | ||
|
|
99ee317fd0 | ||
|
|
456f6e0003 | ||
|
|
1ccd2c4d0f | ||
|
|
f42b5b1088 | ||
|
|
ed64986af8 | ||
|
|
1b90a28acd | ||
|
|
cd0e0ce4d1 | ||
|
|
7cb4ea9273 | ||
|
|
66e374a343 | ||
|
|
5e8262d3c0 | ||
|
|
6bb14d0874 | ||
|
|
c3fdab8ec5 | ||
|
|
237554d84a | ||
|
|
6ed7aca5be | ||
|
|
a13ce094b3 | ||
|
|
6806b8f5a7 | ||
|
|
e3d9386239 | ||
|
|
fbdf92367e | ||
|
|
2ec96d7f13 | ||
|
|
1c457d3428 | ||
|
|
fe1193f374 | ||
|
|
abbf3db2ac | ||
|
|
5a1009520d | ||
|
|
b49fb7fcf9 | ||
|
|
9e12c563bc | ||
|
|
530e28cbc3 | ||
|
|
637dd6bf0a | ||
|
|
fdc9530352 | ||
|
|
4990f7a2c8 | ||
|
|
b5f274bf56 | ||
|
|
ac2d01a60a | ||
|
|
95bdaf072b | ||
|
|
af1500825a | ||
|
|
cd2ef15a8a | ||
|
|
02359e5e84 | ||
|
|
d873cc0257 | ||
|
|
ea2acea668 | ||
|
|
84052c3ac5 | ||
|
|
4a40732cad | ||
|
|
cd9f32ced5 | ||
|
|
2bedc6b181 | ||
|
|
e26deb472e | ||
|
|
78d0111a6c | ||
|
|
d61c85c171 | ||
|
|
03f0034d33 | ||
|
|
3f2e698684 | ||
|
|
259aa53de4 | ||
|
|
7915fb3fb6 | ||
|
|
fbb348bc82 | ||
|
|
a8552e6b96 | ||
|
|
4be3fe1628 | ||
|
|
a087045322 | ||
|
|
248229a383 | ||
|
|
0ff22d319f | ||
|
|
a1dfcc73dd | ||
|
|
3e98115dc2 | ||
|
|
ddc52fa21c | ||
|
|
986e2e6057 | ||
|
|
793057c202 | ||
|
|
3bf9cacaec | ||
|
|
bed4593d04 | ||
|
|
e8082173ad | ||
|
|
b1f4035530 | ||
|
|
0d4a92a351 | ||
|
|
89803e7523 | ||
|
|
613ce92cfd | ||
|
|
8bde277be2 | ||
|
|
3be7bbbf88 | ||
|
|
d8aa276f25 | ||
|
|
dcddef09dc | ||
|
|
ad442aaae3 | ||
|
|
21ecc7618a | ||
|
|
8f8a0b118f | ||
|
|
0358b46fcd | ||
|
|
1a29077b45 | ||
|
|
c249b841e8 | ||
|
|
7d12942cf7 | ||
|
|
c52b0a22e0 | ||
|
|
840145f947 | ||
|
|
10d6e55d62 | ||
|
|
80112bac64 | ||
|
|
49ff9d5a7c | ||
|
|
1044709803 | ||
|
|
252f5cebb7 | ||
|
|
e8ddee4782 | ||
|
|
8daa1c032c | ||
|
|
beccf28d09 | ||
|
|
5ac3414490 | ||
|
|
5d49f5a1d2 | ||
|
|
41bf5f0926 | ||
|
|
4c5a16a1db | ||
|
|
85fb9aa99f | ||
|
|
57d34087dd | ||
|
|
2d65b4b2a1 | ||
|
|
d068faa35e | ||
|
|
1c33cd4470 | ||
|
|
21e410cc77 | ||
|
|
68ebd87127 | ||
|
|
62069e9e59 | ||
|
|
14a2088606 | ||
|
|
114c3854e7 | ||
|
|
26ca593fad | ||
|
|
ec785f9d6d | ||
|
|
f54ef35a7a | ||
|
|
e0b57fc74e | ||
|
|
4754a84a8a | ||
|
|
02fdf41969 | ||
|
|
92e75ee89b | ||
|
|
7c2b6a3161 | ||
|
|
26a8647444 | ||
|
|
cae7c4d0a7 | ||
|
|
27a5e17a3e | ||
|
|
a9ba133506 | ||
|
|
eb20724d78 | ||
|
|
1b9e486c49 | ||
|
|
7ef167fcd0 | ||
|
|
9db106e3f0 | ||
|
|
b4052e5a64 | ||
|
|
9a77f18ced | ||
|
|
03996f2b82 | ||
|
|
53ca96fcee | ||
|
|
c1ca4ab703 | ||
|
|
43bcf401b2 | ||
|
|
f1c495dc0a | ||
|
|
98eb28704c | ||
|
|
1f3582c9c3 | ||
|
|
62f7bddd4d | ||
|
|
b097569607 | ||
|
|
da6f72c20a | ||
|
|
00e94d976a | ||
|
|
d1d6db877d | ||
|
|
da3e3c6bb4 | ||
|
|
e57be09823 | ||
|
|
7598a97888 | ||
|
|
ebaf51ce56 | ||
|
|
0cf8b154a4 | ||
|
|
b420d6bbb2 | ||
|
|
6086cc5e18 | ||
|
|
c3ed12d8d4 | ||
|
|
2d98c9e3c4 | ||
|
|
0933040d0b | ||
|
|
12046e698e | ||
|
|
73ac83bd06 | ||
|
|
631685472d | ||
|
|
32bcf999b8 | ||
|
|
008f6d1839 | ||
|
|
1746a640cc | ||
|
|
d5937e4af5 | ||
|
|
1336796c0c | ||
|
|
2efcfcf239 | ||
|
|
8f2ffe8526 | ||
|
|
8cf74759a6 | ||
|
|
22a1a8e41f | ||
|
|
74009eb4a4 | ||
|
|
5932358f9d | ||
|
|
1ad5364fec | ||
|
|
201330295c | ||
|
|
a7b7f643a5 | ||
|
|
4fd6f17ced | ||
|
|
e67679658a | ||
|
|
d67f924b73 | ||
|
|
961daf6c36 | ||
|
|
748e7641ef | ||
|
|
6321adc411 | ||
|
|
02e451a2b1 | ||
|
|
8cac47038c | ||
|
|
59ab8e0b04 | ||
|
|
577d96c026 | ||
|
|
7031c68a85 | ||
|
|
3a7326726e | ||
|
|
f01d79df46 | ||
|
|
df6de3446c | ||
|
|
eaeef59583 | ||
|
|
f9c7ca2941 | ||
|
|
50935372ca | ||
|
|
d8f89d49d4 | ||
|
|
7e823057b9 | ||
|
|
e4d69984d3 | ||
|
|
acd04e7181 | ||
|
|
22a53bb1dc | ||
|
|
aaef16f51b | ||
|
|
8613c88a60 | ||
|
|
6070bd562e | ||
|
|
01c4ac822c | ||
|
|
05dbdd4473 | ||
|
|
64323b394a | ||
|
|
70f6f1cd03 | ||
|
|
e9d4a23dad | ||
|
|
3cdbc66375 | ||
|
|
5128638071 | ||
|
|
1f80791f8f | ||
|
|
44d8e693b0 | ||
|
|
3bdc61f5ee | ||
|
|
a7e4d265e2 | ||
|
|
0ac497ab59 | ||
|
|
dbb0200147 | ||
|
|
ff7a93f364 | ||
|
|
8f6a660f3d | ||
|
|
64c542502b | ||
|
|
b4974a80bb | ||
|
|
95f23dafe5 | ||
|
|
02dc42154b | ||
|
|
4047780c08 | ||
|
|
c648af2cb4 | ||
|
|
4a698ffdff | ||
|
|
1babdb069f | ||
|
|
b49213bef6 | ||
|
|
42e877671b | ||
|
|
14c18727db | ||
|
|
aacfcaaa23 | ||
|
|
9f3428e1c3 | ||
|
|
52de09a032 | ||
|
|
be6bb879f3 | ||
|
|
f7371c4a9f | ||
|
|
bd7cf8cdd1 | ||
|
|
70b39cbd2c | ||
|
|
199a5cff4b | ||
|
|
501e213dce | ||
|
|
d663007e60 | ||
|
|
a07ca443f0 | ||
|
|
84df8baa5f | ||
|
|
241c0aeedd | ||
|
|
ae85399193 | ||
|
|
17f70bb87c | ||
|
|
7a1f2f4b3b | ||
|
|
599d3ac92c | ||
|
|
02f8e57e66 | ||
|
|
b6ac6d2959 | ||
|
|
c681175685 | ||
|
|
5e600d02a8 | ||
|
|
b9edb6dbc9 | ||
|
|
6e5302e5ec | ||
|
|
4b472c8564 | ||
|
|
4ccf6f0e69 | ||
|
|
eac3d8336d | ||
|
|
53475c9643 | ||
|
|
3c0361fd5c | ||
|
|
0d14c168a4 | ||
|
|
00ecfe7a80 | ||
|
|
fd64b2c5d5 | ||
|
|
099cd868ae | ||
|
|
3071394ef4 | ||
|
|
d1b4e59e7d | ||
|
|
50750a59d9 | ||
|
|
e41afbee58 | ||
|
|
9ea2aca9cb | ||
|
|
c7ab89507e | ||
|
|
c197fd5086 | ||
|
|
b6e607f60e | ||
|
|
38d8b7f501 | ||
|
|
514b4929b3 | ||
|
|
e8cef536f6 | ||
|
|
4ea3475d2b | ||
|
|
15a276e3a5 | ||
|
|
f6e58ea212 | ||
|
|
1b191b5aea | ||
|
|
c2346f41cb | ||
|
|
3f40f47104 | ||
|
|
3dfb7beb6b | ||
|
|
6a222a6139 | ||
|
|
b34864c55e | ||
|
|
26655315c7 | ||
|
|
8aaa8809e6 | ||
|
|
cbac0e0d3b | ||
|
|
22b8c594b8 | ||
|
|
7a8065b2bb | ||
|
|
6070479e0a | ||
|
|
fd70dc24df | ||
|
|
8cb8cfdb46 | ||
|
|
79f25ec0a3 | ||
|
|
2235417a25 | ||
|
|
ce449790df | ||
|
|
79e36ab11d | ||
|
|
dde3abdfa0 | ||
|
|
7ea166f98c | ||
|
|
faceca6fec | ||
|
|
6589b2044b | ||
|
|
f00e44aba6 | ||
|
|
6591b574a0 | ||
|
|
ca91051d1a | ||
|
|
29f24de5d5 | ||
|
|
2014c64732 | ||
|
|
b5c6cdeaa1 | ||
|
|
bf7c569060 | ||
|
|
bbc0afd083 | ||
|
|
8857f92f7c | ||
|
|
70f568b1cc | ||
|
|
c586166006 | ||
|
|
96f266ce5e | ||
|
|
e5549d6ce8 | ||
|
|
b60717bb8c | ||
|
|
83eefd343c | ||
|
|
03e8be6368 | ||
|
|
a58e9e4df3 | ||
|
|
0a78187c69 | ||
|
|
61112c2527 | ||
|
|
67cfefd2df | ||
|
|
3dfd16c033 | ||
|
|
67b9d2e1c0 | ||
|
|
a076a0c44e | ||
|
|
f152729c79 | ||
|
|
3c0e36d5d4 | ||
|
|
887f37b72c | ||
|
|
e30dd08dec | ||
|
|
2d1bbeda0c | ||
|
|
68603a9cc7 | ||
|
|
6c83db9977 | ||
|
|
6d16cafbc8 | ||
|
|
e503cedd8f | ||
|
|
1a498d1afc | ||
|
|
33a46cc633 | ||
|
|
b3b9ec11dd | ||
|
|
a7afdec2e1 | ||
|
|
56a0bedac9 | ||
|
|
f451fe68e1 | ||
|
|
946816e377 | ||
|
|
99af09fce5 | ||
|
|
0888e5ad69 | ||
|
|
c423ccec67 | ||
|
|
03f72f498e | ||
|
|
fbd7c566f4 | ||
|
|
e09d35bbb9 | ||
|
|
e644775ad1 | ||
|
|
6ad471a914 | ||
|
|
476ffabae9 | ||
|
|
4b7a9e149f | ||
|
|
49c18bd83d | ||
|
|
67717761bd | ||
|
|
b10196cdac | ||
|
|
fa0ddba436 | ||
|
|
0fb3be359f | ||
|
|
26662e99de | ||
|
|
5513d4ca43 | ||
|
|
2b07ec925c | ||
|
|
efb4c9d540 | ||
|
|
b8de9625ee | ||
|
|
607daa345e | ||
|
|
35e6df6f6b | ||
|
|
cb1ef965d0 | ||
|
|
2ab057a24d | ||
|
|
12f8588c03 | ||
|
|
3571f35578 | ||
|
|
803fe321d1 | ||
|
|
cf42670e97 | ||
|
|
ac36b9d328 | ||
|
|
9a9f72f07a | ||
|
|
4b9a844c92 | ||
|
|
a273ad31d4 | ||
|
|
16f3164865 | ||
|
|
5fb9de775f | ||
|
|
05879dc02a | ||
|
|
d5cb36151f | ||
|
|
b6fd95c7b8 | ||
|
|
8ce570cea7 | ||
|
|
5b82ed2fd9 | ||
|
|
37a4dbf822 | ||
|
|
ef86160d88 | ||
|
|
5f31bdbb3e | ||
|
|
810e2d70d3 | ||
|
|
85dd065f91 | ||
|
|
2a61e357de | ||
|
|
e34fdfae1a | ||
|
|
58e94a35cb | ||
|
|
93acf9feb4 | ||
|
|
0362148989 | ||
|
|
985ea5ebdc | ||
|
|
64ebf14256 | ||
|
|
cfebe5a5ba | ||
|
|
99e0e45bfc | ||
|
|
83845078a7 | ||
|
|
7c102509bd | ||
|
|
1af90b9db3 | ||
|
|
d4de650f90 | ||
|
|
5de0324441 | ||
|
|
5fa2a87747 | ||
|
|
68ef9d7858 | ||
|
|
a286e066d1 | ||
|
|
94a712b820 | ||
|
|
c8aa73ac18 | ||
|
|
a74b8e6328 | ||
|
|
ff773695d0 | ||
|
|
c4ebb0a31d | ||
|
|
f9b3d6304c | ||
|
|
1c85f530b1 | ||
|
|
d65d7bcd7e | ||
|
|
c11633c5db | ||
|
|
ea0a708f35 | ||
|
|
00254b93dc | ||
|
|
6932df3564 | ||
|
|
9e3a48aa8d | ||
|
|
6e17462bd0 | ||
|
|
d29e7e6f3a | ||
|
|
049e222e88 | ||
|
|
caef7812a3 | ||
|
|
68efa7316b | ||
|
|
5396d5f99e | ||
|
|
4576cbd0a1 | ||
|
|
1fa9180fee | ||
|
|
801c80d7a2 | ||
|
|
eba1989c9f | ||
|
|
90591811df | ||
|
|
c959506ae9 | ||
|
|
25f9029a82 | ||
|
|
4f75b3d9f6 | ||
|
|
974d79f2be | ||
|
|
c0a8a91281 | ||
|
|
2219139605 | ||
|
|
966e38babf | ||
|
|
5f39083df6 | ||
|
|
565b002bfe | ||
|
|
1dd5a8dbf2 | ||
|
|
7ef17b8dee | ||
|
|
d01a0e022d | ||
|
|
3258556d5d | ||
|
|
5f77200108 | ||
|
|
b12865f1e5 | ||
|
|
ee90fc8761 | ||
|
|
e6585ee526 | ||
|
|
b68be0c2ce | ||
|
|
3b95ed0b5a | ||
|
|
50490e6a93 | ||
|
|
d466345e4e | ||
|
|
4ece47c64c | ||
|
|
2b85af0f88 | ||
|
|
e0491097b0 | ||
|
|
fa3d658f33 | ||
|
|
6dcd115765 | ||
|
|
88cffee902 | ||
|
|
b12d526a60 | ||
|
|
3af7fe0b08 | ||
|
|
d7548c0b20 | ||
|
|
f79e16d1a6 | ||
|
|
ad47ea3bab | ||
|
|
505910edb5 | ||
|
|
aee0ec8016 | ||
|
|
613c185428 | ||
|
|
501227f23f | ||
|
|
56d075fd32 | ||
|
|
9ae908c741 | ||
|
|
81500a4d1d | ||
|
|
b819033da0 | ||
|
|
35243ef7a6 | ||
|
|
655c45d43f | ||
|
|
34c4809f68 | ||
|
|
f9b6800831 | ||
|
|
b5254e3662 | ||
|
|
148cb71839 | ||
|
|
62700ca5d1 | ||
|
|
b1d6fcd6c8 | ||
|
|
8afebc1f17 | ||
|
|
447cd95bc5 | ||
|
|
5224380947 | ||
|
|
7aeb685412 | ||
|
|
b6911f8ad2 | ||
|
|
a7d06275c1 | ||
|
|
d581eefcdf | ||
|
|
47f58162c5 | ||
|
|
ee72ed4b53 | ||
|
|
5cd7f33d00 | ||
|
|
d6674c7548 | ||
|
|
a46d7b3262 | ||
|
|
0f902124d1 | ||
|
|
d4a218e268 | ||
|
|
22bef146f8 | ||
|
|
b26ed47ab8 | ||
|
|
7ba08edffa | ||
|
|
c958a6a286 | ||
|
|
1583fedba2 | ||
|
|
307a6fad4f | ||
|
|
958d5bcc6a | ||
|
|
c5a9aa21bf | ||
|
|
13b5d7c179 | ||
|
|
bd84ee83a5 | ||
|
|
97f633312f | ||
|
|
b290690b19 | ||
|
|
fc57ed76a0 | ||
|
|
a6fdb71178 | ||
|
|
fe2f668306 | ||
|
|
45d007fa9a | ||
|
|
662ec11031 | ||
|
|
1d8a3486cd | ||
|
|
c195afa0b3 | ||
|
|
63e0d9b3f3 | ||
|
|
659cbedc3c | ||
|
|
0ebba2cd15 | ||
|
|
1f091a4ccd | ||
|
|
d1aafa3764 | ||
|
|
faefe41ad5 | ||
|
|
473d0daf58 | ||
|
|
a10abfebde | ||
|
|
78172b5f5b | ||
|
|
1caeb248ca | ||
|
|
8527d02dc8 | ||
|
|
0e73f26e88 | ||
|
|
ed24db4460 | ||
|
|
127886144b | ||
|
|
c83877ec74 | ||
|
|
8d6fcd9939 | ||
|
|
1dc5e40308 | ||
|
|
cc832d26aa | ||
|
|
9fcb70387d | ||
|
|
236ad883d4 | ||
|
|
12c9c466c7 | ||
|
|
5a1cb0e48d | ||
|
|
5196caabb5 | ||
|
|
0f99592903 | ||
|
|
56e9645700 | ||
|
|
0d8c6cc0fd | ||
|
|
20c7949be3 | ||
|
|
7cc6773bf8 | ||
|
|
055700a5d1 | ||
|
|
85b14075cd | ||
|
|
149c3989f1 | ||
|
|
3b5a34f331 | ||
|
|
b4fe2d8592 | ||
|
|
67d06c73e0 | ||
|
|
81a942d7a1 | ||
|
|
521473cd81 | ||
|
|
676d422511 | ||
|
|
f2dbb531fe | ||
|
|
84fce86152 | ||
|
|
8307c66256 | ||
|
|
ac71676d79 | ||
|
|
70e6d83259 | ||
|
|
3bbac4a35f | ||
|
|
87455ed6dd | ||
|
|
e1735f0a5e | ||
|
|
8521f85742 | ||
|
|
b1b15e2eef | ||
|
|
36e304839b | ||
|
|
5a14a6d0cc | ||
|
|
85901893a0 | ||
|
|
49d7f2a88f | ||
|
|
8d8c5f99c1 | ||
|
|
4069515cad | ||
|
|
3c1cd67f60 | ||
|
|
580948e46b | ||
|
|
4ffd7b89f3 | ||
|
|
2441c18a85 | ||
|
|
ee89fa45b6 | ||
|
|
3976e5858d | ||
|
|
4e542f9cff | ||
|
|
ce1ecfad4d | ||
|
|
d9d5aaffa1 | ||
|
|
21809350f7 | ||
|
|
418b063067 | ||
|
|
dcf838872c | ||
|
|
456b32e6a8 | ||
|
|
acad9c5570 | ||
|
|
4b2cfb4825 | ||
|
|
7733562587 | ||
|
|
eaa70fa80f | ||
|
|
44843ea977 | ||
|
|
cac041b869 | ||
|
|
49684e4c25 | ||
|
|
47268c2344 | ||
|
|
da0a1e7903 | ||
|
|
eca1582678 | ||
|
|
2049058b45 | ||
|
|
c2b5e7116d | ||
|
|
9c1b076a5f | ||
|
|
51f7e10cb6 | ||
|
|
25ad6446ba | ||
|
|
1af5255501 | ||
|
|
49d61db8f9 | ||
|
|
601471c1e6 | ||
|
|
3c4141589d | ||
|
|
c5f768accc | ||
|
|
2e6671ff91 | ||
|
|
f4171c32cf | ||
|
|
449c64d80b | ||
|
|
735cb57b10 | ||
|
|
81cb4b31e1 | ||
|
|
e564466ac8 | ||
|
|
63e0d903c7 | ||
|
|
dbc1ddcd7b | ||
|
|
a00d0d5222 | ||
|
|
428d125340 | ||
|
|
f94314d8ec | ||
|
|
bb94ca3b18 | ||
|
|
5823d421fd | ||
|
|
045a64496e | ||
|
|
b8905e3e48 | ||
|
|
7c6f27c6d7 | ||
|
|
995b144f0b | ||
|
|
ba93803d3f | ||
|
|
96b13907e2 | ||
|
|
2f7aa14f61 | ||
|
|
f93b94f073 | ||
|
|
30835b5ce4 | ||
|
|
98db89e45a | ||
|
|
84c4b3ca8f | ||
|
|
cd32abc405 | ||
|
|
bae1b29505 | ||
|
|
5061a0c717 | ||
|
|
404de45103 | ||
|
|
39c8674da5 | ||
|
|
954b90befb | ||
|
|
62422ae4d9 | ||
|
|
6594d9d911 | ||
|
|
6e9676e0be | ||
|
|
6764830f2d | ||
|
|
747eed4db7 | ||
|
|
28f32eebfc | ||
|
|
3dbd57ffe4 | ||
|
|
e63a9c801b | ||
|
|
0fbea75513 | ||
|
|
4b3129e30a | ||
|
|
10c16e8a71 | ||
|
|
21efdd2e0e | ||
|
|
ac1add3fcb | ||
|
|
b4d2fecf4b | ||
|
|
ec81768fb5 | ||
|
|
0f60165135 | ||
|
|
7c54502dc8 | ||
|
|
38668b2c4a | ||
|
|
d210645aee | ||
|
|
444c30d720 | ||
|
|
22bc26905f | ||
|
|
9f4479582a | ||
|
|
7bd49b56c4 | ||
|
|
9015761d4d | ||
|
|
36eabc1c39 | ||
|
|
2f792427f9 | ||
|
|
cc06101cdc | ||
|
|
7387c56af9 | ||
|
|
998364d500 | ||
|
|
e7cf69a82e | ||
|
|
8dbb5a097c | ||
|
|
91818a116d | ||
|
|
82e8f8f090 | ||
|
|
2a0ada9848 | ||
|
|
b87b03300a | ||
|
|
ecd88680dd | ||
|
|
45c39cfd7a | ||
|
|
46ad23fb30 | ||
|
|
0e6a050921 | ||
|
|
f72f8b054a | ||
|
|
1d61b24eb0 | ||
|
|
5a73a8d7bb | ||
|
|
b2507d14c0 | ||
|
|
b6f932ea15 | ||
|
|
bb1afb3356 | ||
|
|
d35ac32f0a | ||
|
|
cb6781a143 | ||
|
|
e7fa1ae52c | ||
|
|
8b7ddc5679 | ||
|
|
3323d85067 | ||
|
|
9019e6b0f5 | ||
|
|
c6c2fc9f2a | ||
|
|
6ea15901d6 | ||
|
|
400e28c3f7 | ||
|
|
f2281b8e6e | ||
|
|
ad88e51228 | ||
|
|
2b17b22d33 | ||
|
|
da6f6dd94f | ||
|
|
09d444222a | ||
|
|
a5c9993b61 | ||
|
|
f03eb87892 | ||
|
|
a7c4761fef | ||
|
|
e2156c3854 | ||
|
|
bf53958887 | ||
|
|
e4d532e212 | ||
|
|
9bf582a89a | ||
|
|
470995a541 | ||
|
|
79ce903817 | ||
|
|
6fa8f9e401 | ||
|
|
fb99ef56e3 | ||
|
|
be2dffe863 | ||
|
|
e3804a0596 | ||
|
|
9ebea05933 | ||
|
|
a453258a51 | ||
|
|
246ef58e7b | ||
|
|
d55d1facd5 | ||
|
|
a5979d3b4d | ||
|
|
af9049da6e | ||
|
|
6b5e125592 | ||
|
|
ee5c86913d | ||
|
|
0ff3bf1e5e | ||
|
|
f5b79c0285 | ||
|
|
c417b5dd79 | ||
|
|
bb74c73f6f | ||
|
|
df101e5a60 | ||
|
|
aff6191b11 | ||
|
|
269f056e52 | ||
|
|
9c77488937 | ||
|
|
2ceed78924 | ||
|
|
df99b1d394 | ||
|
|
57633ceeb2 | ||
|
|
7aa041c4d1 | ||
|
|
8031be75ab | ||
|
|
3103307601 | ||
|
|
6568189839 | ||
|
|
c653dd7e72 | ||
|
|
1c771da848 | ||
|
|
5b5ac16830 | ||
|
|
67221e5907 | ||
|
|
6a5271c16f | ||
|
|
c3418fddb5 | ||
|
|
faf414e3d8 | ||
|
|
c6144a1dfa | ||
|
|
ad153499a3 | ||
|
|
2767660722 | ||
|
|
9433d41588 | ||
|
|
96b522cf6c | ||
|
|
f35a82562b | ||
|
|
bfda997fdf | ||
|
|
9c09923b86 | ||
|
|
3ef126fbd7 | ||
|
|
9fdaa91fa9 | ||
|
|
0987141970 | ||
|
|
c73db051c1 | ||
|
|
9a8d28d107 | ||
|
|
0b11a35358 | ||
|
|
524ab86d24 | ||
|
|
0060daf2e8 | ||
|
|
f5eb52f7c9 | ||
|
|
59944d6aa6 | ||
|
|
a6a48dc7a3 | ||
|
|
1b951aa2d5 | ||
|
|
a66c6c9d23 | ||
|
|
dddcec4be3 | ||
|
|
1a290a38c4 | ||
|
|
dcdc70de49 | ||
|
|
f8b10a2c0a | ||
|
|
5960f51f13 | ||
|
|
59e0518e6d | ||
|
|
afc2953538 | ||
|
|
f58966acf8 | ||
|
|
cb44704d38 | ||
|
|
ab4177fae1 | ||
|
|
867662ba5a | ||
|
|
6cb4493b8e | ||
|
|
0444ab0bc5 | ||
|
|
51a2da7e05 | ||
|
|
d625e99dd0 | ||
|
|
43dca13f26 | ||
|
|
bc8c4a0323 | ||
|
|
d8e68255a0 | ||
|
|
781ec74310 | ||
|
|
1df60186f0 | ||
|
|
b8e297c5ba | ||
|
|
486ffed4bd | ||
|
|
cb703aea18 | ||
|
|
5084cb0887 | ||
|
|
5d6c12d900 | ||
|
|
2f47fddda9 | ||
|
|
42e2c53e5e | ||
|
|
8080752815 | ||
|
|
2dec484676 | ||
|
|
3d0a59cf74 | ||
|
|
5169568c3b | ||
|
|
44a5dc0cd0 | ||
|
|
1f38004114 | ||
|
|
8e7143556b | ||
|
|
2f519cba30 | ||
|
|
02444d801e | ||
|
|
85d4991cb3 | ||
|
|
4ae4bab254 | ||
|
|
3514d5c05c | ||
|
|
9236a36ef4 | ||
|
|
b2318ce957 | ||
|
|
3879e33cce | ||
|
|
eb6de90059 | ||
|
|
6b633efdba | ||
|
|
02cef8297c | ||
|
|
adb425aeb3 | ||
|
|
b1fa5be7b1 | ||
|
|
d7cfa4ee96 | ||
|
|
46a79f43bb | ||
|
|
5a71caf09c | ||
|
|
a4003d7d91 | ||
|
|
b35fe6cdb2 | ||
|
|
d728869690 | ||
|
|
6b6dd70110 | ||
|
|
fc9681f6d5 | ||
|
|
e4caa1d729 | ||
|
|
4a577fabfc | ||
|
|
314ad4ea4d | ||
|
|
2b446c75dd | ||
|
|
ecf22c2c50 | ||
|
|
6f234b57fc | ||
|
|
ddb6c810eb | ||
|
|
8f2c9cbd11 | ||
|
|
a4f0c1c04c | ||
|
|
7642db332a | ||
|
|
8e1f710312 | ||
|
|
83cae29dbe | ||
|
|
b2853cc56b | ||
|
|
d8c9941f6b | ||
|
|
716a73dfb4 | ||
|
|
cded1d3125 | ||
|
|
7b05fc4180 | ||
|
|
78e9280a93 | ||
|
|
ca2adb85b0 | ||
|
|
fca612e873 | ||
|
|
07e35780d3 | ||
|
|
521cbf9104 | ||
|
|
a6427364e0 | ||
|
|
c30ce6e73a | ||
|
|
e4abe46d16 | ||
|
|
71cf19b850 | ||
|
|
a734a045ae | ||
|
|
141da27715 | ||
|
|
7971b94001 | ||
|
|
95b3c6a594 | ||
|
|
0d849142ba | ||
|
|
f96c7379e0 | ||
|
|
6fb9dd961a | ||
|
|
a9c9b3cea8 | ||
|
|
ff2810654e | ||
|
|
80e4161b40 | ||
|
|
0473ce3259 | ||
|
|
0a211c1461 | ||
|
|
5573794a1f | ||
|
|
d0a1313f33 | ||
|
|
aca4f27ee8 | ||
|
|
bcd00004b8 | ||
|
|
eefc0a9632 | ||
|
|
dcf43b6fee | ||
|
|
6d218aaf0d | ||
|
|
20d80c1a2e | ||
|
|
24c4215820 | ||
|
|
0066b3f33a | ||
|
|
daf483309e | ||
|
|
49b1296d6e | ||
|
|
9f12f069ee | ||
|
|
10852a5d96 | ||
|
|
3347245c2e | ||
|
|
3e8e88c363 | ||
|
|
e4dfa45057 | ||
|
|
b58e90e8dd | ||
|
|
0e18cea11a | ||
|
|
e950932e43 | ||
|
|
45738773ca | ||
|
|
054bcc9cb8 | ||
|
|
4d49b749c5 | ||
|
|
4d86774266 | ||
|
|
20171fe4f2 | ||
|
|
308a47a784 | ||
|
|
2226bf0faa | ||
|
|
65cf8509f9 | ||
|
|
523ec7f453 | ||
|
|
8a1bc39eb2 | ||
|
|
fd1785fe65 | ||
|
|
45c22a24a6 | ||
|
|
c236293185 | ||
|
|
bfb6d4d142 | ||
|
|
723efe1755 | ||
|
|
e029547035 | ||
|
|
d9ede95cf7 | ||
|
|
70c3487bc7 | ||
|
|
808b7fb4dc | ||
|
|
ed1009096d | ||
|
|
580a2d7e45 | ||
|
|
87d3d6c577 | ||
|
|
ae87fa1785 | ||
|
|
2b00bc0fdb | ||
|
|
43b8ad80c7 | ||
|
|
65b462f62c | ||
|
|
7e7740cf77 | ||
|
|
a3d1b1403c | ||
|
|
31977e6523 | ||
|
|
9164713dd9 | ||
|
|
bfb01e3729 | ||
|
|
fc1709ba6c | ||
|
|
1b79aae836 | ||
|
|
6355fb3f3e | ||
|
|
c8a772d19a | ||
|
|
5bc44aef0f | ||
|
|
b455b67da3 | ||
|
|
351d70aafe | ||
|
|
8a2276f398 | ||
|
|
65552575f8 | ||
|
|
4c84a77053 | ||
|
|
6b810a1f72 | ||
|
|
c36bde0f2d | ||
|
|
1a44dd8a2b | ||
|
|
1c7b6bcf7d | ||
|
|
e2c6f5e393 | ||
|
|
85d5043992 | ||
|
|
47dfeafdc8 | ||
|
|
b843cef986 | ||
|
|
0e95691cde | ||
|
|
54aa14c4f5 | ||
|
|
dfcb3cc2ea | ||
|
|
587202ce43 | ||
|
|
6b2529bc80 | ||
|
|
52137f310a | ||
|
|
ad90145aa7 | ||
|
|
05f7ac0802 | ||
|
|
fccca823c5 | ||
|
|
441373ea13 | ||
|
|
57d2df4922 | ||
|
|
632e778376 | ||
|
|
d47b1503b2 | ||
|
|
938c75737b | ||
|
|
55a5d10859 | ||
|
|
0c354cf268 | ||
|
|
485600801c | ||
|
|
4916933139 | ||
|
|
73f1eb9c30 | ||
|
|
e788384d42 | ||
|
|
633d8df1a4 | ||
|
|
aff72ad983 | ||
|
|
c9763c4d70 | ||
|
|
931a13e505 | ||
|
|
97e76a88e3 | ||
|
|
b5be876e61 | ||
|
|
7370a8f296 | ||
|
|
11b773573e | ||
|
|
67dc2cb0fa | ||
|
|
bad9ecf3b1 | ||
|
|
ef835649fd | ||
|
|
e9bb56f3cf | ||
|
|
58acc9c2b7 | ||
|
|
f923a4ea9b | ||
|
|
5957dfecf0 | ||
|
|
aee61b35e4 | ||
|
|
169d5ab826 | ||
|
|
de312d87dc | ||
|
|
ecabd557a7 | ||
|
|
f246a01484 | ||
|
|
0617b87f36 | ||
|
|
715ac64ae6 | ||
|
|
78c0afe006 | ||
|
|
df03932f89 | ||
|
|
15196c847a | ||
|
|
b2b4471851 | ||
|
|
5ffb73c5f5 | ||
|
|
ef93fcc89e | ||
|
|
0af60d9a7e | ||
|
|
750803c3cc | ||
|
|
b318b0a288 | ||
|
|
2989af0a3f | ||
|
|
3f168772aa | ||
|
|
2ba25f096d | ||
|
|
6d35e19571 | ||
|
|
0d9583f7e7 | ||
|
|
fe6b18135c | ||
|
|
e89fe57def | ||
|
|
85b1d50945 | ||
|
|
856443319c | ||
|
|
9da4ff10da | ||
|
|
76831e9b9d | ||
|
|
997daf537e | ||
|
|
c7aadca25c | ||
|
|
6cbbd4d97f | ||
|
|
e4c5ec278d | ||
|
|
cce1e41519 | ||
|
|
b942050c4e | ||
|
|
d8d671e36f | ||
|
|
49adb8de0c | ||
|
|
fb6b60bee3 | ||
|
|
e0fca277f2 | ||
|
|
0effb5f8b0 | ||
|
|
1839746bf8 | ||
|
|
1a28c324f1 | ||
|
|
c1b28f58d0 | ||
|
|
565e4e0a2f | ||
|
|
7487da89a1 | ||
|
|
fe5d88585c | ||
|
|
bd6e62e9bf | ||
|
|
b76930d2a3 | ||
|
|
00d439f681 | ||
|
|
963cfbf380 | ||
|
|
031ea167e8 | ||
|
|
dde52f2bc8 | ||
|
|
46cc681eba | ||
|
|
b0619f4f01 | ||
|
|
2baf05acdb | ||
|
|
890870bf45 | ||
|
|
9da9c3aceb | ||
|
|
c8fedb0f70 | ||
|
|
a203f56bdb | ||
|
|
18880c40d5 | ||
|
|
bd62661ef3 | ||
|
|
8d285c03ad | ||
|
|
7a4ee78805 | ||
|
|
6105d2a36c | ||
|
|
7db90ba35e | ||
|
|
fb34b1674b | ||
|
|
eaf978da0a | ||
|
|
ecea572192 | ||
|
|
5552baa5e2 | ||
|
|
3b86ccc1a4 | ||
|
|
8fd81d1098 | ||
|
|
b7badede86 | ||
|
|
4c4e633395 | ||
|
|
1cd5e89f85 | ||
|
|
768050f36c | ||
|
|
f7f286db6c | ||
|
|
6d2ec59653 | ||
|
|
924d0111fd | ||
|
|
fe87838dbe | ||
|
|
1b2f0fc85d | ||
|
|
e3bec5f186 | ||
|
|
729b459701 | ||
|
|
1609bd5d07 | ||
|
|
78222a530c | ||
|
|
6613ee3c87 | ||
|
|
356b2f5ffb | ||
|
|
a52cc7280f | ||
|
|
0d38e3065c | ||
|
|
3d13d501e7 | ||
|
|
ccf1f6205c | ||
|
|
8d2b6df385 | ||
|
|
62fd13c892 | ||
|
|
cbf9f321c6 | ||
|
|
c975305e95 | ||
|
|
8afd12103d | ||
|
|
5d106afca6 | ||
|
|
8e43a23766 | ||
|
|
d9d72ad8df | ||
|
|
1c5af81a4e | ||
|
|
014fc4cda9 | ||
|
|
f29992741d | ||
|
|
5fa5f08607 | ||
|
|
d4921c4a2f | ||
|
|
64238062ca | ||
|
|
00f977fff9 | ||
|
|
c7ae2cd540 | ||
|
|
293d88b1b9 | ||
|
|
fa2d19a5ca | ||
|
|
f0f22041ca | ||
|
|
321316f99f | ||
|
|
4d915020a8 | ||
|
|
350eff27b7 | ||
|
|
f9732db799 | ||
|
|
73a7842a85 | ||
|
|
b13a402675 | ||
|
|
915cd5e4bc | ||
|
|
151adfd5ed | ||
|
|
37519a038b | ||
|
|
d0cc1b0b1d | ||
|
|
869ad9d561 | ||
|
|
b31a4d6242 | ||
|
|
439a855383 | ||
|
|
37f51690d0 | ||
|
|
1bd807a1a0 | ||
|
|
ac6fef2e29 | ||
|
|
e873086ddf | ||
|
|
dd6159b062 | ||
|
|
7511563865 | ||
|
|
9923216558 | ||
|
|
d026d21073 | ||
|
|
5bfe706b56 | ||
|
|
2407015620 | ||
|
|
a8dd9d4bfd | ||
|
|
8d247bd1b6 | ||
|
|
533666d40c | ||
|
|
b85ee0b7a0 | ||
|
|
9466038e62 | ||
|
|
e5eb9bf4f2 | ||
|
|
a3615ad0d3 | ||
|
|
2f6b5566d8 | ||
|
|
79b40cab14 | ||
|
|
6276b5d79e | ||
|
|
fac7ec1e00 | ||
|
|
356e5babd0 | ||
|
|
b2de090581 | ||
|
|
364ec1fa2c | ||
|
|
afc64b8287 | ||
|
|
5953f86c7e | ||
|
|
cfad012f92 | ||
|
|
2e8c2f40d6 | ||
|
|
377c805fe7 | ||
|
|
bbb97da3fc | ||
|
|
78fde6f812 | ||
|
|
09081c0d2d | ||
|
|
abeb507ea0 | ||
|
|
d8c2759a72 | ||
|
|
f0fc39e1d0 | ||
|
|
81d604d85a | ||
|
|
0c978a8def | ||
|
|
c6ac239c5a | ||
|
|
370ad6cdd7 | ||
|
|
2bcd725e04 | ||
|
|
0b487546bb | ||
|
|
67d8d832c9 | ||
|
|
fa99782f02 | ||
|
|
60a30518bc | ||
|
|
122fb5f9f1 | ||
|
|
5daf444c8a | ||
|
|
41fbb8cdc4 | ||
|
|
edfb69f8e9 | ||
|
|
14b0d8e7a6 | ||
|
|
a085ff855d | ||
|
|
b392b48b28 | ||
|
|
93355a6884 | ||
|
|
b28b30eb0f | ||
|
|
c0851fc643 | ||
|
|
de7592b351 | ||
|
|
5530bbad53 | ||
|
|
4f0067e408 | ||
|
|
b444c28a19 | ||
|
|
a4cc409c95 | ||
|
|
fcb08501c0 | ||
|
|
cb2d00cefc | ||
|
|
1cb9fc8a40 | ||
|
|
85cfc04bdb | ||
|
|
6555a3604f | ||
|
|
a97262d022 | ||
|
|
8ad54271e9 | ||
|
|
e5b9a20d09 | ||
|
|
0d37d182ea | ||
|
|
6690634a3f | ||
|
|
8f3730bae3 | ||
|
|
8f4e726774 | ||
|
|
5b8eda0f08 | ||
|
|
f5f62bbd71 | ||
|
|
24c3edc7ec | ||
|
|
0e3d46ef5e | ||
|
|
a3546b65f7 | ||
|
|
01b694b6ab | ||
|
|
3598f83091 | ||
|
|
2085dd7b02 | ||
|
|
65d916332d | ||
|
|
1937efce88 | ||
|
|
501d607b3a | ||
|
|
7d6670c59e | ||
|
|
fe608db16a | ||
|
|
be1f313d57 | ||
|
|
cb77c55d2c | ||
|
|
417aa35c60 | ||
|
|
18877eb676 | ||
|
|
a9670c9510 | ||
|
|
8474369575 | ||
|
|
4739d121a2 | ||
|
|
e47f5a2548 | ||
|
|
51f5628383 | ||
|
|
aa67a1b285 | ||
|
|
d22e51fd84 | ||
|
|
cde4af40fe | ||
|
|
a147755d13 | ||
|
|
7b6c293069 | ||
|
|
b3f1244641 | ||
|
|
e6423700b9 | ||
|
|
9462a26a05 | ||
|
|
c059a52d0e | ||
|
|
a221db8a59 | ||
|
|
df43ed0219 | ||
|
|
90430f52c6 | ||
|
|
4e7f0b4591 | ||
|
|
bda76c22ec | ||
|
|
d67223c321 | ||
|
|
21278ff595 | ||
|
|
21fd6b27e2 | ||
|
|
cc8d57b242 | ||
|
|
6081f4573c | ||
|
|
ea2cafa715 | ||
|
|
a34c993e31 | ||
|
|
1a5fc3a21a | ||
|
|
c15a9a72f5 | ||
|
|
5b35058338 | ||
|
|
a0ca6e18c8 | ||
|
|
1917004292 | ||
|
|
8ee3bb08bc | ||
|
|
7e96059fb5 | ||
|
|
4f7f06d316 | ||
|
|
448b5392be | ||
|
|
6f5f3e091a | ||
|
|
fa6a2069ce | ||
|
|
09fd371b9d | ||
|
|
a598745938 | ||
|
|
7751f693c8 | ||
|
|
7ade9ca43e | ||
|
|
061a66e437 | ||
|
|
39536e2727 | ||
|
|
38038626d4 | ||
|
|
c3d34abe89 | ||
|
|
baf5005998 | ||
|
|
107c3c0cf9 | ||
|
|
2d1bd37816 | ||
|
|
de017b15d0 | ||
|
|
3b0974ae3e | ||
|
|
cf6cbc16df | ||
|
|
bd60a8d9cd | ||
|
|
c77240c6b4 | ||
|
|
14d803c604 | ||
|
|
f764829ca9 | ||
|
|
418eedd7bd | ||
|
|
b9f1fe56c8 | ||
|
|
7e50a957ff | ||
|
|
137cff6127 | ||
|
|
807b99e5e5 | ||
|
|
e21c69f4e3 | ||
|
|
9f7daca86e | ||
|
|
1b89e274c9 | ||
|
|
dd768dc080 | ||
|
|
4aea481967 | ||
|
|
265629d127 | ||
|
|
cef0cb809f | ||
|
|
57fe1e27b6 | ||
|
|
83253eb7d0 | ||
|
|
9b5e8ff45d | ||
|
|
cdfacc6247 | ||
|
|
10d747cc8c | ||
|
|
a6b366602c | ||
|
|
80fb9dec3c | ||
|
|
68c86cf620 | ||
|
|
e550d48bcd | ||
|
|
1aaaa8919c | ||
|
|
72c2ffc40b | ||
|
|
f7ab2fb13a | ||
|
|
3a1272246f | ||
|
|
6039a33bf8 | ||
|
|
2d68fb2536 | ||
|
|
845df282ef | ||
|
|
1406dc28d9 | ||
|
|
67884dd255 | ||
|
|
2bf05ac631 | ||
|
|
8cb04e4737 | ||
|
|
733126591e | ||
|
|
d4d801c246 | ||
|
|
84ba32a8fe | ||
|
|
ea386d02b6 | ||
|
|
77cac63443 | ||
|
|
9350ee9479 | ||
|
|
025d156068 | ||
|
|
7a4aee592b | ||
|
|
f427c5e961 | ||
|
|
51af2d4a56 | ||
|
|
a68812b223 | ||
|
|
e05f8c7034 | ||
|
|
182377581a | ||
|
|
e647ae2ac4 | ||
|
|
1311da99ff | ||
|
|
8badf226a2 | ||
|
|
6909d6a541 | ||
|
|
e287dc9a32 | ||
|
|
152d0f3244 | ||
|
|
a6e2cfc90a | ||
|
|
18c30e4f12 | ||
|
|
3c4f4d302c | ||
|
|
2abebfbce7 | ||
|
|
0b517c51d8 | ||
|
|
9fbbda11b8 | ||
|
|
6f6831f812 | ||
|
|
d425bb31c4 | ||
|
|
334425a08f | ||
|
|
3e74da96a6 | ||
|
|
ad119d789b | ||
|
|
6c8d246af9 | ||
|
|
26b7a0b91d | ||
|
|
0b6c6227b9 | ||
|
|
94fd7673fd | ||
|
|
f598acb8fc | ||
|
|
b621205a06 | ||
|
|
9fa9c6a5d0 | ||
|
|
1a84051679 | ||
|
|
d987719889 | ||
|
|
96813c37b7 | ||
|
|
70f007525d | ||
|
|
e3496b0660 | ||
|
|
24b4c99635 | ||
|
|
27b4a8ba73 | ||
|
|
51b3f38f55 | ||
|
|
a35be4a666 | ||
|
|
5770d0c12d | ||
|
|
0629c584e1 | ||
|
|
480df323e5 | ||
|
|
a995b53c38 | ||
|
|
35fa50dbee | ||
|
|
d86c3f4d48 | ||
|
|
4696c0ebb6 | ||
|
|
09724e9787 | ||
|
|
636548cdec | ||
|
|
b3970808df | ||
|
|
d573b83c94 | ||
|
|
e63f072e40 | ||
|
|
a329147d28 | ||
|
|
18ba986eba | ||
|
|
8d9f418b2b | ||
|
|
623bac1a40 | ||
|
|
702d00da91 | ||
|
|
3a12472be8 | ||
|
|
6524449ad1 | ||
|
|
86cab26a69 | ||
|
|
3d068fe3cd | ||
|
|
f98236046b | ||
|
|
ed3bd4ef75 | ||
|
|
7d3ae7a91b | ||
|
|
0409c431b8 | ||
|
|
ffbb841b03 | ||
|
|
e9a7dbc2ff | ||
|
|
10dc8950c1 | ||
|
|
fe0fb1ccd2 | ||
|
|
e9170a1d4b | ||
|
|
02bd8581d8 | ||
|
|
ca574201a4 | ||
|
|
8e744d94e6 | ||
|
|
6a28330dd1 | ||
|
|
4537b52c18 | ||
|
|
29e61e24a6 | ||
|
|
041c8a4c2d | ||
|
|
433dfd8fa9 | ||
|
|
2b46043419 | ||
|
|
d31c8b0190 | ||
|
|
9003fdc1a2 | ||
|
|
b1f4a2853e | ||
|
|
07412f047d | ||
|
|
26ac21b908 | ||
|
|
4cc496a8e5 | ||
|
|
4f4e0881b5 | ||
|
|
9fe164665c | ||
|
|
c74193b5d7 | ||
|
|
31ef06ef2b | ||
|
|
83a95d66d1 | ||
|
|
4451b76f89 | ||
|
|
a1075b63ec | ||
|
|
97c41228e0 | ||
|
|
8903d2abcb | ||
|
|
328e13fbfe | ||
|
|
b7cd5fec76 | ||
|
|
6086dbcd84 | ||
|
|
5f88e02aa3 | ||
|
|
96a4f585cd | ||
|
|
73ec980e01 | ||
|
|
e5ed7ce0d3 | ||
|
|
08a7b8afb7 | ||
|
|
bb7a588f6b | ||
|
|
9faa0734c1 | ||
|
|
cf55b34b4e | ||
|
|
5881899cc2 | ||
|
|
4e64ef8ab3 | ||
|
|
7e5532ac84 | ||
|
|
3d638df08c | ||
|
|
bf984a38ed | ||
|
|
e68f2ce141 | ||
|
|
d0a3244108 | ||
|
|
d09901d512 | ||
|
|
2d46bac351 | ||
|
|
2285c76cbf | ||
|
|
c003ab4e42 | ||
|
|
78e97a217a | ||
|
|
720585170c | ||
|
|
19d54f3f4d | ||
|
|
23a0aec1e6 | ||
|
|
6b0db01c13 | ||
|
|
93c14c3a1f | ||
|
|
b66760fc5c | ||
|
|
64a801cc55 | ||
|
|
35fc8ee3e8 | ||
|
|
887c566f7c | ||
|
|
2f59499087 | ||
|
|
b4a239569c | ||
|
|
e4073a844b | ||
|
|
f313ad37b3 | ||
|
|
8de69c639a | ||
|
|
0714dbee0d | ||
|
|
ead8a836be | ||
|
|
d471e66073 | ||
|
|
4ddef1f60b | ||
|
|
7b9da896e8 | ||
|
|
41786f4ab8 | ||
|
|
4661da729f | ||
|
|
97dc40a585 | ||
|
|
f2082f3f52 | ||
|
|
f87c8ced3f | ||
|
|
f914eea8ae | ||
|
|
b41d239301 | ||
|
|
8bb1a1cb5a | ||
|
|
2f61bc0b05 | ||
|
|
d22557947a | ||
|
|
3e44d07541 | ||
|
|
f56b27e1c7 | ||
|
|
12075df3ba | ||
|
|
a8bb9620e2 | ||
|
|
9ed4e21429 | ||
|
|
5b293d675f | ||
|
|
5972d6576d | ||
|
|
19ce514b5c | ||
|
|
144ed80c56 | ||
|
|
4d34e56589 | ||
|
|
9045770192 | ||
|
|
4ea21d2a9c | ||
|
|
774a188d19 | ||
|
|
bd5c125561 | ||
|
|
420feea0aa | ||
|
|
b298f547f9 | ||
|
|
a7fe76c336 | ||
|
|
9f777ba152 | ||
|
|
cc3b56ddcb | ||
|
|
0c42942a88 | ||
|
|
0803c6f3fa | ||
|
|
02d9d37c1e | ||
|
|
c121e9219c | ||
|
|
297d9aaa32 | ||
|
|
11644cbc31 | ||
|
|
4c6be15edc | ||
|
|
e1028e4dd8 | ||
|
|
861ff1c91f | ||
|
|
80bb0b4aff | ||
|
|
06d238a9f9 | ||
|
|
71ce28d9e6 | ||
|
|
c48429e5c3 | ||
|
|
34e3f7bbaf | ||
|
|
db624460bc | ||
|
|
16c12f816b | ||
|
|
ea6fed56a2 | ||
|
|
22f11f1a97 | ||
|
|
7c21ccb8f9 | ||
|
|
8f86b0eac2 | ||
|
|
9c8fa32e5c | ||
|
|
9d348c6da2 | ||
|
|
4dc87240f9 | ||
|
|
a60d11a763 | ||
|
|
391cc77996 | ||
|
|
7a3287fa25 | ||
|
|
32244b2641 | ||
|
|
122fdc69e3 | ||
|
|
39e4e47763 | ||
|
|
2ea4dc9d7e | ||
|
|
b2590e7c9a | ||
|
|
af6fe6baa0 | ||
|
|
ce799dadbe | ||
|
|
217e6f88d9 | ||
|
|
a363baffce | ||
|
|
bbe47d81e9 | ||
|
|
a105b41647 | ||
|
|
fc8919adce | ||
|
|
f21877ae27 | ||
|
|
99e7967e22 | ||
|
|
766fe9d845 | ||
|
|
2c60faee26 | ||
|
|
097f1d4695 | ||
|
|
a6efc3952f | ||
|
|
dadd76bd62 | ||
|
|
282c0c2655 | ||
|
|
14f2391f49 | ||
|
|
b5860190e3 | ||
|
|
d8ecb88867 | ||
|
|
f5b2efdc87 | ||
|
|
fab26180cb | ||
|
|
3968d40bf4 | ||
|
|
cb2d1cde36 | ||
|
|
da7a9b7232 | ||
|
|
4f15225665 | ||
|
|
90708c123b | ||
|
|
384f467d4a | ||
|
|
37064f20d1 | ||
|
|
9e579f9de3 | ||
|
|
b2c688ef14 | ||
|
|
9717acd988 | ||
|
|
d06c5b12c2 | ||
|
|
e97a120602 | ||
|
|
5b806b08dd | ||
|
|
fd5dfcc6d8 | ||
|
|
3979317b10 | ||
|
|
8d2595a6db | ||
|
|
3c2c452501 | ||
|
|
af48f86e55 | ||
|
|
73957ea14e | ||
|
|
bb824e9167 | ||
|
|
b996e77606 | ||
|
|
9a20bbd4e1 | ||
|
|
8195b7565f | ||
|
|
0569f9b242 | ||
|
|
8ffa8ea2c8 | ||
|
|
fd7cff6109 | ||
|
|
a3b292066a | ||
|
|
8f6d38468e | ||
|
|
4af5cc66ba | ||
|
|
33c3c7e106 | ||
|
|
5c75f12b78 | ||
|
|
1ae6638861 | ||
|
|
d8999471c5 | ||
|
|
90c0de1a7f | ||
|
|
d13ea1cbbe | ||
|
|
03cf28fccd | ||
|
|
8e757d2099 | ||
|
|
2989732637 | ||
|
|
db45068357 | ||
|
|
735aea86e0 | ||
|
|
d8c8c6d2f3 | ||
|
|
3b4cb47597 | ||
|
|
f55e758d47 | ||
|
|
c5a5e5600a | ||
|
|
6989e8b8cf | ||
|
|
7d2e550b84 | ||
|
|
7f17c45b69 | ||
|
|
b0c86ab8db | ||
|
|
4c0c2c75c6 | ||
|
|
1549b9b506 | ||
|
|
057eeb3629 | ||
|
|
0dea4e8b7d | ||
|
|
d3573a565c | ||
|
|
1275b49ebb | ||
|
|
56f9e16a8b | ||
|
|
a4b0954532 | ||
|
|
fc73787849 | ||
|
|
30a5493414 | ||
|
|
a729bdfbe6 | ||
|
|
dab88e482d | ||
|
|
6482f67a0c | ||
|
|
a1bf95ec2c | ||
|
|
6961fde327 | ||
|
|
c0fe0420fc | ||
|
|
2ba000a987 | ||
|
|
a90e93e150 | ||
|
|
b6ab12d3c1 | ||
|
|
71ccd87435 | ||
|
|
d07045f134 | ||
|
|
bede4a0aa1 | ||
|
|
de1cff356a | ||
|
|
1bee098fb6 | ||
|
|
e36e175e08 | ||
|
|
9db45d2fcb | ||
|
|
558f5d0c8a | ||
|
|
e32a887091 | ||
|
|
1b9a6c3c59 | ||
|
|
aef03b5592 | ||
|
|
3eaeb533e9 | ||
|
|
04cc94a450 | ||
|
|
dae7be076d | ||
|
|
3cb7573edb | ||
|
|
a96a5de12d | ||
|
|
45b6c8dad3 | ||
|
|
cf17ebac33 | ||
|
|
f0a34fdb5e | ||
|
|
e124115e8d | ||
|
|
249b8498d9 | ||
|
|
15c69e3b7d | ||
|
|
98208b8eec | ||
|
|
0690e73320 | ||
|
|
766ac7e500 | ||
|
|
51ac57c657 | ||
|
|
89603586da | ||
|
|
a35f5a1650 | ||
|
|
f1df29d27e | ||
|
|
08c24e2705 | ||
|
|
b1171864e3 | ||
|
|
5af59cecda | ||
|
|
0c3a38b24b | ||
|
|
ac5d163aa0 | ||
|
|
dfe2dbea6d | ||
|
|
909ffc187b | ||
|
|
92dfa99059 | ||
|
|
0065876702 | ||
|
|
23bf28702f | ||
|
|
066873bd06 | ||
|
|
98c00bd8b1 | ||
|
|
fd47b03fac | ||
|
|
8e689c39f4 | ||
|
|
738fa9150e | ||
|
|
5405e182c3 | ||
|
|
ab1326f858 | ||
|
|
f013815b2a | ||
|
|
5b24fc2543 | ||
|
|
b103e40ba8 | ||
|
|
d5c9a5cf3c | ||
|
|
30d7425b98 | ||
|
|
34819b289d | ||
|
|
71d9ebd859 | ||
|
|
c1910d47f0 | ||
|
|
769d354792 | ||
|
|
a7678e779e | ||
|
|
294f74b209 | ||
|
|
fa8b4a4203 | ||
|
|
7205862dbf | ||
|
|
37bc47c772 | ||
|
|
baaa8ba2c1 | ||
|
|
05f8e2445a | ||
|
|
753b003107 | ||
|
|
97092c91db | ||
|
|
20859d2796 | ||
|
|
06f8943bc4 | ||
|
|
e797a67e97 | ||
|
|
a1eca58d7a | ||
|
|
aefe97e09e | ||
|
|
59ae901f57 | ||
|
|
811f484d3b | ||
|
|
ff08b99190 | ||
|
|
44dc4efe57 | ||
|
|
f7e2ac83f2 | ||
|
|
7e60162d65 | ||
|
|
cd06ee4544 | ||
|
|
6d0a777de6 | ||
|
|
dd7a48a00c | ||
|
|
582dcef097 | ||
|
|
b9501d7b77 | ||
|
|
a523fcf804 | ||
|
|
cd07745af1 | ||
|
|
6c15881bfe | ||
|
|
7ff358ee00 | ||
|
|
79e5fad326 | ||
|
|
93f5e966b2 | ||
|
|
d0e9c004a0 | ||
|
|
4814a47560 | ||
|
|
3c81d91072 | ||
|
|
de21f9a1f9 | ||
|
|
9f4dab89a5 | ||
|
|
9def3df16f | ||
|
|
44dd56e344 | ||
|
|
e630bd06db | ||
|
|
1fbd4937bc | ||
|
|
cc54bdddc6 | ||
|
|
f750455519 | ||
|
|
3d383bcc57 | ||
|
|
cdab6eaa5d | ||
|
|
7937cb6ea3 | ||
|
|
57f5236c9b | ||
|
|
f7bdd0e7f6 | ||
|
|
a108e385fe | ||
|
|
6549c9878b | ||
|
|
a3a760e1e6 | ||
|
|
576b9be78c | ||
|
|
528548eb8c | ||
|
|
9a2415e34e | ||
|
|
c9b7162a5f | ||
|
|
7fd9ab5e88 | ||
|
|
b44edbd90e | ||
|
|
a1b3703a0d | ||
|
|
874dffc13f | ||
|
|
8b572dc63f | ||
|
|
659b29a62d | ||
|
|
7a558898e1 | ||
|
|
7dee553558 | ||
|
|
9f6f18466a | ||
|
|
ef003366da | ||
|
|
aaaadc2a47 | ||
|
|
f94287c9ae | ||
|
|
c56bfdca67 | ||
|
|
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 |
21
.devcontainer/Dockerfile
Normal file
21
.devcontainer/Dockerfile
Normal file
@@ -0,0 +1,21 @@
|
||||
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.233.0/containers/python-3/.devcontainer/base.Dockerfile
|
||||
|
||||
# [Choice] Python version (use -bullseye variants on local arm64/Apple Silicon): 3, 3.10, 3.9, 3.8, 3.7, 3.6, 3-bullseye, 3.10-bullseye, 3.9-bullseye, 3.8-bullseye, 3.7-bullseye, 3.6-bullseye, 3-buster, 3.10-buster, 3.9-buster, 3.8-buster, 3.7-buster, 3.6-buster
|
||||
ARG VARIANT="3.10-bullseye"
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT}
|
||||
|
||||
# [Choice] Node.js version: none, lts/*, 16, 14, 12, 10
|
||||
ARG NODE_VERSION="none"
|
||||
RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi
|
||||
|
||||
# [Optional] If your pip requirements rarely change, uncomment this section to add them to the image.
|
||||
# COPY requirements.txt /tmp/pip-tmp/
|
||||
# RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt \
|
||||
# && rm -rf /tmp/pip-tmp
|
||||
|
||||
# [Optional] Uncomment this section to install additional OS packages.
|
||||
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||
# && apt-get -y install --no-install-recommends <your-package-list-here>
|
||||
|
||||
# [Optional] Uncomment this line to install global node packages.
|
||||
# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g <your-package-here>" 2>&1
|
||||
51
.devcontainer/devcontainer.json
Normal file
51
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,51 @@
|
||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
|
||||
// https://github.com/microsoft/vscode-dev-containers/tree/v0.233.0/containers/python-3
|
||||
{
|
||||
"name": "Python 3",
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile",
|
||||
"context": "..",
|
||||
"args": {
|
||||
// Update 'VARIANT' to pick a Python version: 3, 3.10, 3.9, 3.8, 3.7, 3.6
|
||||
// Append -bullseye or -buster to pin to an OS version.
|
||||
// Use -bullseye variants on local on arm64/Apple Silicon.
|
||||
"VARIANT": "3.10",
|
||||
// Options
|
||||
"NODE_VERSION": "none"
|
||||
}
|
||||
},
|
||||
|
||||
// Set *default* container specific settings.json values on container create.
|
||||
"settings": {
|
||||
"python.defaultInterpreterPath": "/usr/local/bin/python",
|
||||
"python.linting.enabled": true,
|
||||
"python.linting.pylintEnabled": true,
|
||||
"python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8",
|
||||
"python.formatting.blackPath": "/usr/local/py-utils/bin/black",
|
||||
"python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf",
|
||||
"python.linting.banditPath": "/usr/local/py-utils/bin/bandit",
|
||||
"python.linting.flake8Path": "/usr/local/py-utils/bin/flake8",
|
||||
"python.linting.mypyPath": "/usr/local/py-utils/bin/mypy",
|
||||
"python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle",
|
||||
"python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle",
|
||||
"python.linting.pylintPath": "/usr/local/py-utils/bin/pylint"
|
||||
},
|
||||
|
||||
// Add the IDs of extensions you want installed when the container is created.
|
||||
"extensions": [
|
||||
"ms-python.python",
|
||||
"ms-python.vscode-pylance"
|
||||
],
|
||||
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
// "forwardPorts": [],
|
||||
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
"postCreateCommand": "git submodule update --init && pip3 install --user -e .[dev] && pre-commit install",
|
||||
|
||||
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
|
||||
"remoteUser": "vscode",
|
||||
"features": {
|
||||
"git": "latest"
|
||||
}
|
||||
}
|
||||
9
.gitattributes
vendored
Normal file
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
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/
|
||||
|
||||
407
.github/CONTRIBUTING.md
vendored
407
.github/CONTRIBUTING.md
vendored
@@ -1,197 +1,210 @@
|
||||
# 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 [Mandiant Organization](https://github.com/mandiant) 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/mandiant/capa)
|
||||
- [capa-rules](https://github.com/mandiant/capa-rules)
|
||||
- [capa-testfiles](https://github.com/mandiant/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/mandiant/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/mandiant/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/mandiant/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/mandiant/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/mandiant/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/mandiant/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/mandiant/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:
|
||||
|
||||
0. Sign the [Contributor License Agreement](#contributor-license-agreement)
|
||||
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.
|
||||
|
||||
### Contributor License Agreement
|
||||
|
||||
Contributions to this project must be accompanied by a Contributor License
|
||||
Agreement. You (or your employer) retain the copyright to your contribution,
|
||||
this simply gives us permission to use and redistribute your contributions as
|
||||
part of the project. Head over to <https://cla.developers.google.com/> to see
|
||||
your current agreements on file or to sign a new one.
|
||||
|
||||
You generally only need to submit a CLA once, so if you've already submitted one
|
||||
(even if it was for a different project), you probably don't need to do it
|
||||
again.
|
||||
|
||||
## 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/mandiant/capa/blob/master/scripts/lint.py), and
|
||||
2. be formatted with [capafmt](https://github.com/mandiant/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
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/mandiant/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/mandiant/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/mandiant/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/mandiant/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
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/mandiant/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/mandiant/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/mandiant/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/mandiant/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
BIN
.github/capa-explorer-logo.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
BIN
.github/capa-ida.jpg
vendored
BIN
.github/capa-ida.jpg
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 453 KiB |
6
.github/dependabot.yml
vendored
Normal file
6
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
41
.github/flake8.ini
vendored
Normal file
41
.github/flake8.ini
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
[flake8]
|
||||
max-line-length = 120
|
||||
|
||||
extend-ignore =
|
||||
# E203: whitespace before ':' (black does this)
|
||||
E203,
|
||||
# F401: `foo` imported but unused (prefer ruff)
|
||||
F401,
|
||||
# F811 Redefinition of unused `foo` (prefer ruff)
|
||||
F811,
|
||||
# E501 line too long (prefer black)
|
||||
E501,
|
||||
# B010 Do not call setattr with a constant attribute value
|
||||
B010,
|
||||
# G200 Logging statement uses exception in arguments
|
||||
G200,
|
||||
# SIM102 Use a single if-statement instead of nested if-statements
|
||||
# doesn't provide a space for commenting or logical separation of conditions
|
||||
SIM102,
|
||||
# SIM114 Use logical or and a single body
|
||||
# makes logic trees too complex
|
||||
SIM114,
|
||||
# SIM117 Use 'with Foo, Bar:' instead of multiple with statements
|
||||
# makes lines too long
|
||||
SIM117
|
||||
|
||||
per-file-ignores =
|
||||
# T201 print found.
|
||||
#
|
||||
# scripts are meant to print output
|
||||
scripts/*: T201
|
||||
# capa.exe is meant to print output
|
||||
capa/main.py: T201
|
||||
# IDA tests emit results to output window so need to print
|
||||
tests/test_ida_features.py: T201
|
||||
# utility used to find the Binary Ninja API via invoking python.exe
|
||||
capa/features/extractors/binja/find_binja_api.py: T201
|
||||
|
||||
copyright-check = True
|
||||
copyright-min-file-size = 1
|
||||
copyright-regexp = Copyright \(C\) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
91
.github/mypy/mypy.ini
vendored
Normal file
91
.github/mypy/mypy.ini
vendored
Normal file
@@ -0,0 +1,91 @@
|
||||
[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-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_auto.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-ida_bytes.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-ida_nalt.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-ida_kernwin.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-ida_settings.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-ida_funcs.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-ida_loader.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-ida_segment.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-PyQt5.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-binaryninja.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-pytest.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-devtools.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-elftools.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-dncil.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-netnode.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-ghidra.*]
|
||||
ignore_missing_imports = True
|
||||
22
.github/pull_request_template.md
vendored
Normal file
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/mandiant/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
|
||||
172
.github/pyinstaller/hooks/hook-vivisect.py
vendored
172
.github/pyinstaller/hooks/hook-vivisect.py
vendored
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
|
||||
|
||||
from PyInstaller.utils.hooks import copy_metadata
|
||||
|
||||
@@ -13,3 +13,173 @@ 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.emulation",
|
||||
"vivisect.analysis.amd64.golang",
|
||||
"vivisect.analysis.crypto",
|
||||
"vivisect.analysis.crypto.constants",
|
||||
"vivisect.analysis.elf",
|
||||
"vivisect.analysis.elf.elfplt",
|
||||
"vivisect.analysis.elf.elfplt_late",
|
||||
"vivisect.analysis.elf.libc_start_main",
|
||||
"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.linker",
|
||||
"vivisect.analysis.generic.mkpointers",
|
||||
"vivisect.analysis.generic.noret",
|
||||
"vivisect.analysis.generic.pointers",
|
||||
"vivisect.analysis.generic.pointertables",
|
||||
"vivisect.analysis.generic.relocations",
|
||||
"vivisect.analysis.generic.strconst",
|
||||
"vivisect.analysis.generic.switchcase",
|
||||
"vivisect.analysis.generic.symswitchcase",
|
||||
"vivisect.analysis.generic.thunks",
|
||||
"vivisect.analysis.i386",
|
||||
"vivisect.analysis.i386.calling",
|
||||
"vivisect.analysis.i386.golang",
|
||||
"vivisect.analysis.i386.importcalls",
|
||||
"vivisect.analysis.i386.instrhook",
|
||||
"vivisect.analysis.i386.thunk_reg",
|
||||
"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.advapi_32",
|
||||
"vivisect.impapi.windows.advapi_64",
|
||||
"vivisect.impapi.windows.amd64",
|
||||
"vivisect.impapi.windows.gdi_32",
|
||||
"vivisect.impapi.windows.gdi_64",
|
||||
"vivisect.impapi.windows.i386",
|
||||
"vivisect.impapi.windows.kernel_32",
|
||||
"vivisect.impapi.windows.kernel_64",
|
||||
"vivisect.impapi.windows.msvcr100_32",
|
||||
"vivisect.impapi.windows.msvcr100_64",
|
||||
"vivisect.impapi.windows.msvcr110_32",
|
||||
"vivisect.impapi.windows.msvcr110_64",
|
||||
"vivisect.impapi.windows.msvcr120_32",
|
||||
"vivisect.impapi.windows.msvcr120_64",
|
||||
"vivisect.impapi.windows.msvcr71_32",
|
||||
"vivisect.impapi.windows.msvcr80_32",
|
||||
"vivisect.impapi.windows.msvcr80_64",
|
||||
"vivisect.impapi.windows.msvcr90_32",
|
||||
"vivisect.impapi.windows.msvcr90_64",
|
||||
"vivisect.impapi.windows.msvcrt_32",
|
||||
"vivisect.impapi.windows.msvcrt_64",
|
||||
"vivisect.impapi.windows.ntdll_32",
|
||||
"vivisect.impapi.windows.ntdll_64",
|
||||
"vivisect.impapi.windows.ole_32",
|
||||
"vivisect.impapi.windows.ole_64",
|
||||
"vivisect.impapi.windows.rpcrt4_32",
|
||||
"vivisect.impapi.windows.rpcrt4_64",
|
||||
"vivisect.impapi.windows.shell_32",
|
||||
"vivisect.impapi.windows.shell_64",
|
||||
"vivisect.impapi.windows.user_32",
|
||||
"vivisect.impapi.windows.user_64",
|
||||
"vivisect.impapi.windows.ws2plus_32",
|
||||
"vivisect.impapi.windows.ws2plus_64",
|
||||
"vivisect.impapi.winkern",
|
||||
"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",
|
||||
]
|
||||
|
||||
207
.github/pyinstaller/pyinstaller.spec
vendored
207
.github/pyinstaller/pyinstaller.spec
vendored
@@ -1,175 +1,41 @@
|
||||
# -*- mode: python -*-
|
||||
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
|
||||
import os.path
|
||||
import subprocess
|
||||
|
||||
import wcwidth
|
||||
|
||||
|
||||
# when invoking pyinstaller from the project root,
|
||||
# this gets run from the project root.
|
||||
with open('./capa/version.py', 'wb') as f:
|
||||
# git output will look like:
|
||||
#
|
||||
# tags/v1.0.0-0-g3af38dc
|
||||
# ------- tag
|
||||
# - commits since
|
||||
# g------- git hash fragment
|
||||
version = (subprocess.check_output(["git", "describe", "--always", "--tags", "--long"])
|
||||
.strip()
|
||||
.replace("tags/", ""))
|
||||
f.write("__version__ = '%s'" % version)
|
||||
|
||||
a = Analysis(
|
||||
# when invoking pyinstaller from the project root,
|
||||
# this gets invoked from the directory of the spec file,
|
||||
# i.e. ./.github/pyinstaller
|
||||
['../../capa/main.py'],
|
||||
pathex=['capa'],
|
||||
["../../capa/main.py"],
|
||||
pathex=["capa"],
|
||||
binaries=None,
|
||||
datas=[
|
||||
# when invoking pyinstaller from the project root,
|
||||
# this gets invoked from the directory of the spec file,
|
||||
# i.e. ./.github/pyinstaller
|
||||
('../../rules', 'rules'),
|
||||
|
||||
("../../assets", "assets"),
|
||||
("../../rules", "rules"),
|
||||
("../../sigs", "sigs"),
|
||||
("../../cache", "cache"),
|
||||
# capa.render.default uses tabulate that depends on wcwidth.
|
||||
# it seems wcwidth uses a json file `version.json`
|
||||
# and this doesn't get picked up by pyinstaller automatically.
|
||||
# so we manually embed the wcwidth resources here.
|
||||
#
|
||||
# ref: https://stackoverflow.com/a/62278462/87207
|
||||
(os.path.dirname(wcwidth.__file__), 'wcwidth')
|
||||
],
|
||||
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",
|
||||
(os.path.dirname(wcwidth.__file__), "wcwidth"),
|
||||
],
|
||||
# when invoking pyinstaller from the project root,
|
||||
# this gets run from the project root.
|
||||
hookspath=['.github/pyinstaller/hooks'],
|
||||
hookspath=[".github/pyinstaller/hooks"],
|
||||
runtime_hooks=None,
|
||||
excludes=[
|
||||
# ignore packages that would otherwise be bundled with the .exe.
|
||||
# review: build/pyinstaller/xref-pyinstaller.html
|
||||
|
||||
# we don't do any GUI stuff, so ignore these modules
|
||||
"tkinter",
|
||||
"_tkinter",
|
||||
@@ -179,35 +45,52 @@ 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",
|
||||
"binaryninja",
|
||||
],
|
||||
)
|
||||
|
||||
a.binaries = a.binaries - TOC([
|
||||
('tcl85.dll', None, None),
|
||||
('tk85.dll', None, None),
|
||||
('_tkinter', None, None)])
|
||||
a.binaries = a.binaries - TOC([("tcl85.dll", None, None), ("tk85.dll", None, None), ("_tkinter", None, None)])
|
||||
|
||||
pyz = PYZ(a.pure, a.zipped_data)
|
||||
|
||||
exe = EXE(pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
exclude_binaries=False,
|
||||
name='capa',
|
||||
icon='logo.ico',
|
||||
debug=False,
|
||||
strip=None,
|
||||
upx=True,
|
||||
console=True )
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
exclude_binaries=False,
|
||||
name="capa",
|
||||
icon="logo.ico",
|
||||
debug=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
console=True,
|
||||
)
|
||||
|
||||
# enable the following to debug the contents of the .exe
|
||||
#
|
||||
#coll = COLLECT(exe,
|
||||
# coll = COLLECT(exe,
|
||||
# a.binaries,
|
||||
# a.zipfiles,
|
||||
# a.datas,
|
||||
# strip=None,
|
||||
# upx=True,
|
||||
# name='capa-dat')
|
||||
|
||||
|
||||
43
.github/ruff.toml
vendored
Normal file
43
.github/ruff.toml
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
# Enable the pycodestyle (`E`) and Pyflakes (`F`) rules by default.
|
||||
# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or
|
||||
# McCabe complexity (`C901`) by default.
|
||||
select = ["E", "F"]
|
||||
|
||||
# Allow autofix for all enabled rules (when `--fix`) is provided.
|
||||
fixable = ["ALL"]
|
||||
unfixable = []
|
||||
|
||||
# E402 module level import not at top of file
|
||||
# E722 do not use bare 'except'
|
||||
# E501 line too long
|
||||
ignore = ["E402", "E722", "E501"]
|
||||
|
||||
line-length = 120
|
||||
|
||||
exclude = [
|
||||
# Exclude a variety of commonly ignored directories.
|
||||
".bzr",
|
||||
".direnv",
|
||||
".eggs",
|
||||
".git",
|
||||
".git-rewrite",
|
||||
".hg",
|
||||
".mypy_cache",
|
||||
".nox",
|
||||
".pants.d",
|
||||
".pytype",
|
||||
".ruff_cache",
|
||||
".svn",
|
||||
".tox",
|
||||
".venv",
|
||||
"__pypackages__",
|
||||
"_build",
|
||||
"buck-out",
|
||||
"build",
|
||||
"dist",
|
||||
"node_modules",
|
||||
"venv",
|
||||
# protobuf generated files
|
||||
"*_pb2.py",
|
||||
"*_pb2.pyi"
|
||||
]
|
||||
10
.github/tox.ini
vendored
10
.github/tox.ini
vendored
@@ -1,10 +0,0 @@
|
||||
[pycodestyle]
|
||||
; E402: module level import not at top of file
|
||||
; W503: line break before binary operator
|
||||
; E231 missing whitespace after ',' (emitted by black)
|
||||
; E203 whitespace before ':' (emitted by black)
|
||||
ignore = E402,W503,E203,E231
|
||||
max-line-length = 160
|
||||
statistics = True
|
||||
count = True
|
||||
exclude = .*
|
||||
212
.github/workflows/build.yml
vendored
212
.github/workflows/build.yml
vendored
@@ -1,77 +1,135 @@
|
||||
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:
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
release:
|
||||
types: [edited, published]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: PyInstaller for ${{ matrix.os }} / Py ${{ matrix.python_version }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
# set to false for debugging
|
||||
fail-fast: true
|
||||
matrix:
|
||||
# using Python 3.8 to support running across multiple operating systems including Windows 7
|
||||
include:
|
||||
- os: ubuntu-20.04
|
||||
# use old linux so that the shared library versioning is more portable
|
||||
artifact_name: capa
|
||||
asset_name: linux
|
||||
python_version: 3.8
|
||||
- os: ubuntu-20.04
|
||||
artifact_name: capa
|
||||
asset_name: linux-py311
|
||||
python_version: 3.11
|
||||
- os: windows-2019
|
||||
artifact_name: capa.exe
|
||||
asset_name: windows
|
||||
python_version: 3.8
|
||||
- os: macos-11
|
||||
# use older macOS for assumed better portability
|
||||
artifact_name: capa
|
||||
asset_name: macos
|
||||
python_version: 3.8
|
||||
steps:
|
||||
- name: Checkout capa
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
with:
|
||||
submodules: true
|
||||
- name: Set up Python ${{ matrix.python_version }}
|
||||
uses: actions/setup-python@d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435 # v4.5.0
|
||||
with:
|
||||
python-version: ${{ matrix.python_version }}
|
||||
- if: matrix.os == 'ubuntu-20.04'
|
||||
run: sudo apt-get install -y libyaml-dev
|
||||
- name: Upgrade pip, setuptools
|
||||
run: python -m pip install --upgrade pip setuptools
|
||||
- name: Install capa with build requirements
|
||||
run: pip install -e .[build]
|
||||
- name: Cache the rule set
|
||||
run: python ./scripts/cache-ruleset.py ./rules/ ./cache/
|
||||
- name: Build standalone executable
|
||||
run: pyinstaller --log-level DEBUG .github/pyinstaller/pyinstaller.spec
|
||||
- name: Does it run (PE)?
|
||||
run: dist/capa "tests/data/Practical Malware Analysis Lab 01-01.dll_"
|
||||
- name: Does it run (Shellcode)?
|
||||
run: dist/capa "tests/data/499c2a85f6e8142c3f48d4251c9c7cd6.raw32"
|
||||
- name: Does it run (ELF)?
|
||||
run: dist/capa "tests/data/7351f8a40c5450557b24622417fc478d.elf_"
|
||||
- name: Does it run (CAPE)?
|
||||
run: |
|
||||
7z e "tests/data/dynamic/cape/v2.2/d46900384c78863420fb3e297d0a2f743cd2b6b3f7f82bf64059a168e07aceb7.json.gz"
|
||||
dist/capa "d46900384c78863420fb3e297d0a2f743cd2b6b3f7f82bf64059a168e07aceb7.json"
|
||||
- uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
|
||||
with:
|
||||
name: ${{ matrix.asset_name }}
|
||||
path: dist/${{ matrix.artifact_name }}
|
||||
|
||||
test_run:
|
||||
name: Test run on ${{ matrix.os }} / ${{ matrix.asset_name }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: [build]
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
# OSs not already tested above
|
||||
- os: ubuntu-22.04
|
||||
artifact_name: capa
|
||||
asset_name: linux
|
||||
- os: ubuntu-22.04
|
||||
artifact_name: capa
|
||||
asset_name: linux-py311
|
||||
- os: windows-2022
|
||||
artifact_name: capa.exe
|
||||
asset_name: windows
|
||||
steps:
|
||||
- name: Download ${{ matrix.asset_name }}
|
||||
uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
|
||||
with:
|
||||
name: ${{ matrix.asset_name }}
|
||||
- name: Set executable flag
|
||||
if: matrix.os != 'windows-2022'
|
||||
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: linux-py311
|
||||
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@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
|
||||
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@2728235f7dc9ff598bd86ce3c274b74f802d2208 # v2
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN}}
|
||||
file: ${{ env.zip_name }}
|
||||
tag: ${{ github.ref }}
|
||||
|
||||
43
.github/workflows/changelog.yml
vendored
Normal file
43
.github/workflows/changelog.yml
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
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]
|
||||
|
||||
permissions: read-all
|
||||
|
||||
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@e0c398b7065a8d84700c471b6afc4116d1ba4e96 # v2.2.0
|
||||
- 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@0cf4e8a17ba79344ed3fdd7fed6dd0311d08a9d4 # v0.1.0
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
event: REQUEST_CHANGES
|
||||
body: "Please add bug fixes, new features, breaking changes and anything else you think is worthwhile mentioning to the `master (unreleased)` section of CHANGELOG.md. If no CHANGELOG update is needed add the following to the PR description: `${{ env.NO_CHANGELOG }}`"
|
||||
allow_duplicate: false
|
||||
- name: Dismiss previous review if CHANGELOG update
|
||||
uses: Ana06/automatic-pull-request-review@0cf4e8a17ba79344ed3fdd7fed6dd0311d08a9d4 # v0.1.0
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
event: DISMISS
|
||||
body: "CHANGELOG updated or no update needed, thanks! :smile:"
|
||||
21
.github/workflows/pip-audit.yml
vendored
Normal file
21
.github/workflows/pip-audit.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: PIP audit
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 8 * * 1'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.11"]
|
||||
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- uses: pypa/gh-action-pip-audit@v1.0.8
|
||||
with:
|
||||
inputs: .
|
||||
41
.github/workflows/publish.yml
vendored
Normal file
41
.github/workflows/publish.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
# use PyPI trusted publishing, as described here:
|
||||
# https://blog.trailofbits.com/2023/05/23/trusted-publishing-a-new-benchmark-for-packaging-security/
|
||||
name: publish to pypi
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
pypi-publish:
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: release
|
||||
permissions:
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435 # v4.5.0
|
||||
with:
|
||||
python-version: '3.8'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -e .[build]
|
||||
- name: build package
|
||||
run: |
|
||||
python -m build
|
||||
- name: upload package artifacts
|
||||
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
|
||||
with:
|
||||
path: dist/*
|
||||
- name: publish package
|
||||
uses: pypa/gh-action-pypi-publish@f5622bde02b04381239da3573277701ceca8f6a0 # release/v1
|
||||
with:
|
||||
skip-existing: true
|
||||
verbose: true
|
||||
print-hash: true
|
||||
72
.github/workflows/scorecard.yml
vendored
Normal file
72
.github/workflows/scorecard.yml
vendored
Normal file
@@ -0,0 +1,72 @@
|
||||
# This workflow uses actions that are not certified by GitHub. They are provided
|
||||
# by a third-party and are governed by separate terms of service, privacy
|
||||
# policy, and support documentation.
|
||||
|
||||
name: Scorecard supply-chain security
|
||||
on:
|
||||
# For Branch-Protection check. Only the default branch is supported. See
|
||||
# https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection
|
||||
branch_protection_rule:
|
||||
# To guarantee Maintained check is occasionally updated. See
|
||||
# https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained
|
||||
schedule:
|
||||
- cron: '43 4 * * 3'
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
|
||||
# Declare default permissions as read only.
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
analysis:
|
||||
name: Scorecard analysis
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
# Needed to upload the results to code-scanning dashboard.
|
||||
security-events: write
|
||||
# Needed to publish results and get a badge (see publish_results below).
|
||||
id-token: write
|
||||
# Uncomment the permissions below if installing in a private repository.
|
||||
# contents: read
|
||||
# actions: read
|
||||
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # v3.1.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: "Run analysis"
|
||||
uses: ossf/scorecard-action@99c53751e09b9529366343771cc321ec74e9bd3d # v2.0.6
|
||||
with:
|
||||
results_file: results.sarif
|
||||
results_format: sarif
|
||||
# (Optional) "write" PAT token. Uncomment the `repo_token` line below if:
|
||||
# - you want to enable the Branch-Protection check on a *public* repository, or
|
||||
# - you are installing Scorecard on a *private* repository
|
||||
# To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat.
|
||||
# repo_token: ${{ secrets.SCORECARD_TOKEN }}
|
||||
|
||||
# Public repositories:
|
||||
# - Publish results to OpenSSF REST API for easy access by consumers
|
||||
# - Allows the repository to include the Scorecard badge.
|
||||
# - See https://github.com/ossf/scorecard-action#publishing-results.
|
||||
# For private repositories:
|
||||
# - `publish_results` will always be set to `false`, regardless
|
||||
# of the value entered here.
|
||||
publish_results: true
|
||||
|
||||
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
|
||||
# format to the repository Actions tab.
|
||||
- name: "Upload artifact"
|
||||
uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 # v3.1.0
|
||||
with:
|
||||
name: SARIF file
|
||||
path: results.sarif
|
||||
retention-days: 5
|
||||
|
||||
# Upload the results to GitHub's code scanning dashboard.
|
||||
- name: "Upload to code-scanning"
|
||||
uses: github/codeql-action/upload-sarif@807578363a7869ca324a79039e6db9c843e0e100 # v2.1.27
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
32
.github/workflows/tag.yml
vendored
Normal file
32
.github/workflows/tag.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
name: tag
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
tag:
|
||||
name: Tag capa rules
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout capa-rules
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
with:
|
||||
repository: mandiant/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@mandiant.com'
|
||||
git config user.name 'Capa Bot'
|
||||
name=${{ github.event.release.tag_name }}
|
||||
git tag $name -m "https://github.com/mandiant/capa/releases/$name"
|
||||
# TODO update branch name-major=${name%%.*}
|
||||
- name: Push tag to capa-rules
|
||||
uses: ad-m/github-push-action@0fafdd62b84042d49ec0cb92d9cac7f7ce4ec79e # master
|
||||
with:
|
||||
repository: mandiant/capa-rules
|
||||
github_token: ${{ secrets.CAPA_TOKEN }}
|
||||
tags: true
|
||||
198
.github/workflows/tests.yml
vendored
198
.github/workflows/tests.yml
vendored
@@ -6,63 +6,199 @@ on:
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
permissions: read-all
|
||||
|
||||
# save workspaces to speed up testing
|
||||
env:
|
||||
CAPA_SAVE_WORKSPACE: "True"
|
||||
|
||||
jobs:
|
||||
code_style:
|
||||
runs-on: ubuntu-latest
|
||||
changelog_format:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout capa
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
# The sync GH action in capa-rules relies on a single '- *$' in the CHANGELOG file
|
||||
- name: Ensure CHANGELOG has '- *$'
|
||||
run: |
|
||||
number=$(grep '\- *$' CHANGELOG.md | wc -l)
|
||||
if [ $number != 1 ]; then exit 1; fi
|
||||
|
||||
code_style:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout capa
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
# use latest available python to take advantage of best performance
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435 # v4.5.0
|
||||
with:
|
||||
python-version: 3.8
|
||||
python-version: "3.11"
|
||||
- name: Install dependencies
|
||||
run: pip install 'isort==5.*' black
|
||||
run: pip install -e .[dev]
|
||||
- name: Lint with ruff
|
||||
run: pre-commit run ruff
|
||||
- name: Lint with isort
|
||||
run: isort --profile black --length-sort --line-width 120 -c .
|
||||
run: pre-commit run isort --show-diff-on-failure
|
||||
- name: Lint with black
|
||||
run: black -l 120 --check .
|
||||
run: pre-commit run black --show-diff-on-failure
|
||||
- name: Lint with flake8
|
||||
run: pre-commit run flake8 --hook-stage manual
|
||||
- name: Check types with mypy
|
||||
run: pre-commit run mypy --hook-stage manual
|
||||
|
||||
rule_linter:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout capa with rules submodule
|
||||
uses: actions/checkout@v2
|
||||
- name: Checkout capa with submodules
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
with:
|
||||
submodules: true
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/setup-python@v2
|
||||
submodules: recursive
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435 # v4.5.0
|
||||
with:
|
||||
python-version: 3.8
|
||||
# We don't need vivisect, so we can install capa using Python3
|
||||
python-version: "3.11"
|
||||
- name: Install capa
|
||||
run: pip install -e .
|
||||
run: pip install -e .[dev]
|
||||
- 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-11]
|
||||
# across all operating systems
|
||||
python-version: ["3.8", "3.11"]
|
||||
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.8"
|
||||
- os: ubuntu-20.04
|
||||
python-version: "3.9"
|
||||
- os: ubuntu-20.04
|
||||
python-version: "3.10"
|
||||
steps:
|
||||
- name: Checkout capa with submodules
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
with:
|
||||
submodules: true
|
||||
- name: Set up Python ${{ matrix.python }}
|
||||
uses: actions/setup-python@v2
|
||||
submodules: recursive
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435 # v4.5.0
|
||||
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 (fast)
|
||||
# this set of tests runs about 80% of the cases in 20% of the time,
|
||||
# and should catch most errors quickly.
|
||||
run: pre-commit run pytest-fast --all-files --hook-stage manual
|
||||
- name: Run tests
|
||||
run: pytest tests/
|
||||
run: pytest -v tests/
|
||||
|
||||
binja-tests:
|
||||
name: Binary Ninja tests for ${{ matrix.python-version }}
|
||||
env:
|
||||
BN_SERIAL: ${{ secrets.BN_SERIAL }}
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [tests]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.8", "3.11"]
|
||||
steps:
|
||||
- name: Checkout capa with submodules
|
||||
# do only run if BN_SERIAL is available, have to do this in every step, see https://github.com/orgs/community/discussions/26726#discussioncomment-3253118
|
||||
if: ${{ env.BN_SERIAL != 0 }}
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
if: ${{ env.BN_SERIAL != 0 }}
|
||||
uses: actions/setup-python@d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435 # v4.5.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install pyyaml
|
||||
if: ${{ env.BN_SERIAL != 0 }}
|
||||
run: sudo apt-get install -y libyaml-dev
|
||||
- name: Install capa
|
||||
if: ${{ env.BN_SERIAL != 0 }}
|
||||
run: pip install -e .[dev]
|
||||
- name: install Binary Ninja
|
||||
if: ${{ env.BN_SERIAL != 0 }}
|
||||
run: |
|
||||
mkdir ./.github/binja
|
||||
curl "https://raw.githubusercontent.com/Vector35/binaryninja-api/6812c97/scripts/download_headless.py" -o ./.github/binja/download_headless.py
|
||||
python ./.github/binja/download_headless.py --serial ${{ env.BN_SERIAL }} --output .github/binja/BinaryNinja-headless.zip
|
||||
unzip .github/binja/BinaryNinja-headless.zip -d .github/binja/
|
||||
python .github/binja/binaryninja/scripts/install_api.py --install-on-root --silent
|
||||
- name: Run tests
|
||||
if: ${{ env.BN_SERIAL != 0 }}
|
||||
env:
|
||||
BN_LICENSE: ${{ secrets.BN_LICENSE }}
|
||||
run: pytest -v tests/test_binja_features.py # explicitly refer to the binja tests for performance. other tests run above.
|
||||
|
||||
ghidra-tests:
|
||||
name: Ghidra tests for ${{ matrix.python-version }}
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [tests]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.8", "3.11"]
|
||||
java-version: ["17"]
|
||||
gradle-version: ["7.3"]
|
||||
ghidra-version: ["10.3"]
|
||||
public-version: ["PUBLIC_20230510"] # for ghidra releases
|
||||
jep-version: ["4.1.1"]
|
||||
ghidrathon-version: ["3.0.0"]
|
||||
steps:
|
||||
- name: Checkout capa with submodules
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
with:
|
||||
submodules: true
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435 # v4.5.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Set up Java ${{ matrix.java-version }}
|
||||
uses: actions/setup-java@5ffc13f4174014e2d4d4572b3d74c3fa61aeb2c2 # v3
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: ${{ matrix.java-version }}
|
||||
- name: Set up Gradle ${{ matrix.gradle-version }}
|
||||
uses: gradle/gradle-build-action@40b6781dcdec2762ad36556682ac74e31030cfe2 # v2.5.1
|
||||
with:
|
||||
gradle-version: ${{ matrix.gradle-version }}
|
||||
- name: Install Jep ${{ matrix.jep-version }}
|
||||
run : pip install jep==${{ matrix.jep-version }}
|
||||
- name: Install Ghidra ${{ matrix.ghidra-version }}
|
||||
run: |
|
||||
mkdir ./.github/ghidra
|
||||
wget "https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_${{ matrix.ghidra-version }}_build/ghidra_${{ matrix.ghidra-version }}_${{ matrix.public-version }}.zip" -O ./.github/ghidra/ghidra_${{ matrix.ghidra-version }}_PUBLIC.zip
|
||||
unzip .github/ghidra/ghidra_${{ matrix.ghidra-version }}_PUBLIC.zip -d .github/ghidra/
|
||||
- name: Install Ghidrathon
|
||||
run : |
|
||||
mkdir ./.github/ghidrathon
|
||||
curl -o ./.github/ghidrathon/ghidrathon-${{ matrix.ghidrathon-version }}.zip "https://codeload.github.com/mandiant/Ghidrathon/zip/refs/tags/v${{ matrix.ghidrathon-version }}"
|
||||
unzip .github/ghidrathon/ghidrathon-${{ matrix.ghidrathon-version }}.zip -d .github/ghidrathon/
|
||||
gradle -p ./.github/ghidrathon/Ghidrathon-${{ matrix.ghidrathon-version }}/ -PGHIDRA_INSTALL_DIR=$(pwd)/.github/ghidra/ghidra_${{ matrix.ghidra-version }}_PUBLIC
|
||||
unzip .github/ghidrathon/Ghidrathon-${{ matrix.ghidrathon-version }}/dist/*.zip -d .github/ghidra/ghidra_${{ matrix.ghidra-version }}_PUBLIC/Ghidra/Extensions
|
||||
- name: Install pyyaml
|
||||
run: sudo apt-get install -y libyaml-dev
|
||||
- name: Install capa
|
||||
run: pip install -e .[dev]
|
||||
- name: Run tests
|
||||
run: |
|
||||
mkdir ./.github/ghidra/project
|
||||
.github/ghidra/ghidra_${{ matrix.ghidra-version }}_PUBLIC/support/analyzeHeadless .github/ghidra/project ghidra_test -Import ./tests/data/mimikatz.exe_ -ScriptPath ./tests/ -PostScript test_ghidra_features.py > ../output.log
|
||||
cat ../output.log
|
||||
exit_code=$(cat ../output.log | grep exit | awk '{print $NF}')
|
||||
exit $exit_code
|
||||
|
||||
20
.gitignore
vendored
20
.gitignore
vendored
@@ -108,9 +108,21 @@ venv.bak/
|
||||
*.viv
|
||||
*.idb
|
||||
*.i64
|
||||
.vscode
|
||||
|
||||
!rules/lib
|
||||
|
||||
# hooks/ci.sh output
|
||||
isort-output.log
|
||||
black-output.log
|
||||
rule-linter-output.log
|
||||
scripts/perf/*.txt
|
||||
scripts/perf/*.svg
|
||||
scripts/perf/*.zip
|
||||
|
||||
.direnv
|
||||
.envrc
|
||||
.DS_Store
|
||||
*/.DS_Store
|
||||
Pipfile
|
||||
Pipfile.lock
|
||||
/cache/
|
||||
.github/binja/binaryninja
|
||||
.github/binja/download_headless.py
|
||||
.github/binja/BinaryNinja-headless.zip
|
||||
|
||||
2
.gitmodules
vendored
2
.gitmodules
vendored
@@ -1,6 +1,8 @@
|
||||
[submodule "rules"]
|
||||
path = rules
|
||||
url = ../capa-rules.git
|
||||
branch = dynamic-syntax
|
||||
[submodule "tests/data"]
|
||||
path = tests/data
|
||||
url = ../capa-testfiles.git
|
||||
branch = dynamic-feature-extractor
|
||||
|
||||
129
.pre-commit-config.yaml
Normal file
129
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,129 @@
|
||||
# install the pre-commit hooks:
|
||||
#
|
||||
# ❯ pre-commit install --hook-type pre-commit
|
||||
# pre-commit installed at .git/hooks/pre-commit
|
||||
#
|
||||
# ❯ pre-commit install --hook-type pre-push
|
||||
# pre-commit installed at .git/hooks/pre-push
|
||||
#
|
||||
# run all linters liks:
|
||||
#
|
||||
# ❯ pre-commit run --all-files
|
||||
# isort....................................................................Passed
|
||||
# black....................................................................Passed
|
||||
# ruff.....................................................................Passed
|
||||
# flake8...................................................................Passed
|
||||
# mypy.....................................................................Passed
|
||||
#
|
||||
# run a single linter like:
|
||||
#
|
||||
# ❯ pre-commit run --all-files isort
|
||||
# isort....................................................................Passed
|
||||
|
||||
repos:
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: isort
|
||||
name: isort
|
||||
stages: [commit, push, manual]
|
||||
language: system
|
||||
entry: isort
|
||||
args:
|
||||
- "--length-sort"
|
||||
- "--profile"
|
||||
- "black"
|
||||
- "--line-length=120"
|
||||
- "--skip-glob"
|
||||
- "*_pb2.py"
|
||||
- "capa/"
|
||||
- "scripts/"
|
||||
- "tests/"
|
||||
always_run: true
|
||||
pass_filenames: false
|
||||
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: black
|
||||
name: black
|
||||
stages: [commit, push, manual]
|
||||
language: system
|
||||
entry: black
|
||||
args:
|
||||
- "--line-length=120"
|
||||
- "--extend-exclude"
|
||||
- ".*_pb2.py"
|
||||
- "capa/"
|
||||
- "scripts/"
|
||||
- "tests/"
|
||||
always_run: true
|
||||
pass_filenames: false
|
||||
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: ruff
|
||||
name: ruff
|
||||
stages: [commit, push, manual]
|
||||
language: system
|
||||
entry: ruff
|
||||
args:
|
||||
- "check"
|
||||
- "--config"
|
||||
- ".github/ruff.toml"
|
||||
- "capa/"
|
||||
- "scripts/"
|
||||
- "tests/"
|
||||
always_run: true
|
||||
pass_filenames: false
|
||||
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: flake8
|
||||
name: flake8
|
||||
stages: [push, manual]
|
||||
language: system
|
||||
entry: flake8
|
||||
args:
|
||||
- "--config"
|
||||
- ".github/flake8.ini"
|
||||
- "--extend-exclude"
|
||||
- "capa/render/proto/capa_pb2.py"
|
||||
- "capa/"
|
||||
- "scripts/"
|
||||
- "tests/"
|
||||
always_run: true
|
||||
pass_filenames: false
|
||||
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: mypy
|
||||
name: mypy
|
||||
stages: [push, manual]
|
||||
language: system
|
||||
entry: mypy
|
||||
args:
|
||||
- "--check-untyped-defs"
|
||||
- "--ignore-missing-imports"
|
||||
- "--config-file=.github/mypy/mypy.ini"
|
||||
- "capa/"
|
||||
- "scripts/"
|
||||
- "tests/"
|
||||
always_run: true
|
||||
pass_filenames: false
|
||||
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: pytest-fast
|
||||
name: pytest (fast)
|
||||
stages: [manual]
|
||||
language: system
|
||||
entry: pytest
|
||||
args:
|
||||
- "tests/"
|
||||
- "--ignore=tests/test_binja_features.py"
|
||||
- "--ignore=tests/test_ghidra_features.py"
|
||||
- "--ignore=tests/test_ida_features.py"
|
||||
- "--ignore=tests/test_viv_features.py"
|
||||
- "--ignore=tests/test_main.py"
|
||||
- "--ignore=tests/test_scripts.py"
|
||||
always_run: true
|
||||
pass_filenames: false
|
||||
1815
CHANGELOG.md
1815
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -187,7 +187,7 @@
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright (C) 2020 FireEye, Inc.
|
||||
Copyright (C) 2023 Mandiant, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
215
README.md
215
README.md
@@ -1,14 +1,21 @@
|
||||

|
||||

|
||||
|
||||
[](https://github.com/fireeye/capa/actions?query=workflow%3ACI+event%3Apush+branch%3Amaster)
|
||||
[](https://github.com/fireeye/capa-rules)
|
||||
[](https://pypi.org/project/flare-capa)
|
||||
[](https://github.com/mandiant/capa/releases)
|
||||
[](https://github.com/mandiant/capa-rules)
|
||||
[](https://github.com/mandiant/capa/actions?query=workflow%3ACI+event%3Apush+branch%3Amaster)
|
||||
[](https://github.com/mandiant/capa/releases)
|
||||
[](LICENSE.txt)
|
||||
|
||||
capa detects capabilities in executable files.
|
||||
You run it against a PE file or shellcode and it tells you what it thinks the program can do.
|
||||
You run it against a PE, ELF, .NET module, shellcode file, or a sandbox report and it tells you what it thinks the program can do.
|
||||
For example, it might suggest that the file is a backdoor, is capable of installing services, or relies on HTTP to communicate.
|
||||
|
||||
Check out the overview in our first [capa blog post](https://www.fireeye.com/blog/threat-research/2020/07/capa-automatically-identify-malware-capabilities.html).
|
||||
Check out:
|
||||
- the overview in our first [capa blog post](https://www.mandiant.com/resources/capa-automatically-identify-malware-capabilities)
|
||||
- the major version 2.0 updates described in our [second blog post](https://www.mandiant.com/resources/capa-2-better-stronger-faster)
|
||||
- the major version 3.0 (ELF support) described in the [third blog post](https://www.mandiant.com/resources/elfant-in-the-room-capa-v3)
|
||||
- the major version 4.0 (.NET support) described in the [fourth blog post](https://www.mandiant.com/resources/blog/capa-v4-casting-wider-net)
|
||||
|
||||
```
|
||||
$ capa.exe suspicious.exe
|
||||
@@ -60,18 +67,11 @@ $ capa.exe suspicious.exe
|
||||
|
||||
# download and usage
|
||||
|
||||
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.
|
||||
Download stable releases of the standalone capa binaries [here](https://github.com/mandiant/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/mandiant/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/mandiant/capa/blob/master/doc/usage.md).
|
||||
|
||||
# example
|
||||
|
||||
@@ -88,34 +88,133 @@ 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
|
||||
author matthew.williams@fireeye.com
|
||||
author matthew.williams@mandiant.com
|
||||
scope function
|
||||
att&ck Execution::Command and Scripting Interpreter::Windows Command Shell [T1059.003]
|
||||
references https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/ns-processthreadsapi-startupinfoa
|
||||
examples Practical Malware Analysis Lab 14-02.exe_:0x4011C0
|
||||
function @ 0x10003A13
|
||||
function @ 0x4011C0
|
||||
and:
|
||||
match: create a process with modified I/O handles and window @ 0x10003A13
|
||||
match: create a process with modified I/O handles and window @ 0x4011C0
|
||||
and:
|
||||
number: 257 = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW @ 0x4012B8
|
||||
or:
|
||||
api: kernel32.CreateProcess @ 0x10003D6D
|
||||
number: 0x101 @ 0x10003B03
|
||||
or:
|
||||
number: 0x44 @ 0x10003ADC
|
||||
optional:
|
||||
api: kernel32.GetStartupInfo @ 0x10003AE4
|
||||
match: create pipe @ 0x10003A13
|
||||
number: 68 = StartupInfo.cb (size) @ 0x401282
|
||||
or: = API functions that accept a pointer to a STARTUPINFO structure
|
||||
api: kernel32.CreateProcess @ 0x401343
|
||||
match: create pipe @ 0x4011C0
|
||||
or:
|
||||
api: kernel32.CreatePipe @ 0x10003ACB
|
||||
api: kernel32.CreatePipe @ 0x40126F, 0x401280
|
||||
optional:
|
||||
match: create thread @ 0x40136A, 0x4013BA
|
||||
or:
|
||||
and:
|
||||
os: windows
|
||||
or:
|
||||
api: kernel32.CreateThread @ 0x4013D7
|
||||
or:
|
||||
and:
|
||||
os: windows
|
||||
or:
|
||||
api: kernel32.CreateThread @ 0x401395
|
||||
or:
|
||||
string: cmd.exe /c @ 0x10003AED
|
||||
string: "cmd.exe" @ 0x4012FD
|
||||
...
|
||||
```
|
||||
|
||||
Additionally, capa also supports analyzing [CAPE](https://github.com/kevoreilly/CAPEv2) sandbox reports for dynamic capabilty extraction.
|
||||
In order to use this, you first submit your sample to CAPE for analysis, and then run capa against the generated report (JSON).
|
||||
|
||||
Here's an example of running capa against a packed binary, and then running capa against the CAPE report of that binary:
|
||||
|
||||
```yaml
|
||||
$ capa 05be49819139a3fdcdbddbdefd298398779521f3d68daa25275cc77508e42310.exe
|
||||
WARNING:capa.capabilities.common:--------------------------------------------------------------------------------
|
||||
WARNING:capa.capabilities.common: This sample appears to be packed.
|
||||
WARNING:capa.capabilities.common:
|
||||
WARNING:capa.capabilities.common: Packed samples have often been obfuscated to hide their logic.
|
||||
WARNING:capa.capabilities.common: capa cannot handle obfuscation well using static analysis. This means the results may be misleading or incomplete.
|
||||
WARNING:capa.capabilities.common: If possible, you should try to unpack this input file before analyzing it with capa.
|
||||
WARNING:capa.capabilities.common: Alternatively, run the sample in a supported sandbox and invoke capa against the report to obtain dynamic analysis results.
|
||||
WARNING:capa.capabilities.common:
|
||||
WARNING:capa.capabilities.common: Identified via rule: (internal) packer file limitation
|
||||
WARNING:capa.capabilities.common:
|
||||
WARNING:capa.capabilities.common: Use -v or -vv if you really want to see the capabilities identified by capa.
|
||||
WARNING:capa.capabilities.common:--------------------------------------------------------------------------------
|
||||
|
||||
$ capa 05be49819139a3fdcdbddbdefd298398779521f3d68daa25275cc77508e42310.json
|
||||
|
||||
┍━━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┑
|
||||
│ ATT&CK Tactic │ ATT&CK Technique │
|
||||
┝━━━━━━━━━━━━━━━━━━━━━━━━┿━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┥
|
||||
│ CREDENTIAL ACCESS │ Credentials from Password Stores T1555 │
|
||||
├────────────────────────┼────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ DEFENSE EVASION │ File and Directory Permissions Modification T1222 │
|
||||
│ │ Modify Registry T1112 │
|
||||
│ │ Obfuscated Files or Information T1027 │
|
||||
│ │ Virtualization/Sandbox Evasion::User Activity Based Checks T1497.002 │
|
||||
├────────────────────────┼────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ DISCOVERY │ Account Discovery T1087 │
|
||||
│ │ Application Window Discovery T1010 │
|
||||
│ │ File and Directory Discovery T1083 │
|
||||
│ │ Query Registry T1012 │
|
||||
│ │ System Information Discovery T1082 │
|
||||
│ │ System Location Discovery::System Language Discovery T1614.001 │
|
||||
│ │ System Owner/User Discovery T1033 │
|
||||
├────────────────────────┼────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ EXECUTION │ System Services::Service Execution T1569.002 │
|
||||
├────────────────────────┼────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ PERSISTENCE │ Boot or Logon Autostart Execution::Registry Run Keys / Startup Folder T1547.001 │
|
||||
│ │ Boot or Logon Autostart Execution::Winlogon Helper DLL T1547.004 │
|
||||
│ │ Create or Modify System Process::Windows Service T1543.003 │
|
||||
┕━━━━━━━━━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┙
|
||||
|
||||
┍━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┑
|
||||
│ Capability │ Namespace │
|
||||
┝━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┿━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┥
|
||||
│ check for unmoving mouse cursor (3 matches) │ anti-analysis/anti-vm/vm-detection │
|
||||
│ gather bitkinex information │ collection/file-managers │
|
||||
│ gather classicftp information │ collection/file-managers │
|
||||
│ gather filezilla information │ collection/file-managers │
|
||||
│ gather total-commander information │ collection/file-managers │
|
||||
│ gather ultrafxp information │ collection/file-managers │
|
||||
│ resolve DNS (23 matches) │ communication/dns │
|
||||
│ initialize Winsock library (7 matches) │ communication/socket │
|
||||
│ act as TCP client (3 matches) │ communication/tcp/client │
|
||||
│ create new key via CryptAcquireContext │ data-manipulation/encryption │
|
||||
│ encrypt or decrypt via WinCrypt │ data-manipulation/encryption │
|
||||
│ hash data via WinCrypt │ data-manipulation/hashing │
|
||||
│ initialize hashing via WinCrypt │ data-manipulation/hashing │
|
||||
│ hash data with MD5 │ data-manipulation/hashing/md5 │
|
||||
│ generate random numbers via WinAPI │ data-manipulation/prng │
|
||||
│ extract resource via kernel32 functions (2 matches) │ executable/resource │
|
||||
│ interact with driver via control codes (2 matches) │ host-interaction/driver │
|
||||
│ get Program Files directory (18 matches) │ host-interaction/file-system │
|
||||
│ get common file path (575 matches) │ host-interaction/file-system │
|
||||
│ create directory (2 matches) │ host-interaction/file-system/create │
|
||||
│ delete file │ host-interaction/file-system/delete │
|
||||
│ get file attributes (122 matches) │ host-interaction/file-system/meta │
|
||||
│ set file attributes (8 matches) │ host-interaction/file-system/meta │
|
||||
│ move file │ host-interaction/file-system/move │
|
||||
│ find taskbar (3 matches) │ host-interaction/gui/taskbar/find │
|
||||
│ get keyboard layout (12 matches) │ host-interaction/hardware/keyboard │
|
||||
│ get disk size │ host-interaction/hardware/storage │
|
||||
│ get hostname (4 matches) │ host-interaction/os/hostname │
|
||||
│ allocate or change RWX memory (3 matches) │ host-interaction/process/inject │
|
||||
│ query or enumerate registry key (3 matches) │ host-interaction/registry │
|
||||
│ query or enumerate registry value (8 matches) │ host-interaction/registry │
|
||||
│ delete registry key │ host-interaction/registry/delete │
|
||||
│ start service │ host-interaction/service/start │
|
||||
│ get session user name │ host-interaction/session │
|
||||
│ persist via Run registry key │ persistence/registry/run │
|
||||
│ persist via Winlogon Helper DLL registry key │ persistence/registry/winlogon-helper │
|
||||
│ persist via Windows service (2 matches) │ persistence/service │
|
||||
┕━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┙
|
||||
```
|
||||
|
||||
capa uses a collection of rules to identify capabilities within a program.
|
||||
These rules are easy to write, even for those new to reverse engineering.
|
||||
By authoring rules, you can extend the capabilities that capa recognizes.
|
||||
@@ -126,40 +225,52 @@ Here's an example rule used by capa:
|
||||
```yaml
|
||||
rule:
|
||||
meta:
|
||||
name: hash data with CRC32
|
||||
namespace: data-manipulation/checksum/crc32
|
||||
author: moritz.raabe@fireeye.com
|
||||
scope: function
|
||||
name: create TCP socket
|
||||
namespace: communication/socket/tcp
|
||||
authors:
|
||||
- william.ballenthin@mandiant.com
|
||||
- joakim@intezer.com
|
||||
- anushka.virgaonkar@mandiant.com
|
||||
scopes:
|
||||
static: basic block
|
||||
dynamic: call
|
||||
mbc:
|
||||
- Communication::Socket Communication::Create TCP Socket [C0001.011]
|
||||
examples:
|
||||
- 2D3EDC218A90F03089CC01715A9F047F:0x403CBD
|
||||
- 7D28CB106CB54876B2A5C111724A07CD:0x402350 # RtlComputeCrc32
|
||||
- Practical Malware Analysis Lab 01-01.dll_:0x10001010
|
||||
features:
|
||||
- or:
|
||||
- and:
|
||||
- mnemonic: shr
|
||||
- number: 0xEDB88320
|
||||
- number: 8
|
||||
- characteristic: nzxor
|
||||
- api: RtlComputeCrc32
|
||||
- number: 6 = IPPROTO_TCP
|
||||
- number: 1 = SOCK_STREAM
|
||||
- number: 2 = AF_INET
|
||||
- or:
|
||||
- api: ws2_32.socket
|
||||
- api: ws2_32.WSASocket
|
||||
- api: socket
|
||||
- property/read: System.Net.Sockets.TcpClient::Client
|
||||
```
|
||||
|
||||
The [github.com/fireeye/capa-rules](https://github.com/fireeye/capa-rules) repository contains hundreds of standard library rules that are distributed with capa.
|
||||
The [github.com/mandiant/capa-rules](https://github.com/mandiant/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/mandiant/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.
|
||||
|
||||

|
||||

|
||||
|
||||
If you use Ghidra, you can use the Python 3 [Ghidra feature extractor](/capa/ghidra/). This integration enables capa to extract features directly from your Ghidra database, which can help you identify capabilities in programs that you analyze using Ghidra.
|
||||
|
||||
# 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/mandiant/capa/blob/master/doc/installation.md)
|
||||
- [Usage](https://github.com/mandiant/capa/blob/master/doc/usage.md)
|
||||
- [Limitations](https://github.com/mandiant/capa/blob/master/doc/limitations.md)
|
||||
- [Contributing Guide](https://github.com/mandiant/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-rules repository](https://github.com/mandiant/capa-rules)
|
||||
- [capa-rules rule format](https://github.com/mandiant/capa-rules/blob/master/doc/format.md)
|
||||
|
||||
## capa testfiles
|
||||
The [capa-testfiles repository](https://github.com/mandiant/capa-testfiles) contains the data we use to test capa's code and rules
|
||||
|
||||
BIN
assets/classes.json.gz
Normal file
BIN
assets/classes.json.gz
Normal file
Binary file not shown.
BIN
assets/interfaces.json.gz
Normal file
BIN
assets/interfaces.json.gz
Normal file
Binary file not shown.
79
capa/capabilities/common.py
Normal file
79
capa/capabilities/common.py
Normal file
@@ -0,0 +1,79 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
import logging
|
||||
import itertools
|
||||
import collections
|
||||
from typing import Any, Tuple
|
||||
|
||||
from capa.rules import Scope, RuleSet
|
||||
from capa.engine import FeatureSet, MatchResults
|
||||
from capa.features.address import NO_ADDRESS
|
||||
from capa.features.extractors.base_extractor import FeatureExtractor, StaticFeatureExtractor, DynamicFeatureExtractor
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def find_file_capabilities(ruleset: RuleSet, extractor: FeatureExtractor, function_features: FeatureSet):
|
||||
file_features: FeatureSet = collections.defaultdict(set)
|
||||
|
||||
for feature, va in itertools.chain(extractor.extract_file_features(), extractor.extract_global_features()):
|
||||
# not all file features may have virtual addresses.
|
||||
# if not, then at least ensure the feature shows up in the index.
|
||||
# the set of addresses will still be empty.
|
||||
if va:
|
||||
file_features[feature].add(va)
|
||||
else:
|
||||
if feature not in file_features:
|
||||
file_features[feature] = set()
|
||||
|
||||
logger.debug("analyzed file and extracted %d features", len(file_features))
|
||||
|
||||
file_features.update(function_features)
|
||||
|
||||
_, matches = ruleset.match(Scope.FILE, file_features, NO_ADDRESS)
|
||||
return matches, len(file_features)
|
||||
|
||||
|
||||
def has_file_limitation(rules: RuleSet, capabilities: MatchResults, is_standalone=True) -> bool:
|
||||
file_limitation_rules = list(filter(lambda r: r.is_file_limitation_rule(), rules.rules.values()))
|
||||
|
||||
for file_limitation_rule in file_limitation_rules:
|
||||
if file_limitation_rule.name not in capabilities:
|
||||
continue
|
||||
|
||||
logger.warning("-" * 80)
|
||||
for line in file_limitation_rule.meta.get("description", "").split("\n"):
|
||||
logger.warning(" %s", line)
|
||||
logger.warning(" Identified via rule: %s", file_limitation_rule.name)
|
||||
if is_standalone:
|
||||
logger.warning(" ")
|
||||
logger.warning(" Use -v or -vv if you really want to see the capabilities identified by capa.")
|
||||
logger.warning("-" * 80)
|
||||
|
||||
# bail on first file limitation
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def find_capabilities(
|
||||
ruleset: RuleSet, extractor: FeatureExtractor, disable_progress=None, **kwargs
|
||||
) -> Tuple[MatchResults, Any]:
|
||||
from capa.capabilities.static import find_static_capabilities
|
||||
from capa.capabilities.dynamic import find_dynamic_capabilities
|
||||
|
||||
if isinstance(extractor, StaticFeatureExtractor):
|
||||
# for the time being, extractors are either static or dynamic.
|
||||
# Remove this assertion once that has changed
|
||||
assert not isinstance(extractor, DynamicFeatureExtractor)
|
||||
return find_static_capabilities(ruleset, extractor, disable_progress=disable_progress, **kwargs)
|
||||
if isinstance(extractor, DynamicFeatureExtractor):
|
||||
return find_dynamic_capabilities(ruleset, extractor, disable_progress=disable_progress, **kwargs)
|
||||
|
||||
raise ValueError(f"unexpected extractor type: {extractor.__class__.__name__}")
|
||||
198
capa/capabilities/dynamic.py
Normal file
198
capa/capabilities/dynamic.py
Normal file
@@ -0,0 +1,198 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
import logging
|
||||
import itertools
|
||||
import collections
|
||||
from typing import Any, Tuple
|
||||
|
||||
import tqdm
|
||||
|
||||
import capa.perf
|
||||
import capa.features.freeze as frz
|
||||
import capa.render.result_document as rdoc
|
||||
from capa.rules import Scope, RuleSet
|
||||
from capa.engine import FeatureSet, MatchResults
|
||||
from capa.helpers import redirecting_print_to_tqdm
|
||||
from capa.capabilities.common import find_file_capabilities
|
||||
from capa.features.extractors.base_extractor import CallHandle, ThreadHandle, ProcessHandle, DynamicFeatureExtractor
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def find_call_capabilities(
|
||||
ruleset: RuleSet, extractor: DynamicFeatureExtractor, ph: ProcessHandle, th: ThreadHandle, ch: CallHandle
|
||||
) -> Tuple[FeatureSet, MatchResults]:
|
||||
"""
|
||||
find matches for the given rules for the given call.
|
||||
|
||||
returns: tuple containing (features for call, match results for call)
|
||||
"""
|
||||
# all features found for the call.
|
||||
features: FeatureSet = collections.defaultdict(set)
|
||||
|
||||
for feature, addr in itertools.chain(
|
||||
extractor.extract_call_features(ph, th, ch), extractor.extract_global_features()
|
||||
):
|
||||
features[feature].add(addr)
|
||||
|
||||
# matches found at this thread.
|
||||
_, matches = ruleset.match(Scope.CALL, features, ch.address)
|
||||
|
||||
for rule_name, res in matches.items():
|
||||
rule = ruleset[rule_name]
|
||||
for addr, _ in res:
|
||||
capa.engine.index_rule_matches(features, rule, [addr])
|
||||
|
||||
return features, matches
|
||||
|
||||
|
||||
def find_thread_capabilities(
|
||||
ruleset: RuleSet, extractor: DynamicFeatureExtractor, ph: ProcessHandle, th: ThreadHandle
|
||||
) -> Tuple[FeatureSet, MatchResults, MatchResults]:
|
||||
"""
|
||||
find matches for the given rules within the given thread.
|
||||
|
||||
returns: tuple containing (features for thread, match results for thread, match results for calls)
|
||||
"""
|
||||
# all features found within this thread,
|
||||
# includes features found within calls.
|
||||
features: FeatureSet = collections.defaultdict(set)
|
||||
|
||||
# matches found at the call scope.
|
||||
# might be found at different calls, thats ok.
|
||||
call_matches: MatchResults = collections.defaultdict(list)
|
||||
|
||||
for ch in extractor.get_calls(ph, th):
|
||||
ifeatures, imatches = find_call_capabilities(ruleset, extractor, ph, th, ch)
|
||||
for feature, vas in ifeatures.items():
|
||||
features[feature].update(vas)
|
||||
|
||||
for rule_name, res in imatches.items():
|
||||
call_matches[rule_name].extend(res)
|
||||
|
||||
for feature, va in itertools.chain(extractor.extract_thread_features(ph, th), extractor.extract_global_features()):
|
||||
features[feature].add(va)
|
||||
|
||||
# matches found within this thread.
|
||||
_, matches = ruleset.match(Scope.THREAD, features, th.address)
|
||||
|
||||
for rule_name, res in matches.items():
|
||||
rule = ruleset[rule_name]
|
||||
for va, _ in res:
|
||||
capa.engine.index_rule_matches(features, rule, [va])
|
||||
|
||||
return features, matches, call_matches
|
||||
|
||||
|
||||
def find_process_capabilities(
|
||||
ruleset: RuleSet, extractor: DynamicFeatureExtractor, ph: ProcessHandle
|
||||
) -> Tuple[MatchResults, MatchResults, MatchResults, int]:
|
||||
"""
|
||||
find matches for the given rules within the given process.
|
||||
|
||||
returns: tuple containing (match results for process, match results for threads, match results for calls, number of features)
|
||||
"""
|
||||
# all features found within this process,
|
||||
# includes features found within threads (and calls).
|
||||
process_features: FeatureSet = collections.defaultdict(set)
|
||||
|
||||
# matches found at the basic threads.
|
||||
# might be found at different threads, thats ok.
|
||||
thread_matches: MatchResults = collections.defaultdict(list)
|
||||
|
||||
# matches found at the call scope.
|
||||
# might be found at different calls, thats ok.
|
||||
call_matches: MatchResults = collections.defaultdict(list)
|
||||
|
||||
for th in extractor.get_threads(ph):
|
||||
features, tmatches, cmatches = find_thread_capabilities(ruleset, extractor, ph, th)
|
||||
for feature, vas in features.items():
|
||||
process_features[feature].update(vas)
|
||||
|
||||
for rule_name, res in tmatches.items():
|
||||
thread_matches[rule_name].extend(res)
|
||||
|
||||
for rule_name, res in cmatches.items():
|
||||
call_matches[rule_name].extend(res)
|
||||
|
||||
for feature, va in itertools.chain(extractor.extract_process_features(ph), extractor.extract_global_features()):
|
||||
process_features[feature].add(va)
|
||||
|
||||
_, process_matches = ruleset.match(Scope.PROCESS, process_features, ph.address)
|
||||
return process_matches, thread_matches, call_matches, len(process_features)
|
||||
|
||||
|
||||
def find_dynamic_capabilities(
|
||||
ruleset: RuleSet, extractor: DynamicFeatureExtractor, disable_progress=None
|
||||
) -> Tuple[MatchResults, Any]:
|
||||
all_process_matches: MatchResults = collections.defaultdict(list)
|
||||
all_thread_matches: MatchResults = collections.defaultdict(list)
|
||||
all_call_matches: MatchResults = collections.defaultdict(list)
|
||||
|
||||
feature_counts = rdoc.DynamicFeatureCounts(file=0, processes=())
|
||||
|
||||
assert isinstance(extractor, DynamicFeatureExtractor)
|
||||
with redirecting_print_to_tqdm(disable_progress):
|
||||
with tqdm.contrib.logging.logging_redirect_tqdm():
|
||||
pbar = tqdm.tqdm
|
||||
if disable_progress:
|
||||
# do not use tqdm to avoid unnecessary side effects when caller intends
|
||||
# to disable progress completely
|
||||
def pbar(s, *args, **kwargs):
|
||||
return s
|
||||
|
||||
processes = list(extractor.get_processes())
|
||||
|
||||
pb = pbar(processes, desc="matching", unit=" processes", leave=False)
|
||||
for p in pb:
|
||||
process_matches, thread_matches, call_matches, feature_count = find_process_capabilities(
|
||||
ruleset, extractor, p
|
||||
)
|
||||
feature_counts.processes += (
|
||||
rdoc.ProcessFeatureCount(address=frz.Address.from_capa(p.address), count=feature_count),
|
||||
)
|
||||
logger.debug("analyzed %s and extracted %d features", p.address, feature_count)
|
||||
|
||||
for rule_name, res in process_matches.items():
|
||||
all_process_matches[rule_name].extend(res)
|
||||
for rule_name, res in thread_matches.items():
|
||||
all_thread_matches[rule_name].extend(res)
|
||||
for rule_name, res in call_matches.items():
|
||||
all_call_matches[rule_name].extend(res)
|
||||
|
||||
# collection of features that captures the rule matches within process and thread scopes.
|
||||
# mapping from feature (matched rule) to set of addresses at which it matched.
|
||||
process_and_lower_features: FeatureSet = collections.defaultdict(set)
|
||||
for rule_name, results in itertools.chain(
|
||||
all_process_matches.items(), all_thread_matches.items(), all_call_matches.items()
|
||||
):
|
||||
locations = {p[0] for p in results}
|
||||
rule = ruleset[rule_name]
|
||||
capa.engine.index_rule_matches(process_and_lower_features, rule, locations)
|
||||
|
||||
all_file_matches, feature_count = find_file_capabilities(ruleset, extractor, process_and_lower_features)
|
||||
feature_counts.file = feature_count
|
||||
|
||||
matches = dict(
|
||||
itertools.chain(
|
||||
# each rule exists in exactly one scope,
|
||||
# so there won't be any overlap among these following MatchResults,
|
||||
# and we can merge the dictionaries naively.
|
||||
all_thread_matches.items(),
|
||||
all_process_matches.items(),
|
||||
all_call_matches.items(),
|
||||
all_file_matches.items(),
|
||||
)
|
||||
)
|
||||
|
||||
meta = {
|
||||
"feature_counts": feature_counts,
|
||||
}
|
||||
|
||||
return matches, meta
|
||||
233
capa/capabilities/static.py
Normal file
233
capa/capabilities/static.py
Normal file
@@ -0,0 +1,233 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
import time
|
||||
import logging
|
||||
import itertools
|
||||
import collections
|
||||
from typing import Any, Tuple
|
||||
|
||||
import tqdm.contrib.logging
|
||||
|
||||
import capa.perf
|
||||
import capa.features.freeze as frz
|
||||
import capa.render.result_document as rdoc
|
||||
from capa.rules import Scope, RuleSet
|
||||
from capa.engine import FeatureSet, MatchResults
|
||||
from capa.helpers import redirecting_print_to_tqdm
|
||||
from capa.capabilities.common import find_file_capabilities
|
||||
from capa.features.extractors.base_extractor import BBHandle, InsnHandle, FunctionHandle, StaticFeatureExtractor
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def find_instruction_capabilities(
|
||||
ruleset: RuleSet, extractor: StaticFeatureExtractor, f: FunctionHandle, bb: BBHandle, insn: InsnHandle
|
||||
) -> Tuple[FeatureSet, MatchResults]:
|
||||
"""
|
||||
find matches for the given rules for the given instruction.
|
||||
|
||||
returns: tuple containing (features for instruction, match results for instruction)
|
||||
"""
|
||||
# all features found for the instruction.
|
||||
features: FeatureSet = collections.defaultdict(set)
|
||||
|
||||
for feature, addr in itertools.chain(
|
||||
extractor.extract_insn_features(f, bb, insn), extractor.extract_global_features()
|
||||
):
|
||||
features[feature].add(addr)
|
||||
|
||||
# matches found at this instruction.
|
||||
_, matches = ruleset.match(Scope.INSTRUCTION, features, insn.address)
|
||||
|
||||
for rule_name, res in matches.items():
|
||||
rule = ruleset[rule_name]
|
||||
for addr, _ in res:
|
||||
capa.engine.index_rule_matches(features, rule, [addr])
|
||||
|
||||
return features, matches
|
||||
|
||||
|
||||
def find_basic_block_capabilities(
|
||||
ruleset: RuleSet, extractor: StaticFeatureExtractor, f: FunctionHandle, bb: BBHandle
|
||||
) -> Tuple[FeatureSet, MatchResults, MatchResults]:
|
||||
"""
|
||||
find matches for the given rules within the given basic block.
|
||||
|
||||
returns: tuple containing (features for basic block, match results for basic block, match results for instructions)
|
||||
"""
|
||||
# all features found within this basic block,
|
||||
# includes features found within instructions.
|
||||
features: FeatureSet = collections.defaultdict(set)
|
||||
|
||||
# matches found at the instruction scope.
|
||||
# might be found at different instructions, thats ok.
|
||||
insn_matches: MatchResults = collections.defaultdict(list)
|
||||
|
||||
for insn in extractor.get_instructions(f, bb):
|
||||
ifeatures, imatches = find_instruction_capabilities(ruleset, extractor, f, bb, insn)
|
||||
for feature, vas in ifeatures.items():
|
||||
features[feature].update(vas)
|
||||
|
||||
for rule_name, res in imatches.items():
|
||||
insn_matches[rule_name].extend(res)
|
||||
|
||||
for feature, va in itertools.chain(
|
||||
extractor.extract_basic_block_features(f, bb), extractor.extract_global_features()
|
||||
):
|
||||
features[feature].add(va)
|
||||
|
||||
# matches found within this basic block.
|
||||
_, matches = ruleset.match(Scope.BASIC_BLOCK, features, bb.address)
|
||||
|
||||
for rule_name, res in matches.items():
|
||||
rule = ruleset[rule_name]
|
||||
for va, _ in res:
|
||||
capa.engine.index_rule_matches(features, rule, [va])
|
||||
|
||||
return features, matches, insn_matches
|
||||
|
||||
|
||||
def find_code_capabilities(
|
||||
ruleset: RuleSet, extractor: StaticFeatureExtractor, fh: FunctionHandle
|
||||
) -> Tuple[MatchResults, MatchResults, MatchResults, int]:
|
||||
"""
|
||||
find matches for the given rules within the given function.
|
||||
|
||||
returns: tuple containing (match results for function, match results for basic blocks, match results for instructions, number of features)
|
||||
"""
|
||||
# all features found within this function,
|
||||
# includes features found within basic blocks (and instructions).
|
||||
function_features: FeatureSet = collections.defaultdict(set)
|
||||
|
||||
# matches found at the basic block scope.
|
||||
# might be found at different basic blocks, thats ok.
|
||||
bb_matches: MatchResults = collections.defaultdict(list)
|
||||
|
||||
# matches found at the instruction scope.
|
||||
# might be found at different instructions, thats ok.
|
||||
insn_matches: MatchResults = collections.defaultdict(list)
|
||||
|
||||
for bb in extractor.get_basic_blocks(fh):
|
||||
features, bmatches, imatches = find_basic_block_capabilities(ruleset, extractor, fh, bb)
|
||||
for feature, vas in features.items():
|
||||
function_features[feature].update(vas)
|
||||
|
||||
for rule_name, res in bmatches.items():
|
||||
bb_matches[rule_name].extend(res)
|
||||
|
||||
for rule_name, res in imatches.items():
|
||||
insn_matches[rule_name].extend(res)
|
||||
|
||||
for feature, va in itertools.chain(extractor.extract_function_features(fh), extractor.extract_global_features()):
|
||||
function_features[feature].add(va)
|
||||
|
||||
_, function_matches = ruleset.match(Scope.FUNCTION, function_features, fh.address)
|
||||
return function_matches, bb_matches, insn_matches, len(function_features)
|
||||
|
||||
|
||||
def find_static_capabilities(
|
||||
ruleset: RuleSet, extractor: StaticFeatureExtractor, disable_progress=None
|
||||
) -> Tuple[MatchResults, Any]:
|
||||
all_function_matches: MatchResults = collections.defaultdict(list)
|
||||
all_bb_matches: MatchResults = collections.defaultdict(list)
|
||||
all_insn_matches: MatchResults = collections.defaultdict(list)
|
||||
|
||||
feature_counts = rdoc.StaticFeatureCounts(file=0, functions=())
|
||||
library_functions: Tuple[rdoc.LibraryFunction, ...] = ()
|
||||
|
||||
assert isinstance(extractor, StaticFeatureExtractor)
|
||||
with redirecting_print_to_tqdm(disable_progress):
|
||||
with tqdm.contrib.logging.logging_redirect_tqdm():
|
||||
pbar = tqdm.tqdm
|
||||
if capa.helpers.is_runtime_ghidra():
|
||||
# Ghidrathon interpreter cannot properly handle
|
||||
# the TMonitor thread that is created via a monitor_interval
|
||||
# > 0
|
||||
pbar.monitor_interval = 0
|
||||
if disable_progress:
|
||||
# do not use tqdm to avoid unnecessary side effects when caller intends
|
||||
# to disable progress completely
|
||||
def pbar(s, *args, **kwargs):
|
||||
return s
|
||||
|
||||
functions = list(extractor.get_functions())
|
||||
n_funcs = len(functions)
|
||||
|
||||
pb = pbar(functions, desc="matching", unit=" functions", postfix="skipped 0 library functions", leave=False)
|
||||
for f in pb:
|
||||
t0 = time.time()
|
||||
if extractor.is_library_function(f.address):
|
||||
function_name = extractor.get_function_name(f.address)
|
||||
logger.debug("skipping library function 0x%x (%s)", f.address, function_name)
|
||||
library_functions += (
|
||||
rdoc.LibraryFunction(address=frz.Address.from_capa(f.address), name=function_name),
|
||||
)
|
||||
n_libs = len(library_functions)
|
||||
percentage = round(100 * (n_libs / n_funcs))
|
||||
if isinstance(pb, tqdm.tqdm):
|
||||
pb.set_postfix_str(f"skipped {n_libs} library functions ({percentage}%)")
|
||||
continue
|
||||
|
||||
function_matches, bb_matches, insn_matches, feature_count = find_code_capabilities(
|
||||
ruleset, extractor, f
|
||||
)
|
||||
feature_counts.functions += (
|
||||
rdoc.FunctionFeatureCount(address=frz.Address.from_capa(f.address), count=feature_count),
|
||||
)
|
||||
t1 = time.time()
|
||||
|
||||
match_count = sum(len(res) for res in function_matches.values())
|
||||
match_count += sum(len(res) for res in bb_matches.values())
|
||||
match_count += sum(len(res) for res in insn_matches.values())
|
||||
logger.debug(
|
||||
"analyzed function 0x%x and extracted %d features, %d matches in %0.02fs",
|
||||
f.address,
|
||||
feature_count,
|
||||
match_count,
|
||||
t1 - t0,
|
||||
)
|
||||
|
||||
for rule_name, res in function_matches.items():
|
||||
all_function_matches[rule_name].extend(res)
|
||||
for rule_name, res in bb_matches.items():
|
||||
all_bb_matches[rule_name].extend(res)
|
||||
for rule_name, res in insn_matches.items():
|
||||
all_insn_matches[rule_name].extend(res)
|
||||
|
||||
# collection of features that captures the rule matches within function, BB, and instruction scopes.
|
||||
# mapping from feature (matched rule) to set of addresses at which it matched.
|
||||
function_and_lower_features: FeatureSet = collections.defaultdict(set)
|
||||
for rule_name, results in itertools.chain(
|
||||
all_function_matches.items(), all_bb_matches.items(), all_insn_matches.items()
|
||||
):
|
||||
locations = {p[0] for p in results}
|
||||
rule = ruleset[rule_name]
|
||||
capa.engine.index_rule_matches(function_and_lower_features, rule, locations)
|
||||
|
||||
all_file_matches, feature_count = find_file_capabilities(ruleset, extractor, function_and_lower_features)
|
||||
feature_counts.file = feature_count
|
||||
|
||||
matches = dict(
|
||||
itertools.chain(
|
||||
# each rule exists in exactly one scope,
|
||||
# so there won't be any overlap among these following MatchResults,
|
||||
# and we can merge the dictionaries naively.
|
||||
all_insn_matches.items(),
|
||||
all_bb_matches.items(),
|
||||
all_function_matches.items(),
|
||||
all_file_matches.items(),
|
||||
)
|
||||
)
|
||||
|
||||
meta = {
|
||||
"feature_counts": feature_counts,
|
||||
"library_functions": library_functions,
|
||||
}
|
||||
|
||||
return matches, meta
|
||||
334
capa/engine.py
334
capa/engine.py
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
@@ -6,14 +6,31 @@
|
||||
# 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 TYPE_CHECKING, Set, Dict, List, Tuple, Union, Mapping, Iterable, Iterator
|
||||
|
||||
import capa.features
|
||||
import capa.perf
|
||||
import capa.features.common
|
||||
from capa.features.common import Result, Feature
|
||||
from capa.features.address import Address
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# circular import, otherwise
|
||||
import capa.rules
|
||||
|
||||
|
||||
class Statement(object):
|
||||
# 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[Address]]
|
||||
|
||||
|
||||
class Statement:
|
||||
"""
|
||||
superclass for structural nodes, such as and/or/not.
|
||||
this exists to provide a default impl for `__str__` and `__repr__`,
|
||||
@@ -21,157 +38,194 @@ class Statement(object):
|
||||
"""
|
||||
|
||||
def __init__(self, description=None):
|
||||
super(Statement, self).__init__()
|
||||
super().__init__()
|
||||
self.name = self.__class__.__name__
|
||||
self.description = description
|
||||
|
||||
def __str__(self):
|
||||
name = self.name.lower()
|
||||
children = ",".join(map(str, self.get_children()))
|
||||
if self.description:
|
||||
return "%s(%s = %s)" % (self.name.lower(), ",".join(map(str, self.get_children())), self.description)
|
||||
return f"{name}({children} = {self.description})"
|
||||
else:
|
||||
return "%s(%s)" % (self.name.lower(), ",".join(map(str, self.get_children())))
|
||||
return f"{name}({children})"
|
||||
|
||||
def __repr__(self):
|
||||
return str(self)
|
||||
|
||||
def evaluate(self, ctx):
|
||||
def evaluate(self, features: FeatureSet, short_circuit=True) -> Result:
|
||||
"""
|
||||
classes that inherit `Statement` must implement `evaluate`
|
||||
|
||||
args:
|
||||
ctx (defaultdict[Feature, set[VA]])
|
||||
|
||||
returns:
|
||||
Result
|
||||
short_circuit (bool): if true, then statements like and/or/some may short circuit.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_children(self):
|
||||
def get_children(self) -> Iterator[Union["Statement", Feature]]:
|
||||
if hasattr(self, "child"):
|
||||
yield self.child
|
||||
# this really confuses mypy because the property may not exist
|
||||
# since its defined in the subclasses.
|
||||
child = self.child # type: ignore
|
||||
assert isinstance(child, (Statement, Feature))
|
||||
yield child
|
||||
|
||||
if hasattr(self, "children"):
|
||||
for child in self.children:
|
||||
assert isinstance(child, (Statement, Feature))
|
||||
yield child
|
||||
|
||||
def replace_child(self, existing, new):
|
||||
if hasattr(self, "child"):
|
||||
if self.child is existing:
|
||||
# this really confuses mypy because the property may not exist
|
||||
# since its defined in the subclasses.
|
||||
if self.child is existing: # type: ignore
|
||||
self.child = new
|
||||
|
||||
if hasattr(self, "children"):
|
||||
for i, child in enumerate(self.children):
|
||||
children = self.children
|
||||
for i, child in enumerate(children):
|
||||
if child is existing:
|
||||
self.children[i] = new
|
||||
|
||||
|
||||
class Result(object):
|
||||
"""
|
||||
represents the results of an evaluation of statements against features.
|
||||
|
||||
instances of this class should behave like a bool,
|
||||
e.g. `assert Result(True, ...) == True`
|
||||
|
||||
instances track additional metadata about evaluation results.
|
||||
they contain references to the statement node (e.g. an And statement),
|
||||
as well as the children Result instances.
|
||||
|
||||
we need this so that we can render the tree of expressions and their results.
|
||||
"""
|
||||
|
||||
def __init__(self, success, statement, children, locations=None):
|
||||
"""
|
||||
args:
|
||||
success (bool)
|
||||
statement (capa.engine.Statement or capa.features.Feature)
|
||||
children (list[Result])
|
||||
locations (iterable[VA])
|
||||
"""
|
||||
super(Result, self).__init__()
|
||||
self.success = success
|
||||
self.statement = statement
|
||||
self.children = children
|
||||
self.locations = locations if locations is not None else ()
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, bool):
|
||||
return self.success == other
|
||||
return False
|
||||
|
||||
def __bool__(self):
|
||||
return self.success
|
||||
|
||||
def __nonzero__(self):
|
||||
return self.success
|
||||
children[i] = new
|
||||
|
||||
|
||||
class And(Statement):
|
||||
"""match if all of the children evaluate to True."""
|
||||
"""
|
||||
match if all of the children evaluate to True.
|
||||
|
||||
the order of evaluation is dictated by the property
|
||||
`And.children` (type: List[Statement|Feature]).
|
||||
a query optimizer may safely manipulate the order of these children.
|
||||
"""
|
||||
|
||||
def __init__(self, children, description=None):
|
||||
super(And, self).__init__(description=description)
|
||||
super().__init__(description=description)
|
||||
self.children = children
|
||||
|
||||
def evaluate(self, ctx):
|
||||
results = [child.evaluate(ctx) for child in self.children]
|
||||
success = all(results)
|
||||
return Result(success, self, results)
|
||||
def evaluate(self, ctx, short_circuit=True):
|
||||
capa.perf.counters["evaluate.feature"] += 1
|
||||
capa.perf.counters["evaluate.feature.and"] += 1
|
||||
|
||||
if short_circuit:
|
||||
results = []
|
||||
for child in self.children:
|
||||
result = child.evaluate(ctx, short_circuit=short_circuit)
|
||||
results.append(result)
|
||||
if not result:
|
||||
# short circuit
|
||||
return Result(False, self, results)
|
||||
|
||||
return Result(True, self, results)
|
||||
else:
|
||||
results = [child.evaluate(ctx, short_circuit=short_circuit) for child in self.children]
|
||||
success = all(results)
|
||||
return Result(success, self, results)
|
||||
|
||||
|
||||
class Or(Statement):
|
||||
"""match if any of the children evaluate to True."""
|
||||
"""
|
||||
match if any of the children evaluate to True.
|
||||
|
||||
the order of evaluation is dictated by the property
|
||||
`Or.children` (type: List[Statement|Feature]).
|
||||
a query optimizer may safely manipulate the order of these children.
|
||||
"""
|
||||
|
||||
def __init__(self, children, description=None):
|
||||
super(Or, self).__init__(description=description)
|
||||
super().__init__(description=description)
|
||||
self.children = children
|
||||
|
||||
def evaluate(self, ctx):
|
||||
results = [child.evaluate(ctx) for child in self.children]
|
||||
success = any(results)
|
||||
return Result(success, self, results)
|
||||
def evaluate(self, ctx, short_circuit=True):
|
||||
capa.perf.counters["evaluate.feature"] += 1
|
||||
capa.perf.counters["evaluate.feature.or"] += 1
|
||||
|
||||
if short_circuit:
|
||||
results = []
|
||||
for child in self.children:
|
||||
result = child.evaluate(ctx, short_circuit=short_circuit)
|
||||
results.append(result)
|
||||
if result:
|
||||
# short circuit as soon as we hit one match
|
||||
return Result(True, self, results)
|
||||
|
||||
return Result(False, self, results)
|
||||
else:
|
||||
results = [child.evaluate(ctx, short_circuit=short_circuit) for child in self.children]
|
||||
success = any(results)
|
||||
return Result(success, self, results)
|
||||
|
||||
|
||||
class Not(Statement):
|
||||
"""match only if the child evaluates to False."""
|
||||
|
||||
def __init__(self, child, description=None):
|
||||
super(Not, self).__init__(description=description)
|
||||
super().__init__(description=description)
|
||||
self.child = child
|
||||
|
||||
def evaluate(self, ctx):
|
||||
results = [self.child.evaluate(ctx)]
|
||||
def evaluate(self, ctx, short_circuit=True):
|
||||
capa.perf.counters["evaluate.feature"] += 1
|
||||
capa.perf.counters["evaluate.feature.not"] += 1
|
||||
|
||||
results = [self.child.evaluate(ctx, short_circuit=short_circuit)]
|
||||
success = not results[0]
|
||||
return Result(success, self, results)
|
||||
|
||||
|
||||
class Some(Statement):
|
||||
"""match if at least N of the children evaluate to True."""
|
||||
"""
|
||||
match if at least N of the children evaluate to True.
|
||||
|
||||
the order of evaluation is dictated by the property
|
||||
`Some.children` (type: List[Statement|Feature]).
|
||||
a query optimizer may safely manipulate the order of these children.
|
||||
"""
|
||||
|
||||
def __init__(self, count, children, description=None):
|
||||
super(Some, self).__init__(description=description)
|
||||
super().__init__(description=description)
|
||||
self.count = count
|
||||
self.children = children
|
||||
|
||||
def evaluate(self, ctx):
|
||||
results = [child.evaluate(ctx) for child in self.children]
|
||||
# note that here we cast the child result as a bool
|
||||
# because we've overridden `__bool__` above.
|
||||
#
|
||||
# we can't use `if child is True` because the instance is not True.
|
||||
success = sum([1 for child in results if bool(child) is True]) >= self.count
|
||||
return Result(success, self, results)
|
||||
def evaluate(self, ctx, short_circuit=True):
|
||||
capa.perf.counters["evaluate.feature"] += 1
|
||||
capa.perf.counters["evaluate.feature.some"] += 1
|
||||
|
||||
if short_circuit:
|
||||
results = []
|
||||
satisfied_children_count = 0
|
||||
for child in self.children:
|
||||
result = child.evaluate(ctx, short_circuit=short_circuit)
|
||||
results.append(result)
|
||||
if result:
|
||||
satisfied_children_count += 1
|
||||
|
||||
if satisfied_children_count >= self.count:
|
||||
# short circuit as soon as we hit the threshold
|
||||
return Result(True, self, results)
|
||||
|
||||
return Result(False, self, results)
|
||||
else:
|
||||
results = [child.evaluate(ctx, short_circuit=short_circuit) for child in self.children]
|
||||
# note that here we cast the child result as a bool
|
||||
# because we've overridden `__bool__` above.
|
||||
#
|
||||
# we can't use `if child is True` because the instance is not True.
|
||||
success = sum([1 for child in results if bool(child) is True]) >= self.count
|
||||
return Result(success, self, results)
|
||||
|
||||
|
||||
class Range(Statement):
|
||||
"""match if the child is contained in the ctx set with a count in the given range."""
|
||||
|
||||
def __init__(self, child, min=None, max=None, description=None):
|
||||
super(Range, self).__init__(description=description)
|
||||
super().__init__(description=description)
|
||||
self.child = child
|
||||
self.min = min if min is not None else 0
|
||||
self.max = max if max is not None else (1 << 64 - 1)
|
||||
|
||||
def evaluate(self, ctx):
|
||||
def evaluate(self, ctx, **kwargs):
|
||||
capa.perf.counters["evaluate.feature"] += 1
|
||||
capa.perf.counters["evaluate.feature.range"] += 1
|
||||
|
||||
count = len(ctx.get(self.child, []))
|
||||
if self.min == 0 and count == 0:
|
||||
return Result(True, self, [])
|
||||
@@ -180,9 +234,9 @@ class Range(Statement):
|
||||
|
||||
def __str__(self):
|
||||
if self.max == (1 << 64 - 1):
|
||||
return "range(%s, min=%d, max=infinity)" % (str(self.child), self.min)
|
||||
return f"range({str(self.child)}, min={self.min}, max=infinity)"
|
||||
else:
|
||||
return "range(%s, min=%d, max=%d)" % (str(self.child), self.min, self.max)
|
||||
return f"range({str(self.child)}, min={self.min}, max={self.max})"
|
||||
|
||||
|
||||
class Subscope(Statement):
|
||||
@@ -191,59 +245,66 @@ class Subscope(Statement):
|
||||
the engine should preprocess rules to extract subscope statements into their own rules.
|
||||
"""
|
||||
|
||||
def __init__(self, scope, child):
|
||||
super(Subscope, self).__init__()
|
||||
def __init__(self, scope, child, description=None):
|
||||
super().__init__(description=description)
|
||||
self.scope = scope
|
||||
self.child = child
|
||||
|
||||
def evaluate(self, ctx):
|
||||
def evaluate(self, ctx, **kwargs):
|
||||
raise ValueError("cannot evaluate a subscope directly!")
|
||||
|
||||
|
||||
def topologically_order_rules(rules):
|
||||
# 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[Address, Result]]]
|
||||
|
||||
|
||||
def index_rule_matches(features: FeatureSet, rule: "capa.rules.Rule", locations: Iterable[Address]):
|
||||
"""
|
||||
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.
|
||||
record into the given featureset that the given rule matched at the given locations.
|
||||
|
||||
assumes that the rule dependency graph is a DAG.
|
||||
naively, this is just adding a MatchedRule feature;
|
||||
however, we also want to record matches for the rule's namespaces.
|
||||
|
||||
updates `features` in-place. doesn't modify the remaining arguments.
|
||||
"""
|
||||
# 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
|
||||
features[capa.features.common.MatchedRule(rule.name)].update(locations)
|
||||
namespace = rule.meta.get("namespace")
|
||||
if namespace:
|
||||
while namespace:
|
||||
features[capa.features.common.MatchedRule(namespace)].update(locations)
|
||||
namespace, _, _ = namespace.rpartition("/")
|
||||
|
||||
|
||||
def match(rules, features, va):
|
||||
def match(rules: List["capa.rules.Rule"], features: FeatureSet, addr: Address) -> Tuple[FeatureSet, MatchResults]:
|
||||
"""
|
||||
Args:
|
||||
rules (List[capa.rules.Rule]): these must already be ordered topologically by dependency.
|
||||
features (Mapping[capa.features.Feature, int]):
|
||||
va (int): location of the features
|
||||
match the given rules against the given features,
|
||||
returning an updated set of features and the matches.
|
||||
|
||||
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)
|
||||
the updated features are just like the input,
|
||||
but extended to include the match features (e.g. names of rules that matched).
|
||||
the given feature set is not modified; an updated copy is returned.
|
||||
|
||||
the given list of rules must be ordered topologically by dependency,
|
||||
or else `match` statements will not be handled correctly.
|
||||
|
||||
this routine should be fairly optimized, but is not guaranteed to be the fastest matcher possible.
|
||||
it has a particularly convenient signature: (rules, features) -> matches
|
||||
other strategies can be imagined that match differently; implement these elsewhere.
|
||||
specifically, this routine does "top down" matching of the given rules against the feature set.
|
||||
"""
|
||||
results = collections.defaultdict(list)
|
||||
results: MatchResults = collections.defaultdict(list)
|
||||
|
||||
# copy features so that we can modify it
|
||||
# without affecting the caller (keep this function pure)
|
||||
@@ -252,15 +313,22 @@ def match(rules, features, va):
|
||||
features = collections.defaultdict(set, copy.copy(features))
|
||||
|
||||
for rule in rules:
|
||||
res = rule.evaluate(features)
|
||||
res = rule.evaluate(features, short_circuit=True)
|
||||
if res:
|
||||
results[rule.name].append((va, res))
|
||||
features[capa.features.MatchedRule(rule.name)].add(va)
|
||||
# we first matched the rule with short circuiting enabled.
|
||||
# this is much faster than without short circuiting.
|
||||
# however, we want to collect all results thoroughly,
|
||||
# so once we've found a match quickly,
|
||||
# go back and capture results without short circuiting.
|
||||
res = rule.evaluate(features, short_circuit=False)
|
||||
|
||||
namespace = rule.meta.get("namespace")
|
||||
if namespace:
|
||||
while namespace:
|
||||
features[capa.features.MatchedRule(namespace)].add(va)
|
||||
namespace, _, _ = namespace.rpartition("/")
|
||||
# sanity check
|
||||
assert bool(res) is True
|
||||
|
||||
results[rule.name].append((addr, res))
|
||||
# we need to update the current `features`
|
||||
# because subsequent iterations of this loop may use newly added features,
|
||||
# such as rule or namespace matches.
|
||||
index_rule_matches(features, rule, [addr])
|
||||
|
||||
return (features, results)
|
||||
|
||||
25
capa/exceptions.py
Normal file
25
capa/exceptions.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
class UnsupportedRuntimeError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
class UnsupportedFormatError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class UnsupportedArchError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class UnsupportedOSError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class EmptyReportError(ValueError):
|
||||
pass
|
||||
@@ -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])
|
||||
|
||||
222
capa/features/address.py
Normal file
222
capa/features/address.py
Normal file
@@ -0,0 +1,222 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
import abc
|
||||
|
||||
|
||||
class Address(abc.ABC):
|
||||
@abc.abstractmethod
|
||||
def __eq__(self, other):
|
||||
...
|
||||
|
||||
@abc.abstractmethod
|
||||
def __lt__(self, other):
|
||||
# implement < so that addresses can be sorted from low to high
|
||||
...
|
||||
|
||||
@abc.abstractmethod
|
||||
def __hash__(self):
|
||||
# implement hash so that addresses can be used in sets and dicts
|
||||
...
|
||||
|
||||
@abc.abstractmethod
|
||||
def __repr__(self):
|
||||
# implement repr to help during debugging
|
||||
...
|
||||
|
||||
|
||||
class AbsoluteVirtualAddress(int, Address):
|
||||
"""an absolute memory address"""
|
||||
|
||||
def __new__(cls, v):
|
||||
assert v >= 0
|
||||
return int.__new__(cls, v)
|
||||
|
||||
def __repr__(self):
|
||||
return f"absolute(0x{self:x})"
|
||||
|
||||
def __hash__(self):
|
||||
return int.__hash__(self)
|
||||
|
||||
|
||||
class ProcessAddress(Address):
|
||||
"""an address of a process in a dynamic execution trace"""
|
||||
|
||||
def __init__(self, pid: int, ppid: int = 0):
|
||||
assert ppid >= 0
|
||||
assert pid > 0
|
||||
self.ppid = ppid
|
||||
self.pid = pid
|
||||
|
||||
def __repr__(self):
|
||||
return "process(%s%s)" % (
|
||||
f"ppid: {self.ppid}, " if self.ppid > 0 else "",
|
||||
f"pid: {self.pid}",
|
||||
)
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.ppid, self.pid))
|
||||
|
||||
def __eq__(self, other):
|
||||
assert isinstance(other, ProcessAddress)
|
||||
return (self.ppid, self.pid) == (other.ppid, other.pid)
|
||||
|
||||
def __lt__(self, other):
|
||||
assert isinstance(other, ProcessAddress)
|
||||
return (self.ppid, self.pid) < (other.ppid, other.pid)
|
||||
|
||||
|
||||
class ThreadAddress(Address):
|
||||
"""addresses a thread in a dynamic execution trace"""
|
||||
|
||||
def __init__(self, process: ProcessAddress, tid: int):
|
||||
assert tid >= 0
|
||||
self.process = process
|
||||
self.tid = tid
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.process}, thread(tid: {self.tid})"
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.process, self.tid))
|
||||
|
||||
def __eq__(self, other):
|
||||
assert isinstance(other, ThreadAddress)
|
||||
return (self.process, self.tid) == (other.process, other.tid)
|
||||
|
||||
def __lt__(self, other):
|
||||
assert isinstance(other, ThreadAddress)
|
||||
return (self.process, self.tid) < (other.process, other.tid)
|
||||
|
||||
|
||||
class DynamicCallAddress(Address):
|
||||
"""addesses a call in a dynamic execution trace"""
|
||||
|
||||
def __init__(self, thread: ThreadAddress, id: int):
|
||||
assert id >= 0
|
||||
self.thread = thread
|
||||
self.id = id
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.thread}, call(id: {self.id})"
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.thread, self.id))
|
||||
|
||||
def __eq__(self, other):
|
||||
assert isinstance(other, DynamicCallAddress)
|
||||
return (self.thread, self.id) == (other.thread, other.id)
|
||||
|
||||
def __lt__(self, other):
|
||||
assert isinstance(other, DynamicCallAddress)
|
||||
return (self.thread, self.id) < (other.thread, other.id)
|
||||
|
||||
|
||||
class RelativeVirtualAddress(int, Address):
|
||||
"""a memory address relative to a base address"""
|
||||
|
||||
def __repr__(self):
|
||||
return f"relative(0x{self:x})"
|
||||
|
||||
def __hash__(self):
|
||||
return int.__hash__(self)
|
||||
|
||||
|
||||
class FileOffsetAddress(int, Address):
|
||||
"""an address relative to the start of a file"""
|
||||
|
||||
def __new__(cls, v):
|
||||
assert v >= 0
|
||||
return int.__new__(cls, v)
|
||||
|
||||
def __repr__(self):
|
||||
return f"file(0x{self:x})"
|
||||
|
||||
def __hash__(self):
|
||||
return int.__hash__(self)
|
||||
|
||||
|
||||
class DNTokenAddress(int, Address):
|
||||
"""a .NET token"""
|
||||
|
||||
def __new__(cls, token: int):
|
||||
return int.__new__(cls, token)
|
||||
|
||||
def __repr__(self):
|
||||
return f"token(0x{self:x})"
|
||||
|
||||
def __hash__(self):
|
||||
return int.__hash__(self)
|
||||
|
||||
|
||||
class DNTokenOffsetAddress(Address):
|
||||
"""an offset into an object specified by a .NET token"""
|
||||
|
||||
def __init__(self, token: int, offset: int):
|
||||
assert offset >= 0
|
||||
self.token = token
|
||||
self.offset = offset
|
||||
|
||||
def __eq__(self, other):
|
||||
return (self.token, self.offset) == (other.token, other.offset)
|
||||
|
||||
def __lt__(self, other):
|
||||
return (self.token, self.offset) < (other.token, other.offset)
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.token, self.offset))
|
||||
|
||||
def __repr__(self):
|
||||
return f"token(0x{self.token:x})+(0x{self.offset:x})"
|
||||
|
||||
def __index__(self):
|
||||
return self.token + self.offset
|
||||
|
||||
|
||||
class DexMethodAddress(int, Address):
|
||||
def __new__(cls, offset: int):
|
||||
return int.__new__(cls, offset)
|
||||
|
||||
def __repr__(self):
|
||||
return f"DexMethodAddress(offset={hex(self)})"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return repr(self)
|
||||
|
||||
def __hash__(self):
|
||||
return int.__hash__(self)
|
||||
|
||||
|
||||
class DexClassAddress(int, Address):
|
||||
def __new__(cls, offset: int):
|
||||
return int.__new__(cls, offset)
|
||||
|
||||
def __repr__(self):
|
||||
return f"DexClassAddress(offset={hex(self)})"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return repr(self)
|
||||
|
||||
def __hash__(self):
|
||||
return int.__hash__(self)
|
||||
|
||||
|
||||
class _NoAddress(Address):
|
||||
def __eq__(self, other):
|
||||
return True
|
||||
|
||||
def __lt__(self, other):
|
||||
return False
|
||||
|
||||
def __hash__(self):
|
||||
return hash(0)
|
||||
|
||||
def __repr__(self):
|
||||
return "no address"
|
||||
|
||||
|
||||
NO_ADDRESS = _NoAddress()
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
@@ -6,22 +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.
|
||||
|
||||
from capa.features import Feature
|
||||
from capa.features.common import Feature
|
||||
|
||||
|
||||
class BasicBlock(Feature):
|
||||
def __init__(self):
|
||||
super(BasicBlock, self).__init__(None)
|
||||
def __init__(self, description=None):
|
||||
super().__init__(0, description=description)
|
||||
|
||||
def __str__(self):
|
||||
return "basic block"
|
||||
|
||||
def get_value_str(self):
|
||||
return ""
|
||||
|
||||
def freeze_serialize(self):
|
||||
return (self.__class__.__name__, [])
|
||||
|
||||
@classmethod
|
||||
def freeze_deserialize(cls, args):
|
||||
return cls()
|
||||
|
||||
492
capa/features/common.py
Normal file
492
capa/features/common.py
Normal file
@@ -0,0 +1,492 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import re
|
||||
import abc
|
||||
import codecs
|
||||
import typing
|
||||
import logging
|
||||
import collections
|
||||
from typing import TYPE_CHECKING, Set, Dict, List, Union, Optional
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# circular import, otherwise
|
||||
import capa.engine
|
||||
|
||||
import capa.perf
|
||||
import capa.features
|
||||
import capa.features.extractors.elf
|
||||
from capa.features.address import Address
|
||||
|
||||
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
|
||||
|
||||
|
||||
class FeatureAccess:
|
||||
READ = "read"
|
||||
WRITE = "write"
|
||||
|
||||
|
||||
VALID_FEATURE_ACCESS = (FeatureAccess.READ, FeatureAccess.WRITE)
|
||||
|
||||
|
||||
def bytes_to_str(b: bytes) -> str:
|
||||
return str(codecs.encode(b, "hex").decode("utf-8"))
|
||||
|
||||
|
||||
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 Result:
|
||||
"""
|
||||
represents the results of an evaluation of statements against features.
|
||||
|
||||
instances of this class should behave like a bool,
|
||||
e.g. `assert Result(True, ...) == True`
|
||||
|
||||
instances track additional metadata about evaluation results.
|
||||
they contain references to the statement node (e.g. an And statement),
|
||||
as well as the children Result instances.
|
||||
|
||||
we need this so that we can render the tree of expressions and their results.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
success: bool,
|
||||
statement: Union["capa.engine.Statement", "Feature"],
|
||||
children: List["Result"],
|
||||
locations: Optional[Set[Address]] = None,
|
||||
):
|
||||
super().__init__()
|
||||
self.success = success
|
||||
self.statement = statement
|
||||
self.children = children
|
||||
self.locations = locations if locations is not None else set()
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, bool):
|
||||
return self.success == other
|
||||
return False
|
||||
|
||||
def __bool__(self):
|
||||
return self.success
|
||||
|
||||
def __nonzero__(self):
|
||||
return self.success
|
||||
|
||||
|
||||
class Feature(abc.ABC): # noqa: B024
|
||||
# this is an abstract class, since we don't want anyone to instantiate it directly,
|
||||
# but it doesn't have any abstract methods.
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
value: Union[str, int, float, bytes],
|
||||
description: Optional[str] = None,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
value (any): the value of the feature, such as the number or string.
|
||||
description (str): a human-readable description that explains the feature value.
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
self.name = self.__class__.__name__.lower()
|
||||
self.value = value
|
||||
self.description = description
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.name, self.value))
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.name == other.name and self.value == other.value
|
||||
|
||||
def __lt__(self, other):
|
||||
# implementing sorting by serializing to JSON is a huge hack.
|
||||
# its slow, inelegant, and probably doesn't work intuitively;
|
||||
# however, we only use it for deterministic output, so it's good enough for now.
|
||||
|
||||
# circular import
|
||||
# we should fix if this wasn't already a huge hack.
|
||||
import capa.features.freeze.features
|
||||
|
||||
return (
|
||||
capa.features.freeze.features.feature_from_capa(self).model_dump_json()
|
||||
< capa.features.freeze.features.feature_from_capa(other).model_dump_json()
|
||||
)
|
||||
|
||||
def get_name_str(self) -> str:
|
||||
"""
|
||||
render the name of this feature, for use by `__str__` and friends.
|
||||
subclasses should override to customize the rendering.
|
||||
"""
|
||||
return self.name
|
||||
|
||||
def get_value_str(self) -> str:
|
||||
"""
|
||||
render the value of this feature, for use by `__str__` and friends.
|
||||
subclasses should override to customize the rendering.
|
||||
"""
|
||||
return str(self.value)
|
||||
|
||||
def __str__(self):
|
||||
if self.value is not None:
|
||||
if self.description:
|
||||
return f"{self.get_name_str()}({self.get_value_str()} = {self.description})"
|
||||
else:
|
||||
return f"{self.get_name_str()}({self.get_value_str()})"
|
||||
else:
|
||||
return f"{self.get_name_str()}"
|
||||
|
||||
def __repr__(self):
|
||||
return str(self)
|
||||
|
||||
def evaluate(self, ctx: Dict["Feature", Set[Address]], **kwargs) -> Result:
|
||||
capa.perf.counters["evaluate.feature"] += 1
|
||||
capa.perf.counters["evaluate.feature." + self.name] += 1
|
||||
return Result(self in ctx, self, [], locations=ctx.get(self, set()))
|
||||
|
||||
|
||||
class MatchedRule(Feature):
|
||||
def __init__(self, value: str, description=None):
|
||||
super().__init__(value, description=description)
|
||||
self.name = "match"
|
||||
|
||||
|
||||
class Characteristic(Feature):
|
||||
def __init__(self, value: str, description=None):
|
||||
super().__init__(value, description=description)
|
||||
|
||||
|
||||
class String(Feature):
|
||||
def __init__(self, value: str, description=None):
|
||||
super().__init__(value, description=description)
|
||||
|
||||
def get_value_str(self) -> str:
|
||||
assert isinstance(self.value, str)
|
||||
return escape_string(self.value)
|
||||
|
||||
|
||||
class Class(Feature):
|
||||
def __init__(self, value: str, description=None):
|
||||
super().__init__(value, description=description)
|
||||
|
||||
|
||||
class Namespace(Feature):
|
||||
def __init__(self, value: str, description=None):
|
||||
super().__init__(value, description=description)
|
||||
|
||||
|
||||
class Substring(String):
|
||||
def __init__(self, value: str, description=None):
|
||||
super().__init__(value, description=description)
|
||||
self.value = value
|
||||
|
||||
def evaluate(self, ctx, short_circuit=True):
|
||||
capa.perf.counters["evaluate.feature"] += 1
|
||||
capa.perf.counters["evaluate.feature.substring"] += 1
|
||||
|
||||
# mapping from string value to list of locations.
|
||||
# will unique the locations later on.
|
||||
matches: typing.DefaultDict[str, Set[Address]] = collections.defaultdict(set)
|
||||
|
||||
assert isinstance(self.value, str)
|
||||
for feature, locations in ctx.items():
|
||||
if not isinstance(feature, (String,)):
|
||||
continue
|
||||
|
||||
if not isinstance(feature.value, str):
|
||||
# this is a programming error: String should only contain str
|
||||
raise ValueError("unexpected feature value type")
|
||||
|
||||
if self.value in feature.value:
|
||||
matches[feature.value].update(locations)
|
||||
if short_circuit:
|
||||
# we found one matching string, thats sufficient to match.
|
||||
# don't collect other matching strings in this mode.
|
||||
break
|
||||
|
||||
if matches:
|
||||
# collect all locations
|
||||
locations = set()
|
||||
for locs in matches.values():
|
||||
locations.update(locs)
|
||||
|
||||
# unlike other features, we cannot return put a reference to `self` directly in a `Result`.
|
||||
# this is because `self` may match on many strings, so we can't stuff the matched value into it.
|
||||
# instead, return a new instance that has a reference to both the substring and the matched values.
|
||||
return Result(True, _MatchedSubstring(self, dict(matches)), [], locations=locations)
|
||||
else:
|
||||
return Result(False, _MatchedSubstring(self, {}), [])
|
||||
|
||||
def get_value_str(self) -> str:
|
||||
assert isinstance(self.value, str)
|
||||
return escape_string(self.value)
|
||||
|
||||
def __str__(self):
|
||||
assert isinstance(self.value, str)
|
||||
return f"substring({escape_string(self.value)})"
|
||||
|
||||
|
||||
class _MatchedSubstring(Substring):
|
||||
"""
|
||||
this represents specific match instances of a substring feature.
|
||||
treat it the same as a `Substring` except it has the `matches` field that contains the complete strings that matched.
|
||||
|
||||
note: this type should only ever be constructed by `Substring.evaluate()`. it is not part of the public API.
|
||||
"""
|
||||
|
||||
def __init__(self, substring: Substring, matches: Dict[str, Set[Address]]):
|
||||
"""
|
||||
args:
|
||||
substring: the substring feature that matches.
|
||||
match: mapping from matching string to its locations.
|
||||
"""
|
||||
super().__init__(str(substring.value), description=substring.description)
|
||||
# we want this to collide with the name of `Substring` above,
|
||||
# so that it works nicely with the renderers.
|
||||
self.name = "substring"
|
||||
# this may be None if the substring doesn't match
|
||||
self.matches = matches
|
||||
|
||||
def __str__(self):
|
||||
matches = ", ".join(f'"{s}"' for s in (self.matches or {}).keys())
|
||||
assert isinstance(self.value, str)
|
||||
return f'substring("{self.value}", matches = {matches})'
|
||||
|
||||
|
||||
class Regex(String):
|
||||
def __init__(self, value: str, description=None):
|
||||
super().__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 as exc:
|
||||
if value.endswith("/i"):
|
||||
value = value[: -len("i")]
|
||||
raise ValueError(
|
||||
f"invalid regular expression: {value} it should use Python syntax, try it at https://pythex.org"
|
||||
) from exc
|
||||
|
||||
def evaluate(self, ctx, short_circuit=True):
|
||||
capa.perf.counters["evaluate.feature"] += 1
|
||||
capa.perf.counters["evaluate.feature.regex"] += 1
|
||||
|
||||
# mapping from string value to list of locations.
|
||||
# will unique the locations later on.
|
||||
matches: typing.DefaultDict[str, Set[Address]] = collections.defaultdict(set)
|
||||
|
||||
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].update(locations)
|
||||
if short_circuit:
|
||||
# we found one matching string, thats sufficient to match.
|
||||
# don't collect other matching strings in this mode.
|
||||
break
|
||||
|
||||
if matches:
|
||||
# collect all locations
|
||||
locations = set()
|
||||
for locs in matches.values():
|
||||
locations.update(locs)
|
||||
|
||||
# unlike other features, we cannot return put a reference to `self` directly in a `Result`.
|
||||
# this is because `self` may match on many strings, so we can't stuff the matched value into it.
|
||||
# instead, return a new instance that has a reference to both the regex and the matched values.
|
||||
# see #262.
|
||||
return Result(True, _MatchedRegex(self, dict(matches)), [], locations=locations)
|
||||
else:
|
||||
return Result(False, _MatchedRegex(self, {}), [])
|
||||
|
||||
def __str__(self):
|
||||
assert isinstance(self.value, str)
|
||||
return f"regex(string =~ {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: Dict[str, Set[Address]]):
|
||||
"""
|
||||
args:
|
||||
regex: the regex feature that matches.
|
||||
matches: mapping from matching string to its locations.
|
||||
"""
|
||||
super().__init__(str(regex.value), description=regex.description)
|
||||
# we want this to collide with the name of `Regex` above,
|
||||
# so that it works nicely with the renderers.
|
||||
self.name = "regex"
|
||||
# this may be None if the regex doesn't match
|
||||
self.matches = matches
|
||||
|
||||
def __str__(self):
|
||||
matches = ", ".join(f'"{s}"' for s in (self.matches or {}).keys())
|
||||
assert isinstance(self.value, str)
|
||||
return f"regex(string =~ {self.value}, matches = {matches})"
|
||||
|
||||
|
||||
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().__init__(value, description=description)
|
||||
self.value = value
|
||||
|
||||
def evaluate(self, ctx, **kwargs):
|
||||
capa.perf.counters["evaluate.feature"] += 1
|
||||
capa.perf.counters["evaluate.feature.bytes"] += 1
|
||||
|
||||
assert isinstance(self.value, bytes)
|
||||
for feature, locations in ctx.items():
|
||||
if not isinstance(feature, (Bytes,)):
|
||||
continue
|
||||
|
||||
assert isinstance(feature.value, bytes)
|
||||
if feature.value.startswith(self.value):
|
||||
return Result(True, self, [], locations=locations)
|
||||
|
||||
return Result(False, self, [])
|
||||
|
||||
def get_value_str(self):
|
||||
assert isinstance(self.value, bytes)
|
||||
return hex_string(bytes_to_str(self.value))
|
||||
|
||||
|
||||
# other candidates here: https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#machine-types
|
||||
ARCH_I386 = "i386"
|
||||
ARCH_AMD64 = "amd64"
|
||||
# dotnet
|
||||
ARCH_ANY = "any"
|
||||
# dex
|
||||
ARCH_DALVIK = "dalvik"
|
||||
VALID_ARCH = (ARCH_I386, ARCH_AMD64, ARCH_ANY, ARCH_DALVIK)
|
||||
|
||||
|
||||
class Arch(Feature):
|
||||
def __init__(self, value: str, description=None):
|
||||
super().__init__(value, description=description)
|
||||
self.name = "arch"
|
||||
|
||||
|
||||
OS_WINDOWS = "windows"
|
||||
OS_LINUX = "linux"
|
||||
OS_MACOS = "macos"
|
||||
OS_ANDROID = "android"
|
||||
# dotnet
|
||||
OS_ANY = "any"
|
||||
VALID_OS = {os.value for os in capa.features.extractors.elf.OS}
|
||||
VALID_OS.update({OS_WINDOWS, OS_LINUX, OS_MACOS, OS_ANY, OS_ANDROID})
|
||||
# internal only, not to be used in rules
|
||||
OS_AUTO = "auto"
|
||||
|
||||
|
||||
class OS(Feature):
|
||||
def __init__(self, value: str, description=None):
|
||||
super().__init__(value, description=description)
|
||||
self.name = "os"
|
||||
|
||||
def evaluate(self, ctx, **kwargs):
|
||||
capa.perf.counters["evaluate.feature"] += 1
|
||||
capa.perf.counters["evaluate.feature." + self.name] += 1
|
||||
|
||||
for feature, locations in ctx.items():
|
||||
if not isinstance(feature, (OS,)):
|
||||
continue
|
||||
|
||||
assert isinstance(feature.value, str)
|
||||
if OS_ANY in (self.value, feature.value) or self.value == feature.value:
|
||||
return Result(True, self, [], locations=locations)
|
||||
|
||||
return Result(False, self, [])
|
||||
|
||||
|
||||
FORMAT_PE = "pe"
|
||||
FORMAT_ELF = "elf"
|
||||
FORMAT_DOTNET = "dotnet"
|
||||
FORMAT_DEX = "dex"
|
||||
VALID_FORMAT = (FORMAT_PE, FORMAT_ELF, FORMAT_DOTNET, FORMAT_DEX)
|
||||
# internal only, not to be used in rules
|
||||
FORMAT_AUTO = "auto"
|
||||
FORMAT_SC32 = "sc32"
|
||||
FORMAT_SC64 = "sc64"
|
||||
FORMAT_CAPE = "cape"
|
||||
STATIC_FORMATS = {
|
||||
FORMAT_SC32,
|
||||
FORMAT_SC64,
|
||||
FORMAT_PE,
|
||||
FORMAT_ELF,
|
||||
FORMAT_DOTNET,
|
||||
FORMAT_DEX,
|
||||
}
|
||||
DYNAMIC_FORMATS = {
|
||||
FORMAT_CAPE,
|
||||
}
|
||||
FORMAT_FREEZE = "freeze"
|
||||
FORMAT_RESULT = "result"
|
||||
FORMAT_UNKNOWN = "unknown"
|
||||
|
||||
|
||||
class Format(Feature):
|
||||
def __init__(self, value: str, description=None):
|
||||
super().__init__(value, description=description)
|
||||
self.name = "format"
|
||||
|
||||
|
||||
def is_global_feature(feature):
|
||||
"""
|
||||
is this a feature that is extracted at every scope?
|
||||
today, these are OS and arch features.
|
||||
"""
|
||||
return isinstance(feature, (OS, Arch))
|
||||
@@ -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
|
||||
|
||||
470
capa/features/extractors/base_extractor.py
Normal file
470
capa/features/extractors/base_extractor.py
Normal file
@@ -0,0 +1,470 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import abc
|
||||
import hashlib
|
||||
import dataclasses
|
||||
from typing import Any, Dict, Tuple, Union, Iterator
|
||||
from dataclasses import dataclass
|
||||
|
||||
# TODO(williballenthin): use typing.TypeAlias directly when Python 3.9 is deprecated
|
||||
# https://github.com/mandiant/capa/issues/1699
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
import capa.features.address
|
||||
from capa.features.common import Feature
|
||||
from capa.features.address import Address, ThreadAddress, ProcessAddress, DynamicCallAddress, AbsoluteVirtualAddress
|
||||
|
||||
# feature extractors may reference functions, BBs, insns by opaque handle values.
|
||||
# you can use the `.address` property to get and render the address of the feature.
|
||||
#
|
||||
# these handles are only consumed by routines on
|
||||
# the feature extractor from which they were created.
|
||||
|
||||
|
||||
@dataclass
|
||||
class SampleHashes:
|
||||
md5: str
|
||||
sha1: str
|
||||
sha256: str
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, buf: bytes) -> "SampleHashes":
|
||||
md5 = hashlib.md5()
|
||||
sha1 = hashlib.sha1()
|
||||
sha256 = hashlib.sha256()
|
||||
md5.update(buf)
|
||||
sha1.update(buf)
|
||||
sha256.update(buf)
|
||||
|
||||
return cls(md5=md5.hexdigest(), sha1=sha1.hexdigest(), sha256=sha256.hexdigest())
|
||||
|
||||
|
||||
@dataclass
|
||||
class FunctionHandle:
|
||||
"""reference to a function recognized by a feature extractor.
|
||||
|
||||
Attributes:
|
||||
address: the address of the function.
|
||||
inner: extractor-specific data.
|
||||
ctx: a context object for the extractor.
|
||||
"""
|
||||
|
||||
address: Address
|
||||
inner: Any
|
||||
ctx: Dict[str, Any] = dataclasses.field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class BBHandle:
|
||||
"""reference to a basic block recognized by a feature extractor.
|
||||
|
||||
Attributes:
|
||||
address: the address of the basic block start address.
|
||||
inner: extractor-specific data.
|
||||
"""
|
||||
|
||||
address: Address
|
||||
inner: Any
|
||||
|
||||
|
||||
@dataclass
|
||||
class InsnHandle:
|
||||
"""reference to a instruction recognized by a feature extractor.
|
||||
|
||||
Attributes:
|
||||
address: the address of the instruction address.
|
||||
inner: extractor-specific data.
|
||||
"""
|
||||
|
||||
address: Address
|
||||
inner: Any
|
||||
|
||||
|
||||
class StaticFeatureExtractor:
|
||||
"""
|
||||
StaticFeatureExtractor defines the interface for fetching features from a
|
||||
sample without running it; extractors that rely on the execution trace of
|
||||
a sample must implement the other sibling class, DynamicFeatureExtracor.
|
||||
|
||||
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 StaticFeatureExtractor 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, hashes: SampleHashes):
|
||||
#
|
||||
# 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().__init__()
|
||||
self._sample_hashes = hashes
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_base_address(self) -> Union[AbsoluteVirtualAddress, capa.features.address._NoAddress]:
|
||||
"""
|
||||
fetch the preferred load address at which the sample was analyzed.
|
||||
|
||||
when the base address is `NO_ADDRESS`, then the loader has no concept of a preferred load address.
|
||||
such as: shellcode, .NET modules, etc.
|
||||
in these scenarios, RelativeVirtualAddresses aren't used.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_sample_hashes(self) -> SampleHashes:
|
||||
"""
|
||||
fetch the hashes for the sample contained within the extractor.
|
||||
"""
|
||||
return self._sample_hashes
|
||||
|
||||
@abc.abstractmethod
|
||||
def extract_global_features(self) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
extract features found at every scope ("global").
|
||||
|
||||
example::
|
||||
|
||||
extractor = VivisectFeatureExtractor(vw, path)
|
||||
for feature, va in extractor.get_global_features():
|
||||
print('0x%x: %s', va, feature)
|
||||
|
||||
yields:
|
||||
Tuple[Feature, Address]: feature and its location
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
def extract_file_features(self) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
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, Address]: feature and its location
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@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 NotImplementedError()
|
||||
|
||||
def is_library_function(self, addr: Address) -> bool:
|
||||
"""
|
||||
is the given address a library function?
|
||||
the backend may implement its own function matching algorithm, or none at all.
|
||||
we accept an address 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:
|
||||
addr (Address): the 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, addr: Address) -> 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:
|
||||
addr (Address): the address of a function.
|
||||
|
||||
returns:
|
||||
str: the function name
|
||||
|
||||
raises:
|
||||
KeyError: when the given function does not have a name.
|
||||
"""
|
||||
raise KeyError(addr)
|
||||
|
||||
@abc.abstractmethod
|
||||
def extract_function_features(self, f: FunctionHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
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, address in extractor.extract_function_features(function):
|
||||
print('0x%x: %s', address, feature)
|
||||
|
||||
args:
|
||||
f [FunctionHandle]: an opaque value previously fetched from `.get_functions()`.
|
||||
|
||||
yields:
|
||||
Tuple[Feature, Address]: feature and its location
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@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 NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
def extract_basic_block_features(self, f: FunctionHandle, bb: BBHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
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, address in extractor.extract_basic_block_features(function, bb):
|
||||
print('0x%x: %s', address, 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, Address]: feature and its location
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@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 NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
def extract_insn_features(
|
||||
self, f: FunctionHandle, bb: BBHandle, insn: InsnHandle
|
||||
) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
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, address in extractor.extract_insn_features(function, bb, insn):
|
||||
print('0x%x: %s', address, 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, Address]: feature and its location
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProcessHandle:
|
||||
"""
|
||||
reference to a process extracted by the sandbox.
|
||||
|
||||
Attributes:
|
||||
address: process's address (pid)
|
||||
inner: sandbox-specific data
|
||||
"""
|
||||
|
||||
address: ProcessAddress
|
||||
inner: Any
|
||||
|
||||
|
||||
@dataclass
|
||||
class ThreadHandle:
|
||||
"""
|
||||
reference to a thread extracted by the sandbox.
|
||||
|
||||
Attributes:
|
||||
address: thread's address (tid)
|
||||
inner: sandbox-specific data
|
||||
"""
|
||||
|
||||
address: ThreadAddress
|
||||
inner: Any
|
||||
|
||||
|
||||
@dataclass
|
||||
class CallHandle:
|
||||
"""
|
||||
reference to an api call extracted by the sandbox.
|
||||
|
||||
Attributes:
|
||||
address: call's address, such as event index or id
|
||||
inner: sandbox-specific data
|
||||
"""
|
||||
|
||||
address: DynamicCallAddress
|
||||
inner: Any
|
||||
|
||||
|
||||
class DynamicFeatureExtractor:
|
||||
"""
|
||||
DynamicFeatureExtractor defines the interface for fetching features from a
|
||||
sandbox' analysis of a sample; extractors that rely on statically analyzing
|
||||
a sample must implement the sibling extractor, StaticFeatureExtractor.
|
||||
|
||||
Features are grouped mainly into threads that alongside their meta-features are also grouped into
|
||||
processes (that also have their own features). Other scopes (such as function and file) may also apply
|
||||
for a specific sandbox.
|
||||
|
||||
This class is not instantiated directly; it is the base class for other implementations.
|
||||
"""
|
||||
|
||||
__metaclass__ = abc.ABCMeta
|
||||
|
||||
def __init__(self, hashes: SampleHashes):
|
||||
#
|
||||
# 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().__init__()
|
||||
self._sample_hashes = hashes
|
||||
|
||||
def get_sample_hashes(self) -> SampleHashes:
|
||||
"""
|
||||
fetch the hashes for the sample contained within the extractor.
|
||||
"""
|
||||
return self._sample_hashes
|
||||
|
||||
@abc.abstractmethod
|
||||
def extract_global_features(self) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
extract features found at every scope ("global").
|
||||
|
||||
example::
|
||||
|
||||
extractor = CapeFeatureExtractor.from_report(json.loads(buf))
|
||||
for feature, addr in extractor.get_global_features():
|
||||
print(addr, feature)
|
||||
|
||||
yields:
|
||||
Tuple[Feature, Address]: feature and its location
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
def extract_file_features(self) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
extract file-scope features.
|
||||
|
||||
example::
|
||||
|
||||
extractor = CapeFeatureExtractor.from_report(json.loads(buf))
|
||||
for feature, addr in extractor.get_file_features():
|
||||
print(addr, feature)
|
||||
|
||||
yields:
|
||||
Tuple[Feature, Address]: feature and its location
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_processes(self) -> Iterator[ProcessHandle]:
|
||||
"""
|
||||
Enumerate processes in the trace.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
def extract_process_features(self, ph: ProcessHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
Yields all the features of a process. These include:
|
||||
- file features of the process' image
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_process_name(self, ph: ProcessHandle) -> str:
|
||||
"""
|
||||
Returns the human-readable name for the given process,
|
||||
such as the filename.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_threads(self, ph: ProcessHandle) -> Iterator[ThreadHandle]:
|
||||
"""
|
||||
Enumerate threads in the given process.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
def extract_thread_features(self, ph: ProcessHandle, th: ThreadHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
Yields all the features of a thread. These include:
|
||||
- sequenced api traces
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_calls(self, ph: ProcessHandle, th: ThreadHandle) -> Iterator[CallHandle]:
|
||||
"""
|
||||
Enumerate calls in the given thread
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
def extract_call_features(
|
||||
self, ph: ProcessHandle, th: ThreadHandle, ch: CallHandle
|
||||
) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
Yields all features of a call. These include:
|
||||
- api name
|
||||
- bytes/strings/numbers extracted from arguments
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_call_name(self, ph: ProcessHandle, th: ThreadHandle, ch: CallHandle) -> str:
|
||||
"""
|
||||
Returns the human-readable name for the given call,
|
||||
such as as rendered API log entry, like:
|
||||
|
||||
Foo(1, "two", b"\x00\x11") -> -1
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
FeatureExtractor: TypeAlias = Union[StaticFeatureExtractor, DynamicFeatureExtractor]
|
||||
0
capa/features/extractors/binja/__init__.py
Normal file
0
capa/features/extractors/binja/__init__.py
Normal file
184
capa/features/extractors/binja/basicblock.py
Normal file
184
capa/features/extractors/binja/basicblock.py
Normal file
@@ -0,0 +1,184 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import string
|
||||
import struct
|
||||
from typing import Tuple, Iterator
|
||||
|
||||
from binaryninja import Function, Settings
|
||||
from binaryninja import BasicBlock as BinjaBasicBlock
|
||||
from binaryninja import (
|
||||
BinaryView,
|
||||
SymbolType,
|
||||
RegisterValueType,
|
||||
VariableSourceType,
|
||||
MediumLevelILSetVar,
|
||||
MediumLevelILOperation,
|
||||
MediumLevelILBasicBlock,
|
||||
MediumLevelILInstruction,
|
||||
)
|
||||
|
||||
from capa.features.common import Feature, Characteristic
|
||||
from capa.features.address import Address
|
||||
from capa.features.basicblock import BasicBlock
|
||||
from capa.features.extractors.helpers import MIN_STACKSTRING_LEN
|
||||
from capa.features.extractors.base_extractor import BBHandle, FunctionHandle
|
||||
|
||||
use_const_outline: bool = False
|
||||
settings: Settings = Settings()
|
||||
if settings.contains("analysis.outlining.builtins") and settings.get_bool("analysis.outlining.builtins"):
|
||||
use_const_outline = True
|
||||
|
||||
|
||||
def get_printable_len_ascii(s: bytes) -> int:
|
||||
"""Return string length if all operand bytes are ascii or utf16-le printable"""
|
||||
count = 0
|
||||
for c in s:
|
||||
if c == 0:
|
||||
return count
|
||||
if c < 127 and chr(c) in string.printable:
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
def get_printable_len_wide(s: bytes) -> int:
|
||||
"""Return string length if all operand bytes are ascii or utf16-le printable"""
|
||||
if all(c == 0x00 for c in s[1::2]):
|
||||
return get_printable_len_ascii(s[::2])
|
||||
return 0
|
||||
|
||||
|
||||
def get_stack_string_len(f: Function, il: MediumLevelILInstruction) -> int:
|
||||
bv: BinaryView = f.view
|
||||
|
||||
if il.operation != MediumLevelILOperation.MLIL_CALL:
|
||||
return 0
|
||||
|
||||
target = il.dest
|
||||
if target.operation not in [MediumLevelILOperation.MLIL_CONST, MediumLevelILOperation.MLIL_CONST_PTR]:
|
||||
return 0
|
||||
|
||||
addr = target.value.value
|
||||
sym = bv.get_symbol_at(addr)
|
||||
if not sym or sym.type != SymbolType.LibraryFunctionSymbol:
|
||||
return 0
|
||||
|
||||
if sym.name not in ["__builtin_strncpy", "__builtin_strcpy", "__builtin_wcscpy"]:
|
||||
return 0
|
||||
|
||||
if len(il.params) < 2:
|
||||
return 0
|
||||
|
||||
dest = il.params[0]
|
||||
if dest.operation in [MediumLevelILOperation.MLIL_ADDRESS_OF, MediumLevelILOperation.MLIL_VAR]:
|
||||
var = dest.src
|
||||
else:
|
||||
return 0
|
||||
|
||||
if var.source_type != VariableSourceType.StackVariableSourceType:
|
||||
return 0
|
||||
|
||||
src = il.params[1]
|
||||
if src.value.type != RegisterValueType.ConstantDataAggregateValue:
|
||||
return 0
|
||||
|
||||
s = f.get_constant_data(RegisterValueType.ConstantDataAggregateValue, src.value.value)
|
||||
return max(get_printable_len_ascii(bytes(s)), get_printable_len_wide(bytes(s)))
|
||||
|
||||
|
||||
def get_printable_len(il: MediumLevelILSetVar) -> int:
|
||||
"""Return string length if all operand bytes are ascii or utf16-le printable"""
|
||||
width = il.dest.type.width
|
||||
value = il.src.value.value
|
||||
|
||||
if width == 1:
|
||||
chars = struct.pack("<B", value & 0xFF)
|
||||
elif width == 2:
|
||||
chars = struct.pack("<H", value & 0xFFFF)
|
||||
elif width == 4:
|
||||
chars = struct.pack("<I", value & 0xFFFFFFFF)
|
||||
elif width == 8:
|
||||
chars = struct.pack("<Q", value & 0xFFFFFFFFFFFFFFFF)
|
||||
else:
|
||||
return 0
|
||||
|
||||
def is_printable_ascii(chars_: bytes):
|
||||
return all(c < 127 and chr(c) in string.printable for c in chars_)
|
||||
|
||||
def is_printable_utf16le(chars_: bytes):
|
||||
if all(c == 0x00 for c in chars_[1::2]):
|
||||
return is_printable_ascii(chars_[::2])
|
||||
|
||||
if is_printable_ascii(chars):
|
||||
return width
|
||||
|
||||
if is_printable_utf16le(chars):
|
||||
return width // 2
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def is_mov_imm_to_stack(il: MediumLevelILInstruction) -> bool:
|
||||
"""verify instruction moves immediate onto stack"""
|
||||
if il.operation != MediumLevelILOperation.MLIL_SET_VAR:
|
||||
return False
|
||||
|
||||
if il.src.operation != MediumLevelILOperation.MLIL_CONST:
|
||||
return False
|
||||
|
||||
if il.dest.source_type != VariableSourceType.StackVariableSourceType:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def bb_contains_stackstring(f: Function, bb: MediumLevelILBasicBlock) -> bool:
|
||||
"""check basic block for stackstring indicators
|
||||
|
||||
true if basic block contains enough moves of constant bytes to the stack
|
||||
"""
|
||||
count = 0
|
||||
for il in bb:
|
||||
if use_const_outline:
|
||||
count += get_stack_string_len(f, il)
|
||||
else:
|
||||
if is_mov_imm_to_stack(il):
|
||||
count += get_printable_len(il)
|
||||
|
||||
if count > MIN_STACKSTRING_LEN:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def extract_bb_stackstring(fh: FunctionHandle, bbh: BBHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""extract stackstring indicators from basic block"""
|
||||
bb: Tuple[BinjaBasicBlock, MediumLevelILBasicBlock] = bbh.inner
|
||||
if bb[1] is not None and bb_contains_stackstring(fh.inner, bb[1]):
|
||||
yield Characteristic("stack string"), bbh.address
|
||||
|
||||
|
||||
def extract_bb_tight_loop(fh: FunctionHandle, bbh: BBHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""extract tight loop indicators from a basic block"""
|
||||
bb: Tuple[BinjaBasicBlock, MediumLevelILBasicBlock] = bbh.inner
|
||||
for edge in bb[0].outgoing_edges:
|
||||
if edge.target.start == bb[0].start:
|
||||
yield Characteristic("tight loop"), bbh.address
|
||||
|
||||
|
||||
def extract_features(fh: FunctionHandle, bbh: BBHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""extract basic block features"""
|
||||
for bb_handler in BASIC_BLOCK_HANDLERS:
|
||||
for feature, addr in bb_handler(fh, bbh):
|
||||
yield feature, addr
|
||||
yield BasicBlock(), bbh.address
|
||||
|
||||
|
||||
BASIC_BLOCK_HANDLERS = (
|
||||
extract_bb_tight_loop,
|
||||
extract_bb_stackstring,
|
||||
)
|
||||
81
capa/features/extractors/binja/extractor.py
Normal file
81
capa/features/extractors/binja/extractor.py
Normal file
@@ -0,0 +1,81 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
from typing import List, Tuple, Iterator
|
||||
|
||||
import binaryninja as binja
|
||||
|
||||
import capa.features.extractors.elf
|
||||
import capa.features.extractors.binja.file
|
||||
import capa.features.extractors.binja.insn
|
||||
import capa.features.extractors.binja.global_
|
||||
import capa.features.extractors.binja.function
|
||||
import capa.features.extractors.binja.basicblock
|
||||
from capa.features.common import Feature
|
||||
from capa.features.address import Address, AbsoluteVirtualAddress
|
||||
from capa.features.extractors.base_extractor import (
|
||||
BBHandle,
|
||||
InsnHandle,
|
||||
SampleHashes,
|
||||
FunctionHandle,
|
||||
StaticFeatureExtractor,
|
||||
)
|
||||
|
||||
|
||||
class BinjaFeatureExtractor(StaticFeatureExtractor):
|
||||
def __init__(self, bv: binja.BinaryView):
|
||||
super().__init__(hashes=SampleHashes.from_bytes(bv.file.raw.read(0, len(bv.file.raw))))
|
||||
self.bv = bv
|
||||
self.global_features: List[Tuple[Feature, Address]] = []
|
||||
self.global_features.extend(capa.features.extractors.binja.file.extract_file_format(self.bv))
|
||||
self.global_features.extend(capa.features.extractors.binja.global_.extract_os(self.bv))
|
||||
self.global_features.extend(capa.features.extractors.binja.global_.extract_arch(self.bv))
|
||||
|
||||
def get_base_address(self):
|
||||
return AbsoluteVirtualAddress(self.bv.start)
|
||||
|
||||
def extract_global_features(self):
|
||||
yield from self.global_features
|
||||
|
||||
def extract_file_features(self):
|
||||
yield from capa.features.extractors.binja.file.extract_features(self.bv)
|
||||
|
||||
def get_functions(self) -> Iterator[FunctionHandle]:
|
||||
for f in self.bv.functions:
|
||||
yield FunctionHandle(address=AbsoluteVirtualAddress(f.start), inner=f)
|
||||
|
||||
def extract_function_features(self, fh: FunctionHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
yield from capa.features.extractors.binja.function.extract_features(fh)
|
||||
|
||||
def get_basic_blocks(self, fh: FunctionHandle) -> Iterator[BBHandle]:
|
||||
f: binja.Function = fh.inner
|
||||
# Set up a MLIL basic block dict look up to associate the disassembly basic block with its MLIL basic block
|
||||
mlil_lookup = {}
|
||||
for mlil_bb in f.mlil.basic_blocks:
|
||||
mlil_lookup[mlil_bb.source_block.start] = mlil_bb
|
||||
|
||||
for bb in f.basic_blocks:
|
||||
mlil_bb = mlil_lookup.get(bb.start)
|
||||
|
||||
yield BBHandle(address=AbsoluteVirtualAddress(bb.start), inner=(bb, mlil_bb))
|
||||
|
||||
def extract_basic_block_features(self, fh: FunctionHandle, bbh: BBHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
yield from capa.features.extractors.binja.basicblock.extract_features(fh, bbh)
|
||||
|
||||
def get_instructions(self, fh: FunctionHandle, bbh: BBHandle) -> Iterator[InsnHandle]:
|
||||
import capa.features.extractors.binja.helpers as binja_helpers
|
||||
|
||||
bb: Tuple[binja.BasicBlock, binja.MediumLevelILBasicBlock] = bbh.inner
|
||||
addr = bb[0].start
|
||||
|
||||
for text, length in bb[0]:
|
||||
insn = binja_helpers.DisassemblyInstruction(addr, length, text)
|
||||
yield InsnHandle(address=AbsoluteVirtualAddress(addr), inner=insn)
|
||||
addr += length
|
||||
|
||||
def extract_insn_features(self, fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle):
|
||||
yield from capa.features.extractors.binja.insn.extract_features(fh, bbh, ih)
|
||||
187
capa/features/extractors/binja/file.py
Normal file
187
capa/features/extractors/binja/file.py
Normal file
@@ -0,0 +1,187 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import struct
|
||||
from typing import Tuple, Iterator
|
||||
|
||||
from binaryninja import Segment, BinaryView, SymbolType, SymbolBinding
|
||||
|
||||
import capa.features.extractors.common
|
||||
import capa.features.extractors.helpers
|
||||
import capa.features.extractors.strings
|
||||
from capa.features.file import Export, Import, Section, FunctionName
|
||||
from capa.features.common import FORMAT_PE, FORMAT_ELF, Format, String, Feature, Characteristic
|
||||
from capa.features.address import NO_ADDRESS, Address, FileOffsetAddress, AbsoluteVirtualAddress
|
||||
from capa.features.extractors.binja.helpers import read_c_string, unmangle_c_name
|
||||
|
||||
|
||||
def check_segment_for_pe(bv: BinaryView, seg: Segment) -> Iterator[Tuple[int, int]]:
|
||||
"""check segment for embedded PE
|
||||
|
||||
adapted for binja from:
|
||||
https://github.com/vivisect/vivisect/blob/7be4037b1cecc4551b397f840405a1fc606f9b53/PE/carve.py#L19
|
||||
"""
|
||||
mz_xor = [
|
||||
(
|
||||
capa.features.extractors.helpers.xor_static(b"MZ", i),
|
||||
capa.features.extractors.helpers.xor_static(b"PE", i),
|
||||
i,
|
||||
)
|
||||
for i in range(256)
|
||||
]
|
||||
|
||||
todo = []
|
||||
# If this is the first segment of the binary, skip the first bytes. Otherwise, there will always be a matched
|
||||
# PE at the start of the binaryview.
|
||||
start = seg.start
|
||||
if bv.view_type == "PE" and start == bv.start:
|
||||
start += 1
|
||||
|
||||
for mzx, pex, i in mz_xor:
|
||||
for off, _ in bv.find_all_data(start, seg.end, mzx):
|
||||
todo.append((off, mzx, pex, i))
|
||||
|
||||
while len(todo):
|
||||
off, mzx, pex, i = todo.pop()
|
||||
|
||||
# The MZ header has one field we will check e_lfanew is at 0x3c
|
||||
e_lfanew = off + 0x3C
|
||||
|
||||
if seg.end < (e_lfanew + 4):
|
||||
continue
|
||||
|
||||
newoff = struct.unpack("<I", capa.features.extractors.helpers.xor_static(bv.read(e_lfanew, 4), i))[0]
|
||||
|
||||
peoff = off + newoff
|
||||
if seg.end < (peoff + 2):
|
||||
continue
|
||||
|
||||
if bv.read(peoff, 2) == pex:
|
||||
yield off, i
|
||||
|
||||
|
||||
def extract_file_embedded_pe(bv: BinaryView) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""extract embedded PE features"""
|
||||
for seg in bv.segments:
|
||||
for ea, _ in check_segment_for_pe(bv, seg):
|
||||
yield Characteristic("embedded pe"), FileOffsetAddress(ea)
|
||||
|
||||
|
||||
def extract_file_export_names(bv: BinaryView) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""extract function exports"""
|
||||
for sym in bv.get_symbols_of_type(SymbolType.FunctionSymbol):
|
||||
if sym.binding in [SymbolBinding.GlobalBinding, SymbolBinding.WeakBinding]:
|
||||
name = sym.short_name
|
||||
yield Export(name), AbsoluteVirtualAddress(sym.address)
|
||||
unmangled_name = unmangle_c_name(name)
|
||||
if name != unmangled_name:
|
||||
yield Export(unmangled_name), AbsoluteVirtualAddress(sym.address)
|
||||
|
||||
for sym in bv.get_symbols_of_type(SymbolType.DataSymbol):
|
||||
if sym.binding not in [SymbolBinding.GlobalBinding]:
|
||||
continue
|
||||
|
||||
name = sym.short_name
|
||||
if not name.startswith("__forwarder_name"):
|
||||
continue
|
||||
|
||||
# Due to https://github.com/Vector35/binaryninja-api/issues/4641, in binja version 3.5, the symbol's name
|
||||
# does not contain the DLL name. As a workaround, we read the C string at the symbol's address, which contains
|
||||
# both the DLL name and the function name.
|
||||
# Once the above issue is closed in the next binjs stable release, we can update the code here to use the
|
||||
# symbol name directly.
|
||||
name = read_c_string(bv, sym.address, 1024)
|
||||
forwarded_name = capa.features.extractors.helpers.reformat_forwarded_export_name(name)
|
||||
yield Export(forwarded_name), AbsoluteVirtualAddress(sym.address)
|
||||
yield Characteristic("forwarded export"), AbsoluteVirtualAddress(sym.address)
|
||||
|
||||
|
||||
def extract_file_import_names(bv: BinaryView) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""extract function imports
|
||||
|
||||
1. imports by ordinal:
|
||||
- modulename.#ordinal
|
||||
|
||||
2. imports by name, results in two features to support importname-only
|
||||
matching:
|
||||
- modulename.importname
|
||||
- importname
|
||||
"""
|
||||
for sym in bv.get_symbols_of_type(SymbolType.ImportAddressSymbol):
|
||||
lib_name = str(sym.namespace)
|
||||
addr = AbsoluteVirtualAddress(sym.address)
|
||||
for name in capa.features.extractors.helpers.generate_symbols(lib_name, sym.short_name, include_dll=True):
|
||||
yield Import(name), addr
|
||||
|
||||
ordinal = sym.ordinal
|
||||
if ordinal != 0 and (lib_name != ""):
|
||||
ordinal_name = f"#{ordinal}"
|
||||
for name in capa.features.extractors.helpers.generate_symbols(lib_name, ordinal_name, include_dll=True):
|
||||
yield Import(name), addr
|
||||
|
||||
|
||||
def extract_file_section_names(bv: BinaryView) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""extract section names"""
|
||||
for name, section in bv.sections.items():
|
||||
yield Section(name), AbsoluteVirtualAddress(section.start)
|
||||
|
||||
|
||||
def extract_file_strings(bv: BinaryView) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""extract ASCII and UTF-16 LE strings"""
|
||||
for s in bv.strings:
|
||||
yield String(s.value), FileOffsetAddress(s.start)
|
||||
|
||||
|
||||
def extract_file_function_names(bv: BinaryView) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
extract the names of statically-linked library functions.
|
||||
"""
|
||||
for sym_name in bv.symbols:
|
||||
for sym in bv.symbols[sym_name]:
|
||||
if sym.type not in [SymbolType.LibraryFunctionSymbol, SymbolType.FunctionSymbol]:
|
||||
continue
|
||||
|
||||
name = sym.short_name
|
||||
yield FunctionName(name), sym.address
|
||||
if name.startswith("_"):
|
||||
# some linkers may prefix linked routines with a `_` to avoid name collisions.
|
||||
# extract features for both the mangled and un-mangled representations.
|
||||
# e.g. `_fwrite` -> `fwrite`
|
||||
# see: https://stackoverflow.com/a/2628384/87207
|
||||
yield FunctionName(name[1:]), sym.address
|
||||
|
||||
|
||||
def extract_file_format(bv: BinaryView) -> Iterator[Tuple[Feature, Address]]:
|
||||
view_type = bv.view_type
|
||||
if view_type in ["PE", "COFF"]:
|
||||
yield Format(FORMAT_PE), NO_ADDRESS
|
||||
elif view_type == "ELF":
|
||||
yield Format(FORMAT_ELF), NO_ADDRESS
|
||||
elif view_type == "Raw":
|
||||
# no file type to return when processing a binary file, but we want to continue processing
|
||||
return
|
||||
else:
|
||||
raise NotImplementedError(f"unexpected file format: {view_type}")
|
||||
|
||||
|
||||
def extract_features(bv: BinaryView) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""extract file features"""
|
||||
for file_handler in FILE_HANDLERS:
|
||||
for feature, addr in file_handler(bv):
|
||||
yield feature, addr
|
||||
|
||||
|
||||
FILE_HANDLERS = (
|
||||
extract_file_export_names,
|
||||
extract_file_import_names,
|
||||
extract_file_strings,
|
||||
extract_file_section_names,
|
||||
extract_file_embedded_pe,
|
||||
extract_file_function_names,
|
||||
extract_file_format,
|
||||
)
|
||||
35
capa/features/extractors/binja/find_binja_api.py
Normal file
35
capa/features/extractors/binja/find_binja_api.py
Normal file
@@ -0,0 +1,35 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
# When the script gets executed as a standalone executable (via PyInstaller), `import binaryninja` does not work because
|
||||
# we have excluded the binaryninja module in `pyinstaller.spec`. The trick here is to call the system Python and try
|
||||
# to find out the path of the binaryninja module that has been installed.
|
||||
# Note, including the binaryninja module in the `pyintaller.spec` would not work, since the binaryninja module tries to
|
||||
# find the binaryninja core e.g., `libbinaryninjacore.dylib`, using a relative path. And this does not work when the
|
||||
# binaryninja module is extracted by the PyInstaller.
|
||||
code = r"""
|
||||
from pathlib import Path
|
||||
from importlib import util
|
||||
spec = util.find_spec('binaryninja')
|
||||
if spec is not None:
|
||||
if len(spec.submodule_search_locations) > 0:
|
||||
path = Path(spec.submodule_search_locations[0])
|
||||
# encode the path with utf8 then convert to hex, make sure it can be read and restored properly
|
||||
print(str(path.parent).encode('utf8').hex())
|
||||
"""
|
||||
|
||||
|
||||
def find_binja_path() -> Path:
|
||||
raw_output = subprocess.check_output(["python", "-c", code]).decode("ascii").strip()
|
||||
return Path(bytes.fromhex(raw_output).decode("utf8"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(find_binja_path())
|
||||
104
capa/features/extractors/binja/function.py
Normal file
104
capa/features/extractors/binja/function.py
Normal file
@@ -0,0 +1,104 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
from typing import Tuple, Iterator
|
||||
|
||||
from binaryninja import Function, BinaryView, SymbolType, RegisterValueType, LowLevelILOperation
|
||||
|
||||
from capa.features.file import FunctionName
|
||||
from capa.features.common import Feature, Characteristic
|
||||
from capa.features.address import Address, AbsoluteVirtualAddress
|
||||
from capa.features.extractors import loops
|
||||
from capa.features.extractors.base_extractor import FunctionHandle
|
||||
|
||||
|
||||
def extract_function_calls_to(fh: FunctionHandle):
|
||||
"""extract callers to a function"""
|
||||
func: Function = fh.inner
|
||||
|
||||
for caller in func.caller_sites:
|
||||
# Everything that is a code reference to the current function is considered a caller, which actually includes
|
||||
# many other references that are NOT a caller. For example, an instruction `push function_start` will also be
|
||||
# considered a caller to the function
|
||||
llil = caller.llil
|
||||
if (llil is None) or llil.operation not in [
|
||||
LowLevelILOperation.LLIL_CALL,
|
||||
LowLevelILOperation.LLIL_CALL_STACK_ADJUST,
|
||||
LowLevelILOperation.LLIL_JUMP,
|
||||
LowLevelILOperation.LLIL_TAILCALL,
|
||||
]:
|
||||
continue
|
||||
|
||||
if llil.dest.value.type not in [
|
||||
RegisterValueType.ImportedAddressValue,
|
||||
RegisterValueType.ConstantValue,
|
||||
RegisterValueType.ConstantPointerValue,
|
||||
]:
|
||||
continue
|
||||
|
||||
address = llil.dest.value.value
|
||||
if address != func.start:
|
||||
continue
|
||||
|
||||
yield Characteristic("calls to"), AbsoluteVirtualAddress(caller.address)
|
||||
|
||||
|
||||
def extract_function_loop(fh: FunctionHandle):
|
||||
"""extract loop indicators from a function"""
|
||||
func: Function = fh.inner
|
||||
|
||||
edges = []
|
||||
|
||||
# construct control flow graph
|
||||
for bb in func.basic_blocks:
|
||||
for edge in bb.outgoing_edges:
|
||||
edges.append((bb.start, edge.target.start))
|
||||
|
||||
if loops.has_loop(edges):
|
||||
yield Characteristic("loop"), fh.address
|
||||
|
||||
|
||||
def extract_recursive_call(fh: FunctionHandle):
|
||||
"""extract recursive function call"""
|
||||
func: Function = fh.inner
|
||||
bv: BinaryView = func.view
|
||||
if bv is None:
|
||||
return
|
||||
|
||||
for ref in bv.get_code_refs(func.start):
|
||||
if ref.function == func:
|
||||
yield Characteristic("recursive call"), fh.address
|
||||
|
||||
|
||||
def extract_function_name(fh: FunctionHandle):
|
||||
"""extract function names (e.g., symtab names)"""
|
||||
func: Function = fh.inner
|
||||
bv: BinaryView = func.view
|
||||
if bv is None:
|
||||
return
|
||||
|
||||
for sym in bv.get_symbols(func.start):
|
||||
if sym.type not in [SymbolType.LibraryFunctionSymbol, SymbolType.FunctionSymbol]:
|
||||
continue
|
||||
|
||||
name = sym.short_name
|
||||
yield FunctionName(name), sym.address
|
||||
if name.startswith("_"):
|
||||
# some linkers may prefix linked routines with a `_` to avoid name collisions.
|
||||
# extract features for both the mangled and un-mangled representations.
|
||||
# e.g. `_fwrite` -> `fwrite`
|
||||
# see: https://stackoverflow.com/a/2628384/87207
|
||||
yield FunctionName(name[1:]), sym.address
|
||||
|
||||
|
||||
def extract_features(fh: FunctionHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
for func_handler in FUNCTION_HANDLERS:
|
||||
for feature, addr in func_handler(fh):
|
||||
yield feature, addr
|
||||
|
||||
|
||||
FUNCTION_HANDLERS = (extract_function_calls_to, extract_function_loop, extract_recursive_call, extract_function_name)
|
||||
60
capa/features/extractors/binja/global_.py
Normal file
60
capa/features/extractors/binja/global_.py
Normal file
@@ -0,0 +1,60 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
import logging
|
||||
from typing import Tuple, Iterator
|
||||
|
||||
from binaryninja import BinaryView
|
||||
|
||||
from capa.features.common import OS, OS_MACOS, ARCH_I386, ARCH_AMD64, OS_WINDOWS, Arch, Feature
|
||||
from capa.features.address import NO_ADDRESS, Address
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def extract_os(bv: BinaryView) -> Iterator[Tuple[Feature, Address]]:
|
||||
name = bv.platform.name
|
||||
if "-" in name:
|
||||
name = name.split("-")[0]
|
||||
|
||||
if name == "windows":
|
||||
yield OS(OS_WINDOWS), NO_ADDRESS
|
||||
|
||||
elif name == "macos":
|
||||
yield OS(OS_MACOS), NO_ADDRESS
|
||||
|
||||
elif name in ["linux", "freebsd", "decree"]:
|
||||
yield OS(name), NO_ADDRESS
|
||||
|
||||
else:
|
||||
# we likely end up here:
|
||||
# 1. handling shellcode, or
|
||||
# 2. handling a new file format (e.g. macho)
|
||||
#
|
||||
# for (1) we can't do much - its shellcode and all bets are off.
|
||||
# we could maybe accept a further CLI argument to specify the OS,
|
||||
# but i think this would be rarely used.
|
||||
# rules that rely on OS conditions will fail to match on shellcode.
|
||||
#
|
||||
# for (2), this logic will need to be updated as the format is implemented.
|
||||
logger.debug("unsupported file format: %s, will not guess OS", name)
|
||||
return
|
||||
|
||||
|
||||
def extract_arch(bv: BinaryView) -> Iterator[Tuple[Feature, Address]]:
|
||||
arch = bv.arch.name
|
||||
if arch == "x86_64":
|
||||
yield Arch(ARCH_AMD64), NO_ADDRESS
|
||||
elif arch == "x86":
|
||||
yield Arch(ARCH_I386), NO_ADDRESS
|
||||
else:
|
||||
# we likely end up here:
|
||||
# 1. handling a new architecture (e.g. aarch64)
|
||||
#
|
||||
# for (1), this logic will need to be updated as the format is implemented.
|
||||
logger.debug("unsupported architecture: %s", arch)
|
||||
return
|
||||
69
capa/features/extractors/binja/helpers.py
Normal file
69
capa/features/extractors/binja/helpers.py
Normal file
@@ -0,0 +1,69 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
import re
|
||||
from typing import List, Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
from binaryninja import BinaryView, LowLevelILInstruction
|
||||
from binaryninja.architecture import InstructionTextToken
|
||||
|
||||
|
||||
@dataclass
|
||||
class DisassemblyInstruction:
|
||||
address: int
|
||||
length: int
|
||||
text: List[InstructionTextToken]
|
||||
|
||||
|
||||
LLIL_VISITOR = Callable[[LowLevelILInstruction, LowLevelILInstruction, int], bool]
|
||||
|
||||
|
||||
def visit_llil_exprs(il: LowLevelILInstruction, func: LLIL_VISITOR):
|
||||
# BN does not really support operand index at the disassembly level, so use the LLIL operand index as a substitute.
|
||||
# Note, this is NOT always guaranteed to be the same as disassembly operand.
|
||||
for i, op in enumerate(il.operands):
|
||||
if isinstance(op, LowLevelILInstruction) and func(op, il, i):
|
||||
visit_llil_exprs(op, func)
|
||||
|
||||
|
||||
def unmangle_c_name(name: str) -> str:
|
||||
# https://learn.microsoft.com/en-us/cpp/build/reference/decorated-names?view=msvc-170#FormatC
|
||||
# Possible variations for BaseThreadInitThunk:
|
||||
# @BaseThreadInitThunk@12
|
||||
# _BaseThreadInitThunk
|
||||
# _BaseThreadInitThunk@12
|
||||
# It is also possible for a function to have a `Stub` appended to its name:
|
||||
# _lstrlenWStub@4
|
||||
|
||||
# A small optimization to avoid running the regex too many times
|
||||
# this still increases the unit test execution time from 170s to 200s, should be able to accelerate it
|
||||
#
|
||||
# TODO(xusheng): performance optimizations to improve test execution time
|
||||
# https://github.com/mandiant/capa/issues/1610
|
||||
if name[0] in ["@", "_"]:
|
||||
match = re.match(r"^[@|_](.*?)(Stub)?(@\d+)?$", name)
|
||||
if match:
|
||||
return match.group(1)
|
||||
|
||||
return name
|
||||
|
||||
|
||||
def read_c_string(bv: BinaryView, offset: int, max_len: int) -> str:
|
||||
s: List[str] = []
|
||||
while len(s) < max_len:
|
||||
try:
|
||||
c = bv.read(offset + len(s), 1)[0]
|
||||
except Exception:
|
||||
break
|
||||
|
||||
if c == 0:
|
||||
break
|
||||
|
||||
s.append(chr(c))
|
||||
|
||||
return "".join(s)
|
||||
586
capa/features/extractors/binja/insn.py
Normal file
586
capa/features/extractors/binja/insn.py
Normal file
@@ -0,0 +1,586 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
from typing import Any, List, Tuple, Iterator, Optional
|
||||
|
||||
from binaryninja import Function
|
||||
from binaryninja import BasicBlock as BinjaBasicBlock
|
||||
from binaryninja import (
|
||||
BinaryView,
|
||||
ILRegister,
|
||||
SymbolType,
|
||||
BinaryReader,
|
||||
RegisterValueType,
|
||||
LowLevelILOperation,
|
||||
LowLevelILInstruction,
|
||||
)
|
||||
|
||||
import capa.features.extractors.helpers
|
||||
from capa.features.insn import API, MAX_STRUCTURE_SIZE, Number, Offset, Mnemonic, OperandNumber, OperandOffset
|
||||
from capa.features.common import MAX_BYTES_FEATURE_SIZE, Bytes, String, Feature, Characteristic
|
||||
from capa.features.address import Address, AbsoluteVirtualAddress
|
||||
from capa.features.extractors.binja.helpers import DisassemblyInstruction, visit_llil_exprs
|
||||
from capa.features.extractors.base_extractor import BBHandle, InsnHandle, FunctionHandle
|
||||
|
||||
# security cookie checks may perform non-zeroing XORs, these are expected within a certain
|
||||
# byte range within the first and returning basic blocks, this helps to reduce FP features
|
||||
SECURITY_COOKIE_BYTES_DELTA = 0x40
|
||||
|
||||
|
||||
# check if a function is a stub function to another function/symbol. The criteria is:
|
||||
# 1. The function must only have one basic block
|
||||
# 2. The function must only make one call/jump to another address
|
||||
# If the function being checked is a stub function, returns the target address. Otherwise, return None.
|
||||
def is_stub_function(bv: BinaryView, addr: int) -> Optional[int]:
|
||||
funcs = bv.get_functions_at(addr)
|
||||
for func in funcs:
|
||||
if len(func.basic_blocks) != 1:
|
||||
continue
|
||||
|
||||
call_count = 0
|
||||
call_target = None
|
||||
for il in func.llil.instructions:
|
||||
if il.operation in [
|
||||
LowLevelILOperation.LLIL_CALL,
|
||||
LowLevelILOperation.LLIL_CALL_STACK_ADJUST,
|
||||
LowLevelILOperation.LLIL_JUMP,
|
||||
LowLevelILOperation.LLIL_TAILCALL,
|
||||
]:
|
||||
call_count += 1
|
||||
if il.dest.value.type in [
|
||||
RegisterValueType.ImportedAddressValue,
|
||||
RegisterValueType.ConstantValue,
|
||||
RegisterValueType.ConstantPointerValue,
|
||||
]:
|
||||
call_target = il.dest.value.value
|
||||
|
||||
if call_count == 1 and call_target is not None:
|
||||
return call_target
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def extract_insn_api_features(fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
parse instruction API features
|
||||
|
||||
example:
|
||||
call dword [0x00473038]
|
||||
"""
|
||||
func: Function = fh.inner
|
||||
bv: BinaryView = func.view
|
||||
|
||||
for llil in func.get_llils_at(ih.address):
|
||||
if llil.operation in [
|
||||
LowLevelILOperation.LLIL_CALL,
|
||||
LowLevelILOperation.LLIL_CALL_STACK_ADJUST,
|
||||
LowLevelILOperation.LLIL_JUMP,
|
||||
LowLevelILOperation.LLIL_TAILCALL,
|
||||
]:
|
||||
if llil.dest.value.type not in [
|
||||
RegisterValueType.ImportedAddressValue,
|
||||
RegisterValueType.ConstantValue,
|
||||
RegisterValueType.ConstantPointerValue,
|
||||
]:
|
||||
continue
|
||||
address = llil.dest.value.value
|
||||
candidate_addrs = [address]
|
||||
stub_addr = is_stub_function(bv, address)
|
||||
if stub_addr is not None:
|
||||
candidate_addrs.append(stub_addr)
|
||||
|
||||
for address in candidate_addrs:
|
||||
for sym in func.view.get_symbols(address):
|
||||
if sym is None or sym.type not in [
|
||||
SymbolType.ImportAddressSymbol,
|
||||
SymbolType.ImportedFunctionSymbol,
|
||||
SymbolType.FunctionSymbol,
|
||||
]:
|
||||
continue
|
||||
|
||||
sym_name = sym.short_name
|
||||
|
||||
lib_name = ""
|
||||
import_lib = bv.lookup_imported_object_library(sym.address)
|
||||
if import_lib is not None:
|
||||
lib_name = import_lib[0].name
|
||||
if lib_name.endswith(".dll"):
|
||||
lib_name = lib_name[:-4]
|
||||
elif lib_name.endswith(".so"):
|
||||
lib_name = lib_name[:-3]
|
||||
|
||||
for name in capa.features.extractors.helpers.generate_symbols(lib_name, sym_name):
|
||||
yield API(name), ih.address
|
||||
|
||||
if sym_name.startswith("_"):
|
||||
for name in capa.features.extractors.helpers.generate_symbols(lib_name, sym_name[1:]):
|
||||
yield API(name), ih.address
|
||||
|
||||
|
||||
def extract_insn_number_features(
|
||||
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
|
||||
) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
parse instruction number features
|
||||
example:
|
||||
push 3136B0h ; dwControlCode
|
||||
"""
|
||||
func: Function = fh.inner
|
||||
|
||||
results: List[Tuple[Any[Number, OperandNumber], Address]] = []
|
||||
|
||||
def llil_checker(il: LowLevelILInstruction, parent: LowLevelILInstruction, index: int) -> bool:
|
||||
if il.operation == LowLevelILOperation.LLIL_LOAD:
|
||||
return False
|
||||
|
||||
if il.operation not in [LowLevelILOperation.LLIL_CONST, LowLevelILOperation.LLIL_CONST_PTR]:
|
||||
return True
|
||||
|
||||
for op in parent.operands:
|
||||
if isinstance(op, ILRegister) and op.name in ["esp", "ebp", "rsp", "rbp", "sp"]:
|
||||
return False
|
||||
elif isinstance(op, LowLevelILInstruction) and op.operation == LowLevelILOperation.LLIL_REG:
|
||||
if op.src.name in ["esp", "ebp", "rsp", "rbp", "sp"]:
|
||||
return False
|
||||
|
||||
raw_value = il.value.value
|
||||
if parent.operation == LowLevelILOperation.LLIL_SUB:
|
||||
raw_value = -raw_value
|
||||
|
||||
results.append((Number(raw_value), ih.address))
|
||||
results.append((OperandNumber(index, raw_value), ih.address))
|
||||
|
||||
return False
|
||||
|
||||
for llil in func.get_llils_at(ih.address):
|
||||
visit_llil_exprs(llil, llil_checker)
|
||||
|
||||
yield from results
|
||||
|
||||
|
||||
def extract_insn_bytes_features(fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
parse referenced byte sequences
|
||||
example:
|
||||
push offset iid_004118d4_IShellLinkA ; riid
|
||||
"""
|
||||
func: Function = fh.inner
|
||||
bv: BinaryView = func.view
|
||||
|
||||
candidate_addrs = set()
|
||||
|
||||
llil = func.get_llil_at(ih.address)
|
||||
if llil is None or llil.operation in [LowLevelILOperation.LLIL_CALL, LowLevelILOperation.LLIL_CALL_STACK_ADJUST]:
|
||||
return
|
||||
|
||||
for ref in bv.get_code_refs_from(ih.address):
|
||||
if ref == ih.address:
|
||||
continue
|
||||
|
||||
if len(bv.get_functions_containing(ref)) > 0:
|
||||
continue
|
||||
|
||||
candidate_addrs.add(ref)
|
||||
|
||||
# collect candidate address by enumerating all integers, https://github.com/Vector35/binaryninja-api/issues/3966
|
||||
def llil_checker(il: LowLevelILInstruction, parent: LowLevelILInstruction, index: int) -> bool:
|
||||
if il.operation in [LowLevelILOperation.LLIL_CONST, LowLevelILOperation.LLIL_CONST_PTR]:
|
||||
value = il.value.value
|
||||
if value > 0:
|
||||
candidate_addrs.add(value)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
for llil in func.get_llils_at(ih.address):
|
||||
visit_llil_exprs(llil, llil_checker)
|
||||
|
||||
for addr in candidate_addrs:
|
||||
extracted_bytes = bv.read(addr, MAX_BYTES_FEATURE_SIZE)
|
||||
if extracted_bytes and not capa.features.extractors.helpers.all_zeros(extracted_bytes):
|
||||
if bv.get_string_at(addr) is None:
|
||||
# don't extract byte features for obvious strings
|
||||
yield Bytes(extracted_bytes), ih.address
|
||||
|
||||
|
||||
def extract_insn_string_features(
|
||||
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
|
||||
) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
parse instruction string features
|
||||
|
||||
example:
|
||||
push offset aAcr ; "ACR > "
|
||||
"""
|
||||
func: Function = fh.inner
|
||||
bv: BinaryView = func.view
|
||||
|
||||
candidate_addrs = set()
|
||||
|
||||
# collect candidate address from code refs directly
|
||||
for ref in bv.get_code_refs_from(ih.address):
|
||||
if ref == ih.address:
|
||||
continue
|
||||
|
||||
if len(bv.get_functions_containing(ref)) > 0:
|
||||
continue
|
||||
|
||||
candidate_addrs.add(ref)
|
||||
|
||||
# collect candidate address by enumerating all integers, https://github.com/Vector35/binaryninja-api/issues/3966
|
||||
def llil_checker(il: LowLevelILInstruction, parent: LowLevelILInstruction, index: int) -> bool:
|
||||
if il.operation in [LowLevelILOperation.LLIL_CONST, LowLevelILOperation.LLIL_CONST_PTR]:
|
||||
value = il.value.value
|
||||
if value > 0:
|
||||
candidate_addrs.add(value)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
for llil in func.get_llils_at(ih.address):
|
||||
visit_llil_exprs(llil, llil_checker)
|
||||
|
||||
# Now we have all the candidate address, check them for string or pointer to string
|
||||
br = BinaryReader(bv)
|
||||
for addr in candidate_addrs:
|
||||
found = bv.get_string_at(addr)
|
||||
if found:
|
||||
yield String(found.value), ih.address
|
||||
|
||||
br.seek(addr)
|
||||
pointer = None
|
||||
if bv.arch.address_size == 4:
|
||||
pointer = br.read32()
|
||||
elif bv.arch.address_size == 8:
|
||||
pointer = br.read64()
|
||||
|
||||
if pointer is not None:
|
||||
found = bv.get_string_at(pointer)
|
||||
if found:
|
||||
yield String(found.value), ih.address
|
||||
|
||||
|
||||
def extract_insn_offset_features(
|
||||
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
|
||||
) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
parse instruction structure offset features
|
||||
|
||||
example:
|
||||
.text:0040112F cmp [esi+4], ebx
|
||||
"""
|
||||
func: Function = fh.inner
|
||||
|
||||
results: List[Tuple[Any[Offset, OperandOffset], Address]] = []
|
||||
address_size = func.view.arch.address_size * 8
|
||||
|
||||
def llil_checker(il: LowLevelILInstruction, parent: LowLevelILInstruction, index: int) -> bool:
|
||||
# The most common case, read/write dereference to something like `dword [eax+0x28]`
|
||||
if il.operation in [LowLevelILOperation.LLIL_ADD, LowLevelILOperation.LLIL_SUB]:
|
||||
left = il.left
|
||||
right = il.right
|
||||
# Exclude offsets based on stack/franme pointers
|
||||
if left.operation == LowLevelILOperation.LLIL_REG and left.src.name in ["esp", "ebp", "rsp", "rbp", "sp"]:
|
||||
return True
|
||||
|
||||
if right.operation != LowLevelILOperation.LLIL_CONST:
|
||||
return True
|
||||
|
||||
raw_value = right.value.value
|
||||
# If this is not a dereference, then this must be an add and the offset must be in the range \
|
||||
# [0, MAX_STRUCTURE_SIZE]. For example,
|
||||
# add eax, 0x10,
|
||||
# lea ebx, [eax + 1]
|
||||
if parent.operation not in [LowLevelILOperation.LLIL_LOAD, LowLevelILOperation.LLIL_STORE]:
|
||||
if il.operation != LowLevelILOperation.LLIL_ADD or (not 0 < raw_value < MAX_STRUCTURE_SIZE):
|
||||
return False
|
||||
|
||||
if address_size > 0:
|
||||
# BN also encodes the constant value as two's complement, we need to restore its original value
|
||||
value = capa.features.extractors.helpers.twos_complement(raw_value, address_size)
|
||||
else:
|
||||
value = raw_value
|
||||
|
||||
results.append((Offset(value), ih.address))
|
||||
results.append((OperandOffset(index, value), ih.address))
|
||||
return False
|
||||
|
||||
# An edge case: for code like `push dword [esi]`, we need to generate a feature for offset 0x0
|
||||
elif il.operation in [LowLevelILOperation.LLIL_LOAD, LowLevelILOperation.LLIL_STORE]:
|
||||
if il.operands[0].operation == LowLevelILOperation.LLIL_REG:
|
||||
results.append((Offset(0), ih.address))
|
||||
results.append((OperandOffset(index, 0), ih.address))
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
for llil in func.get_llils_at(ih.address):
|
||||
visit_llil_exprs(llil, llil_checker)
|
||||
|
||||
yield from results
|
||||
|
||||
|
||||
def is_nzxor_stack_cookie(f: Function, bb: BinjaBasicBlock, llil: LowLevelILInstruction) -> bool:
|
||||
"""check if nzxor exists within stack cookie delta"""
|
||||
# TODO(xusheng): use LLIL SSA to do more accurate analysis
|
||||
# https://github.com/mandiant/capa/issues/1609
|
||||
|
||||
reg_names = []
|
||||
if llil.left.operation == LowLevelILOperation.LLIL_REG:
|
||||
reg_names.append(llil.left.src.name)
|
||||
|
||||
if llil.right.operation == LowLevelILOperation.LLIL_REG:
|
||||
reg_names.append(llil.right.src.name)
|
||||
|
||||
# stack cookie reg should be stack/frame pointer
|
||||
if not any(reg in ["ebp", "esp", "rbp", "rsp", "sp"] for reg in reg_names):
|
||||
return False
|
||||
|
||||
# expect security cookie init in first basic block within first bytes (instructions)
|
||||
if len(bb.incoming_edges) == 0 and llil.address < (bb.start + SECURITY_COOKIE_BYTES_DELTA):
|
||||
return True
|
||||
|
||||
# ... or within last bytes (instructions) before a return
|
||||
if len(bb.outgoing_edges) == 0 and llil.address > (bb.end - SECURITY_COOKIE_BYTES_DELTA):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def extract_insn_nzxor_characteristic_features(
|
||||
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
|
||||
) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
parse instruction non-zeroing XOR instruction
|
||||
ignore expected non-zeroing XORs, e.g. security cookies
|
||||
"""
|
||||
func: Function = fh.inner
|
||||
|
||||
results = []
|
||||
|
||||
def llil_checker(il: LowLevelILInstruction, parent: LowLevelILInstruction, index: int) -> bool:
|
||||
# If the two operands of the xor instruction are the same, the LLIL will be translated to other instructions,
|
||||
# e.g., <llil: eax = 0>, (LLIL_SET_REG). So we do not need to check whether the two operands are the same.
|
||||
if il.operation == LowLevelILOperation.LLIL_XOR:
|
||||
# Exclude cases related to the stack cookie
|
||||
if is_nzxor_stack_cookie(fh.inner, bbh.inner[0], il):
|
||||
return False
|
||||
results.append((Characteristic("nzxor"), ih.address))
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
for llil in func.get_llils_at(ih.address):
|
||||
visit_llil_exprs(llil, llil_checker)
|
||||
|
||||
yield from results
|
||||
|
||||
|
||||
def extract_insn_mnemonic_features(
|
||||
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
|
||||
) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""parse instruction mnemonic features"""
|
||||
insn: DisassemblyInstruction = ih.inner
|
||||
yield Mnemonic(insn.text[0].text), ih.address
|
||||
|
||||
|
||||
def extract_insn_obfs_call_plus_5_characteristic_features(
|
||||
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
|
||||
) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
parse call $+5 instruction from the given instruction.
|
||||
"""
|
||||
insn: DisassemblyInstruction = ih.inner
|
||||
if insn.text[0].text == "call" and insn.text[2].text == "$+5" and insn.length == 5:
|
||||
yield Characteristic("call $+5"), ih.address
|
||||
|
||||
|
||||
def extract_insn_peb_access_characteristic_features(
|
||||
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
|
||||
) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""parse instruction peb access
|
||||
|
||||
fs:[0x30] on x86, gs:[0x60] on x64
|
||||
"""
|
||||
func: Function = fh.inner
|
||||
|
||||
results = []
|
||||
|
||||
def llil_checker(il: LowLevelILInstruction, parent: LowLevelILOperation, index: int) -> bool:
|
||||
if il.operation != LowLevelILOperation.LLIL_LOAD:
|
||||
return True
|
||||
|
||||
src = il.src
|
||||
if src.operation != LowLevelILOperation.LLIL_ADD:
|
||||
return True
|
||||
|
||||
left = src.left
|
||||
right = src.right
|
||||
|
||||
if left.operation != LowLevelILOperation.LLIL_REG:
|
||||
return True
|
||||
|
||||
reg = left.src.name
|
||||
|
||||
if right.operation != LowLevelILOperation.LLIL_CONST:
|
||||
return True
|
||||
|
||||
value = right.value.value
|
||||
if (reg, value) not in (("fsbase", 0x30), ("gsbase", 0x60)):
|
||||
return True
|
||||
|
||||
results.append((Characteristic("peb access"), ih.address))
|
||||
return False
|
||||
|
||||
for llil in func.get_llils_at(ih.address):
|
||||
visit_llil_exprs(llil, llil_checker)
|
||||
|
||||
yield from results
|
||||
|
||||
|
||||
def extract_insn_segment_access_features(
|
||||
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
|
||||
) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""parse instruction fs or gs access"""
|
||||
func: Function = fh.inner
|
||||
|
||||
results = []
|
||||
|
||||
def llil_checker(il: LowLevelILInstruction, parent: LowLevelILInstruction, index: int) -> bool:
|
||||
if il.operation == LowLevelILOperation.LLIL_REG:
|
||||
reg = il.src.name
|
||||
if reg == "fsbase":
|
||||
results.append((Characteristic("fs access"), ih.address))
|
||||
return False
|
||||
elif reg == "gsbase":
|
||||
results.append((Characteristic("gs access"), ih.address))
|
||||
return False
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
for llil in func.get_llils_at(ih.address):
|
||||
visit_llil_exprs(llil, llil_checker)
|
||||
|
||||
yield from results
|
||||
|
||||
|
||||
def extract_insn_cross_section_cflow(
|
||||
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
|
||||
) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""inspect the instruction for a CALL or JMP that crosses section boundaries"""
|
||||
func: Function = fh.inner
|
||||
bv: BinaryView = func.view
|
||||
|
||||
if bv is None:
|
||||
return
|
||||
|
||||
seg1 = bv.get_segment_at(ih.address)
|
||||
sections1 = bv.get_sections_at(ih.address)
|
||||
for ref in bv.get_code_refs_from(ih.address):
|
||||
if len(bv.get_functions_at(ref)) == 0:
|
||||
continue
|
||||
|
||||
seg2 = bv.get_segment_at(ref)
|
||||
sections2 = bv.get_sections_at(ref)
|
||||
if seg1 != seg2 or sections1 != sections2:
|
||||
yield Characteristic("cross section flow"), ih.address
|
||||
|
||||
|
||||
def extract_function_calls_from(fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""extract functions calls from features
|
||||
|
||||
most relevant at the function scope, however, its most efficient to extract at the instruction scope
|
||||
"""
|
||||
func: Function = fh.inner
|
||||
bv: BinaryView = func.view
|
||||
|
||||
if bv is None:
|
||||
return
|
||||
|
||||
for il in func.get_llils_at(ih.address):
|
||||
if il.operation not in [
|
||||
LowLevelILOperation.LLIL_CALL,
|
||||
LowLevelILOperation.LLIL_CALL_STACK_ADJUST,
|
||||
LowLevelILOperation.LLIL_TAILCALL,
|
||||
]:
|
||||
continue
|
||||
|
||||
dest = il.dest
|
||||
if dest.operation == LowLevelILOperation.LLIL_CONST_PTR:
|
||||
value = dest.value.value
|
||||
yield Characteristic("calls from"), AbsoluteVirtualAddress(value)
|
||||
elif dest.operation == LowLevelILOperation.LLIL_CONST:
|
||||
yield Characteristic("calls from"), AbsoluteVirtualAddress(dest.value)
|
||||
elif dest.operation == LowLevelILOperation.LLIL_LOAD:
|
||||
indirect_src = dest.src
|
||||
if indirect_src.operation == LowLevelILOperation.LLIL_CONST_PTR:
|
||||
value = indirect_src.value.value
|
||||
yield Characteristic("calls from"), AbsoluteVirtualAddress(value)
|
||||
elif indirect_src.operation == LowLevelILOperation.LLIL_CONST:
|
||||
yield Characteristic("calls from"), AbsoluteVirtualAddress(indirect_src.value)
|
||||
elif dest.operation == LowLevelILOperation.LLIL_REG:
|
||||
if dest.value.type in [
|
||||
RegisterValueType.ImportedAddressValue,
|
||||
RegisterValueType.ConstantValue,
|
||||
RegisterValueType.ConstantPointerValue,
|
||||
]:
|
||||
yield Characteristic("calls from"), AbsoluteVirtualAddress(dest.value.value)
|
||||
|
||||
|
||||
def extract_function_indirect_call_characteristic_features(
|
||||
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
|
||||
) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""extract indirect function calls (e.g., call eax or call dword ptr [edx+4])
|
||||
does not include calls like => call ds:dword_ABD4974
|
||||
|
||||
most relevant at the function or basic block scope;
|
||||
however, its most efficient to extract at the instruction scope
|
||||
"""
|
||||
func: Function = fh.inner
|
||||
|
||||
llil = func.get_llil_at(ih.address)
|
||||
if llil is None or llil.operation not in [
|
||||
LowLevelILOperation.LLIL_CALL,
|
||||
LowLevelILOperation.LLIL_CALL_STACK_ADJUST,
|
||||
LowLevelILOperation.LLIL_TAILCALL,
|
||||
]:
|
||||
return
|
||||
|
||||
if llil.dest.operation in [LowLevelILOperation.LLIL_CONST, LowLevelILOperation.LLIL_CONST_PTR]:
|
||||
return
|
||||
|
||||
if llil.dest.operation == LowLevelILOperation.LLIL_LOAD:
|
||||
src = llil.dest.src
|
||||
if src.operation in [LowLevelILOperation.LLIL_CONST, LowLevelILOperation.LLIL_CONST_PTR]:
|
||||
return
|
||||
|
||||
yield Characteristic("indirect call"), ih.address
|
||||
|
||||
|
||||
def extract_features(f: FunctionHandle, bbh: BBHandle, insn: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""extract instruction features"""
|
||||
for inst_handler in INSTRUCTION_HANDLERS:
|
||||
for feature, ea in inst_handler(f, bbh, insn):
|
||||
yield feature, ea
|
||||
|
||||
|
||||
INSTRUCTION_HANDLERS = (
|
||||
extract_insn_api_features,
|
||||
extract_insn_number_features,
|
||||
extract_insn_bytes_features,
|
||||
extract_insn_string_features,
|
||||
extract_insn_offset_features,
|
||||
extract_insn_nzxor_characteristic_features,
|
||||
extract_insn_mnemonic_features,
|
||||
extract_insn_obfs_call_plus_5_characteristic_features,
|
||||
extract_insn_peb_access_characteristic_features,
|
||||
extract_insn_cross_section_cflow,
|
||||
extract_insn_segment_access_features,
|
||||
extract_function_calls_from,
|
||||
extract_function_indirect_call_characteristic_features,
|
||||
)
|
||||
0
capa/features/extractors/cape/__init__.py
Normal file
0
capa/features/extractors/cape/__init__.py
Normal file
62
capa/features/extractors/cape/call.py
Normal file
62
capa/features/extractors/cape/call.py
Normal file
@@ -0,0 +1,62 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import logging
|
||||
from typing import Tuple, Iterator
|
||||
|
||||
from capa.helpers import assert_never
|
||||
from capa.features.insn import API, Number
|
||||
from capa.features.common import String, Feature
|
||||
from capa.features.address import Address
|
||||
from capa.features.extractors.cape.models import Call
|
||||
from capa.features.extractors.base_extractor import CallHandle, ThreadHandle, ProcessHandle
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def extract_call_features(ph: ProcessHandle, th: ThreadHandle, ch: CallHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
this method extracts the given call's features (such as API name and arguments),
|
||||
and returns them as API, Number, and String features.
|
||||
|
||||
args:
|
||||
ph: process handle (for defining the extraction scope)
|
||||
th: thread handle (for defining the extraction scope)
|
||||
ch: call handle (for defining the extraction scope)
|
||||
|
||||
yields:
|
||||
Feature, address; where Feature is either: API, Number, or String.
|
||||
"""
|
||||
call: Call = ch.inner
|
||||
|
||||
# list similar to disassembly: arguments right-to-left, call
|
||||
for arg in reversed(call.arguments):
|
||||
value = arg.value
|
||||
if isinstance(value, list) and len(value) == 0:
|
||||
# unsure why CAPE captures arguments as empty lists?
|
||||
continue
|
||||
|
||||
elif isinstance(value, str):
|
||||
yield String(value), ch.address
|
||||
|
||||
elif isinstance(value, int):
|
||||
yield Number(value), ch.address
|
||||
|
||||
else:
|
||||
assert_never(value)
|
||||
|
||||
yield API(call.api), ch.address
|
||||
|
||||
|
||||
def extract_features(ph: ProcessHandle, th: ThreadHandle, ch: CallHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
for handler in CALL_HANDLERS:
|
||||
for feature, addr in handler(ph, th, ch):
|
||||
yield feature, addr
|
||||
|
||||
|
||||
CALL_HANDLERS = (extract_call_features,)
|
||||
145
capa/features/extractors/cape/extractor.py
Normal file
145
capa/features/extractors/cape/extractor.py
Normal file
@@ -0,0 +1,145 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import logging
|
||||
from typing import Dict, Tuple, Union, Iterator
|
||||
|
||||
import capa.features.extractors.cape.call
|
||||
import capa.features.extractors.cape.file
|
||||
import capa.features.extractors.cape.thread
|
||||
import capa.features.extractors.cape.global_
|
||||
import capa.features.extractors.cape.process
|
||||
from capa.exceptions import EmptyReportError, UnsupportedFormatError
|
||||
from capa.features.common import Feature, Characteristic
|
||||
from capa.features.address import NO_ADDRESS, Address, AbsoluteVirtualAddress, _NoAddress
|
||||
from capa.features.extractors.cape.models import Call, Static, Process, CapeReport
|
||||
from capa.features.extractors.base_extractor import (
|
||||
CallHandle,
|
||||
SampleHashes,
|
||||
ThreadHandle,
|
||||
ProcessHandle,
|
||||
DynamicFeatureExtractor,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
TESTED_VERSIONS = {"2.2-CAPE", "2.4-CAPE"}
|
||||
|
||||
|
||||
class CapeExtractor(DynamicFeatureExtractor):
|
||||
def __init__(self, report: CapeReport):
|
||||
super().__init__(
|
||||
hashes=SampleHashes(
|
||||
md5=report.target.file.md5.lower(),
|
||||
sha1=report.target.file.sha1.lower(),
|
||||
sha256=report.target.file.sha256.lower(),
|
||||
)
|
||||
)
|
||||
self.report: CapeReport = report
|
||||
|
||||
# pre-compute these because we'll yield them at *every* scope.
|
||||
self.global_features = list(capa.features.extractors.cape.global_.extract_features(self.report))
|
||||
|
||||
def get_base_address(self) -> Union[AbsoluteVirtualAddress, _NoAddress, None]:
|
||||
# value according to the PE header, the actual trace may use a different imagebase
|
||||
assert self.report.static is not None and self.report.static.pe is not None
|
||||
return AbsoluteVirtualAddress(self.report.static.pe.imagebase)
|
||||
|
||||
def extract_global_features(self) -> Iterator[Tuple[Feature, Address]]:
|
||||
yield from self.global_features
|
||||
|
||||
def extract_file_features(self) -> Iterator[Tuple[Feature, Address]]:
|
||||
yield from capa.features.extractors.cape.file.extract_features(self.report)
|
||||
|
||||
def get_processes(self) -> Iterator[ProcessHandle]:
|
||||
yield from capa.features.extractors.cape.file.get_processes(self.report)
|
||||
|
||||
def extract_process_features(self, ph: ProcessHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
yield from capa.features.extractors.cape.process.extract_features(ph)
|
||||
|
||||
def get_process_name(self, ph) -> str:
|
||||
process: Process = ph.inner
|
||||
return process.process_name
|
||||
|
||||
def get_threads(self, ph: ProcessHandle) -> Iterator[ThreadHandle]:
|
||||
yield from capa.features.extractors.cape.process.get_threads(ph)
|
||||
|
||||
def extract_thread_features(self, ph: ProcessHandle, th: ThreadHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
if False:
|
||||
# force this routine to be a generator,
|
||||
# but we don't actually have any elements to generate.
|
||||
yield Characteristic("never"), NO_ADDRESS
|
||||
return
|
||||
|
||||
def get_calls(self, ph: ProcessHandle, th: ThreadHandle) -> Iterator[CallHandle]:
|
||||
yield from capa.features.extractors.cape.thread.get_calls(ph, th)
|
||||
|
||||
def extract_call_features(
|
||||
self, ph: ProcessHandle, th: ThreadHandle, ch: CallHandle
|
||||
) -> Iterator[Tuple[Feature, Address]]:
|
||||
yield from capa.features.extractors.cape.call.extract_features(ph, th, ch)
|
||||
|
||||
def get_call_name(self, ph, th, ch) -> str:
|
||||
call: Call = ch.inner
|
||||
|
||||
parts = []
|
||||
parts.append(call.api)
|
||||
parts.append("(")
|
||||
for argument in call.arguments:
|
||||
parts.append(argument.name)
|
||||
parts.append("=")
|
||||
|
||||
if argument.pretty_value:
|
||||
parts.append(argument.pretty_value)
|
||||
else:
|
||||
if isinstance(argument.value, int):
|
||||
parts.append(hex(argument.value))
|
||||
elif isinstance(argument.value, str):
|
||||
parts.append('"')
|
||||
parts.append(argument.value)
|
||||
parts.append('"')
|
||||
elif isinstance(argument.value, list):
|
||||
pass
|
||||
else:
|
||||
capa.helpers.assert_never(argument.value)
|
||||
|
||||
parts.append(", ")
|
||||
if call.arguments:
|
||||
# remove the trailing comma
|
||||
parts.pop()
|
||||
parts.append(")")
|
||||
parts.append(" -> ")
|
||||
if call.pretty_return:
|
||||
parts.append(call.pretty_return)
|
||||
else:
|
||||
parts.append(hex(call.return_))
|
||||
|
||||
return "".join(parts)
|
||||
|
||||
@classmethod
|
||||
def from_report(cls, report: Dict) -> "CapeExtractor":
|
||||
cr = CapeReport.model_validate(report)
|
||||
|
||||
if cr.info.version not in TESTED_VERSIONS:
|
||||
logger.warning("CAPE version '%s' not tested/supported yet", cr.info.version)
|
||||
|
||||
# observed in 2.4-CAPE reports from capesandbox.com
|
||||
if cr.static is None and cr.target.file.pe is not None:
|
||||
cr.static = Static()
|
||||
cr.static.pe = cr.target.file.pe
|
||||
|
||||
if cr.static is None:
|
||||
raise UnsupportedFormatError("CAPE report missing static analysis")
|
||||
|
||||
if cr.static.pe is None:
|
||||
raise UnsupportedFormatError("CAPE report missing PE analysis")
|
||||
|
||||
if len(cr.behavior.processes) == 0:
|
||||
raise EmptyReportError("CAPE did not capture any processes")
|
||||
|
||||
return cls(cr)
|
||||
132
capa/features/extractors/cape/file.py
Normal file
132
capa/features/extractors/cape/file.py
Normal file
@@ -0,0 +1,132 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import logging
|
||||
from typing import Tuple, Iterator
|
||||
|
||||
from capa.features.file import Export, Import, Section
|
||||
from capa.features.common import String, Feature
|
||||
from capa.features.address import NO_ADDRESS, Address, ProcessAddress, AbsoluteVirtualAddress
|
||||
from capa.features.extractors.helpers import generate_symbols
|
||||
from capa.features.extractors.cape.models import CapeReport
|
||||
from capa.features.extractors.base_extractor import ProcessHandle
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_processes(report: CapeReport) -> Iterator[ProcessHandle]:
|
||||
"""
|
||||
get all the created processes for a sample
|
||||
"""
|
||||
seen_processes = {}
|
||||
for process in report.behavior.processes:
|
||||
addr = ProcessAddress(pid=process.process_id, ppid=process.parent_id)
|
||||
yield ProcessHandle(address=addr, inner=process)
|
||||
|
||||
# check for pid and ppid reuse
|
||||
if addr not in seen_processes:
|
||||
seen_processes[addr] = [process]
|
||||
else:
|
||||
logger.warning(
|
||||
"pid and ppid reuse detected between process %s and process%s: %s",
|
||||
process,
|
||||
"es" if len(seen_processes[addr]) > 1 else "",
|
||||
seen_processes[addr],
|
||||
)
|
||||
seen_processes[addr].append(process)
|
||||
|
||||
|
||||
def extract_import_names(report: CapeReport) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
extract imported function names
|
||||
"""
|
||||
assert report.static is not None and report.static.pe is not None
|
||||
imports = report.static.pe.imports
|
||||
|
||||
if isinstance(imports, dict):
|
||||
imports = list(imports.values())
|
||||
|
||||
assert isinstance(imports, list)
|
||||
|
||||
for library in imports:
|
||||
for function in library.imports:
|
||||
if not function.name:
|
||||
continue
|
||||
|
||||
for name in generate_symbols(library.dll, function.name, include_dll=True):
|
||||
yield Import(name), AbsoluteVirtualAddress(function.address)
|
||||
|
||||
|
||||
def extract_export_names(report: CapeReport) -> Iterator[Tuple[Feature, Address]]:
|
||||
assert report.static is not None and report.static.pe is not None
|
||||
for function in report.static.pe.exports:
|
||||
yield Export(function.name), AbsoluteVirtualAddress(function.address)
|
||||
|
||||
|
||||
def extract_section_names(report: CapeReport) -> Iterator[Tuple[Feature, Address]]:
|
||||
assert report.static is not None and report.static.pe is not None
|
||||
for section in report.static.pe.sections:
|
||||
yield Section(section.name), AbsoluteVirtualAddress(section.virtual_address)
|
||||
|
||||
|
||||
def extract_file_strings(report: CapeReport) -> Iterator[Tuple[Feature, Address]]:
|
||||
if report.strings is not None:
|
||||
for string in report.strings:
|
||||
yield String(string), NO_ADDRESS
|
||||
|
||||
|
||||
def extract_used_regkeys(report: CapeReport) -> Iterator[Tuple[Feature, Address]]:
|
||||
for regkey in report.behavior.summary.keys:
|
||||
yield String(regkey), NO_ADDRESS
|
||||
|
||||
|
||||
def extract_used_files(report: CapeReport) -> Iterator[Tuple[Feature, Address]]:
|
||||
for file in report.behavior.summary.files:
|
||||
yield String(file), NO_ADDRESS
|
||||
|
||||
|
||||
def extract_used_mutexes(report: CapeReport) -> Iterator[Tuple[Feature, Address]]:
|
||||
for mutex in report.behavior.summary.mutexes:
|
||||
yield String(mutex), NO_ADDRESS
|
||||
|
||||
|
||||
def extract_used_commands(report: CapeReport) -> Iterator[Tuple[Feature, Address]]:
|
||||
for cmd in report.behavior.summary.executed_commands:
|
||||
yield String(cmd), NO_ADDRESS
|
||||
|
||||
|
||||
def extract_used_apis(report: CapeReport) -> Iterator[Tuple[Feature, Address]]:
|
||||
for symbol in report.behavior.summary.resolved_apis:
|
||||
yield String(symbol), NO_ADDRESS
|
||||
|
||||
|
||||
def extract_used_services(report: CapeReport) -> Iterator[Tuple[Feature, Address]]:
|
||||
for svc in report.behavior.summary.created_services:
|
||||
yield String(svc), NO_ADDRESS
|
||||
for svc in report.behavior.summary.started_services:
|
||||
yield String(svc), NO_ADDRESS
|
||||
|
||||
|
||||
def extract_features(report: CapeReport) -> Iterator[Tuple[Feature, Address]]:
|
||||
for handler in FILE_HANDLERS:
|
||||
for feature, addr in handler(report):
|
||||
yield feature, addr
|
||||
|
||||
|
||||
FILE_HANDLERS = (
|
||||
extract_import_names,
|
||||
extract_export_names,
|
||||
extract_section_names,
|
||||
extract_file_strings,
|
||||
extract_used_regkeys,
|
||||
extract_used_files,
|
||||
extract_used_mutexes,
|
||||
extract_used_commands,
|
||||
extract_used_apis,
|
||||
extract_used_services,
|
||||
)
|
||||
93
capa/features/extractors/cape/global_.py
Normal file
93
capa/features/extractors/cape/global_.py
Normal file
@@ -0,0 +1,93 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import logging
|
||||
from typing import Tuple, Iterator
|
||||
|
||||
from capa.features.common import (
|
||||
OS,
|
||||
OS_ANY,
|
||||
OS_LINUX,
|
||||
ARCH_I386,
|
||||
FORMAT_PE,
|
||||
ARCH_AMD64,
|
||||
FORMAT_ELF,
|
||||
OS_WINDOWS,
|
||||
Arch,
|
||||
Format,
|
||||
Feature,
|
||||
)
|
||||
from capa.features.address import NO_ADDRESS, Address
|
||||
from capa.features.extractors.cape.models import CapeReport
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def extract_arch(report: CapeReport) -> Iterator[Tuple[Feature, Address]]:
|
||||
if "Intel 80386" in report.target.file.type:
|
||||
yield Arch(ARCH_I386), NO_ADDRESS
|
||||
elif "x86-64" in report.target.file.type:
|
||||
yield Arch(ARCH_AMD64), NO_ADDRESS
|
||||
else:
|
||||
logger.warning("unrecognized Architecture: %s", report.target.file.type)
|
||||
raise ValueError(
|
||||
f"unrecognized Architecture from the CAPE report; output of file command: {report.target.file.type}"
|
||||
)
|
||||
|
||||
|
||||
def extract_format(report: CapeReport) -> Iterator[Tuple[Feature, Address]]:
|
||||
if "PE" in report.target.file.type:
|
||||
yield Format(FORMAT_PE), NO_ADDRESS
|
||||
elif "ELF" in report.target.file.type:
|
||||
yield Format(FORMAT_ELF), NO_ADDRESS
|
||||
else:
|
||||
logger.warning("unknown file format, file command output: %s", report.target.file.type)
|
||||
raise ValueError(
|
||||
"unrecognized file format from the CAPE report; output of file command: {report.target.file.type}"
|
||||
)
|
||||
|
||||
|
||||
def extract_os(report: CapeReport) -> Iterator[Tuple[Feature, Address]]:
|
||||
# this variable contains the output of the file command
|
||||
file_output = report.target.file.type
|
||||
|
||||
if "windows" in file_output.lower():
|
||||
yield OS(OS_WINDOWS), NO_ADDRESS
|
||||
elif "elf" in file_output.lower():
|
||||
# operating systems recognized by the file command: https://github.com/file/file/blob/master/src/readelf.c#L609
|
||||
if "Linux" in file_output:
|
||||
yield OS(OS_LINUX), NO_ADDRESS
|
||||
elif "Hurd" in file_output:
|
||||
yield OS("hurd"), NO_ADDRESS
|
||||
elif "Solaris" in file_output:
|
||||
yield OS("solaris"), NO_ADDRESS
|
||||
elif "kFreeBSD" in file_output:
|
||||
yield OS("freebsd"), NO_ADDRESS
|
||||
elif "kNetBSD" in file_output:
|
||||
yield OS("netbsd"), NO_ADDRESS
|
||||
else:
|
||||
# if the operating system information is missing from the cape report, it's likely a bug
|
||||
logger.warning("unrecognized OS: %s", file_output)
|
||||
raise ValueError("unrecognized OS from the CAPE report; output of file command: {file_output}")
|
||||
else:
|
||||
# the sample is shellcode
|
||||
logger.debug("unsupported file format, file command output: %s", file_output)
|
||||
yield OS(OS_ANY), NO_ADDRESS
|
||||
|
||||
|
||||
def extract_features(report: CapeReport) -> Iterator[Tuple[Feature, Address]]:
|
||||
for global_handler in GLOBAL_HANDLER:
|
||||
for feature, addr in global_handler(report):
|
||||
yield feature, addr
|
||||
|
||||
|
||||
GLOBAL_HANDLER = (
|
||||
extract_format,
|
||||
extract_os,
|
||||
extract_arch,
|
||||
)
|
||||
29
capa/features/extractors/cape/helpers.py
Normal file
29
capa/features/extractors/cape/helpers.py
Normal file
@@ -0,0 +1,29 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from capa.features.extractors.base_extractor import ProcessHandle
|
||||
|
||||
|
||||
def find_process(processes: List[Dict[str, Any]], ph: ProcessHandle) -> Dict[str, Any]:
|
||||
"""
|
||||
find a specific process identified by a process handler.
|
||||
|
||||
args:
|
||||
processes: a list of processes extracted by CAPE
|
||||
ph: handle of the sought process
|
||||
|
||||
return:
|
||||
a CAPE-defined dictionary for the sought process' information
|
||||
"""
|
||||
|
||||
for process in processes:
|
||||
if ph.address.ppid == process["parent_id"] and ph.address.pid == process["process_id"]:
|
||||
return process
|
||||
return {}
|
||||
446
capa/features/extractors/cape/models.py
Normal file
446
capa/features/extractors/cape/models.py
Normal file
@@ -0,0 +1,446 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
import binascii
|
||||
from typing import Any, Dict, List, Union, Literal, Optional
|
||||
|
||||
from pydantic import Field, BaseModel, ConfigDict
|
||||
from typing_extensions import Annotated, TypeAlias
|
||||
from pydantic.functional_validators import BeforeValidator
|
||||
|
||||
|
||||
def validate_hex_int(value):
|
||||
if isinstance(value, str):
|
||||
return int(value, 16) if value.startswith("0x") else int(value, 10)
|
||||
else:
|
||||
return value
|
||||
|
||||
|
||||
def validate_hex_bytes(value):
|
||||
return binascii.unhexlify(value) if isinstance(value, str) else value
|
||||
|
||||
|
||||
HexInt = Annotated[int, BeforeValidator(validate_hex_int)]
|
||||
HexBytes = Annotated[bytes, BeforeValidator(validate_hex_bytes)]
|
||||
|
||||
|
||||
# a model that *cannot* have extra fields
|
||||
# if they do, pydantic raises an exception.
|
||||
# use this for models we rely upon and cannot change.
|
||||
#
|
||||
# for things that may be extended and we don't care,
|
||||
# use FlexibleModel.
|
||||
class ExactModel(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
# a model that can have extra fields that we ignore.
|
||||
# use this if we don't want to raise an exception for extra
|
||||
# data fields that we didn't expect.
|
||||
class FlexibleModel(BaseModel):
|
||||
pass
|
||||
|
||||
|
||||
# use this type to indicate that we won't model this data.
|
||||
# because its not relevant to our use in capa.
|
||||
#
|
||||
# while its nice to have full coverage of the data shape,
|
||||
# it can easily change and break our parsing.
|
||||
# so we really only want to describe what we'll use.
|
||||
Skip: TypeAlias = Optional[Any]
|
||||
|
||||
|
||||
# mark fields that we haven't seen yet and need to model.
|
||||
# pydantic should raise an error when encountering data
|
||||
# in a field with this type.
|
||||
# then we can update the model with the discovered shape.
|
||||
TODO: TypeAlias = None
|
||||
ListTODO: TypeAlias = List[None]
|
||||
DictTODO: TypeAlias = ExactModel
|
||||
|
||||
EmptyDict: TypeAlias = BaseModel
|
||||
EmptyList: TypeAlias = List[Any]
|
||||
|
||||
|
||||
class Info(FlexibleModel):
|
||||
version: str
|
||||
|
||||
|
||||
class ImportedSymbol(ExactModel):
|
||||
address: HexInt
|
||||
name: Optional[str] = None
|
||||
|
||||
|
||||
class ImportedDll(ExactModel):
|
||||
dll: str
|
||||
imports: List[ImportedSymbol]
|
||||
|
||||
|
||||
class DirectoryEntry(ExactModel):
|
||||
name: str
|
||||
virtual_address: HexInt
|
||||
size: HexInt
|
||||
|
||||
|
||||
class Section(ExactModel):
|
||||
name: str
|
||||
raw_address: HexInt
|
||||
virtual_address: HexInt
|
||||
virtual_size: HexInt
|
||||
size_of_data: HexInt
|
||||
characteristics: str
|
||||
characteristics_raw: HexInt
|
||||
entropy: float
|
||||
|
||||
|
||||
class Resource(ExactModel):
|
||||
name: str
|
||||
language: Optional[str] = None
|
||||
sublanguage: str
|
||||
filetype: Optional[str]
|
||||
offset: HexInt
|
||||
size: HexInt
|
||||
entropy: float
|
||||
|
||||
|
||||
class DigitalSigner(FlexibleModel):
|
||||
md5_fingerprint: str
|
||||
not_after: str
|
||||
not_before: str
|
||||
serial_number: str
|
||||
sha1_fingerprint: str
|
||||
sha256_fingerprint: str
|
||||
|
||||
issuer_commonName: Optional[str] = None
|
||||
issuer_countryName: Optional[str] = None
|
||||
issuer_localityName: Optional[str] = None
|
||||
issuer_organizationName: Optional[str] = None
|
||||
issuer_stateOrProvinceName: Optional[str] = None
|
||||
|
||||
subject_commonName: Optional[str] = None
|
||||
subject_countryName: Optional[str] = None
|
||||
subject_localityName: Optional[str] = None
|
||||
subject_organizationName: Optional[str] = None
|
||||
subject_stateOrProvinceName: Optional[str] = None
|
||||
|
||||
extensions_authorityInfoAccess_caIssuers: Optional[str] = None
|
||||
extensions_authorityKeyIdentifier: Optional[str] = None
|
||||
extensions_cRLDistributionPoints_0: Optional[str] = None
|
||||
extensions_certificatePolicies_0: Optional[str] = None
|
||||
extensions_subjectAltName_0: Optional[str] = None
|
||||
extensions_subjectKeyIdentifier: Optional[str] = None
|
||||
|
||||
|
||||
class AuxSigner(ExactModel):
|
||||
name: str
|
||||
issued_to: str = Field(alias="Issued to")
|
||||
issued_by: str = Field(alias="Issued by")
|
||||
expires: str = Field(alias="Expires")
|
||||
sha1_hash: str = Field(alias="SHA1 hash")
|
||||
|
||||
|
||||
class Signer(ExactModel):
|
||||
aux_sha1: Optional[str] = None
|
||||
aux_timestamp: Optional[str] = None
|
||||
aux_valid: Optional[bool] = None
|
||||
aux_error: Optional[bool] = None
|
||||
aux_error_desc: Optional[str] = None
|
||||
aux_signers: Optional[List[AuxSigner]] = None
|
||||
|
||||
|
||||
class Overlay(ExactModel):
|
||||
offset: HexInt
|
||||
size: HexInt
|
||||
|
||||
|
||||
class KV(ExactModel):
|
||||
name: str
|
||||
value: str
|
||||
|
||||
|
||||
class ExportedSymbol(ExactModel):
|
||||
address: HexInt
|
||||
name: str
|
||||
ordinal: int
|
||||
|
||||
|
||||
class PE(ExactModel):
|
||||
peid_signatures: TODO
|
||||
imagebase: HexInt
|
||||
entrypoint: HexInt
|
||||
reported_checksum: HexInt
|
||||
actual_checksum: HexInt
|
||||
osversion: str
|
||||
pdbpath: Optional[str] = None
|
||||
timestamp: str
|
||||
|
||||
# List[ImportedDll], or Dict[basename(dll), ImportedDll]
|
||||
imports: Union[List[ImportedDll], Dict[str, ImportedDll]]
|
||||
imported_dll_count: Optional[int] = None
|
||||
imphash: str
|
||||
|
||||
exported_dll_name: Optional[str] = None
|
||||
exports: List[ExportedSymbol]
|
||||
|
||||
dirents: List[DirectoryEntry]
|
||||
sections: List[Section]
|
||||
|
||||
ep_bytes: Optional[HexBytes] = None
|
||||
|
||||
overlay: Optional[Overlay] = None
|
||||
resources: List[Resource]
|
||||
versioninfo: List[KV]
|
||||
|
||||
# base64 encoded data
|
||||
icon: Optional[str] = None
|
||||
# MD5-like hash
|
||||
icon_hash: Optional[str] = None
|
||||
# MD5-like hash
|
||||
icon_fuzzy: Optional[str] = None
|
||||
# short hex string
|
||||
icon_dhash: Optional[str] = None
|
||||
|
||||
digital_signers: List[DigitalSigner]
|
||||
guest_signers: Signer
|
||||
|
||||
|
||||
# TODO(mr-tz): target.file.dotnet, target.file.extracted_files, target.file.extracted_files_tool,
|
||||
# target.file.extracted_files_time
|
||||
# https://github.com/mandiant/capa/issues/1814
|
||||
class File(FlexibleModel):
|
||||
type: str
|
||||
cape_type_code: Optional[int] = None
|
||||
cape_type: Optional[str] = None
|
||||
|
||||
pid: Optional[Union[int, Literal[""]]] = None
|
||||
name: Union[List[str], str]
|
||||
path: str
|
||||
guest_paths: Union[List[str], str, None]
|
||||
timestamp: Optional[str] = None
|
||||
|
||||
#
|
||||
# hashes
|
||||
#
|
||||
crc32: str
|
||||
md5: str
|
||||
sha1: str
|
||||
sha256: str
|
||||
sha512: str
|
||||
sha3_384: str
|
||||
ssdeep: str
|
||||
# unsure why this would ever be "False"
|
||||
tlsh: Optional[Union[str, bool]] = None
|
||||
rh_hash: Optional[str] = None
|
||||
|
||||
#
|
||||
# other metadata, static analysis
|
||||
#
|
||||
size: int
|
||||
pe: Optional[PE] = None
|
||||
ep_bytes: Optional[HexBytes] = None
|
||||
entrypoint: Optional[int] = None
|
||||
data: Optional[str] = None
|
||||
strings: Optional[List[str]] = None
|
||||
|
||||
#
|
||||
# detections (skip)
|
||||
#
|
||||
yara: Skip = None
|
||||
cape_yara: Skip = None
|
||||
clamav: Skip = None
|
||||
virustotal: Skip = None
|
||||
|
||||
|
||||
class ProcessFile(File):
|
||||
#
|
||||
# like a File, but also has dynamic analysis results
|
||||
#
|
||||
pid: Optional[int] = None
|
||||
process_path: Optional[str] = None
|
||||
process_name: Optional[str] = None
|
||||
module_path: Optional[str] = None
|
||||
virtual_address: Optional[HexInt] = None
|
||||
target_pid: Optional[Union[int, str]] = None
|
||||
target_path: Optional[str] = None
|
||||
target_process: Optional[str] = None
|
||||
|
||||
|
||||
class Argument(ExactModel):
|
||||
name: str
|
||||
# unsure why empty list is provided here
|
||||
value: Union[HexInt, int, str, EmptyList]
|
||||
pretty_value: Optional[str] = None
|
||||
|
||||
|
||||
class Call(ExactModel):
|
||||
timestamp: str
|
||||
thread_id: int
|
||||
category: str
|
||||
|
||||
api: str
|
||||
|
||||
arguments: List[Argument]
|
||||
status: bool
|
||||
return_: HexInt = Field(alias="return")
|
||||
pretty_return: Optional[str] = None
|
||||
|
||||
repeated: int
|
||||
|
||||
# virtual addresses
|
||||
caller: HexInt
|
||||
parentcaller: HexInt
|
||||
|
||||
# index into calls array
|
||||
id: int
|
||||
|
||||
|
||||
class Process(ExactModel):
|
||||
process_id: int
|
||||
process_name: str
|
||||
parent_id: int
|
||||
module_path: str
|
||||
first_seen: str
|
||||
calls: List[Call]
|
||||
threads: List[int]
|
||||
environ: Dict[str, str]
|
||||
|
||||
|
||||
class ProcessTree(ExactModel):
|
||||
name: str
|
||||
pid: int
|
||||
parent_id: int
|
||||
module_path: str
|
||||
threads: List[int]
|
||||
environ: Dict[str, str]
|
||||
children: List["ProcessTree"]
|
||||
|
||||
|
||||
class Summary(ExactModel):
|
||||
files: List[str]
|
||||
read_files: List[str]
|
||||
write_files: List[str]
|
||||
delete_files: List[str]
|
||||
keys: List[str]
|
||||
read_keys: List[str]
|
||||
write_keys: List[str]
|
||||
delete_keys: List[str]
|
||||
executed_commands: List[str]
|
||||
resolved_apis: List[str]
|
||||
mutexes: List[str]
|
||||
created_services: List[str]
|
||||
started_services: List[str]
|
||||
|
||||
|
||||
class EncryptedBuffer(ExactModel):
|
||||
process_name: str
|
||||
pid: int
|
||||
|
||||
api_call: str
|
||||
buffer: str
|
||||
buffer_size: Optional[int] = None
|
||||
crypt_key: Optional[Union[HexInt, str]] = None
|
||||
|
||||
|
||||
class Behavior(ExactModel):
|
||||
summary: Summary
|
||||
|
||||
# list of processes, of threads, of calls
|
||||
processes: List[Process]
|
||||
# tree of processes
|
||||
processtree: List[ProcessTree]
|
||||
|
||||
anomaly: List[str]
|
||||
encryptedbuffers: List[EncryptedBuffer]
|
||||
# these are small objects that describe atomic events,
|
||||
# like file move, registery access.
|
||||
# we'll detect the same with our API call analyis.
|
||||
enhanced: Skip = None
|
||||
|
||||
|
||||
class Target(ExactModel):
|
||||
category: str
|
||||
file: File
|
||||
pe: Optional[PE] = None
|
||||
|
||||
|
||||
class Static(ExactModel):
|
||||
pe: Optional[PE] = None
|
||||
flare_capa: Skip = None
|
||||
|
||||
|
||||
class Cape(ExactModel):
|
||||
payloads: List[ProcessFile]
|
||||
configs: Skip = None
|
||||
|
||||
|
||||
# flexible because there may be more sorts of analysis
|
||||
# but we only care about the ones described here.
|
||||
class CapeReport(FlexibleModel):
|
||||
# the input file, I think
|
||||
target: Target
|
||||
# info about the processing job, like machine and distributed metadata.
|
||||
info: Info
|
||||
|
||||
#
|
||||
# static analysis results
|
||||
#
|
||||
static: Optional[Static] = None
|
||||
strings: Optional[List[str]] = None
|
||||
|
||||
#
|
||||
# dynamic analysis results
|
||||
#
|
||||
# post-processed results: process tree, anomalies, etc
|
||||
behavior: Behavior
|
||||
|
||||
# post-processed results: payloads and extracted configs
|
||||
CAPE: Optional[Cape] = None
|
||||
dropped: Optional[List[File]] = None
|
||||
procdump: Optional[List[ProcessFile]] = None
|
||||
procmemory: ListTODO
|
||||
|
||||
# =========================================================================
|
||||
# information we won't use in capa
|
||||
#
|
||||
|
||||
#
|
||||
# NBIs and HBIs
|
||||
# these are super interesting, but they don't enable use to detect behaviors.
|
||||
# they take a lot of code to model and details to maintain.
|
||||
#
|
||||
# if we come up with a future use for this, go ahead and re-enable!
|
||||
#
|
||||
network: Skip = None
|
||||
suricata: Skip = None
|
||||
curtain: Skip = None
|
||||
sysmon: Skip = None
|
||||
url_analysis: Skip = None
|
||||
|
||||
# screenshot hash values
|
||||
deduplicated_shots: Skip = None
|
||||
# k-v pairs describing the time it took to run each stage.
|
||||
statistics: Skip = None
|
||||
# k-v pairs of ATT&CK ID to signature name or similar.
|
||||
ttps: Skip = None
|
||||
# debug log messages
|
||||
debug: Skip = None
|
||||
|
||||
# various signature matches
|
||||
# we could potentially extend capa to use this info one day,
|
||||
# though it would be quite sandbox-specific,
|
||||
# and more detection-oriented than capability detection.
|
||||
signatures: Skip = None
|
||||
malfamily_tag: Optional[str] = None
|
||||
malscore: float
|
||||
detections: Skip = None
|
||||
detections2pid: Optional[Dict[int, List[str]]] = None
|
||||
# AV detections for the sample.
|
||||
virustotal: Skip = None
|
||||
|
||||
@classmethod
|
||||
def from_buf(cls, buf: bytes) -> "CapeReport":
|
||||
return cls.model_validate_json(buf)
|
||||
48
capa/features/extractors/cape/process.py
Normal file
48
capa/features/extractors/cape/process.py
Normal file
@@ -0,0 +1,48 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import logging
|
||||
from typing import List, Tuple, Iterator
|
||||
|
||||
from capa.features.common import String, Feature
|
||||
from capa.features.address import Address, ThreadAddress
|
||||
from capa.features.extractors.cape.models import Process
|
||||
from capa.features.extractors.base_extractor import ThreadHandle, ProcessHandle
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_threads(ph: ProcessHandle) -> Iterator[ThreadHandle]:
|
||||
"""
|
||||
get the threads associated with a given process
|
||||
"""
|
||||
process: Process = ph.inner
|
||||
threads: List[int] = process.threads
|
||||
|
||||
for thread in threads:
|
||||
address: ThreadAddress = ThreadAddress(process=ph.address, tid=thread)
|
||||
yield ThreadHandle(address=address, inner={})
|
||||
|
||||
|
||||
def extract_environ_strings(ph: ProcessHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
extract strings from a process' provided environment variables.
|
||||
"""
|
||||
process: Process = ph.inner
|
||||
|
||||
for value in (value for value in process.environ.values() if value):
|
||||
yield String(value), ph.address
|
||||
|
||||
|
||||
def extract_features(ph: ProcessHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
for handler in PROCESS_HANDLERS:
|
||||
for feature, addr in handler(ph):
|
||||
yield feature, addr
|
||||
|
||||
|
||||
PROCESS_HANDLERS = (extract_environ_strings,)
|
||||
32
capa/features/extractors/cape/thread.py
Normal file
32
capa/features/extractors/cape/thread.py
Normal file
@@ -0,0 +1,32 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import logging
|
||||
from typing import Iterator
|
||||
|
||||
from capa.features.address import DynamicCallAddress
|
||||
from capa.features.extractors.helpers import generate_symbols
|
||||
from capa.features.extractors.cape.models import Process
|
||||
from capa.features.extractors.base_extractor import CallHandle, ThreadHandle, ProcessHandle
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_calls(ph: ProcessHandle, th: ThreadHandle) -> Iterator[CallHandle]:
|
||||
process: Process = ph.inner
|
||||
|
||||
tid = th.address.tid
|
||||
for call_index, call in enumerate(process.calls):
|
||||
if call.thread_id != tid:
|
||||
continue
|
||||
|
||||
for symbol in generate_symbols("", call.api):
|
||||
call.api = symbol
|
||||
|
||||
addr = DynamicCallAddress(thread=th.address, id=call_index)
|
||||
yield CallHandle(address=addr, inner=call)
|
||||
154
capa/features/extractors/common.py
Normal file
154
capa/features/extractors/common.py
Normal file
@@ -0,0 +1,154 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
import io
|
||||
import re
|
||||
import logging
|
||||
import binascii
|
||||
import contextlib
|
||||
from typing import Tuple, Iterator
|
||||
|
||||
import pefile
|
||||
|
||||
import capa.features
|
||||
import capa.features.extractors.elf
|
||||
import capa.features.extractors.pefile
|
||||
import capa.features.extractors.strings
|
||||
from capa.features.common import (
|
||||
OS,
|
||||
OS_ANY,
|
||||
OS_AUTO,
|
||||
ARCH_ANY,
|
||||
FORMAT_PE,
|
||||
FORMAT_DEX,
|
||||
FORMAT_ELF,
|
||||
OS_ANDROID,
|
||||
OS_WINDOWS,
|
||||
ARCH_DALVIK,
|
||||
FORMAT_FREEZE,
|
||||
FORMAT_RESULT,
|
||||
Arch,
|
||||
Format,
|
||||
String,
|
||||
Feature,
|
||||
)
|
||||
from capa.features.freeze import is_freeze
|
||||
from capa.features.address import NO_ADDRESS, Address, FileOffsetAddress
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# match strings for formats
|
||||
MATCH_PE = b"MZ"
|
||||
MATCH_ELF = b"\x7fELF"
|
||||
MATCH_DEX = b"dex\n"
|
||||
MATCH_RESULT = b'{"meta":'
|
||||
MATCH_JSON_OBJECT = b'{"'
|
||||
|
||||
|
||||
def extract_file_strings(buf, **kwargs) -> Iterator[Tuple[String, Address]]:
|
||||
"""
|
||||
extract ASCII and UTF-16 LE strings from file
|
||||
"""
|
||||
for s in capa.features.extractors.strings.extract_ascii_strings(buf):
|
||||
yield String(s.s), FileOffsetAddress(s.offset)
|
||||
|
||||
for s in capa.features.extractors.strings.extract_unicode_strings(buf):
|
||||
yield String(s.s), FileOffsetAddress(s.offset)
|
||||
|
||||
|
||||
def extract_format(buf) -> Iterator[Tuple[Feature, Address]]:
|
||||
if buf.startswith(MATCH_PE):
|
||||
yield Format(FORMAT_PE), NO_ADDRESS
|
||||
elif buf.startswith(MATCH_ELF):
|
||||
yield Format(FORMAT_ELF), NO_ADDRESS
|
||||
elif len(buf) > 8 and buf.startswith(MATCH_DEX) and buf[7] == 0x00:
|
||||
yield Format(FORMAT_DEX), NO_ADDRESS
|
||||
elif is_freeze(buf):
|
||||
yield Format(FORMAT_FREEZE), NO_ADDRESS
|
||||
elif buf.startswith(MATCH_RESULT):
|
||||
yield Format(FORMAT_RESULT), NO_ADDRESS
|
||||
elif re.sub(rb"\s", b"", buf[:20]).startswith(MATCH_JSON_OBJECT):
|
||||
# potential start of JSON object data without whitespace
|
||||
# we don't know what it is exactly, but may support it (e.g. a dynamic CAPE sandbox report)
|
||||
# skip verdict here and let subsequent code analyze this further
|
||||
return
|
||||
else:
|
||||
# we likely end up here:
|
||||
# 1. handling a file format (e.g. macho)
|
||||
#
|
||||
# for (1), this logic will need to be updated as the format is implemented.
|
||||
logger.debug("unsupported file format: %s", binascii.hexlify(buf[:4]).decode("ascii"))
|
||||
return
|
||||
|
||||
|
||||
def extract_arch(buf) -> Iterator[Tuple[Feature, Address]]:
|
||||
if buf.startswith(MATCH_PE):
|
||||
yield from capa.features.extractors.pefile.extract_file_arch(pe=pefile.PE(data=buf))
|
||||
|
||||
elif buf.startswith(MATCH_RESULT):
|
||||
yield Arch(ARCH_ANY), NO_ADDRESS
|
||||
|
||||
elif buf.startswith(MATCH_ELF):
|
||||
with contextlib.closing(io.BytesIO(buf)) as f:
|
||||
arch = capa.features.extractors.elf.detect_elf_arch(f)
|
||||
|
||||
if arch not in capa.features.common.VALID_ARCH:
|
||||
logger.debug("unsupported arch: %s", arch)
|
||||
return
|
||||
|
||||
yield Arch(arch), NO_ADDRESS
|
||||
|
||||
elif len(buf) > 8 and buf.startswith(MATCH_DEX) and buf[7] == 0x00:
|
||||
yield Arch(ARCH_DALVIK), NO_ADDRESS
|
||||
|
||||
else:
|
||||
# we likely end up here:
|
||||
# 1. handling shellcode, or
|
||||
# 2. handling a new file format (e.g. macho)
|
||||
#
|
||||
# for (1) we can't do much - its shellcode and all bets are off.
|
||||
# we could maybe accept a further CLI argument to specify the arch,
|
||||
# but i think this would be rarely used.
|
||||
# rules that rely on arch conditions will fail to match on shellcode.
|
||||
#
|
||||
# for (2), this logic will need to be updated as the format is implemented.
|
||||
logger.debug("unsupported file format: %s, will not guess Arch", binascii.hexlify(buf[:4]).decode("ascii"))
|
||||
return
|
||||
|
||||
|
||||
def extract_os(buf, os=OS_AUTO) -> Iterator[Tuple[Feature, Address]]:
|
||||
if os != OS_AUTO:
|
||||
yield OS(os), NO_ADDRESS
|
||||
|
||||
if buf.startswith(MATCH_PE):
|
||||
yield OS(OS_WINDOWS), NO_ADDRESS
|
||||
elif buf.startswith(MATCH_RESULT):
|
||||
yield OS(OS_ANY), NO_ADDRESS
|
||||
elif buf.startswith(MATCH_ELF):
|
||||
with contextlib.closing(io.BytesIO(buf)) as f:
|
||||
os = capa.features.extractors.elf.detect_elf_os(f)
|
||||
|
||||
if os not in capa.features.common.VALID_OS:
|
||||
logger.debug("unsupported os: %s", os)
|
||||
return
|
||||
|
||||
yield OS(os), NO_ADDRESS
|
||||
|
||||
elif len(buf) > 8 and buf.startswith(MATCH_DEX) and buf[7] == 0x00:
|
||||
yield OS(OS_ANDROID), NO_ADDRESS
|
||||
|
||||
else:
|
||||
# we likely end up here:
|
||||
# 1. handling shellcode, or
|
||||
# 2. handling a new file format (e.g. macho)
|
||||
#
|
||||
# for (1) we can't do much - its shellcode and all bets are off.
|
||||
# rules that rely on OS conditions will fail to match on shellcode.
|
||||
#
|
||||
# for (2), this logic will need to be updated as the format is implemented.
|
||||
logger.debug("unsupported file format: %s, will not guess OS", binascii.hexlify(buf[:4]).decode("ascii"))
|
||||
return
|
||||
421
capa/features/extractors/dexfile.py
Normal file
421
capa/features/extractors/dexfile.py
Normal file
@@ -0,0 +1,421 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
import struct
|
||||
import logging
|
||||
from typing import Set, Dict, List, Tuple, Iterator, Optional, TypedDict
|
||||
from pathlib import Path
|
||||
from dataclasses import dataclass
|
||||
|
||||
import dexparser.disassembler as disassembler
|
||||
from dexparser import DEXParser, uleb128_value
|
||||
|
||||
from capa.features.file import Import, FunctionName
|
||||
from capa.features.common import (
|
||||
OS,
|
||||
FORMAT_DEX,
|
||||
OS_ANDROID,
|
||||
ARCH_DALVIK,
|
||||
Arch,
|
||||
Class,
|
||||
Format,
|
||||
String,
|
||||
Feature,
|
||||
Namespace,
|
||||
)
|
||||
from capa.features.address import NO_ADDRESS, Address, DexClassAddress, DexMethodAddress, FileOffsetAddress
|
||||
from capa.features.extractors.base_extractor import (
|
||||
BBHandle,
|
||||
InsnHandle,
|
||||
SampleHashes,
|
||||
FunctionHandle,
|
||||
StaticFeatureExtractor,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Reference: https://source.android.com/docs/core/runtime/dex-format
|
||||
|
||||
|
||||
class DexProtoId(TypedDict):
|
||||
shorty_idx: int
|
||||
return_type_idx: int
|
||||
param_off: int
|
||||
|
||||
|
||||
class DexMethodId(TypedDict):
|
||||
class_idx: int
|
||||
proto_idx: int
|
||||
name_idx: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class DexAnalyzedMethod:
|
||||
class_type: str
|
||||
name: str
|
||||
shorty_descriptor: str
|
||||
return_type: str
|
||||
parameters: List[str]
|
||||
id_offset: int = 0
|
||||
code_offset: int = 0
|
||||
access_flags: Optional[int] = None
|
||||
|
||||
@property
|
||||
def address(self):
|
||||
# NOTE: Some methods do not have code, in that case we use the method_id offset
|
||||
if self.has_code:
|
||||
return self.code_offset
|
||||
else:
|
||||
return self.id_offset
|
||||
|
||||
@property
|
||||
def has_code(self):
|
||||
# NOTE: code_offset is zero if the method is abstract/native or not defined in a class
|
||||
return self.code_offset != 0
|
||||
|
||||
@property
|
||||
def has_definition(self):
|
||||
# NOTE: access_flags is only known if the method is defined in a class
|
||||
return self.access_flags is not None
|
||||
|
||||
@property
|
||||
def qualified_name(self):
|
||||
return f"{self.class_type}::{self.name}"
|
||||
|
||||
|
||||
class DexFieldId(TypedDict):
|
||||
class_idx: int
|
||||
type_idx: int
|
||||
name_idx: int
|
||||
|
||||
|
||||
class DexClassDef(TypedDict):
|
||||
class_idx: int
|
||||
access_flags: int
|
||||
superclass_idx: int
|
||||
interfaces_off: int
|
||||
source_file_idx: int
|
||||
annotations_off: int
|
||||
class_data_off: int
|
||||
static_values_off: int
|
||||
|
||||
|
||||
class DexFieldDef(TypedDict):
|
||||
diff: int
|
||||
access_flags: int
|
||||
|
||||
|
||||
class DexMethodDef(TypedDict):
|
||||
diff: int
|
||||
access_flags: int
|
||||
code_off: int
|
||||
|
||||
|
||||
class DexClassData(TypedDict):
|
||||
static_fields: List[DexFieldDef]
|
||||
instance_fields: List[DexFieldDef]
|
||||
direct_methods: List[DexMethodDef]
|
||||
virtual_methods: List[DexMethodDef]
|
||||
|
||||
|
||||
@dataclass
|
||||
class DexAnalyzedClass:
|
||||
offset: int
|
||||
class_type: str
|
||||
superclass_type: str
|
||||
interfaces: List[str]
|
||||
source_file: str
|
||||
data: Optional[DexClassData]
|
||||
|
||||
|
||||
class DexAnnotation(TypedDict):
|
||||
visibility: int
|
||||
type_idx_diff: int
|
||||
size_diff: int
|
||||
name_idx_diff: int
|
||||
value_type: int
|
||||
encoded_value: int
|
||||
|
||||
|
||||
class DexAnalysis:
|
||||
def get_strings(self):
|
||||
# NOTE: Copied from dexparser, upstream later
|
||||
|
||||
strings: List[Tuple[int, bytes]] = []
|
||||
string_ids_off = self.dex.header_data["string_ids_off"]
|
||||
|
||||
for i in range(self.dex.header_data["string_ids_size"]):
|
||||
offset = struct.unpack("<L", self.dex.data[string_ids_off + (i * 4) : string_ids_off + (i * 4) + 4])[0]
|
||||
c_size, size_offset = uleb128_value(self.dex.data, offset)
|
||||
c_char = self.dex.data[offset + size_offset : offset + size_offset + c_size]
|
||||
strings.append((offset, c_char))
|
||||
|
||||
return strings
|
||||
|
||||
def __init__(self, dex: DEXParser):
|
||||
self.dex = dex
|
||||
|
||||
self.strings = self.get_strings()
|
||||
self.strings_utf8: List[str] = []
|
||||
for _, data in self.strings:
|
||||
# NOTE: This is technically incorrect
|
||||
# Reference: https://source.android.com/devices/tech/dalvik/dex-format#mutf-8
|
||||
self.strings_utf8.append(data.decode("utf-8", errors="backslashreplace"))
|
||||
|
||||
self.type_ids: List[int] = dex.get_typeids()
|
||||
self.method_ids: List[DexMethodId] = dex.get_methods()
|
||||
self.proto_ids: List[DexProtoId] = dex.get_protoids()
|
||||
self.field_ids: List[DexFieldId] = dex.get_fieldids()
|
||||
self.class_defs: List[DexClassDef] = dex.get_classdef_data()
|
||||
|
||||
self._is_analyzing = True
|
||||
self.used_classes: Set[str] = set()
|
||||
self.classes = self._analyze_classes()
|
||||
self.methods = self._analyze_methods()
|
||||
self.methods_by_address: Dict[int, DexAnalyzedMethod] = {m.address: m for m in self.methods}
|
||||
|
||||
self.namespaces: Set[str] = set()
|
||||
for class_type in self.used_classes:
|
||||
idx = class_type.rfind(".")
|
||||
if idx != -1:
|
||||
self.namespaces.add(class_type[:idx])
|
||||
|
||||
for class_type in self.classes:
|
||||
self.used_classes.remove(class_type)
|
||||
|
||||
# Only available after code analysis
|
||||
self._is_analyzing = False
|
||||
|
||||
def analyze_code(self):
|
||||
# Loop over the classes and analyze them
|
||||
# self.classes: List[DexClass] = self.dex.get_class_data(offset=-1)
|
||||
# self.annotations: List[DexAnnotation] = dex.get_annotations(offset=-1)
|
||||
# self.static_values: List[int] = dex.get_static_values(offset=-1)
|
||||
pass
|
||||
|
||||
def get_string(self, index: int) -> str:
|
||||
return self.strings_utf8[index]
|
||||
|
||||
def _decode_descriptor(self, descriptor: str) -> str:
|
||||
first = descriptor[0]
|
||||
if first == "L":
|
||||
pretty = descriptor[1:-1].replace("/", ".")
|
||||
if self._is_analyzing:
|
||||
self.used_classes.add(pretty)
|
||||
elif first == "[":
|
||||
pretty = self._decode_descriptor(descriptor[1:]) + "[]"
|
||||
else:
|
||||
pretty = disassembler.type_descriptor[first]
|
||||
return pretty
|
||||
|
||||
def get_pretty_type(self, index: int) -> str:
|
||||
if index == 0xFFFFFFFF:
|
||||
return "<NO_INDEX>"
|
||||
descriptor = self.get_string(self.type_ids[index])
|
||||
return self._decode_descriptor(descriptor)
|
||||
|
||||
def _analyze_classes(self):
|
||||
classes: Dict[str, DexAnalyzedClass] = {}
|
||||
offset = self.dex.header_data["class_defs_off"]
|
||||
for index, clazz in enumerate(self.class_defs):
|
||||
class_type = self.get_pretty_type(clazz["class_idx"])
|
||||
|
||||
# Superclass
|
||||
superclass_idx = clazz["superclass_idx"]
|
||||
if superclass_idx != 0xFFFFFFFF:
|
||||
superclass_type = self.get_pretty_type(superclass_idx)
|
||||
else:
|
||||
superclass_type = ""
|
||||
|
||||
# Interfaces
|
||||
interfaces = []
|
||||
interfaces_offset = clazz["interfaces_off"]
|
||||
if interfaces_offset != 0:
|
||||
size = struct.unpack("<L", self.dex.data[interfaces_offset : interfaces_offset + 4])[0]
|
||||
for i in range(size):
|
||||
type_idx = struct.unpack(
|
||||
"<H", self.dex.data[interfaces_offset + 4 + i * 2 : interfaces_offset + 6 + i * 2]
|
||||
)[0]
|
||||
interface_type = self.get_pretty_type(type_idx)
|
||||
interfaces.append(interface_type)
|
||||
|
||||
# Source file
|
||||
source_file_idx = clazz["source_file_idx"]
|
||||
if source_file_idx != 0xFFFFFFFF:
|
||||
source_file = self.get_string(source_file_idx)
|
||||
else:
|
||||
source_file = ""
|
||||
|
||||
# Data
|
||||
data_offset = clazz["class_data_off"]
|
||||
if data_offset != 0:
|
||||
data = self.dex.get_class_data(data_offset)
|
||||
else:
|
||||
data = None
|
||||
|
||||
classes[class_type] = DexAnalyzedClass(
|
||||
offset=offset + index * 32,
|
||||
class_type=class_type,
|
||||
superclass_type=superclass_type,
|
||||
interfaces=interfaces,
|
||||
source_file=source_file,
|
||||
data=data,
|
||||
)
|
||||
return classes
|
||||
|
||||
def _analyze_methods(self):
|
||||
methods: List[DexAnalyzedMethod] = []
|
||||
for method_id in self.method_ids:
|
||||
proto = self.proto_ids[method_id["proto_idx"]]
|
||||
parameters = []
|
||||
|
||||
param_off = proto["param_off"]
|
||||
if param_off != 0:
|
||||
size = struct.unpack("<L", self.dex.data[param_off : param_off + 4])[0]
|
||||
for i in range(size):
|
||||
type_idx = struct.unpack("<H", self.dex.data[param_off + 4 + i * 2 : param_off + 6 + i * 2])[0]
|
||||
param_type = self.get_pretty_type(type_idx)
|
||||
parameters.append(param_type)
|
||||
|
||||
methods.append(
|
||||
DexAnalyzedMethod(
|
||||
class_type=self.get_pretty_type(method_id["class_idx"]),
|
||||
name=self.get_string(method_id["name_idx"]),
|
||||
shorty_descriptor=self.get_string(proto["shorty_idx"]),
|
||||
return_type=self.get_pretty_type(proto["return_type_idx"]),
|
||||
parameters=parameters,
|
||||
)
|
||||
)
|
||||
|
||||
# Fill in the missing method data
|
||||
for clazz in self.classes.values():
|
||||
if clazz.data is None:
|
||||
continue
|
||||
|
||||
for method_def in clazz.data["direct_methods"]:
|
||||
diff = method_def["diff"]
|
||||
methods[diff].access_flags = method_def["access_flags"]
|
||||
methods[diff].code_offset = method_def["code_off"]
|
||||
|
||||
for method_def in clazz.data["virtual_methods"]:
|
||||
diff = method_def["diff"]
|
||||
methods[diff].access_flags = method_def["access_flags"]
|
||||
methods[diff].code_offset = method_def["code_off"]
|
||||
|
||||
# Fill in the missing code offsets with fake data
|
||||
offset = self.dex.header_data["method_ids_off"]
|
||||
for index, method in enumerate(methods):
|
||||
method.id_offset = offset + index * 8
|
||||
|
||||
return methods
|
||||
|
||||
def extract_file_features(self) -> Iterator[Tuple[Feature, Address]]:
|
||||
yield Format(FORMAT_DEX), NO_ADDRESS
|
||||
|
||||
for i in range(len(self.strings)):
|
||||
yield String(self.strings_utf8[i]), FileOffsetAddress(self.strings[i][0])
|
||||
|
||||
for method in self.methods:
|
||||
if method.has_definition:
|
||||
yield FunctionName(method.qualified_name), DexMethodAddress(method.address)
|
||||
else:
|
||||
yield Import(method.qualified_name), DexMethodAddress(method.address)
|
||||
|
||||
for namespace in self.namespaces:
|
||||
yield Namespace(namespace), NO_ADDRESS
|
||||
|
||||
for clazz in self.classes.values():
|
||||
yield Class(clazz.class_type), DexClassAddress(clazz.offset)
|
||||
|
||||
for class_type in self.used_classes:
|
||||
yield Class(class_type), NO_ADDRESS
|
||||
|
||||
|
||||
class DexFeatureExtractor(StaticFeatureExtractor):
|
||||
def __init__(self, path: Path, *, code_analysis: bool):
|
||||
super().__init__(hashes=SampleHashes.from_bytes(path.read_bytes()))
|
||||
self.path: Path = path
|
||||
self.code_analysis = code_analysis
|
||||
self.dex = DEXParser(filedir=str(path))
|
||||
self.analysis = DexAnalysis(self.dex)
|
||||
|
||||
# Perform more expensive code analysis only when requested
|
||||
if self.code_analysis:
|
||||
self.analysis.analyze_code()
|
||||
|
||||
def todo(self):
|
||||
import inspect
|
||||
|
||||
message = "[DexparserFeatureExtractor:TODO] " + inspect.stack()[1].function
|
||||
logger.debug(message)
|
||||
|
||||
def get_base_address(self):
|
||||
return NO_ADDRESS
|
||||
|
||||
def extract_global_features(self) -> Iterator[Tuple[Feature, Address]]:
|
||||
# These are hardcoded global features
|
||||
yield Format(FORMAT_DEX), NO_ADDRESS
|
||||
yield OS(OS_ANDROID), NO_ADDRESS
|
||||
yield Arch(ARCH_DALVIK), NO_ADDRESS
|
||||
|
||||
def extract_file_features(self) -> Iterator[Tuple[Feature, Address]]:
|
||||
yield from self.analysis.extract_file_features()
|
||||
|
||||
def is_library_function(self, addr: Address) -> bool:
|
||||
assert isinstance(addr, DexMethodAddress)
|
||||
method = self.analysis.methods_by_address[addr]
|
||||
# exclude androidx/kotlin stuff?
|
||||
return not method.has_definition
|
||||
|
||||
def get_function_name(self, addr: Address) -> str:
|
||||
assert isinstance(addr, DexMethodAddress)
|
||||
method = self.analysis.methods_by_address[addr]
|
||||
return method.qualified_name
|
||||
|
||||
def get_functions(self) -> Iterator[FunctionHandle]:
|
||||
if not self.code_analysis:
|
||||
raise Exception("code analysis is disabled")
|
||||
|
||||
for method in self.analysis.methods:
|
||||
yield FunctionHandle(DexMethodAddress(method.address), method)
|
||||
|
||||
def extract_function_features(self, f: FunctionHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
if not self.code_analysis:
|
||||
raise Exception("code analysis is disabled")
|
||||
method: DexAnalyzedMethod = f.inner
|
||||
if method.has_code:
|
||||
return self.todo()
|
||||
yield
|
||||
|
||||
def get_basic_blocks(self, f: FunctionHandle) -> Iterator[BBHandle]:
|
||||
if not self.code_analysis:
|
||||
raise Exception("code analysis is disabled")
|
||||
method: DexAnalyzedMethod = f.inner
|
||||
if method.has_code:
|
||||
return self.todo()
|
||||
yield
|
||||
|
||||
def extract_basic_block_features(self, f: FunctionHandle, bb: BBHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
if not self.code_analysis:
|
||||
raise Exception("code analysis is disabled")
|
||||
return self.todo()
|
||||
yield
|
||||
|
||||
def get_instructions(self, f: FunctionHandle, bb: BBHandle) -> Iterator[InsnHandle]:
|
||||
if not self.code_analysis:
|
||||
raise Exception("code analysis is disabled")
|
||||
return self.todo()
|
||||
yield
|
||||
|
||||
def extract_insn_features(
|
||||
self, f: FunctionHandle, bb: BBHandle, insn: InsnHandle
|
||||
) -> Iterator[Tuple[Feature, Address]]:
|
||||
if not self.code_analysis:
|
||||
raise Exception("code analysis is disabled")
|
||||
return self.todo()
|
||||
yield
|
||||
0
capa/features/extractors/dnfile/__init__.py
Normal file
0
capa/features/extractors/dnfile/__init__.py
Normal file
161
capa/features/extractors/dnfile/extractor.py
Normal file
161
capa/features/extractors/dnfile/extractor.py
Normal file
@@ -0,0 +1,161 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Dict, List, Tuple, Union, Iterator, Optional
|
||||
from pathlib import Path
|
||||
|
||||
import dnfile
|
||||
from dncil.cil.opcode import OpCodes
|
||||
|
||||
import capa.features.extractors
|
||||
import capa.features.extractors.dotnetfile
|
||||
import capa.features.extractors.dnfile.file
|
||||
import capa.features.extractors.dnfile.insn
|
||||
import capa.features.extractors.dnfile.function
|
||||
from capa.features.common import Feature
|
||||
from capa.features.address import NO_ADDRESS, Address, DNTokenAddress, DNTokenOffsetAddress
|
||||
from capa.features.extractors.dnfile.types import DnType, DnUnmanagedMethod
|
||||
from capa.features.extractors.base_extractor import (
|
||||
BBHandle,
|
||||
InsnHandle,
|
||||
SampleHashes,
|
||||
FunctionHandle,
|
||||
StaticFeatureExtractor,
|
||||
)
|
||||
from capa.features.extractors.dnfile.helpers import (
|
||||
get_dotnet_types,
|
||||
get_dotnet_fields,
|
||||
get_dotnet_managed_imports,
|
||||
get_dotnet_managed_methods,
|
||||
get_dotnet_unmanaged_imports,
|
||||
get_dotnet_managed_method_bodies,
|
||||
)
|
||||
|
||||
|
||||
class DnFileFeatureExtractorCache:
|
||||
def __init__(self, pe: dnfile.dnPE):
|
||||
self.imports: Dict[int, Union[DnType, DnUnmanagedMethod]] = {}
|
||||
self.native_imports: Dict[int, Union[DnType, DnUnmanagedMethod]] = {}
|
||||
self.methods: Dict[int, Union[DnType, DnUnmanagedMethod]] = {}
|
||||
self.fields: Dict[int, Union[DnType, DnUnmanagedMethod]] = {}
|
||||
self.types: Dict[int, Union[DnType, DnUnmanagedMethod]] = {}
|
||||
|
||||
for import_ in get_dotnet_managed_imports(pe):
|
||||
self.imports[import_.token] = import_
|
||||
for native_import in get_dotnet_unmanaged_imports(pe):
|
||||
self.native_imports[native_import.token] = native_import
|
||||
for method in get_dotnet_managed_methods(pe):
|
||||
self.methods[method.token] = method
|
||||
for field in get_dotnet_fields(pe):
|
||||
self.fields[field.token] = field
|
||||
for type_ in get_dotnet_types(pe):
|
||||
self.types[type_.token] = type_
|
||||
|
||||
def get_import(self, token: int) -> Optional[Union[DnType, DnUnmanagedMethod]]:
|
||||
return self.imports.get(token)
|
||||
|
||||
def get_native_import(self, token: int) -> Optional[Union[DnType, DnUnmanagedMethod]]:
|
||||
return self.native_imports.get(token)
|
||||
|
||||
def get_method(self, token: int) -> Optional[Union[DnType, DnUnmanagedMethod]]:
|
||||
return self.methods.get(token)
|
||||
|
||||
def get_field(self, token: int) -> Optional[Union[DnType, DnUnmanagedMethod]]:
|
||||
return self.fields.get(token)
|
||||
|
||||
def get_type(self, token: int) -> Optional[Union[DnType, DnUnmanagedMethod]]:
|
||||
return self.types.get(token)
|
||||
|
||||
|
||||
class DnfileFeatureExtractor(StaticFeatureExtractor):
|
||||
def __init__(self, path: Path):
|
||||
self.pe: dnfile.dnPE = dnfile.dnPE(str(path))
|
||||
super().__init__(hashes=SampleHashes.from_bytes(path.read_bytes()))
|
||||
|
||||
# pre-compute .NET token lookup tables; each .NET method has access to this cache for feature extraction
|
||||
# most relevant at instruction scope
|
||||
self.token_cache: DnFileFeatureExtractorCache = DnFileFeatureExtractorCache(self.pe)
|
||||
|
||||
# pre-compute these because we'll yield them at *every* scope.
|
||||
self.global_features: List[Tuple[Feature, Address]] = []
|
||||
self.global_features.extend(capa.features.extractors.dotnetfile.extract_file_format())
|
||||
self.global_features.extend(capa.features.extractors.dotnetfile.extract_file_os(pe=self.pe))
|
||||
self.global_features.extend(capa.features.extractors.dotnetfile.extract_file_arch(pe=self.pe))
|
||||
|
||||
def get_base_address(self):
|
||||
return NO_ADDRESS
|
||||
|
||||
def extract_global_features(self):
|
||||
yield from self.global_features
|
||||
|
||||
def extract_file_features(self):
|
||||
yield from capa.features.extractors.dnfile.file.extract_features(self.pe)
|
||||
|
||||
def get_functions(self) -> Iterator[FunctionHandle]:
|
||||
# create a method lookup table
|
||||
methods: Dict[Address, FunctionHandle] = {}
|
||||
for token, method in get_dotnet_managed_method_bodies(self.pe):
|
||||
fh: FunctionHandle = FunctionHandle(
|
||||
address=DNTokenAddress(token),
|
||||
inner=method,
|
||||
ctx={"pe": self.pe, "calls_from": set(), "calls_to": set(), "cache": self.token_cache},
|
||||
)
|
||||
|
||||
# method tokens should be unique
|
||||
assert fh.address not in methods.keys()
|
||||
methods[fh.address] = fh
|
||||
|
||||
# calculate unique calls to/from each method
|
||||
for fh in methods.values():
|
||||
for insn in fh.inner.instructions:
|
||||
if insn.opcode not in (
|
||||
OpCodes.Call,
|
||||
OpCodes.Callvirt,
|
||||
OpCodes.Jmp,
|
||||
OpCodes.Newobj,
|
||||
):
|
||||
continue
|
||||
|
||||
address: DNTokenAddress = DNTokenAddress(insn.operand.value)
|
||||
|
||||
# record call to destination method; note: we only consider MethodDef methods for destinations
|
||||
dest: Optional[FunctionHandle] = methods.get(address)
|
||||
if dest is not None:
|
||||
dest.ctx["calls_to"].add(fh.address)
|
||||
|
||||
# record call from source method; note: we record all unique calls from a MethodDef method, not just
|
||||
# those calls to other MethodDef methods e.g. calls to imported MemberRef methods
|
||||
fh.ctx["calls_from"].add(address)
|
||||
|
||||
yield from methods.values()
|
||||
|
||||
def extract_function_features(self, fh) -> Iterator[Tuple[Feature, Address]]:
|
||||
yield from capa.features.extractors.dnfile.function.extract_features(fh)
|
||||
|
||||
def get_basic_blocks(self, f) -> Iterator[BBHandle]:
|
||||
# each dotnet method is considered 1 basic block
|
||||
yield BBHandle(
|
||||
address=f.address,
|
||||
inner=f.inner,
|
||||
)
|
||||
|
||||
def extract_basic_block_features(self, fh, bbh):
|
||||
# we don't support basic block features
|
||||
yield from []
|
||||
|
||||
def get_instructions(self, fh, bbh):
|
||||
for insn in bbh.inner.instructions:
|
||||
yield InsnHandle(
|
||||
address=DNTokenOffsetAddress(bbh.address, insn.offset - (fh.inner.offset + fh.inner.header_size)),
|
||||
inner=insn,
|
||||
)
|
||||
|
||||
def extract_insn_features(self, fh, bbh, ih) -> Iterator[Tuple[Feature, Address]]:
|
||||
yield from capa.features.extractors.dnfile.insn.extract_features(fh, bbh, ih)
|
||||
63
capa/features/extractors/dnfile/file.py
Normal file
63
capa/features/extractors/dnfile/file.py
Normal file
@@ -0,0 +1,63 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Tuple, Iterator
|
||||
|
||||
import dnfile
|
||||
|
||||
import capa.features.extractors.dotnetfile
|
||||
from capa.features.file import Import, FunctionName
|
||||
from capa.features.common import Class, Format, String, Feature, Namespace, Characteristic
|
||||
from capa.features.address import Address
|
||||
|
||||
|
||||
def extract_file_import_names(pe: dnfile.dnPE) -> Iterator[Tuple[Import, Address]]:
|
||||
yield from capa.features.extractors.dotnetfile.extract_file_import_names(pe=pe)
|
||||
|
||||
|
||||
def extract_file_format(pe: dnfile.dnPE) -> Iterator[Tuple[Format, Address]]:
|
||||
yield from capa.features.extractors.dotnetfile.extract_file_format(pe=pe)
|
||||
|
||||
|
||||
def extract_file_function_names(pe: dnfile.dnPE) -> Iterator[Tuple[FunctionName, Address]]:
|
||||
yield from capa.features.extractors.dotnetfile.extract_file_function_names(pe=pe)
|
||||
|
||||
|
||||
def extract_file_strings(pe: dnfile.dnPE) -> Iterator[Tuple[String, Address]]:
|
||||
yield from capa.features.extractors.dotnetfile.extract_file_strings(pe=pe)
|
||||
|
||||
|
||||
def extract_file_mixed_mode_characteristic_features(pe: dnfile.dnPE) -> Iterator[Tuple[Characteristic, Address]]:
|
||||
yield from capa.features.extractors.dotnetfile.extract_file_mixed_mode_characteristic_features(pe=pe)
|
||||
|
||||
|
||||
def extract_file_namespace_features(pe: dnfile.dnPE) -> Iterator[Tuple[Namespace, Address]]:
|
||||
yield from capa.features.extractors.dotnetfile.extract_file_namespace_features(pe=pe)
|
||||
|
||||
|
||||
def extract_file_class_features(pe: dnfile.dnPE) -> Iterator[Tuple[Class, Address]]:
|
||||
yield from capa.features.extractors.dotnetfile.extract_file_class_features(pe=pe)
|
||||
|
||||
|
||||
def extract_features(pe: dnfile.dnPE) -> Iterator[Tuple[Feature, Address]]:
|
||||
for file_handler in FILE_HANDLERS:
|
||||
for feature, address in file_handler(pe):
|
||||
yield feature, address
|
||||
|
||||
|
||||
FILE_HANDLERS = (
|
||||
extract_file_import_names,
|
||||
extract_file_function_names,
|
||||
extract_file_strings,
|
||||
extract_file_format,
|
||||
extract_file_mixed_mode_characteristic_features,
|
||||
extract_file_namespace_features,
|
||||
extract_file_class_features,
|
||||
)
|
||||
50
capa/features/extractors/dnfile/function.py
Normal file
50
capa/features/extractors/dnfile/function.py
Normal file
@@ -0,0 +1,50 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Tuple, Iterator
|
||||
|
||||
from capa.features.common import Feature, Characteristic
|
||||
from capa.features.address import Address
|
||||
from capa.features.extractors.base_extractor import FunctionHandle
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def extract_function_calls_to(fh: FunctionHandle) -> Iterator[Tuple[Characteristic, Address]]:
|
||||
"""extract callers to a function"""
|
||||
for dest in fh.ctx["calls_to"]:
|
||||
yield Characteristic("calls to"), dest
|
||||
|
||||
|
||||
def extract_function_calls_from(fh: FunctionHandle) -> Iterator[Tuple[Characteristic, Address]]:
|
||||
"""extract callers from a function"""
|
||||
for src in fh.ctx["calls_from"]:
|
||||
yield Characteristic("calls from"), src
|
||||
|
||||
|
||||
def extract_recursive_call(fh: FunctionHandle) -> Iterator[Tuple[Characteristic, Address]]:
|
||||
"""extract recursive function call"""
|
||||
if fh.address in fh.ctx["calls_to"]:
|
||||
yield Characteristic("recursive call"), fh.address
|
||||
|
||||
|
||||
def extract_function_loop(fh: FunctionHandle) -> Iterator[Tuple[Characteristic, Address]]:
|
||||
"""extract loop indicators from a function"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def extract_features(fh: FunctionHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
for func_handler in FUNCTION_HANDLERS:
|
||||
for feature, addr in func_handler(fh):
|
||||
yield feature, addr
|
||||
|
||||
|
||||
FUNCTION_HANDLERS = (extract_function_calls_to, extract_function_calls_from, extract_recursive_call)
|
||||
335
capa/features/extractors/dnfile/helpers.py
Normal file
335
capa/features/extractors/dnfile/helpers.py
Normal file
@@ -0,0 +1,335 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Dict, Tuple, Union, Iterator, Optional
|
||||
|
||||
import dnfile
|
||||
from dncil.cil.body import CilMethodBody
|
||||
from dncil.cil.error import MethodBodyFormatError
|
||||
from dncil.clr.token import Token, StringToken, InvalidToken
|
||||
from dncil.cil.body.reader import CilMethodBodyReaderBase
|
||||
|
||||
from capa.features.common import FeatureAccess
|
||||
from capa.features.extractors.dnfile.types import DnType, DnUnmanagedMethod
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DnfileMethodBodyReader(CilMethodBodyReaderBase):
|
||||
def __init__(self, pe: dnfile.dnPE, row: dnfile.mdtable.MethodDefRow):
|
||||
self.pe: dnfile.dnPE = pe
|
||||
self.offset: int = self.pe.get_offset_from_rva(row.Rva)
|
||||
|
||||
def read(self, n: int) -> bytes:
|
||||
data: bytes = self.pe.get_data(self.pe.get_rva_from_offset(self.offset), n)
|
||||
self.offset += n
|
||||
return data
|
||||
|
||||
def tell(self) -> int:
|
||||
return self.offset
|
||||
|
||||
def seek(self, offset: int) -> int:
|
||||
self.offset = offset
|
||||
return self.offset
|
||||
|
||||
|
||||
def resolve_dotnet_token(pe: dnfile.dnPE, token: Token) -> Union[dnfile.base.MDTableRow, InvalidToken, str]:
|
||||
"""map generic token to string or table row"""
|
||||
assert pe.net is not None
|
||||
assert pe.net.mdtables is not None
|
||||
|
||||
if isinstance(token, StringToken):
|
||||
user_string: Optional[str] = read_dotnet_user_string(pe, token)
|
||||
if user_string is None:
|
||||
return InvalidToken(token.value)
|
||||
return user_string
|
||||
|
||||
table: Optional[dnfile.base.ClrMetaDataTable] = pe.net.mdtables.tables.get(token.table)
|
||||
if table is None:
|
||||
# table index is not valid
|
||||
return InvalidToken(token.value)
|
||||
|
||||
try:
|
||||
return table.rows[token.rid - 1]
|
||||
except IndexError:
|
||||
# table index is valid but row index is not valid
|
||||
return InvalidToken(token.value)
|
||||
|
||||
|
||||
def read_dotnet_method_body(pe: dnfile.dnPE, row: dnfile.mdtable.MethodDefRow) -> Optional[CilMethodBody]:
|
||||
"""read dotnet method body"""
|
||||
try:
|
||||
return CilMethodBody(DnfileMethodBodyReader(pe, row))
|
||||
except MethodBodyFormatError as e:
|
||||
logger.debug("failed to parse managed method body @ 0x%08x (%s)", row.Rva, e)
|
||||
return None
|
||||
|
||||
|
||||
def read_dotnet_user_string(pe: dnfile.dnPE, token: StringToken) -> Optional[str]:
|
||||
"""read user string from #US stream"""
|
||||
assert pe.net is not None
|
||||
|
||||
if pe.net.user_strings is None:
|
||||
# stream may not exist (seen in obfuscated .NET)
|
||||
logger.debug("#US stream does not exist for stream index 0x%08x", token.rid)
|
||||
return None
|
||||
|
||||
try:
|
||||
user_string: Optional[dnfile.stream.UserString] = pe.net.user_strings.get_us(token.rid)
|
||||
except UnicodeDecodeError as e:
|
||||
logger.debug("failed to decode #US stream index 0x%08x (%s)", token.rid, e)
|
||||
return None
|
||||
|
||||
if user_string is None:
|
||||
return None
|
||||
|
||||
return user_string.value
|
||||
|
||||
|
||||
def get_dotnet_managed_imports(pe: dnfile.dnPE) -> Iterator[DnType]:
|
||||
"""get managed imports from MemberRef table
|
||||
|
||||
see https://www.ntcore.com/files/dotnetformat.htm
|
||||
|
||||
10 - MemberRef Table
|
||||
Each row represents an imported method
|
||||
Class (index into the TypeRef, ModuleRef, MethodDef, TypeSpec or TypeDef tables)
|
||||
Name (index into String heap)
|
||||
01 - TypeRef Table
|
||||
Each row represents an imported class, its namespace and the assembly which contains it
|
||||
TypeName (index into String heap)
|
||||
TypeNamespace (index into String heap)
|
||||
"""
|
||||
for rid, member_ref in iter_dotnet_table(pe, dnfile.mdtable.MemberRef.number):
|
||||
assert isinstance(member_ref, dnfile.mdtable.MemberRefRow)
|
||||
|
||||
if not isinstance(member_ref.Class.row, dnfile.mdtable.TypeRefRow):
|
||||
# only process class imports from TypeRef table
|
||||
continue
|
||||
|
||||
token: int = calculate_dotnet_token_value(dnfile.mdtable.MemberRef.number, rid)
|
||||
access: Optional[str]
|
||||
|
||||
# assume .NET imports starting with get_/set_ are used to access a property
|
||||
if member_ref.Name.startswith("get_"):
|
||||
access = FeatureAccess.READ
|
||||
elif member_ref.Name.startswith("set_"):
|
||||
access = FeatureAccess.WRITE
|
||||
else:
|
||||
access = None
|
||||
|
||||
member_ref_name: str = member_ref.Name
|
||||
if member_ref_name.startswith(("get_", "set_")):
|
||||
# remove get_/set_ from MemberRef name
|
||||
member_ref_name = member_ref_name[4:]
|
||||
|
||||
yield DnType(
|
||||
token,
|
||||
member_ref.Class.row.TypeName,
|
||||
namespace=member_ref.Class.row.TypeNamespace,
|
||||
member=member_ref_name,
|
||||
access=access,
|
||||
)
|
||||
|
||||
|
||||
def get_dotnet_methoddef_property_accessors(pe: dnfile.dnPE) -> Iterator[Tuple[int, str]]:
|
||||
"""get MethodDef methods used to access properties
|
||||
|
||||
see https://www.ntcore.com/files/dotnetformat.htm
|
||||
|
||||
24 - MethodSemantics Table
|
||||
Links Events and Properties to specific methods. For example one Event can be associated to more methods. A property uses this table to associate get/set methods.
|
||||
Semantics (a 2-byte bitmask of type MethodSemanticsAttributes)
|
||||
Method (index into the MethodDef table)
|
||||
Association (index into the Event or Property table; more precisely, a HasSemantics coded index)
|
||||
"""
|
||||
for rid, method_semantics in iter_dotnet_table(pe, dnfile.mdtable.MethodSemantics.number):
|
||||
assert isinstance(method_semantics, dnfile.mdtable.MethodSemanticsRow)
|
||||
|
||||
if method_semantics.Association.row is None:
|
||||
logger.debug("MethodSemantics[0x%X] Association row is None", rid)
|
||||
continue
|
||||
|
||||
if isinstance(method_semantics.Association.row, dnfile.mdtable.EventRow):
|
||||
# ignore events
|
||||
logger.debug("MethodSemantics[0x%X] ignoring Event", rid)
|
||||
continue
|
||||
|
||||
if method_semantics.Method.table is None:
|
||||
logger.debug("MethodSemantics[0x%X] Method table is None", rid)
|
||||
continue
|
||||
|
||||
token: int = calculate_dotnet_token_value(
|
||||
method_semantics.Method.table.number, method_semantics.Method.row_index
|
||||
)
|
||||
|
||||
if method_semantics.Semantics.msSetter:
|
||||
yield token, FeatureAccess.WRITE
|
||||
elif method_semantics.Semantics.msGetter:
|
||||
yield token, FeatureAccess.READ
|
||||
|
||||
|
||||
def get_dotnet_managed_methods(pe: dnfile.dnPE) -> Iterator[DnType]:
|
||||
"""get managed method names from TypeDef table
|
||||
|
||||
see https://www.ntcore.com/files/dotnetformat.htm
|
||||
|
||||
02 - TypeDef Table
|
||||
Each row represents a class in the current assembly.
|
||||
TypeName (index into String heap)
|
||||
TypeNamespace (index into String heap)
|
||||
MethodList (index into MethodDef table; it marks the first of a contiguous run of Methods owned by this Type)
|
||||
"""
|
||||
accessor_map: Dict[int, str] = {}
|
||||
for methoddef, methoddef_access in get_dotnet_methoddef_property_accessors(pe):
|
||||
accessor_map[methoddef] = methoddef_access
|
||||
|
||||
for rid, typedef in iter_dotnet_table(pe, dnfile.mdtable.TypeDef.number):
|
||||
assert isinstance(typedef, dnfile.mdtable.TypeDefRow)
|
||||
|
||||
for idx, method in enumerate(typedef.MethodList):
|
||||
if method.table is None:
|
||||
logger.debug("TypeDef[0x%X] MethodList[0x%X] table is None", rid, idx)
|
||||
continue
|
||||
if method.row is None:
|
||||
logger.debug("TypeDef[0x%X] MethodList[0x%X] row is None", rid, idx)
|
||||
continue
|
||||
|
||||
token: int = calculate_dotnet_token_value(method.table.number, method.row_index)
|
||||
access: Optional[str] = accessor_map.get(token)
|
||||
|
||||
method_name: str = method.row.Name
|
||||
if method_name.startswith(("get_", "set_")):
|
||||
# remove get_/set_
|
||||
method_name = method_name[4:]
|
||||
|
||||
yield DnType(token, typedef.TypeName, namespace=typedef.TypeNamespace, member=method_name, access=access)
|
||||
|
||||
|
||||
def get_dotnet_fields(pe: dnfile.dnPE) -> Iterator[DnType]:
|
||||
"""get fields from TypeDef table
|
||||
|
||||
see https://www.ntcore.com/files/dotnetformat.htm
|
||||
|
||||
02 - TypeDef Table
|
||||
Each row represents a class in the current assembly.
|
||||
TypeName (index into String heap)
|
||||
TypeNamespace (index into String heap)
|
||||
FieldList (index into Field table; it marks the first of a contiguous run of Fields owned by this Type)
|
||||
"""
|
||||
for rid, typedef in iter_dotnet_table(pe, dnfile.mdtable.TypeDef.number):
|
||||
assert isinstance(typedef, dnfile.mdtable.TypeDefRow)
|
||||
|
||||
for idx, field in enumerate(typedef.FieldList):
|
||||
if field.table is None:
|
||||
logger.debug("TypeDef[0x%X] FieldList[0x%X] table is None", rid, idx)
|
||||
continue
|
||||
if field.row is None:
|
||||
logger.debug("TypeDef[0x%X] FieldList[0x%X] row is None", rid, idx)
|
||||
continue
|
||||
token: int = calculate_dotnet_token_value(field.table.number, field.row_index)
|
||||
yield DnType(token, typedef.TypeName, namespace=typedef.TypeNamespace, member=field.row.Name)
|
||||
|
||||
|
||||
def get_dotnet_managed_method_bodies(pe: dnfile.dnPE) -> Iterator[Tuple[int, CilMethodBody]]:
|
||||
"""get managed methods from MethodDef table"""
|
||||
for rid, method_def in iter_dotnet_table(pe, dnfile.mdtable.MethodDef.number):
|
||||
assert isinstance(method_def, dnfile.mdtable.MethodDefRow)
|
||||
|
||||
if not method_def.ImplFlags.miIL or any((method_def.Flags.mdAbstract, method_def.Flags.mdPinvokeImpl)):
|
||||
# skip methods that do not have a method body
|
||||
continue
|
||||
|
||||
body: Optional[CilMethodBody] = read_dotnet_method_body(pe, method_def)
|
||||
if body is None:
|
||||
logger.debug("MethodDef[0x%X] method body is None", rid)
|
||||
continue
|
||||
|
||||
token: int = calculate_dotnet_token_value(dnfile.mdtable.MethodDef.number, rid)
|
||||
yield token, body
|
||||
|
||||
|
||||
def get_dotnet_unmanaged_imports(pe: dnfile.dnPE) -> Iterator[DnUnmanagedMethod]:
|
||||
"""get unmanaged imports from ImplMap table
|
||||
|
||||
see https://www.ntcore.com/files/dotnetformat.htm
|
||||
|
||||
28 - ImplMap Table
|
||||
ImplMap table holds information about unmanaged methods that can be reached from managed code, using PInvoke dispatch
|
||||
MemberForwarded (index into the Field or MethodDef table; more precisely, a MemberForwarded coded index)
|
||||
ImportName (index into the String heap)
|
||||
ImportScope (index into the ModuleRef table)
|
||||
"""
|
||||
for rid, impl_map in iter_dotnet_table(pe, dnfile.mdtable.ImplMap.number):
|
||||
assert isinstance(impl_map, dnfile.mdtable.ImplMapRow)
|
||||
|
||||
module: str
|
||||
if impl_map.ImportScope.row is None:
|
||||
logger.debug("ImplMap[0x%X] ImportScope row is None", rid)
|
||||
module = ""
|
||||
else:
|
||||
module = impl_map.ImportScope.row.Name
|
||||
method: str = impl_map.ImportName
|
||||
|
||||
member_forward_table: int
|
||||
if impl_map.MemberForwarded.table is None:
|
||||
logger.debug("ImplMap[0x%X] MemberForwarded table is None", rid)
|
||||
continue
|
||||
else:
|
||||
member_forward_table = impl_map.MemberForwarded.table.number
|
||||
member_forward_row: int = impl_map.MemberForwarded.row_index
|
||||
|
||||
# ECMA says "Each row of the ImplMap table associates a row in the MethodDef table (MemberForwarded) with the
|
||||
# name of a routine (ImportName) in some unmanaged DLL (ImportScope)"; so we calculate and map the MemberForwarded
|
||||
# MethodDef table token to help us later record native import method calls made from CIL
|
||||
token: int = calculate_dotnet_token_value(member_forward_table, member_forward_row)
|
||||
|
||||
# like Kernel32.dll
|
||||
if module and "." in module:
|
||||
module = module.split(".")[0]
|
||||
|
||||
# like kernel32.CreateFileA
|
||||
yield DnUnmanagedMethod(token, module, method)
|
||||
|
||||
|
||||
def get_dotnet_types(pe: dnfile.dnPE) -> Iterator[DnType]:
|
||||
"""get .NET types from TypeDef and TypeRef tables"""
|
||||
for rid, typedef in iter_dotnet_table(pe, dnfile.mdtable.TypeDef.number):
|
||||
assert isinstance(typedef, dnfile.mdtable.TypeDefRow)
|
||||
|
||||
typedef_token: int = calculate_dotnet_token_value(dnfile.mdtable.TypeDef.number, rid)
|
||||
yield DnType(typedef_token, typedef.TypeName, namespace=typedef.TypeNamespace)
|
||||
|
||||
for rid, typeref in iter_dotnet_table(pe, dnfile.mdtable.TypeRef.number):
|
||||
assert isinstance(typeref, dnfile.mdtable.TypeRefRow)
|
||||
|
||||
typeref_token: int = calculate_dotnet_token_value(dnfile.mdtable.TypeRef.number, rid)
|
||||
yield DnType(typeref_token, typeref.TypeName, namespace=typeref.TypeNamespace)
|
||||
|
||||
|
||||
def calculate_dotnet_token_value(table: int, rid: int) -> int:
|
||||
return ((table & 0xFF) << Token.TABLE_SHIFT) | (rid & Token.RID_MASK)
|
||||
|
||||
|
||||
def is_dotnet_mixed_mode(pe: dnfile.dnPE) -> bool:
|
||||
assert pe.net is not None
|
||||
assert pe.net.Flags is not None
|
||||
|
||||
return not bool(pe.net.Flags.CLR_ILONLY)
|
||||
|
||||
|
||||
def iter_dotnet_table(pe: dnfile.dnPE, table_index: int) -> Iterator[Tuple[int, dnfile.base.MDTableRow]]:
|
||||
assert pe.net is not None
|
||||
assert pe.net.mdtables is not None
|
||||
|
||||
for rid, row in enumerate(pe.net.mdtables.tables.get(table_index, [])):
|
||||
# .NET tables are 1-indexed
|
||||
yield rid + 1, row
|
||||
227
capa/features/extractors/dnfile/insn.py
Normal file
227
capa/features/extractors/dnfile/insn.py
Normal file
@@ -0,0 +1,227 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Tuple, Union, Iterator, Optional
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from capa.features.extractors.dnfile.extractor import DnFileFeatureExtractorCache
|
||||
|
||||
import dnfile
|
||||
from dncil.clr.token import Token, StringToken, InvalidToken
|
||||
from dncil.cil.opcode import OpCodes
|
||||
|
||||
import capa.features.extractors.helpers
|
||||
from capa.features.insn import API, Number, Property
|
||||
from capa.features.common import Class, String, Feature, Namespace, FeatureAccess, Characteristic
|
||||
from capa.features.address import Address
|
||||
from capa.features.extractors.dnfile.types import DnType, DnUnmanagedMethod
|
||||
from capa.features.extractors.base_extractor import BBHandle, InsnHandle, FunctionHandle
|
||||
from capa.features.extractors.dnfile.helpers import (
|
||||
resolve_dotnet_token,
|
||||
read_dotnet_user_string,
|
||||
calculate_dotnet_token_value,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_callee(
|
||||
pe: dnfile.dnPE, cache: DnFileFeatureExtractorCache, token: Token
|
||||
) -> Optional[Union[DnType, DnUnmanagedMethod]]:
|
||||
"""map .NET token to un/managed (generic) method"""
|
||||
token_: int
|
||||
if token.table == dnfile.mdtable.MethodSpec.number:
|
||||
# map MethodSpec to MethodDef or MemberRef
|
||||
row: Union[dnfile.base.MDTableRow, InvalidToken, str] = resolve_dotnet_token(pe, token)
|
||||
assert isinstance(row, dnfile.mdtable.MethodSpecRow)
|
||||
|
||||
if row.Method.table is None:
|
||||
logger.debug("MethodSpec[0x%X] Method table is None", token.rid)
|
||||
return None
|
||||
|
||||
token_ = calculate_dotnet_token_value(row.Method.table.number, row.Method.row_index)
|
||||
else:
|
||||
token_ = token.value
|
||||
|
||||
callee: Optional[Union[DnType, DnUnmanagedMethod]] = cache.get_import(token_)
|
||||
if callee is None:
|
||||
# we must check unmanaged imports before managed methods because we map forwarded managed methods
|
||||
# to their unmanaged imports; we prefer a forwarded managed method be mapped to its unmanaged import for analysis
|
||||
callee = cache.get_native_import(token_)
|
||||
if callee is None:
|
||||
callee = cache.get_method(token_)
|
||||
return callee
|
||||
|
||||
|
||||
def extract_insn_api_features(fh: FunctionHandle, bh, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""parse instruction API features"""
|
||||
if ih.inner.opcode not in (
|
||||
OpCodes.Call,
|
||||
OpCodes.Callvirt,
|
||||
OpCodes.Jmp,
|
||||
OpCodes.Newobj,
|
||||
):
|
||||
return
|
||||
|
||||
callee: Optional[Union[DnType, DnUnmanagedMethod]] = get_callee(fh.ctx["pe"], fh.ctx["cache"], ih.inner.operand)
|
||||
if isinstance(callee, DnType):
|
||||
# ignore methods used to access properties
|
||||
if callee.access is None:
|
||||
# like System.IO.File::Delete
|
||||
yield API(str(callee)), ih.address
|
||||
elif isinstance(callee, DnUnmanagedMethod):
|
||||
# like kernel32.CreateFileA
|
||||
for name in capa.features.extractors.helpers.generate_symbols(callee.module, callee.method):
|
||||
yield API(name), ih.address
|
||||
|
||||
|
||||
def extract_insn_property_features(fh: FunctionHandle, bh, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""parse instruction property features"""
|
||||
name: Optional[str] = None
|
||||
access: Optional[str] = None
|
||||
|
||||
if ih.inner.opcode in (OpCodes.Call, OpCodes.Callvirt, OpCodes.Jmp):
|
||||
# property access via MethodDef or MemberRef
|
||||
callee: Optional[Union[DnType, DnUnmanagedMethod]] = get_callee(fh.ctx["pe"], fh.ctx["cache"], ih.inner.operand)
|
||||
if isinstance(callee, DnType):
|
||||
if callee.access is not None:
|
||||
name = str(callee)
|
||||
access = callee.access
|
||||
|
||||
elif ih.inner.opcode in (OpCodes.Ldfld, OpCodes.Ldflda, OpCodes.Ldsfld, OpCodes.Ldsflda):
|
||||
# property read via Field
|
||||
read_field: Optional[Union[DnType, DnUnmanagedMethod]] = fh.ctx["cache"].get_field(ih.inner.operand.value)
|
||||
if read_field is not None:
|
||||
name = str(read_field)
|
||||
access = FeatureAccess.READ
|
||||
|
||||
elif ih.inner.opcode in (OpCodes.Stfld, OpCodes.Stsfld):
|
||||
# property write via Field
|
||||
write_field: Optional[Union[DnType, DnUnmanagedMethod]] = fh.ctx["cache"].get_field(ih.inner.operand.value)
|
||||
if write_field is not None:
|
||||
name = str(write_field)
|
||||
access = FeatureAccess.WRITE
|
||||
|
||||
if name is not None:
|
||||
if access is not None:
|
||||
yield Property(name, access=access), ih.address
|
||||
yield Property(name), ih.address
|
||||
|
||||
|
||||
def extract_insn_namespace_class_features(
|
||||
fh: FunctionHandle, bh, ih: InsnHandle
|
||||
) -> Iterator[Tuple[Union[Namespace, Class], Address]]:
|
||||
"""parse instruction namespace and class features"""
|
||||
type_: Optional[Union[DnType, DnUnmanagedMethod]] = None
|
||||
|
||||
if ih.inner.opcode in (
|
||||
OpCodes.Call,
|
||||
OpCodes.Callvirt,
|
||||
OpCodes.Jmp,
|
||||
OpCodes.Ldvirtftn,
|
||||
OpCodes.Ldftn,
|
||||
OpCodes.Newobj,
|
||||
):
|
||||
# method call - includes managed methods (MethodDef, TypeRef) and properties (MethodSemantics, TypeRef)
|
||||
type_ = get_callee(fh.ctx["pe"], fh.ctx["cache"], ih.inner.operand)
|
||||
|
||||
elif ih.inner.opcode in (
|
||||
OpCodes.Ldfld,
|
||||
OpCodes.Ldflda,
|
||||
OpCodes.Ldsfld,
|
||||
OpCodes.Ldsflda,
|
||||
OpCodes.Stfld,
|
||||
OpCodes.Stsfld,
|
||||
):
|
||||
# field access
|
||||
type_ = fh.ctx["cache"].get_field(ih.inner.operand.value)
|
||||
|
||||
# ECMA 335 VI.C.4.10
|
||||
elif ih.inner.opcode in (
|
||||
OpCodes.Initobj,
|
||||
OpCodes.Box,
|
||||
OpCodes.Castclass,
|
||||
OpCodes.Cpobj,
|
||||
OpCodes.Isinst,
|
||||
OpCodes.Ldelem,
|
||||
OpCodes.Ldelema,
|
||||
OpCodes.Ldobj,
|
||||
OpCodes.Mkrefany,
|
||||
OpCodes.Newarr,
|
||||
OpCodes.Refanyval,
|
||||
OpCodes.Sizeof,
|
||||
OpCodes.Stobj,
|
||||
OpCodes.Unbox,
|
||||
OpCodes.Constrained,
|
||||
OpCodes.Stelem,
|
||||
OpCodes.Unbox_Any,
|
||||
):
|
||||
# type access
|
||||
type_ = fh.ctx["cache"].get_type(ih.inner.operand.value)
|
||||
|
||||
if isinstance(type_, DnType):
|
||||
yield Class(DnType.format_name(type_.class_, namespace=type_.namespace)), ih.address
|
||||
if type_.namespace:
|
||||
yield Namespace(type_.namespace), ih.address
|
||||
|
||||
|
||||
def extract_insn_number_features(fh, bh, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""parse instruction number features"""
|
||||
if ih.inner.is_ldc():
|
||||
yield Number(ih.inner.get_ldc()), ih.address
|
||||
|
||||
|
||||
def extract_insn_string_features(fh: FunctionHandle, bh, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""parse instruction string features"""
|
||||
if not ih.inner.is_ldstr():
|
||||
return
|
||||
|
||||
if not isinstance(ih.inner.operand, StringToken):
|
||||
return
|
||||
|
||||
user_string: Optional[str] = read_dotnet_user_string(fh.ctx["pe"], ih.inner.operand)
|
||||
if user_string is None:
|
||||
return
|
||||
|
||||
if len(user_string) >= 4:
|
||||
yield String(user_string), ih.address
|
||||
|
||||
|
||||
def extract_unmanaged_call_characteristic_features(
|
||||
fh: FunctionHandle, bb: BBHandle, ih: InsnHandle
|
||||
) -> Iterator[Tuple[Characteristic, Address]]:
|
||||
if ih.inner.opcode not in (OpCodes.Call, OpCodes.Callvirt, OpCodes.Jmp):
|
||||
return
|
||||
|
||||
row: Union[str, InvalidToken, dnfile.base.MDTableRow] = resolve_dotnet_token(fh.ctx["pe"], ih.inner.operand)
|
||||
if not isinstance(row, dnfile.mdtable.MethodDefRow):
|
||||
return
|
||||
|
||||
if any((row.Flags.mdPinvokeImpl, row.ImplFlags.miUnmanaged, row.ImplFlags.miNative)):
|
||||
yield Characteristic("unmanaged call"), ih.address
|
||||
|
||||
|
||||
def extract_features(fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""extract instruction features"""
|
||||
for inst_handler in INSTRUCTION_HANDLERS:
|
||||
for feature, addr in inst_handler(fh, bbh, ih):
|
||||
assert isinstance(addr, Address)
|
||||
yield feature, addr
|
||||
|
||||
|
||||
INSTRUCTION_HANDLERS = (
|
||||
extract_insn_api_features,
|
||||
extract_insn_property_features,
|
||||
extract_insn_number_features,
|
||||
extract_insn_string_features,
|
||||
extract_insn_namespace_class_features,
|
||||
extract_unmanaged_call_characteristic_features,
|
||||
)
|
||||
74
capa/features/extractors/dnfile/types.py
Normal file
74
capa/features/extractors/dnfile/types.py
Normal file
@@ -0,0 +1,74 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class DnType:
|
||||
def __init__(self, token: int, class_: str, namespace: str = "", member: str = "", access: Optional[str] = None):
|
||||
self.token: int = token
|
||||
self.access: Optional[str] = access
|
||||
self.namespace: str = namespace
|
||||
self.class_: str = class_
|
||||
|
||||
if member == ".ctor":
|
||||
member = "ctor"
|
||||
if member == ".cctor":
|
||||
member = "cctor"
|
||||
|
||||
self.member: str = member
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.token, self.access, self.namespace, self.class_, self.member))
|
||||
|
||||
def __eq__(self, other):
|
||||
return (
|
||||
self.token == other.token
|
||||
and self.access == other.access
|
||||
and self.namespace == other.namespace
|
||||
and self.class_ == other.class_
|
||||
and self.member == other.member
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return DnType.format_name(self.class_, namespace=self.namespace, member=self.member)
|
||||
|
||||
def __repr__(self):
|
||||
return str(self)
|
||||
|
||||
@staticmethod
|
||||
def format_name(class_: str, namespace: str = "", member: str = ""):
|
||||
# like File::OpenRead
|
||||
name: str = f"{class_}::{member}" if member else class_
|
||||
if namespace:
|
||||
# like System.IO.File::OpenRead
|
||||
name = f"{namespace}.{name}"
|
||||
return name
|
||||
|
||||
|
||||
class DnUnmanagedMethod:
|
||||
def __init__(self, token: int, module: str, method: str):
|
||||
self.token: int = token
|
||||
self.module: str = module
|
||||
self.method: str = method
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.token, self.module, self.method))
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.token == other.token and self.module == other.module and self.method == other.method
|
||||
|
||||
def __str__(self):
|
||||
return DnUnmanagedMethod.format_name(self.module, self.method)
|
||||
|
||||
def __repr__(self):
|
||||
return str(self)
|
||||
|
||||
@staticmethod
|
||||
def format_name(module, method):
|
||||
return f"{module}.{method}"
|
||||
239
capa/features/extractors/dotnetfile.py
Normal file
239
capa/features/extractors/dotnetfile.py
Normal file
@@ -0,0 +1,239 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
import logging
|
||||
from typing import Tuple, Iterator
|
||||
from pathlib import Path
|
||||
|
||||
import dnfile
|
||||
import pefile
|
||||
|
||||
import capa.features.extractors.helpers
|
||||
from capa.features.file import Import, FunctionName
|
||||
from capa.features.common import (
|
||||
OS,
|
||||
OS_ANY,
|
||||
ARCH_ANY,
|
||||
ARCH_I386,
|
||||
FORMAT_PE,
|
||||
ARCH_AMD64,
|
||||
FORMAT_DOTNET,
|
||||
Arch,
|
||||
Class,
|
||||
Format,
|
||||
String,
|
||||
Feature,
|
||||
Namespace,
|
||||
Characteristic,
|
||||
)
|
||||
from capa.features.address import NO_ADDRESS, Address, DNTokenAddress
|
||||
from capa.features.extractors.dnfile.types import DnType
|
||||
from capa.features.extractors.base_extractor import SampleHashes, StaticFeatureExtractor
|
||||
from capa.features.extractors.dnfile.helpers import (
|
||||
iter_dotnet_table,
|
||||
is_dotnet_mixed_mode,
|
||||
get_dotnet_managed_imports,
|
||||
get_dotnet_managed_methods,
|
||||
calculate_dotnet_token_value,
|
||||
get_dotnet_unmanaged_imports,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def extract_file_format(**kwargs) -> Iterator[Tuple[Format, Address]]:
|
||||
yield Format(FORMAT_PE), NO_ADDRESS
|
||||
yield Format(FORMAT_DOTNET), NO_ADDRESS
|
||||
|
||||
|
||||
def extract_file_import_names(pe: dnfile.dnPE, **kwargs) -> Iterator[Tuple[Import, Address]]:
|
||||
for method in get_dotnet_managed_imports(pe):
|
||||
# like System.IO.File::OpenRead
|
||||
yield Import(str(method)), DNTokenAddress(method.token)
|
||||
|
||||
for imp in get_dotnet_unmanaged_imports(pe):
|
||||
# like kernel32.CreateFileA
|
||||
for name in capa.features.extractors.helpers.generate_symbols(imp.module, imp.method, include_dll=True):
|
||||
yield Import(name), DNTokenAddress(imp.token)
|
||||
|
||||
|
||||
def extract_file_function_names(pe: dnfile.dnPE, **kwargs) -> Iterator[Tuple[FunctionName, Address]]:
|
||||
for method in get_dotnet_managed_methods(pe):
|
||||
yield FunctionName(str(method)), DNTokenAddress(method.token)
|
||||
|
||||
|
||||
def extract_file_namespace_features(pe: dnfile.dnPE, **kwargs) -> Iterator[Tuple[Namespace, Address]]:
|
||||
"""emit namespace features from TypeRef and TypeDef tables"""
|
||||
|
||||
# namespaces may be referenced multiple times, so we need to filter
|
||||
namespaces = set()
|
||||
|
||||
for _, typedef in iter_dotnet_table(pe, dnfile.mdtable.TypeDef.number):
|
||||
# emit internal .NET namespaces
|
||||
assert isinstance(typedef, dnfile.mdtable.TypeDefRow)
|
||||
namespaces.add(typedef.TypeNamespace)
|
||||
|
||||
for _, typeref in iter_dotnet_table(pe, dnfile.mdtable.TypeRef.number):
|
||||
# emit external .NET namespaces
|
||||
assert isinstance(typeref, dnfile.mdtable.TypeRefRow)
|
||||
namespaces.add(typeref.TypeNamespace)
|
||||
|
||||
# namespaces may be empty, discard
|
||||
namespaces.discard("")
|
||||
|
||||
for namespace in namespaces:
|
||||
# namespace do not have an associated token, so we yield 0x0
|
||||
yield Namespace(namespace), NO_ADDRESS
|
||||
|
||||
|
||||
def extract_file_class_features(pe: dnfile.dnPE, **kwargs) -> Iterator[Tuple[Class, Address]]:
|
||||
"""emit class features from TypeRef and TypeDef tables"""
|
||||
for rid, typedef in iter_dotnet_table(pe, dnfile.mdtable.TypeDef.number):
|
||||
# emit internal .NET classes
|
||||
assert isinstance(typedef, dnfile.mdtable.TypeDefRow)
|
||||
|
||||
token = calculate_dotnet_token_value(dnfile.mdtable.TypeDef.number, rid)
|
||||
yield Class(DnType.format_name(typedef.TypeName, namespace=typedef.TypeNamespace)), DNTokenAddress(token)
|
||||
|
||||
for rid, typeref in iter_dotnet_table(pe, dnfile.mdtable.TypeRef.number):
|
||||
# emit external .NET classes
|
||||
assert isinstance(typeref, dnfile.mdtable.TypeRefRow)
|
||||
|
||||
token = calculate_dotnet_token_value(dnfile.mdtable.TypeRef.number, rid)
|
||||
yield Class(DnType.format_name(typeref.TypeName, namespace=typeref.TypeNamespace)), DNTokenAddress(token)
|
||||
|
||||
|
||||
def extract_file_os(**kwargs) -> Iterator[Tuple[OS, Address]]:
|
||||
yield OS(OS_ANY), NO_ADDRESS
|
||||
|
||||
|
||||
def extract_file_arch(pe: dnfile.dnPE, **kwargs) -> Iterator[Tuple[Arch, Address]]:
|
||||
# to distinguish in more detail, see https://stackoverflow.com/a/23614024/10548020
|
||||
# .NET 4.5 added option: any CPU, 32-bit preferred
|
||||
assert pe.net is not None
|
||||
assert pe.net.Flags is not None
|
||||
|
||||
if pe.net.Flags.CLR_32BITREQUIRED and pe.PE_TYPE == pefile.OPTIONAL_HEADER_MAGIC_PE:
|
||||
yield Arch(ARCH_I386), NO_ADDRESS
|
||||
elif not pe.net.Flags.CLR_32BITREQUIRED and pe.PE_TYPE == pefile.OPTIONAL_HEADER_MAGIC_PE_PLUS:
|
||||
yield Arch(ARCH_AMD64), NO_ADDRESS
|
||||
else:
|
||||
yield Arch(ARCH_ANY), NO_ADDRESS
|
||||
|
||||
|
||||
def extract_file_strings(pe: dnfile.dnPE, **kwargs) -> Iterator[Tuple[String, Address]]:
|
||||
yield from capa.features.extractors.common.extract_file_strings(pe.__data__)
|
||||
|
||||
|
||||
def extract_file_mixed_mode_characteristic_features(
|
||||
pe: dnfile.dnPE, **kwargs
|
||||
) -> Iterator[Tuple[Characteristic, Address]]:
|
||||
if is_dotnet_mixed_mode(pe):
|
||||
yield Characteristic("mixed mode"), NO_ADDRESS
|
||||
|
||||
|
||||
def extract_file_features(pe: dnfile.dnPE) -> Iterator[Tuple[Feature, Address]]:
|
||||
for file_handler in FILE_HANDLERS:
|
||||
for feature, addr in file_handler(pe=pe): # type: ignore
|
||||
yield feature, addr
|
||||
|
||||
|
||||
FILE_HANDLERS = (
|
||||
extract_file_import_names,
|
||||
extract_file_function_names,
|
||||
extract_file_strings,
|
||||
extract_file_format,
|
||||
extract_file_mixed_mode_characteristic_features,
|
||||
extract_file_namespace_features,
|
||||
extract_file_class_features,
|
||||
)
|
||||
|
||||
|
||||
def extract_global_features(pe: dnfile.dnPE) -> Iterator[Tuple[Feature, Address]]:
|
||||
for handler in GLOBAL_HANDLERS:
|
||||
for feature, va in handler(pe=pe): # type: ignore
|
||||
yield feature, va
|
||||
|
||||
|
||||
GLOBAL_HANDLERS = (
|
||||
extract_file_os,
|
||||
extract_file_arch,
|
||||
)
|
||||
|
||||
|
||||
class DotnetFileFeatureExtractor(StaticFeatureExtractor):
|
||||
def __init__(self, path: Path):
|
||||
super().__init__(hashes=SampleHashes.from_bytes(path.read_bytes()))
|
||||
self.path: Path = path
|
||||
self.pe: dnfile.dnPE = dnfile.dnPE(str(path))
|
||||
|
||||
def get_base_address(self):
|
||||
return NO_ADDRESS
|
||||
|
||||
def get_entry_point(self) -> int:
|
||||
# self.pe.net.Flags.CLT_NATIVE_ENTRYPOINT
|
||||
# True: native EP: Token
|
||||
# False: managed EP: RVA
|
||||
assert self.pe.net is not None
|
||||
assert self.pe.net.struct is not None
|
||||
|
||||
return self.pe.net.struct.EntryPointTokenOrRva
|
||||
|
||||
def extract_global_features(self):
|
||||
yield from extract_global_features(self.pe)
|
||||
|
||||
def extract_file_features(self):
|
||||
yield from extract_file_features(self.pe)
|
||||
|
||||
def is_dotnet_file(self) -> bool:
|
||||
return bool(self.pe.net)
|
||||
|
||||
def is_mixed_mode(self) -> bool:
|
||||
return is_dotnet_mixed_mode(self.pe)
|
||||
|
||||
def get_runtime_version(self) -> Tuple[int, int]:
|
||||
assert self.pe.net is not None
|
||||
assert self.pe.net.struct is not None
|
||||
assert self.pe.net.struct.MajorRuntimeVersion is not None
|
||||
assert self.pe.net.struct.MinorRuntimeVersion is not None
|
||||
|
||||
return self.pe.net.struct.MajorRuntimeVersion, self.pe.net.struct.MinorRuntimeVersion
|
||||
|
||||
def get_meta_version_string(self) -> str:
|
||||
assert self.pe.net is not None
|
||||
assert self.pe.net.metadata is not None
|
||||
assert self.pe.net.metadata.struct is not None
|
||||
assert self.pe.net.metadata.struct.Version is not None
|
||||
|
||||
vbuf = self.pe.net.metadata.struct.Version
|
||||
assert isinstance(vbuf, bytes)
|
||||
|
||||
return vbuf.rstrip(b"\x00").decode("utf-8")
|
||||
|
||||
def get_functions(self):
|
||||
raise NotImplementedError("DotnetFileFeatureExtractor can only be used to extract file features")
|
||||
|
||||
def extract_function_features(self, f):
|
||||
raise NotImplementedError("DotnetFileFeatureExtractor can only be used to extract file features")
|
||||
|
||||
def get_basic_blocks(self, f):
|
||||
raise NotImplementedError("DotnetFileFeatureExtractor can only be used to extract file features")
|
||||
|
||||
def extract_basic_block_features(self, f, bb):
|
||||
raise NotImplementedError("DotnetFileFeatureExtractor can only be used to extract file features")
|
||||
|
||||
def get_instructions(self, f, bb):
|
||||
raise NotImplementedError("DotnetFileFeatureExtractor can only be used to extract file features")
|
||||
|
||||
def extract_insn_features(self, f, bb, insn):
|
||||
raise NotImplementedError("DotnetFileFeatureExtractor can only be used to extract file features")
|
||||
|
||||
def is_library_function(self, va):
|
||||
raise NotImplementedError("DotnetFileFeatureExtractor can only be used to extract file features")
|
||||
|
||||
def get_function_name(self, va):
|
||||
raise NotImplementedError("DotnetFileFeatureExtractor can only be used to extract file features")
|
||||
985
capa/features/extractors/elf.py
Normal file
985
capa/features/extractors/elf.py
Normal file
@@ -0,0 +1,985 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
import struct
|
||||
import logging
|
||||
import itertools
|
||||
import collections
|
||||
from enum import Enum
|
||||
from typing import Set, Dict, List, Tuple, BinaryIO, Iterator, Optional
|
||||
from dataclasses import dataclass
|
||||
|
||||
import Elf # from vivisect
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def align(v, alignment):
|
||||
remainder = v % alignment
|
||||
if remainder == 0:
|
||||
return v
|
||||
else:
|
||||
return v + (alignment - remainder)
|
||||
|
||||
|
||||
def read_cstr(buf, offset) -> str:
|
||||
s = buf[offset:]
|
||||
s, _, _ = s.partition(b"\x00")
|
||||
return s.decode("utf-8")
|
||||
|
||||
|
||||
class CorruptElfFile(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class OS(str, Enum):
|
||||
HPUX = "hpux"
|
||||
NETBSD = "netbsd"
|
||||
LINUX = "linux"
|
||||
HURD = "hurd"
|
||||
_86OPEN = "86open"
|
||||
SOLARIS = "solaris"
|
||||
AIX = "aix"
|
||||
IRIX = "irix"
|
||||
FREEBSD = "freebsd"
|
||||
TRU64 = "tru64"
|
||||
MODESTO = "modesto"
|
||||
OPENBSD = "openbsd"
|
||||
OPENVMS = "openvms"
|
||||
NSK = "nsk"
|
||||
AROS = "aros"
|
||||
FENIXOS = "fenixos"
|
||||
CLOUD = "cloud"
|
||||
SYLLABLE = "syllable"
|
||||
NACL = "nacl"
|
||||
ANDROID = "android"
|
||||
|
||||
|
||||
# via readelf: https://github.com/bminor/binutils-gdb/blob/c0e94211e1ac05049a4ce7c192c9d14d1764eb3e/binutils/readelf.c#L19635-L19658
|
||||
# and here: https://github.com/bminor/binutils-gdb/blob/34c54daa337da9fadf87d2706d6a590ae1f88f4d/include/elf/common.h#L933-L939
|
||||
GNU_ABI_TAG = {
|
||||
0: OS.LINUX,
|
||||
1: OS.HURD,
|
||||
2: OS.SOLARIS,
|
||||
3: OS.FREEBSD,
|
||||
4: OS.NETBSD,
|
||||
5: OS.SYLLABLE,
|
||||
6: OS.NACL,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class Phdr:
|
||||
type: int
|
||||
offset: int
|
||||
vaddr: int
|
||||
paddr: int
|
||||
filesz: int
|
||||
buf: bytes
|
||||
|
||||
|
||||
@dataclass
|
||||
class Shdr:
|
||||
name: int
|
||||
type: int
|
||||
flags: int
|
||||
addr: int
|
||||
offset: int
|
||||
size: int
|
||||
link: int
|
||||
entsize: int
|
||||
buf: bytes
|
||||
|
||||
@classmethod
|
||||
def from_viv(cls, section, buf: bytes) -> "Shdr":
|
||||
return cls(
|
||||
section.sh_name,
|
||||
section.sh_type,
|
||||
section.sh_flags,
|
||||
section.sh_addr,
|
||||
section.sh_offset,
|
||||
section.sh_size,
|
||||
section.sh_link,
|
||||
section.sh_entsize,
|
||||
buf,
|
||||
)
|
||||
|
||||
|
||||
class ELF:
|
||||
def __init__(self, f: BinaryIO):
|
||||
self.f = f
|
||||
|
||||
# these will all be initialized in `_parse()`
|
||||
self.bitness: int
|
||||
self.endian: str
|
||||
self.e_phentsize: int
|
||||
self.e_phnum: int
|
||||
self.e_shentsize: int
|
||||
self.e_shnum: int
|
||||
self.phbuf: bytes
|
||||
self.shbuf: bytes
|
||||
|
||||
self._parse()
|
||||
|
||||
def _parse(self):
|
||||
self.f.seek(0x0)
|
||||
self.file_header = self.f.read(0x40)
|
||||
|
||||
if not self.file_header.startswith(b"\x7fELF"):
|
||||
raise CorruptElfFile("missing magic header")
|
||||
|
||||
ei_class, ei_data = struct.unpack_from("BB", self.file_header, 4)
|
||||
logger.debug("ei_class: 0x%02x ei_data: 0x%02x", ei_class, ei_data)
|
||||
if ei_class == 1:
|
||||
self.bitness = 32
|
||||
elif ei_class == 2:
|
||||
self.bitness = 64
|
||||
else:
|
||||
raise CorruptElfFile(f"invalid ei_class: 0x{ei_class:02x}")
|
||||
|
||||
if ei_data == 1:
|
||||
self.endian = "<"
|
||||
elif ei_data == 2:
|
||||
self.endian = ">"
|
||||
else:
|
||||
raise CorruptElfFile(f"not an ELF file: invalid ei_data: 0x{ei_data:02x}")
|
||||
|
||||
if self.bitness == 32:
|
||||
e_phoff, e_shoff = struct.unpack_from(self.endian + "II", self.file_header, 0x1C)
|
||||
self.e_phentsize, self.e_phnum = struct.unpack_from(self.endian + "HH", self.file_header, 0x2A)
|
||||
self.e_shentsize, self.e_shnum = struct.unpack_from(self.endian + "HH", self.file_header, 0x2E)
|
||||
elif self.bitness == 64:
|
||||
e_phoff, e_shoff = struct.unpack_from(self.endian + "QQ", self.file_header, 0x20)
|
||||
self.e_phentsize, self.e_phnum = struct.unpack_from(self.endian + "HH", self.file_header, 0x36)
|
||||
self.e_shentsize, self.e_shnum = struct.unpack_from(self.endian + "HH", self.file_header, 0x3A)
|
||||
else:
|
||||
raise NotImplementedError()
|
||||
|
||||
logger.debug("e_phoff: 0x%02x e_phentsize: 0x%02x e_phnum: %d", e_phoff, self.e_phentsize, self.e_phnum)
|
||||
|
||||
self.f.seek(e_phoff)
|
||||
program_header_size = self.e_phnum * self.e_phentsize
|
||||
self.phbuf = self.f.read(program_header_size)
|
||||
if len(self.phbuf) != program_header_size:
|
||||
logger.warning("failed to read program headers")
|
||||
self.e_phnum = 0
|
||||
|
||||
self.f.seek(e_shoff)
|
||||
section_header_size = self.e_shnum * self.e_shentsize
|
||||
self.shbuf = self.f.read(section_header_size)
|
||||
if len(self.shbuf) != section_header_size:
|
||||
logger.warning("failed to read section headers")
|
||||
self.e_shnum = 0
|
||||
|
||||
OSABI = {
|
||||
# via pyelftools: https://github.com/eliben/pyelftools/blob/0664de05ed2db3d39041e2d51d19622a8ef4fb0f/elftools/elf/enums.py#L35-L58
|
||||
# some candidates are commented out because the are not useful values,
|
||||
# at least when guessing OSes
|
||||
# 0: "SYSV", # too often used when OS is not SYSV
|
||||
1: OS.HPUX,
|
||||
2: OS.NETBSD,
|
||||
3: OS.LINUX,
|
||||
4: OS.HURD,
|
||||
5: OS._86OPEN,
|
||||
6: OS.SOLARIS,
|
||||
7: OS.AIX,
|
||||
8: OS.IRIX,
|
||||
9: OS.FREEBSD,
|
||||
10: OS.TRU64,
|
||||
11: OS.MODESTO,
|
||||
12: OS.OPENBSD,
|
||||
13: OS.OPENVMS,
|
||||
14: OS.NSK,
|
||||
15: OS.AROS,
|
||||
16: OS.FENIXOS,
|
||||
17: OS.CLOUD,
|
||||
# 53: "SORTFIX", # i can't find any reference to this OS, i dont think it exists
|
||||
# 64: "ARM_AEABI", # not an OS
|
||||
# 97: "ARM", # not an OS
|
||||
# 255: "STANDALONE", # not an OS
|
||||
}
|
||||
|
||||
@property
|
||||
def ei_osabi(self) -> Optional[OS]:
|
||||
(ei_osabi,) = struct.unpack_from(self.endian + "B", self.file_header, 7)
|
||||
return ELF.OSABI.get(ei_osabi)
|
||||
|
||||
MACHINE = {
|
||||
# via https://refspecs.linuxfoundation.org/elf/gabi4+/ch4.eheader.html
|
||||
1: "M32",
|
||||
2: "SPARC",
|
||||
3: "i386",
|
||||
4: "68K",
|
||||
5: "88K",
|
||||
6: "486",
|
||||
7: "860",
|
||||
8: "MIPS",
|
||||
9: "S370",
|
||||
10: "MIPS_RS3_LE",
|
||||
11: "RS6000",
|
||||
15: "PA_RISC",
|
||||
16: "nCUBE",
|
||||
17: "VPP500",
|
||||
18: "SPARC32PLUS",
|
||||
19: "960",
|
||||
20: "PPC",
|
||||
21: "PPC64",
|
||||
22: "S390",
|
||||
23: "SPU",
|
||||
36: "V800",
|
||||
37: "FR20",
|
||||
38: "RH32",
|
||||
39: "RCE",
|
||||
40: "ARM",
|
||||
41: "ALPHA",
|
||||
42: "SH",
|
||||
43: "SPARCV9",
|
||||
44: "TRICORE",
|
||||
45: "ARC",
|
||||
46: "H8_300",
|
||||
47: "H8_300H",
|
||||
48: "H8S",
|
||||
49: "H8_500",
|
||||
50: "IA_64",
|
||||
51: "MIPS_X",
|
||||
52: "COLDFIRE",
|
||||
53: "68HC12",
|
||||
54: "MMA",
|
||||
55: "PCP",
|
||||
56: "NCPU",
|
||||
57: "NDR1",
|
||||
58: "STARCORE",
|
||||
59: "ME16",
|
||||
60: "ST100",
|
||||
61: "TINYJ",
|
||||
62: "amd64",
|
||||
63: "PDSP",
|
||||
64: "PDP10",
|
||||
65: "PDP11",
|
||||
66: "FX66",
|
||||
67: "ST9PLUS",
|
||||
68: "ST7",
|
||||
69: "68HC16",
|
||||
70: "68HC11",
|
||||
71: "68HC08",
|
||||
72: "68HC05",
|
||||
73: "SVX",
|
||||
74: "ST19",
|
||||
75: "VAX",
|
||||
76: "CRIS",
|
||||
77: "JAVELIN",
|
||||
78: "FIREPATH",
|
||||
79: "ZSP",
|
||||
80: "MMIX",
|
||||
81: "HUANY",
|
||||
82: "PRISM",
|
||||
83: "AVR",
|
||||
84: "FR30",
|
||||
85: "D10V",
|
||||
86: "D30V",
|
||||
87: "V850",
|
||||
88: "M32R",
|
||||
89: "MN10300",
|
||||
90: "MN10200",
|
||||
91: "PJ",
|
||||
92: "OPENRISC",
|
||||
93: "ARC_A5",
|
||||
94: "XTENSA",
|
||||
95: "VIDEOCORE",
|
||||
96: "TMM_GPP",
|
||||
97: "NS32K",
|
||||
98: "TPC",
|
||||
99: "SNP1K",
|
||||
100: "ST200",
|
||||
}
|
||||
|
||||
@property
|
||||
def e_machine(self) -> Optional[str]:
|
||||
(e_machine,) = struct.unpack_from(self.endian + "H", self.file_header, 0x12)
|
||||
return ELF.MACHINE.get(e_machine)
|
||||
|
||||
def parse_program_header(self, i) -> Phdr:
|
||||
phent_offset = i * self.e_phentsize
|
||||
phent = self.phbuf[phent_offset : phent_offset + self.e_phentsize]
|
||||
|
||||
(p_type,) = struct.unpack_from(self.endian + "I", phent, 0x0)
|
||||
logger.debug("ph:p_type: 0x%04x", p_type)
|
||||
|
||||
if self.bitness == 32:
|
||||
p_offset, p_vaddr, p_paddr, p_filesz = struct.unpack_from(self.endian + "IIII", phent, 0x4)
|
||||
elif self.bitness == 64:
|
||||
p_offset, p_vaddr, p_paddr, p_filesz = struct.unpack_from(self.endian + "QQQQ", phent, 0x8)
|
||||
else:
|
||||
raise NotImplementedError()
|
||||
|
||||
logger.debug("ph:p_offset: 0x%02x p_filesz: 0x%04x", p_offset, p_filesz)
|
||||
|
||||
self.f.seek(p_offset)
|
||||
buf = self.f.read(p_filesz)
|
||||
if len(buf) != p_filesz:
|
||||
raise ValueError("failed to read program header content")
|
||||
|
||||
return Phdr(p_type, p_offset, p_vaddr, p_paddr, p_filesz, buf)
|
||||
|
||||
@property
|
||||
def program_headers(self):
|
||||
for i in range(self.e_phnum):
|
||||
try:
|
||||
yield self.parse_program_header(i)
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
def parse_section_header(self, i) -> Shdr:
|
||||
shent_offset = i * self.e_shentsize
|
||||
shent = self.shbuf[shent_offset : shent_offset + self.e_shentsize]
|
||||
|
||||
if self.bitness == 32:
|
||||
sh_name, sh_type, sh_flags, sh_addr, sh_offset, sh_size, sh_link, _, _, sh_entsize = struct.unpack_from(
|
||||
self.endian + "IIIIIIIIII", shent, 0x0
|
||||
)
|
||||
elif self.bitness == 64:
|
||||
sh_name, sh_type, sh_flags, sh_addr, sh_offset, sh_size, sh_link, _, _, sh_entsize = struct.unpack_from(
|
||||
self.endian + "IIQQQQIIQQ", shent, 0x0
|
||||
)
|
||||
else:
|
||||
raise NotImplementedError()
|
||||
|
||||
logger.debug("sh:sh_offset: 0x%02x sh_size: 0x%04x", sh_offset, sh_size)
|
||||
|
||||
self.f.seek(sh_offset)
|
||||
buf = self.f.read(sh_size)
|
||||
if len(buf) != sh_size:
|
||||
raise ValueError("failed to read section header content")
|
||||
|
||||
return Shdr(sh_name, sh_type, sh_flags, sh_addr, sh_offset, sh_size, sh_link, sh_entsize, buf)
|
||||
|
||||
@property
|
||||
def section_headers(self):
|
||||
for i in range(self.e_shnum):
|
||||
try:
|
||||
yield self.parse_section_header(i)
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
@property
|
||||
def linker(self):
|
||||
PT_INTERP = 0x3
|
||||
for phdr in self.program_headers:
|
||||
if phdr.type != PT_INTERP:
|
||||
continue
|
||||
|
||||
return read_cstr(phdr.buf, 0)
|
||||
|
||||
@property
|
||||
def versions_needed(self) -> Dict[str, Set[str]]:
|
||||
# symbol version requirements are stored in the .gnu.version_r section,
|
||||
# which has type SHT_GNU_verneed (0x6ffffffe).
|
||||
#
|
||||
# this contains a linked list of ElfXX_Verneed structs,
|
||||
# each referencing a linked list of ElfXX_Vernaux structs.
|
||||
# strings are stored in the section referenced by the sh_link field of the section header.
|
||||
# each Verneed struct contains a reference to the name of the library,
|
||||
# each Vernaux struct contains a reference to the name of a symbol.
|
||||
SHT_GNU_VERNEED = 0x6FFFFFFE
|
||||
for shdr in self.section_headers:
|
||||
if shdr.type != SHT_GNU_VERNEED:
|
||||
continue
|
||||
|
||||
# the linked section contains strings referenced by the verneed structures.
|
||||
linked_shdr = self.parse_section_header(shdr.link)
|
||||
|
||||
versions_needed = collections.defaultdict(set)
|
||||
|
||||
# read verneed structures from the start of the section
|
||||
# until the vn_next link is 0x0.
|
||||
# each entry describes a shared object that is required by this binary.
|
||||
vn_offset = 0x0
|
||||
while True:
|
||||
# ElfXX_Verneed layout is the same on 32 and 64 bit
|
||||
vn_version, vn_cnt, vn_file, vn_aux, vn_next = struct.unpack_from(
|
||||
self.endian + "HHIII", shdr.buf, vn_offset
|
||||
)
|
||||
if vn_version != 1:
|
||||
# unexpected format, don't try to keep parsing
|
||||
break
|
||||
|
||||
# shared object names, like: "libdl.so.2"
|
||||
so_name = read_cstr(linked_shdr.buf, vn_file)
|
||||
|
||||
# read vernaux structures linked from the verneed structure.
|
||||
# there should be vn_cnt of these.
|
||||
# each entry describes an ABI name required by the shared object.
|
||||
vna_offset = vn_offset + vn_aux
|
||||
for _ in range(vn_cnt):
|
||||
# ElfXX_Vernaux layout is the same on 32 and 64 bit
|
||||
_, _, _, vna_name, vna_next = struct.unpack_from(self.endian + "IHHII", shdr.buf, vna_offset)
|
||||
|
||||
# ABI names, like: "GLIBC_2.2.5"
|
||||
abi = read_cstr(linked_shdr.buf, vna_name)
|
||||
versions_needed[so_name].add(abi)
|
||||
|
||||
vna_offset += vna_next
|
||||
|
||||
vn_offset += vn_next
|
||||
if vn_next == 0:
|
||||
break
|
||||
|
||||
return dict(versions_needed)
|
||||
|
||||
return {}
|
||||
|
||||
@property
|
||||
def dynamic_entries(self) -> Iterator[Tuple[int, int]]:
|
||||
"""
|
||||
read the entries from the dynamic section,
|
||||
yielding the tag and value for each entry.
|
||||
"""
|
||||
DT_NULL = 0x0
|
||||
PT_DYNAMIC = 0x2
|
||||
for phdr in self.program_headers:
|
||||
if phdr.type != PT_DYNAMIC:
|
||||
continue
|
||||
|
||||
offset = 0x0
|
||||
while True:
|
||||
if self.bitness == 32:
|
||||
d_tag, d_val = struct.unpack_from(self.endian + "II", phdr.buf, offset)
|
||||
offset += 8
|
||||
elif self.bitness == 64:
|
||||
d_tag, d_val = struct.unpack_from(self.endian + "QQ", phdr.buf, offset)
|
||||
offset += 16
|
||||
else:
|
||||
raise NotImplementedError()
|
||||
|
||||
if d_tag == DT_NULL:
|
||||
break
|
||||
|
||||
yield d_tag, d_val
|
||||
|
||||
@property
|
||||
def strtab(self) -> Optional[bytes]:
|
||||
"""
|
||||
fetch the bytes of the string table
|
||||
referenced by the dynamic section.
|
||||
"""
|
||||
DT_STRTAB = 0x5
|
||||
DT_STRSZ = 0xA
|
||||
|
||||
strtab_addr = None
|
||||
strtab_size = None
|
||||
|
||||
for d_tag, d_val in self.dynamic_entries:
|
||||
if d_tag == DT_STRTAB:
|
||||
strtab_addr = d_val
|
||||
break
|
||||
|
||||
for d_tag, d_val in self.dynamic_entries:
|
||||
if d_tag == DT_STRSZ:
|
||||
strtab_size = d_val
|
||||
break
|
||||
|
||||
if strtab_addr is None:
|
||||
return None
|
||||
|
||||
if strtab_size is None:
|
||||
return None
|
||||
|
||||
strtab_offset = None
|
||||
for shdr in self.section_headers:
|
||||
# the section header address should be defined
|
||||
if shdr.addr and shdr.addr <= strtab_addr < shdr.addr + shdr.size:
|
||||
strtab_offset = shdr.offset + (strtab_addr - shdr.addr)
|
||||
break
|
||||
|
||||
if strtab_offset is None:
|
||||
return None
|
||||
|
||||
self.f.seek(strtab_offset)
|
||||
strtab_buf = self.f.read(strtab_size)
|
||||
|
||||
if len(strtab_buf) != strtab_size:
|
||||
return None
|
||||
|
||||
return strtab_buf
|
||||
|
||||
@property
|
||||
def needed(self) -> Iterator[str]:
|
||||
"""
|
||||
read the names of DT_NEEDED entries from the dynamic section,
|
||||
which correspond to dependencies on other shared objects,
|
||||
like: `libpthread.so.0`
|
||||
"""
|
||||
DT_NEEDED = 0x1
|
||||
strtab = self.strtab
|
||||
if not strtab:
|
||||
return
|
||||
|
||||
for d_tag, d_val in self.dynamic_entries:
|
||||
if d_tag != DT_NEEDED:
|
||||
continue
|
||||
|
||||
try:
|
||||
yield read_cstr(strtab, d_val)
|
||||
except UnicodeDecodeError as e:
|
||||
logger.warning("failed to read DT_NEEDED entry: %s", str(e))
|
||||
|
||||
@property
|
||||
def symtab(self) -> Optional[Tuple[Shdr, Shdr]]:
|
||||
"""
|
||||
fetch the Shdr for the symtab and the associated strtab.
|
||||
"""
|
||||
SHT_SYMTAB = 0x2
|
||||
for shdr in self.section_headers:
|
||||
if shdr.type != SHT_SYMTAB:
|
||||
continue
|
||||
|
||||
# the linked section contains strings referenced by the symtab structures.
|
||||
strtab_shdr = self.parse_section_header(shdr.link)
|
||||
|
||||
return shdr, strtab_shdr
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ABITag:
|
||||
os: OS
|
||||
kmajor: int
|
||||
kminor: int
|
||||
kpatch: int
|
||||
|
||||
|
||||
class PHNote:
|
||||
def __init__(self, endian: str, buf: bytes):
|
||||
self.endian = endian
|
||||
self.buf = buf
|
||||
|
||||
# these will be initialized in `_parse()`
|
||||
self.type_: int
|
||||
self.descsz: int
|
||||
self.name: str
|
||||
|
||||
self._parse()
|
||||
|
||||
def _parse(self):
|
||||
namesz, self.descsz, self.type_ = struct.unpack_from(self.endian + "III", self.buf, 0x0)
|
||||
name_offset = 0xC
|
||||
self.desc_offset = name_offset + align(namesz, 0x4)
|
||||
|
||||
logger.debug("ph:namesz: 0x%02x descsz: 0x%02x type: 0x%04x", namesz, self.descsz, self.type_)
|
||||
|
||||
self.name = self.buf[name_offset : name_offset + namesz].partition(b"\x00")[0].decode("ascii")
|
||||
logger.debug("name: %s", self.name)
|
||||
|
||||
@property
|
||||
def abi_tag(self) -> Optional[ABITag]:
|
||||
if self.type_ != 1:
|
||||
# > The type field shall be 1.
|
||||
# Linux Standard Base Specification 1.2
|
||||
# ref: https://refspecs.linuxfoundation.org/LSB_1.2.0/gLSB/noteabitag.html
|
||||
return None
|
||||
|
||||
if self.name != "GNU":
|
||||
return None
|
||||
|
||||
if self.descsz < 16:
|
||||
return None
|
||||
|
||||
desc = self.buf[self.desc_offset : self.desc_offset + self.descsz]
|
||||
abi_tag, kmajor, kminor, kpatch = struct.unpack_from(self.endian + "IIII", desc, 0x0)
|
||||
logger.debug("GNU_ABI_TAG: 0x%02x", abi_tag)
|
||||
|
||||
os = GNU_ABI_TAG.get(abi_tag)
|
||||
if not os:
|
||||
return None
|
||||
|
||||
logger.debug("abi tag: %s earliest compatible kernel: %d.%d.%d", os, kmajor, kminor, kpatch)
|
||||
|
||||
return ABITag(os, kmajor, kminor, kpatch)
|
||||
|
||||
|
||||
class SHNote:
|
||||
def __init__(self, endian: str, buf: bytes):
|
||||
self.endian = endian
|
||||
self.buf = buf
|
||||
|
||||
# these will be initialized in `_parse()`
|
||||
self.type_: int
|
||||
self.descsz: int
|
||||
self.name: str
|
||||
|
||||
self._parse()
|
||||
|
||||
def _parse(self):
|
||||
namesz, self.descsz, self.type_ = struct.unpack_from(self.endian + "III", self.buf, 0x0)
|
||||
name_offset = 0xC
|
||||
self.desc_offset = name_offset + align(namesz, 0x4)
|
||||
|
||||
logger.debug("sh:namesz: 0x%02x descsz: 0x%02x type: 0x%04x", namesz, self.descsz, self.type_)
|
||||
|
||||
name_buf = self.buf[name_offset : name_offset + namesz]
|
||||
self.name = read_cstr(name_buf, 0x0)
|
||||
logger.debug("sh:name: %s", self.name)
|
||||
|
||||
@property
|
||||
def abi_tag(self) -> Optional[ABITag]:
|
||||
if self.name != "GNU":
|
||||
return None
|
||||
|
||||
if self.descsz < 16:
|
||||
return None
|
||||
|
||||
desc = self.buf[self.desc_offset : self.desc_offset + self.descsz]
|
||||
abi_tag, kmajor, kminor, kpatch = struct.unpack_from(self.endian + "IIII", desc, 0x0)
|
||||
logger.debug("GNU_ABI_TAG: 0x%02x", abi_tag)
|
||||
|
||||
os = GNU_ABI_TAG.get(abi_tag)
|
||||
if not os:
|
||||
return None
|
||||
|
||||
logger.debug("abi tag: %s earliest compatible kernel: %d.%d.%d", os, kmajor, kminor, kpatch)
|
||||
return ABITag(os, kmajor, kminor, kpatch)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Symbol:
|
||||
name_offset: int
|
||||
value: int
|
||||
size: int
|
||||
info: int
|
||||
other: int
|
||||
shndx: int
|
||||
|
||||
|
||||
class SymTab:
|
||||
def __init__(
|
||||
self,
|
||||
endian: str,
|
||||
bitness: int,
|
||||
symtab: Shdr,
|
||||
strtab: Shdr,
|
||||
) -> None:
|
||||
self.symbols: List[Symbol] = []
|
||||
|
||||
self.symtab = symtab
|
||||
self.strtab = strtab
|
||||
|
||||
self._parse(endian, bitness, symtab.buf)
|
||||
|
||||
def _parse(self, endian: str, bitness: int, symtab_buf: bytes) -> None:
|
||||
"""
|
||||
return the symbol's information in
|
||||
the order specified by sys/elf32.h
|
||||
"""
|
||||
if self.symtab.entsize == 0:
|
||||
return
|
||||
|
||||
for i in range(int(len(self.symtab.buf) / self.symtab.entsize)):
|
||||
if bitness == 32:
|
||||
name_offset, value, size, info, other, shndx = struct.unpack_from(
|
||||
endian + "IIIBBH", symtab_buf, i * self.symtab.entsize
|
||||
)
|
||||
elif bitness == 64:
|
||||
name_offset, info, other, shndx, value, size = struct.unpack_from(
|
||||
endian + "IBBHQQ", symtab_buf, i * self.symtab.entsize
|
||||
)
|
||||
|
||||
self.symbols.append(Symbol(name_offset, value, size, info, other, shndx))
|
||||
|
||||
def get_name(self, symbol: Symbol) -> str:
|
||||
"""
|
||||
fetch a symbol's name from symtab's
|
||||
associated strings' section (SHT_STRTAB)
|
||||
"""
|
||||
if not self.strtab:
|
||||
raise ValueError("no strings found")
|
||||
|
||||
for i in range(symbol.name_offset, self.strtab.size):
|
||||
if self.strtab.buf[i] == 0:
|
||||
return self.strtab.buf[symbol.name_offset : i].decode("utf-8")
|
||||
|
||||
raise ValueError("symbol name not found")
|
||||
|
||||
def get_symbols(self) -> Iterator[Symbol]:
|
||||
"""
|
||||
return a tuple: (name, value, size, info, other, shndx)
|
||||
for each symbol contained in the symbol table
|
||||
"""
|
||||
yield from self.symbols
|
||||
|
||||
@classmethod
|
||||
def from_viv(cls, elf: Elf.Elf) -> Optional["SymTab"]:
|
||||
endian = "<" if elf.getEndian() == 0 else ">"
|
||||
bitness = elf.bits
|
||||
|
||||
SHT_SYMTAB = 0x2
|
||||
for section in elf.sections:
|
||||
if section.sh_type == SHT_SYMTAB:
|
||||
strtab_section = elf.sections[section.sh_link]
|
||||
sh_symtab = Shdr.from_viv(section, elf.readAtOffset(section.sh_offset, section.sh_size))
|
||||
sh_strtab = Shdr.from_viv(
|
||||
strtab_section, elf.readAtOffset(strtab_section.sh_offset, strtab_section.sh_size)
|
||||
)
|
||||
|
||||
try:
|
||||
return cls(endian, bitness, sh_symtab, sh_strtab)
|
||||
except NameError:
|
||||
return None
|
||||
except Exception:
|
||||
# all exceptions that could be encountered by
|
||||
# cls._parse() imply a faulty symbol's table.
|
||||
raise CorruptElfFile("malformed symbol's table")
|
||||
|
||||
|
||||
def guess_os_from_osabi(elf: ELF) -> Optional[OS]:
|
||||
return elf.ei_osabi
|
||||
|
||||
|
||||
def guess_os_from_ph_notes(elf: ELF) -> Optional[OS]:
|
||||
# search for PT_NOTE sections that specify an OS
|
||||
# for example, on Linux there is a GNU section with minimum kernel version
|
||||
PT_NOTE = 0x4
|
||||
for phdr in elf.program_headers:
|
||||
if phdr.type != PT_NOTE:
|
||||
continue
|
||||
|
||||
note = PHNote(elf.endian, phdr.buf)
|
||||
|
||||
if note.type_ != 1:
|
||||
# > The type field shall be 1.
|
||||
# Linux Standard Base Specification 1.2
|
||||
# ref: https://refspecs.linuxfoundation.org/LSB_1.2.0/gLSB/noteabitag.html
|
||||
continue
|
||||
|
||||
if note.name == "Linux":
|
||||
logger.debug("note owner: %s", "LINUX")
|
||||
return OS.LINUX
|
||||
elif note.name == "OpenBSD":
|
||||
logger.debug("note owner: %s", "OPENBSD")
|
||||
return OS.OPENBSD
|
||||
elif note.name == "NetBSD":
|
||||
logger.debug("note owner: %s", "NETBSD")
|
||||
return OS.NETBSD
|
||||
elif note.name == "FreeBSD":
|
||||
logger.debug("note owner: %s", "FREEBSD")
|
||||
return OS.FREEBSD
|
||||
elif note.name == "Android":
|
||||
logger.debug("note owner: %s", "Android")
|
||||
# see the following for parsing the structure:
|
||||
# https://android.googlesource.com/platform/ndk/+/master/parse_elfnote.py
|
||||
return OS.ANDROID
|
||||
elif note.name == "GNU":
|
||||
abi_tag = note.abi_tag
|
||||
if abi_tag:
|
||||
return abi_tag.os
|
||||
else:
|
||||
# cannot make a guess about the OS, but probably linux or hurd
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def guess_os_from_sh_notes(elf: ELF) -> Optional[OS]:
|
||||
# search for notes stored in sections that aren't visible in program headers.
|
||||
# e.g. .note.Linux in Linux kernel modules.
|
||||
SHT_NOTE = 0x7
|
||||
for shdr in elf.section_headers:
|
||||
if shdr.type != SHT_NOTE:
|
||||
continue
|
||||
|
||||
note = SHNote(elf.endian, shdr.buf)
|
||||
|
||||
if note.name == "Linux":
|
||||
logger.debug("note owner: %s", "LINUX")
|
||||
return OS.LINUX
|
||||
elif note.name == "OpenBSD":
|
||||
logger.debug("note owner: %s", "OPENBSD")
|
||||
return OS.OPENBSD
|
||||
elif note.name == "NetBSD":
|
||||
logger.debug("note owner: %s", "NETBSD")
|
||||
return OS.NETBSD
|
||||
elif note.name == "FreeBSD":
|
||||
logger.debug("note owner: %s", "FREEBSD")
|
||||
return OS.FREEBSD
|
||||
elif note.name == "GNU":
|
||||
abi_tag = note.abi_tag
|
||||
if abi_tag:
|
||||
return abi_tag.os
|
||||
else:
|
||||
# cannot make a guess about the OS, but probably linux or hurd
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def guess_os_from_linker(elf: ELF) -> Optional[OS]:
|
||||
# search for recognizable dynamic linkers (interpreters)
|
||||
# for example, on linux, we see file paths like: /lib64/ld-linux-x86-64.so.2
|
||||
linker = elf.linker
|
||||
if linker and "ld-linux" in elf.linker:
|
||||
return OS.LINUX
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def guess_os_from_abi_versions_needed(elf: ELF) -> Optional[OS]:
|
||||
# then lets look for GLIBC symbol versioning requirements.
|
||||
# this will let us guess about linux/hurd in some cases.
|
||||
|
||||
versions_needed = elf.versions_needed
|
||||
if any(abi.startswith("GLIBC") for abi in itertools.chain(*versions_needed.values())):
|
||||
# there are any GLIBC versions needed
|
||||
|
||||
if elf.e_machine != "i386":
|
||||
# GLIBC runs on Linux and Hurd.
|
||||
# for Hurd, its *only* on i386.
|
||||
# so if we're not on i386, then we're on Linux.
|
||||
return OS.LINUX
|
||||
|
||||
else:
|
||||
# we're on i386, so we could be on either Linux or Hurd.
|
||||
linker = elf.linker
|
||||
|
||||
if linker and "ld-linux" in linker:
|
||||
return OS.LINUX
|
||||
|
||||
elif linker and "/ld.so" in linker:
|
||||
return OS.HURD
|
||||
|
||||
else:
|
||||
# we don't have any good guesses based on versions needed
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def guess_os_from_needed_dependencies(elf: ELF) -> Optional[OS]:
|
||||
for needed in elf.needed:
|
||||
if needed.startswith("libmachuser.so"):
|
||||
return OS.HURD
|
||||
if needed.startswith("libhurduser.so"):
|
||||
return OS.HURD
|
||||
if needed.startswith("libandroid.so"):
|
||||
return OS.ANDROID
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def guess_os_from_symtab(elf: ELF) -> Optional[OS]:
|
||||
shdrs = elf.symtab
|
||||
if not shdrs:
|
||||
# executable does not contain a symbol table
|
||||
# or the symbol's names are stripped
|
||||
return None
|
||||
|
||||
symtab_shdr, strtab_shdr = shdrs
|
||||
symtab = SymTab(elf.endian, elf.bitness, symtab_shdr, strtab_shdr)
|
||||
|
||||
keywords = {
|
||||
OS.LINUX: [
|
||||
"linux",
|
||||
"/linux/",
|
||||
],
|
||||
}
|
||||
|
||||
for symbol in symtab.get_symbols():
|
||||
sym_name = symtab.get_name(symbol)
|
||||
|
||||
for os, hints in keywords.items():
|
||||
if any(hint in sym_name for hint in hints):
|
||||
return os
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def detect_elf_os(f) -> str:
|
||||
"""
|
||||
f: type Union[BinaryIO, IDAIO, GHIDRAIO]
|
||||
"""
|
||||
try:
|
||||
elf = ELF(f)
|
||||
except Exception as e:
|
||||
logger.warning("Error parsing ELF file: %s", e)
|
||||
return "unknown"
|
||||
|
||||
try:
|
||||
osabi_guess = guess_os_from_osabi(elf)
|
||||
logger.debug("guess: osabi: %s", osabi_guess)
|
||||
except Exception as e:
|
||||
logger.warning("Error guessing OS from OSABI: %s", e)
|
||||
osabi_guess = None
|
||||
|
||||
try:
|
||||
ph_notes_guess = guess_os_from_ph_notes(elf)
|
||||
logger.debug("guess: ph notes: %s", ph_notes_guess)
|
||||
except Exception as e:
|
||||
logger.warning("Error guessing OS from program header notes: %s", e)
|
||||
ph_notes_guess = None
|
||||
|
||||
try:
|
||||
sh_notes_guess = guess_os_from_sh_notes(elf)
|
||||
logger.debug("guess: sh notes: %s", sh_notes_guess)
|
||||
except Exception as e:
|
||||
logger.warning("Error guessing OS from section header notes: %s", e)
|
||||
sh_notes_guess = None
|
||||
|
||||
try:
|
||||
linker_guess = guess_os_from_linker(elf)
|
||||
logger.debug("guess: linker: %s", linker_guess)
|
||||
except Exception as e:
|
||||
logger.warning("Error guessing OS from linker: %s", e)
|
||||
linker_guess = None
|
||||
|
||||
try:
|
||||
abi_versions_needed_guess = guess_os_from_abi_versions_needed(elf)
|
||||
logger.debug("guess: ABI versions needed: %s", abi_versions_needed_guess)
|
||||
except Exception as e:
|
||||
logger.warning("Error guessing OS from ABI versions needed: %s", e)
|
||||
abi_versions_needed_guess = None
|
||||
|
||||
try:
|
||||
needed_dependencies_guess = guess_os_from_needed_dependencies(elf)
|
||||
logger.debug("guess: needed dependencies: %s", needed_dependencies_guess)
|
||||
except Exception as e:
|
||||
logger.warning("Error guessing OS from needed dependencies: %s", e)
|
||||
needed_dependencies_guess = None
|
||||
|
||||
try:
|
||||
symtab_guess = guess_os_from_symtab(elf)
|
||||
logger.debug("guess: pertinent symbol name: %s", symtab_guess)
|
||||
except Exception as e:
|
||||
logger.warning("Error guessing OS from symbol table: %s", e)
|
||||
symtab_guess = None
|
||||
|
||||
ret = None
|
||||
|
||||
if osabi_guess:
|
||||
ret = osabi_guess
|
||||
|
||||
elif ph_notes_guess:
|
||||
ret = ph_notes_guess
|
||||
|
||||
elif sh_notes_guess:
|
||||
ret = sh_notes_guess
|
||||
|
||||
elif linker_guess:
|
||||
ret = linker_guess
|
||||
|
||||
elif abi_versions_needed_guess:
|
||||
ret = abi_versions_needed_guess
|
||||
|
||||
elif needed_dependencies_guess:
|
||||
ret = needed_dependencies_guess
|
||||
|
||||
elif symtab_guess:
|
||||
ret = symtab_guess
|
||||
|
||||
return ret.value if ret is not None else "unknown"
|
||||
|
||||
|
||||
def detect_elf_arch(f: BinaryIO) -> str:
|
||||
return ELF(f).e_machine or "unknown"
|
||||
203
capa/features/extractors/elffile.py
Normal file
203
capa/features/extractors/elffile.py
Normal file
@@ -0,0 +1,203 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
import io
|
||||
import logging
|
||||
from typing import Tuple, Iterator
|
||||
from pathlib import Path
|
||||
|
||||
from elftools.elf.elffile import ELFFile, SymbolTableSection
|
||||
from elftools.elf.relocation import RelocationSection
|
||||
|
||||
import capa.features.extractors.common
|
||||
from capa.features.file import Export, Import, Section
|
||||
from capa.features.common import OS, FORMAT_ELF, Arch, Format, Feature
|
||||
from capa.features.address import NO_ADDRESS, FileOffsetAddress, AbsoluteVirtualAddress
|
||||
from capa.features.extractors.base_extractor import SampleHashes, StaticFeatureExtractor
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def extract_file_export_names(elf: ELFFile, **kwargs):
|
||||
for section in elf.iter_sections():
|
||||
if not isinstance(section, SymbolTableSection):
|
||||
continue
|
||||
|
||||
if section["sh_entsize"] == 0:
|
||||
logger.debug("Symbol table '%s' has a sh_entsize of zero!", section.name)
|
||||
continue
|
||||
|
||||
logger.debug("Symbol table '%s' contains %s entries:", section.name, section.num_symbols())
|
||||
|
||||
for symbol in section.iter_symbols():
|
||||
# The following conditions are based on the following article
|
||||
# http://www.m4b.io/elf/export/binary/analysis/2015/05/25/what-is-an-elf-export.html
|
||||
if not symbol.name:
|
||||
continue
|
||||
if symbol.entry.st_info.type not in ["STT_FUNC", "STT_OBJECT", "STT_IFUNC"]:
|
||||
continue
|
||||
if symbol.entry.st_value == 0:
|
||||
continue
|
||||
if symbol.entry.st_shndx == "SHN_UNDEF":
|
||||
continue
|
||||
|
||||
yield Export(symbol.name), AbsoluteVirtualAddress(symbol.entry.st_value)
|
||||
|
||||
|
||||
def extract_file_import_names(elf: ELFFile, **kwargs):
|
||||
# Create a dictionary to store symbol names by their index
|
||||
symbol_names = {}
|
||||
|
||||
# Extract symbol names and store them in the dictionary
|
||||
for section in elf.iter_sections():
|
||||
if not isinstance(section, SymbolTableSection):
|
||||
continue
|
||||
|
||||
for _, symbol in enumerate(section.iter_symbols()):
|
||||
# The following conditions are based on the following article
|
||||
# http://www.m4b.io/elf/export/binary/analysis/2015/05/25/what-is-an-elf-export.html
|
||||
if not symbol.name:
|
||||
continue
|
||||
if symbol.entry.st_info.type not in ["STT_FUNC", "STT_OBJECT", "STT_IFUNC"]:
|
||||
continue
|
||||
if symbol.entry.st_value != 0:
|
||||
continue
|
||||
if symbol.entry.st_shndx != "SHN_UNDEF":
|
||||
continue
|
||||
if symbol.entry.st_name == 0:
|
||||
continue
|
||||
|
||||
symbol_names[_] = symbol.name
|
||||
|
||||
for section in elf.iter_sections():
|
||||
if not isinstance(section, RelocationSection):
|
||||
continue
|
||||
|
||||
if section["sh_entsize"] == 0:
|
||||
logger.debug("Symbol table '%s' has a sh_entsize of zero!", section.name)
|
||||
continue
|
||||
|
||||
logger.debug("Symbol table '%s' contains %s entries:", section.name, section.num_relocations())
|
||||
|
||||
for relocation in section.iter_relocations():
|
||||
# Extract the symbol name from the symbol table using the symbol index in the relocation
|
||||
if relocation["r_info_sym"] not in symbol_names:
|
||||
continue
|
||||
yield Import(symbol_names[relocation["r_info_sym"]]), FileOffsetAddress(relocation["r_offset"])
|
||||
|
||||
|
||||
def extract_file_section_names(elf: ELFFile, **kwargs):
|
||||
for section in elf.iter_sections():
|
||||
if section.name:
|
||||
yield Section(section.name), AbsoluteVirtualAddress(section.header.sh_addr)
|
||||
elif section.is_null():
|
||||
yield Section("NULL"), AbsoluteVirtualAddress(section.header.sh_addr)
|
||||
|
||||
|
||||
def extract_file_strings(buf, **kwargs):
|
||||
yield from capa.features.extractors.common.extract_file_strings(buf)
|
||||
|
||||
|
||||
def extract_file_os(elf: ELFFile, buf, **kwargs):
|
||||
# our current approach does not always get an OS value, e.g. for packed samples
|
||||
# for file limitation purposes, we're more lax here
|
||||
try:
|
||||
os_tuple = next(capa.features.extractors.common.extract_os(buf))
|
||||
yield os_tuple
|
||||
except StopIteration:
|
||||
yield OS("unknown"), NO_ADDRESS
|
||||
|
||||
|
||||
def extract_file_format(**kwargs):
|
||||
yield Format(FORMAT_ELF), NO_ADDRESS
|
||||
|
||||
|
||||
def extract_file_arch(elf: ELFFile, **kwargs):
|
||||
arch = elf.get_machine_arch()
|
||||
if arch == "x86":
|
||||
yield Arch("i386"), NO_ADDRESS
|
||||
elif arch == "x64":
|
||||
yield Arch("amd64"), NO_ADDRESS
|
||||
else:
|
||||
logger.warning("unsupported architecture: %s", arch)
|
||||
|
||||
|
||||
def extract_file_features(elf: ELFFile, buf: bytes) -> Iterator[Tuple[Feature, int]]:
|
||||
for file_handler in FILE_HANDLERS:
|
||||
for feature, addr in file_handler(elf=elf, buf=buf): # type: ignore
|
||||
yield feature, addr
|
||||
|
||||
|
||||
FILE_HANDLERS = (
|
||||
extract_file_export_names,
|
||||
extract_file_import_names,
|
||||
extract_file_section_names,
|
||||
extract_file_strings,
|
||||
# no library matching
|
||||
extract_file_format,
|
||||
)
|
||||
|
||||
|
||||
def extract_global_features(elf: ELFFile, buf: bytes) -> Iterator[Tuple[Feature, int]]:
|
||||
for global_handler in GLOBAL_HANDLERS:
|
||||
for feature, addr in global_handler(elf=elf, buf=buf): # type: ignore
|
||||
yield feature, addr
|
||||
|
||||
|
||||
GLOBAL_HANDLERS = (
|
||||
extract_file_os,
|
||||
extract_file_arch,
|
||||
)
|
||||
|
||||
|
||||
class ElfFeatureExtractor(StaticFeatureExtractor):
|
||||
def __init__(self, path: Path):
|
||||
super().__init__(SampleHashes.from_bytes(path.read_bytes()))
|
||||
self.path: Path = path
|
||||
self.elf = ELFFile(io.BytesIO(path.read_bytes()))
|
||||
|
||||
def get_base_address(self):
|
||||
# virtual address of the first segment with type LOAD
|
||||
for segment in self.elf.iter_segments():
|
||||
if segment.header.p_type == "PT_LOAD":
|
||||
return AbsoluteVirtualAddress(segment.header.p_vaddr)
|
||||
|
||||
def extract_global_features(self):
|
||||
buf = self.path.read_bytes()
|
||||
|
||||
for feature, addr in extract_global_features(self.elf, buf):
|
||||
yield feature, addr
|
||||
|
||||
def extract_file_features(self):
|
||||
buf = self.path.read_bytes()
|
||||
|
||||
for feature, addr in extract_file_features(self.elf, buf):
|
||||
yield feature, addr
|
||||
|
||||
def get_functions(self):
|
||||
raise NotImplementedError("ElfFeatureExtractor can only be used to extract file features")
|
||||
|
||||
def extract_function_features(self, f):
|
||||
raise NotImplementedError("ElfFeatureExtractor can only be used to extract file features")
|
||||
|
||||
def get_basic_blocks(self, f):
|
||||
raise NotImplementedError("ElfFeatureExtractor can only be used to extract file features")
|
||||
|
||||
def extract_basic_block_features(self, f, bb):
|
||||
raise NotImplementedError("ElfFeatureExtractor can only be used to extract file features")
|
||||
|
||||
def get_instructions(self, f, bb):
|
||||
raise NotImplementedError("ElfFeatureExtractor can only be used to extract file features")
|
||||
|
||||
def extract_insn_features(self, f, bb, insn):
|
||||
raise NotImplementedError("ElfFeatureExtractor can only be used to extract file features")
|
||||
|
||||
def is_library_function(self, addr):
|
||||
raise NotImplementedError("ElfFeatureExtractor can only be used to extract file features")
|
||||
|
||||
def get_function_name(self, addr):
|
||||
raise NotImplementedError("ElfFeatureExtractor can only be used to extract file features")
|
||||
0
capa/features/extractors/ghidra/__init__.py
Normal file
0
capa/features/extractors/ghidra/__init__.py
Normal file
152
capa/features/extractors/ghidra/basicblock.py
Normal file
152
capa/features/extractors/ghidra/basicblock.py
Normal file
@@ -0,0 +1,152 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import string
|
||||
import struct
|
||||
from typing import Tuple, Iterator
|
||||
|
||||
import ghidra
|
||||
from ghidra.program.model.lang import OperandType
|
||||
|
||||
import capa.features.extractors.ghidra.helpers
|
||||
from capa.features.common import Feature, Characteristic
|
||||
from capa.features.address import Address
|
||||
from capa.features.basicblock import BasicBlock
|
||||
from capa.features.extractors.helpers import MIN_STACKSTRING_LEN
|
||||
from capa.features.extractors.base_extractor import BBHandle, FunctionHandle
|
||||
|
||||
|
||||
def get_printable_len(op: ghidra.program.model.scalar.Scalar) -> int:
|
||||
"""Return string length if all operand bytes are ascii or utf16-le printable"""
|
||||
op_bit_len = op.bitLength()
|
||||
op_byte_len = op_bit_len // 8
|
||||
op_val = op.getValue()
|
||||
|
||||
if op_bit_len == 8:
|
||||
chars = struct.pack("<B", op_val & 0xFF)
|
||||
elif op_bit_len == 16:
|
||||
chars = struct.pack("<H", op_val & 0xFFFF)
|
||||
elif op_bit_len == 32:
|
||||
chars = struct.pack("<I", op_val & 0xFFFFFFFF)
|
||||
elif op_bit_len == 64:
|
||||
chars = struct.pack("<Q", op_val & 0xFFFFFFFFFFFFFFFF)
|
||||
else:
|
||||
raise ValueError(f"Unhandled operand data type 0x{op_bit_len:x}.")
|
||||
|
||||
def is_printable_ascii(chars_: bytes):
|
||||
return all(c < 127 and chr(c) in string.printable for c in chars_)
|
||||
|
||||
def is_printable_utf16le(chars_: bytes):
|
||||
if all(c == 0x00 for c in chars_[1::2]):
|
||||
return is_printable_ascii(chars_[::2])
|
||||
|
||||
if is_printable_ascii(chars):
|
||||
return op_byte_len
|
||||
|
||||
if is_printable_utf16le(chars):
|
||||
return op_byte_len
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def is_mov_imm_to_stack(insn: ghidra.program.database.code.InstructionDB) -> bool:
|
||||
"""verify instruction moves immediate onto stack"""
|
||||
|
||||
# Ghidra will Bitwise OR the OperandTypes to assign multiple
|
||||
# i.e., the first operand is a stackvar (dynamically allocated),
|
||||
# and the second is a scalar value (single int/char/float/etc.)
|
||||
mov_its_ops = [(OperandType.ADDRESS | OperandType.DYNAMIC), OperandType.SCALAR]
|
||||
found = False
|
||||
|
||||
# MOV dword ptr [EBP + local_*], 0x65
|
||||
if insn.getMnemonicString().startswith("MOV"):
|
||||
found = all(insn.getOperandType(i) == mov_its_ops[i] for i in range(2))
|
||||
|
||||
return found
|
||||
|
||||
|
||||
def bb_contains_stackstring(bb: ghidra.program.model.block.CodeBlock) -> bool:
|
||||
"""check basic block for stackstring indicators
|
||||
|
||||
true if basic block contains enough moves of constant bytes to the stack
|
||||
"""
|
||||
count = 0
|
||||
for insn in currentProgram().getListing().getInstructions(bb, True): # type: ignore [name-defined] # noqa: F821
|
||||
if is_mov_imm_to_stack(insn):
|
||||
count += get_printable_len(insn.getScalar(1))
|
||||
if count > MIN_STACKSTRING_LEN:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _bb_has_tight_loop(bb: ghidra.program.model.block.CodeBlock):
|
||||
"""
|
||||
parse tight loops, true if last instruction in basic block branches to bb start
|
||||
"""
|
||||
# Reverse Ordered, first InstructionDB
|
||||
last_insn = currentProgram().getListing().getInstructions(bb, False).next() # type: ignore [name-defined] # noqa: F821
|
||||
|
||||
if last_insn.getFlowType().isJump():
|
||||
return last_insn.getAddress(0) == bb.getMinAddress()
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def extract_bb_stackstring(fh: FunctionHandle, bbh: BBHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""extract stackstring indicators from basic block"""
|
||||
bb: ghidra.program.model.block.CodeBlock = bbh.inner
|
||||
|
||||
if bb_contains_stackstring(bb):
|
||||
yield Characteristic("stack string"), bbh.address
|
||||
|
||||
|
||||
def extract_bb_tight_loop(fh: FunctionHandle, bbh: BBHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""check basic block for tight loop indicators"""
|
||||
bb: ghidra.program.model.block.CodeBlock = bbh.inner
|
||||
|
||||
if _bb_has_tight_loop(bb):
|
||||
yield Characteristic("tight loop"), bbh.address
|
||||
|
||||
|
||||
BASIC_BLOCK_HANDLERS = (
|
||||
extract_bb_tight_loop,
|
||||
extract_bb_stackstring,
|
||||
)
|
||||
|
||||
|
||||
def extract_features(fh: FunctionHandle, bbh: BBHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
extract features from the given basic block.
|
||||
|
||||
args:
|
||||
bb: the basic block to process.
|
||||
|
||||
yields:
|
||||
Tuple[Feature, int]: the features and their location found in this basic block.
|
||||
"""
|
||||
yield BasicBlock(), bbh.address
|
||||
for bb_handler in BASIC_BLOCK_HANDLERS:
|
||||
for feature, addr in bb_handler(fh, bbh):
|
||||
yield feature, addr
|
||||
|
||||
|
||||
def main():
|
||||
features = []
|
||||
from capa.features.extractors.ghidra.extractor import GhidraFeatureExtractor
|
||||
|
||||
for fh in GhidraFeatureExtractor().get_functions():
|
||||
for bbh in capa.features.extractors.ghidra.helpers.get_function_blocks(fh):
|
||||
features.extend(list(extract_features(fh, bbh)))
|
||||
|
||||
import pprint
|
||||
|
||||
pprint.pprint(features) # noqa: T203
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
93
capa/features/extractors/ghidra/extractor.py
Normal file
93
capa/features/extractors/ghidra/extractor.py
Normal file
@@ -0,0 +1,93 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
from typing import List, Tuple, Iterator
|
||||
|
||||
import capa.features.extractors.ghidra.file
|
||||
import capa.features.extractors.ghidra.insn
|
||||
import capa.features.extractors.ghidra.global_
|
||||
import capa.features.extractors.ghidra.function
|
||||
import capa.features.extractors.ghidra.basicblock
|
||||
from capa.features.common import Feature
|
||||
from capa.features.address import Address, AbsoluteVirtualAddress
|
||||
from capa.features.extractors.base_extractor import (
|
||||
BBHandle,
|
||||
InsnHandle,
|
||||
SampleHashes,
|
||||
FunctionHandle,
|
||||
StaticFeatureExtractor,
|
||||
)
|
||||
|
||||
|
||||
class GhidraFeatureExtractor(StaticFeatureExtractor):
|
||||
def __init__(self):
|
||||
import capa.features.extractors.ghidra.helpers as ghidra_helpers
|
||||
|
||||
super().__init__(
|
||||
SampleHashes(
|
||||
md5=capa.ghidra.helpers.get_file_md5(),
|
||||
# ghidra doesn't expose this hash.
|
||||
# https://ghidra.re/ghidra_docs/api/ghidra/program/model/listing/Program.html
|
||||
#
|
||||
# the hashes are stored in the database, not computed on the fly,
|
||||
# so its probably not trivial to add SHA1.
|
||||
sha1="",
|
||||
sha256=capa.ghidra.helpers.get_file_sha256(),
|
||||
)
|
||||
)
|
||||
|
||||
self.global_features: List[Tuple[Feature, Address]] = []
|
||||
self.global_features.extend(capa.features.extractors.ghidra.file.extract_file_format())
|
||||
self.global_features.extend(capa.features.extractors.ghidra.global_.extract_os())
|
||||
self.global_features.extend(capa.features.extractors.ghidra.global_.extract_arch())
|
||||
self.imports = ghidra_helpers.get_file_imports()
|
||||
self.externs = ghidra_helpers.get_file_externs()
|
||||
self.fakes = ghidra_helpers.map_fake_import_addrs()
|
||||
|
||||
def get_base_address(self):
|
||||
return AbsoluteVirtualAddress(currentProgram().getImageBase().getOffset()) # type: ignore [name-defined] # noqa: F821
|
||||
|
||||
def extract_global_features(self):
|
||||
yield from self.global_features
|
||||
|
||||
def extract_file_features(self):
|
||||
yield from capa.features.extractors.ghidra.file.extract_features()
|
||||
|
||||
def get_functions(self) -> Iterator[FunctionHandle]:
|
||||
import capa.features.extractors.ghidra.helpers as ghidra_helpers
|
||||
|
||||
for fhandle in ghidra_helpers.get_function_symbols():
|
||||
fh: FunctionHandle = FunctionHandle(
|
||||
address=AbsoluteVirtualAddress(fhandle.getEntryPoint().getOffset()),
|
||||
inner=fhandle,
|
||||
ctx={"imports_cache": self.imports, "externs_cache": self.externs, "fakes_cache": self.fakes},
|
||||
)
|
||||
yield fh
|
||||
|
||||
@staticmethod
|
||||
def get_function(addr: int) -> FunctionHandle:
|
||||
func = getFunctionContaining(toAddr(addr)) # type: ignore [name-defined] # noqa: F821
|
||||
return FunctionHandle(address=AbsoluteVirtualAddress(func.getEntryPoint().getOffset()), inner=func)
|
||||
|
||||
def extract_function_features(self, fh: FunctionHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
yield from capa.features.extractors.ghidra.function.extract_features(fh)
|
||||
|
||||
def get_basic_blocks(self, fh: FunctionHandle) -> Iterator[BBHandle]:
|
||||
import capa.features.extractors.ghidra.helpers as ghidra_helpers
|
||||
|
||||
yield from ghidra_helpers.get_function_blocks(fh)
|
||||
|
||||
def extract_basic_block_features(self, fh: FunctionHandle, bbh: BBHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
yield from capa.features.extractors.ghidra.basicblock.extract_features(fh, bbh)
|
||||
|
||||
def get_instructions(self, fh: FunctionHandle, bbh: BBHandle) -> Iterator[InsnHandle]:
|
||||
import capa.features.extractors.ghidra.helpers as ghidra_helpers
|
||||
|
||||
yield from ghidra_helpers.get_insn_in_range(bbh)
|
||||
|
||||
def extract_insn_features(self, fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle):
|
||||
yield from capa.features.extractors.ghidra.insn.extract_features(fh, bbh, ih)
|
||||
202
capa/features/extractors/ghidra/file.py
Normal file
202
capa/features/extractors/ghidra/file.py
Normal file
@@ -0,0 +1,202 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
import re
|
||||
import struct
|
||||
from typing import List, Tuple, Iterator
|
||||
|
||||
from ghidra.program.model.symbol import SourceType, SymbolType
|
||||
|
||||
import capa.features.extractors.common
|
||||
import capa.features.extractors.helpers
|
||||
import capa.features.extractors.strings
|
||||
import capa.features.extractors.ghidra.helpers
|
||||
from capa.features.file import Export, Import, Section, FunctionName
|
||||
from capa.features.common import FORMAT_PE, FORMAT_ELF, Format, String, Feature, Characteristic
|
||||
from capa.features.address import NO_ADDRESS, Address, FileOffsetAddress, AbsoluteVirtualAddress
|
||||
|
||||
MAX_OFFSET_PE_AFTER_MZ = 0x200
|
||||
|
||||
|
||||
def find_embedded_pe(block_bytez: bytes, mz_xor: List[Tuple[bytes, bytes, int]]) -> Iterator[Tuple[int, int]]:
|
||||
"""check segment for embedded PE
|
||||
|
||||
adapted for Ghidra from:
|
||||
https://github.com/vivisect/vivisect/blob/91e8419a861f4977https://github.com/vivisect/vivisect/blob/91e8419a861f49779f18316f155311967e696836/PE/carve.py#L259f18316f155311967e696836/PE/carve.py#L25
|
||||
"""
|
||||
todo = []
|
||||
|
||||
for mzx, pex, i in mz_xor:
|
||||
for match in re.finditer(re.escape(mzx), block_bytez):
|
||||
todo.append((match.start(), mzx, pex, i))
|
||||
|
||||
seg_max = len(block_bytez) # noqa: F821
|
||||
while len(todo):
|
||||
off, mzx, pex, i = todo.pop()
|
||||
|
||||
# MZ header has one field we will check e_lfanew is at 0x3c
|
||||
e_lfanew = off + 0x3C
|
||||
|
||||
if seg_max < e_lfanew + 4:
|
||||
continue
|
||||
|
||||
e_lfanew_bytes = block_bytez[e_lfanew : e_lfanew + 4]
|
||||
newoff = struct.unpack("<I", capa.features.extractors.helpers.xor_static(e_lfanew_bytes, i))[0]
|
||||
|
||||
# assume XOR'd "PE" bytes exist within threshold
|
||||
if newoff > MAX_OFFSET_PE_AFTER_MZ:
|
||||
continue
|
||||
|
||||
peoff = off + newoff
|
||||
if seg_max < peoff + 2:
|
||||
continue
|
||||
|
||||
pe_bytes = block_bytez[peoff : peoff + 2]
|
||||
if pe_bytes == pex:
|
||||
yield off, i
|
||||
|
||||
|
||||
def extract_file_embedded_pe() -> Iterator[Tuple[Feature, Address]]:
|
||||
"""extract embedded PE features"""
|
||||
|
||||
# pre-compute XOR pairs
|
||||
mz_xor: List[Tuple[bytes, bytes, int]] = [
|
||||
(
|
||||
capa.features.extractors.helpers.xor_static(b"MZ", i),
|
||||
capa.features.extractors.helpers.xor_static(b"PE", i),
|
||||
i,
|
||||
)
|
||||
for i in range(256)
|
||||
]
|
||||
|
||||
for block in currentProgram().getMemory().getBlocks(): # type: ignore [name-defined] # noqa: F821
|
||||
if not all((block.isLoaded(), block.isInitialized(), "Headers" not in block.getName())):
|
||||
continue
|
||||
|
||||
for off, _ in find_embedded_pe(capa.features.extractors.ghidra.helpers.get_block_bytes(block), mz_xor):
|
||||
# add offset back to block start
|
||||
ea: int = block.getStart().add(off).getOffset()
|
||||
|
||||
yield Characteristic("embedded pe"), FileOffsetAddress(ea)
|
||||
|
||||
|
||||
def extract_file_export_names() -> Iterator[Tuple[Feature, Address]]:
|
||||
"""extract function exports"""
|
||||
st = currentProgram().getSymbolTable() # type: ignore [name-defined] # noqa: F821
|
||||
for addr in st.getExternalEntryPointIterator():
|
||||
yield Export(st.getPrimarySymbol(addr).getName()), AbsoluteVirtualAddress(addr.getOffset())
|
||||
|
||||
|
||||
def extract_file_import_names() -> Iterator[Tuple[Feature, Address]]:
|
||||
"""extract function imports
|
||||
|
||||
1. imports by ordinal:
|
||||
- modulename.#ordinal
|
||||
|
||||
2. imports by name, results in two features to support importname-only
|
||||
matching:
|
||||
- modulename.importname
|
||||
- importname
|
||||
"""
|
||||
|
||||
for f in currentProgram().getFunctionManager().getExternalFunctions(): # type: ignore [name-defined] # noqa: F821
|
||||
for r in f.getSymbol().getReferences():
|
||||
if r.getReferenceType().isData():
|
||||
addr = r.getFromAddress().getOffset() # gets pointer to fake external addr
|
||||
|
||||
fstr = f.toString().split("::") # format: MODULE.dll::import / MODULE::Ordinal_*
|
||||
if "Ordinal_" in fstr[1]:
|
||||
fstr[1] = f"#{fstr[1].split('_')[1]}"
|
||||
|
||||
for name in capa.features.extractors.helpers.generate_symbols(fstr[0][:-4], fstr[1], include_dll=True):
|
||||
yield Import(name), AbsoluteVirtualAddress(addr)
|
||||
|
||||
|
||||
def extract_file_section_names() -> Iterator[Tuple[Feature, Address]]:
|
||||
"""extract section names"""
|
||||
|
||||
for block in currentProgram().getMemory().getBlocks(): # type: ignore [name-defined] # noqa: F821
|
||||
yield Section(block.getName()), AbsoluteVirtualAddress(block.getStart().getOffset())
|
||||
|
||||
|
||||
def extract_file_strings() -> Iterator[Tuple[Feature, Address]]:
|
||||
"""extract ASCII and UTF-16 LE strings"""
|
||||
|
||||
for block in currentProgram().getMemory().getBlocks(): # type: ignore [name-defined] # noqa: F821
|
||||
if block.isInitialized():
|
||||
p_bytes = capa.features.extractors.ghidra.helpers.get_block_bytes(block)
|
||||
|
||||
for s in capa.features.extractors.strings.extract_ascii_strings(p_bytes):
|
||||
offset = block.getStart().getOffset() + s.offset
|
||||
yield String(s.s), FileOffsetAddress(offset)
|
||||
|
||||
for s in capa.features.extractors.strings.extract_unicode_strings(p_bytes):
|
||||
offset = block.getStart().getOffset() + s.offset
|
||||
yield String(s.s), FileOffsetAddress(offset)
|
||||
|
||||
|
||||
def extract_file_function_names() -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
extract the names of statically-linked library functions.
|
||||
"""
|
||||
|
||||
for sym in currentProgram().getSymbolTable().getAllSymbols(True): # type: ignore [name-defined] # noqa: F821
|
||||
# .isExternal() misses more than this config for the function symbols
|
||||
if sym.getSymbolType() == SymbolType.FUNCTION and sym.getSource() == SourceType.ANALYSIS and sym.isGlobal():
|
||||
name = sym.getName() # starts to resolve names based on Ghidra's FidDB
|
||||
if name.startswith("FID_conflict:"): # format: FID_conflict:<function-name>
|
||||
name = name[13:]
|
||||
addr = AbsoluteVirtualAddress(sym.getAddress().getOffset())
|
||||
yield FunctionName(name), addr
|
||||
if name.startswith("_"):
|
||||
# some linkers may prefix linked routines with a `_` to avoid name collisions.
|
||||
# extract features for both the mangled and un-mangled representations.
|
||||
# e.g. `_fwrite` -> `fwrite`
|
||||
# see: https://stackoverflow.com/a/2628384/87207
|
||||
yield FunctionName(name[1:]), addr
|
||||
|
||||
|
||||
def extract_file_format() -> Iterator[Tuple[Feature, Address]]:
|
||||
ef = currentProgram().getExecutableFormat() # type: ignore [name-defined] # noqa: F821
|
||||
if "PE" in ef:
|
||||
yield Format(FORMAT_PE), NO_ADDRESS
|
||||
elif "ELF" in ef:
|
||||
yield Format(FORMAT_ELF), NO_ADDRESS
|
||||
elif "Raw" in ef:
|
||||
# no file type to return when processing a binary file, but we want to continue processing
|
||||
return
|
||||
else:
|
||||
raise NotImplementedError(f"unexpected file format: {ef}")
|
||||
|
||||
|
||||
def extract_features() -> Iterator[Tuple[Feature, Address]]:
|
||||
"""extract file features"""
|
||||
for file_handler in FILE_HANDLERS:
|
||||
for feature, addr in file_handler():
|
||||
yield feature, addr
|
||||
|
||||
|
||||
FILE_HANDLERS = (
|
||||
extract_file_embedded_pe,
|
||||
extract_file_export_names,
|
||||
extract_file_import_names,
|
||||
extract_file_section_names,
|
||||
extract_file_strings,
|
||||
extract_file_function_names,
|
||||
extract_file_format,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
""" """
|
||||
import pprint
|
||||
|
||||
pprint.pprint(list(extract_features())) # noqa: T203
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
73
capa/features/extractors/ghidra/function.py
Normal file
73
capa/features/extractors/ghidra/function.py
Normal file
@@ -0,0 +1,73 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
from typing import Tuple, Iterator
|
||||
|
||||
import ghidra
|
||||
from ghidra.program.model.block import BasicBlockModel, SimpleBlockIterator
|
||||
|
||||
import capa.features.extractors.ghidra.helpers
|
||||
from capa.features.common import Feature, Characteristic
|
||||
from capa.features.address import Address, AbsoluteVirtualAddress
|
||||
from capa.features.extractors import loops
|
||||
from capa.features.extractors.base_extractor import FunctionHandle
|
||||
|
||||
|
||||
def extract_function_calls_to(fh: FunctionHandle):
|
||||
"""extract callers to a function"""
|
||||
f: ghidra.program.database.function.FunctionDB = fh.inner
|
||||
for ref in f.getSymbol().getReferences():
|
||||
if ref.getReferenceType().isCall():
|
||||
yield Characteristic("calls to"), AbsoluteVirtualAddress(ref.getFromAddress().getOffset())
|
||||
|
||||
|
||||
def extract_function_loop(fh: FunctionHandle):
|
||||
f: ghidra.program.database.function.FunctionDB = fh.inner
|
||||
|
||||
edges = []
|
||||
for block in SimpleBlockIterator(BasicBlockModel(currentProgram()), f.getBody(), monitor()): # type: ignore [name-defined] # noqa: F821
|
||||
dests = block.getDestinations(monitor()) # type: ignore [name-defined] # noqa: F821
|
||||
s_addrs = block.getStartAddresses()
|
||||
|
||||
while dests.hasNext(): # For loop throws Python TypeError
|
||||
for addr in s_addrs:
|
||||
edges.append((addr.getOffset(), dests.next().getDestinationAddress().getOffset()))
|
||||
|
||||
if loops.has_loop(edges):
|
||||
yield Characteristic("loop"), AbsoluteVirtualAddress(f.getEntryPoint().getOffset())
|
||||
|
||||
|
||||
def extract_recursive_call(fh: FunctionHandle):
|
||||
f: ghidra.program.database.function.FunctionDB = fh.inner
|
||||
|
||||
for func in f.getCalledFunctions(monitor()): # type: ignore [name-defined] # noqa: F821
|
||||
if func.getEntryPoint().getOffset() == f.getEntryPoint().getOffset():
|
||||
yield Characteristic("recursive call"), AbsoluteVirtualAddress(f.getEntryPoint().getOffset())
|
||||
|
||||
|
||||
def extract_features(fh: FunctionHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
for func_handler in FUNCTION_HANDLERS:
|
||||
for feature, addr in func_handler(fh):
|
||||
yield feature, addr
|
||||
|
||||
|
||||
FUNCTION_HANDLERS = (extract_function_calls_to, extract_function_loop, extract_recursive_call)
|
||||
|
||||
|
||||
def main():
|
||||
""" """
|
||||
features = []
|
||||
for fhandle in capa.features.extractors.ghidra.helpers.get_function_symbols():
|
||||
features.extend(list(extract_features(fhandle)))
|
||||
|
||||
import pprint
|
||||
|
||||
pprint.pprint(features) # noqa: T203
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
67
capa/features/extractors/ghidra/global_.py
Normal file
67
capa/features/extractors/ghidra/global_.py
Normal file
@@ -0,0 +1,67 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
import logging
|
||||
import contextlib
|
||||
from typing import Tuple, Iterator
|
||||
|
||||
import capa.ghidra.helpers
|
||||
import capa.features.extractors.elf
|
||||
import capa.features.extractors.ghidra.helpers
|
||||
from capa.features.common import OS, ARCH_I386, ARCH_AMD64, OS_WINDOWS, Arch, Feature
|
||||
from capa.features.address import NO_ADDRESS, Address
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def extract_os() -> Iterator[Tuple[Feature, Address]]:
|
||||
format_name: str = currentProgram().getExecutableFormat() # type: ignore [name-defined] # noqa: F821
|
||||
|
||||
if "PE" in format_name:
|
||||
yield OS(OS_WINDOWS), NO_ADDRESS
|
||||
|
||||
elif "ELF" in format_name:
|
||||
with contextlib.closing(capa.ghidra.helpers.GHIDRAIO()) as f:
|
||||
os = capa.features.extractors.elf.detect_elf_os(f)
|
||||
|
||||
yield OS(os), NO_ADDRESS
|
||||
|
||||
else:
|
||||
# we likely end up here:
|
||||
# 1. handling shellcode, or
|
||||
# 2. handling a new file format (e.g. macho)
|
||||
#
|
||||
# for (1) we can't do much - its shellcode and all bets are off.
|
||||
# we could maybe accept a further CLI argument to specify the OS,
|
||||
# but i think this would be rarely used.
|
||||
# rules that rely on OS conditions will fail to match on shellcode.
|
||||
#
|
||||
# for (2), this logic will need to be updated as the format is implemented.
|
||||
logger.debug("unsupported file format: %s, will not guess OS", format_name)
|
||||
return
|
||||
|
||||
|
||||
def extract_arch() -> Iterator[Tuple[Feature, Address]]:
|
||||
lang_id = currentProgram().getMetadata().get("Language ID") # type: ignore [name-defined] # noqa: F821
|
||||
|
||||
if "x86" in lang_id and "64" in lang_id:
|
||||
yield Arch(ARCH_AMD64), NO_ADDRESS
|
||||
|
||||
elif "x86" in lang_id and "32" in lang_id:
|
||||
yield Arch(ARCH_I386), NO_ADDRESS
|
||||
|
||||
elif "x86" not in lang_id:
|
||||
logger.debug("unsupported architecture: non-32-bit nor non-64-bit intel")
|
||||
return
|
||||
|
||||
else:
|
||||
# we likely end up here:
|
||||
# 1. handling a new architecture (e.g. aarch64)
|
||||
#
|
||||
# for (1), this logic will need to be updated as the format is implemented.
|
||||
logger.debug("unsupported architecture: %s", lang_id)
|
||||
return
|
||||
277
capa/features/extractors/ghidra/helpers.py
Normal file
277
capa/features/extractors/ghidra/helpers.py
Normal file
@@ -0,0 +1,277 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
from typing import Dict, List, Iterator
|
||||
|
||||
import ghidra
|
||||
import java.lang
|
||||
from ghidra.program.model.lang import OperandType
|
||||
from ghidra.program.model.block import BasicBlockModel, SimpleBlockIterator
|
||||
from ghidra.program.model.symbol import SourceType, SymbolType
|
||||
from ghidra.program.model.address import AddressSpace
|
||||
|
||||
import capa.features.extractors.helpers
|
||||
from capa.features.common import THUNK_CHAIN_DEPTH_DELTA
|
||||
from capa.features.address import AbsoluteVirtualAddress
|
||||
from capa.features.extractors.base_extractor import BBHandle, InsnHandle, FunctionHandle
|
||||
|
||||
|
||||
def ints_to_bytes(bytez: List[int]) -> bytes:
|
||||
"""convert Java signed ints to Python bytes
|
||||
|
||||
args:
|
||||
bytez: list of Java signed ints
|
||||
"""
|
||||
return bytes([b & 0xFF for b in bytez])
|
||||
|
||||
|
||||
def find_byte_sequence(addr: ghidra.program.model.address.Address, seq: bytes) -> Iterator[int]:
|
||||
"""yield all ea of a given byte sequence
|
||||
|
||||
args:
|
||||
addr: start address
|
||||
seq: bytes to search e.g. b"\x01\x03"
|
||||
"""
|
||||
seqstr = "".join([f"\\x{b:02x}" for b in seq])
|
||||
eas = findBytes(addr, seqstr, java.lang.Integer.MAX_VALUE, 1) # type: ignore [name-defined] # noqa: F821
|
||||
|
||||
yield from eas
|
||||
|
||||
|
||||
def get_bytes(addr: ghidra.program.model.address.Address, length: int) -> bytes:
|
||||
"""yield length bytes at addr
|
||||
|
||||
args:
|
||||
addr: Address to begin pull from
|
||||
length: length of bytes to pull
|
||||
"""
|
||||
try:
|
||||
return ints_to_bytes(getBytes(addr, length)) # type: ignore [name-defined] # noqa: F821
|
||||
except RuntimeError:
|
||||
return b""
|
||||
|
||||
|
||||
def get_block_bytes(block: ghidra.program.model.mem.MemoryBlock) -> bytes:
|
||||
"""yield all bytes in a given block
|
||||
|
||||
args:
|
||||
block: MemoryBlock to pull from
|
||||
"""
|
||||
return get_bytes(block.getStart(), block.getSize())
|
||||
|
||||
|
||||
def get_function_symbols():
|
||||
"""yield all non-external function symbols"""
|
||||
yield from currentProgram().getFunctionManager().getFunctionsNoStubs(True) # type: ignore [name-defined] # noqa: F821
|
||||
|
||||
|
||||
def get_function_blocks(fh: FunctionHandle) -> Iterator[BBHandle]:
|
||||
"""yield BBHandle for each bb in a given function"""
|
||||
|
||||
func: ghidra.program.database.function.FunctionDB = fh.inner
|
||||
for bb in SimpleBlockIterator(BasicBlockModel(currentProgram()), func.getBody(), monitor()): # type: ignore [name-defined] # noqa: F821
|
||||
yield BBHandle(address=AbsoluteVirtualAddress(bb.getMinAddress().getOffset()), inner=bb)
|
||||
|
||||
|
||||
def get_insn_in_range(bbh: BBHandle) -> Iterator[InsnHandle]:
|
||||
"""yield InshHandle for each insn in a given basicblock"""
|
||||
for insn in currentProgram().getListing().getInstructions(bbh.inner, True): # type: ignore [name-defined] # noqa: F821
|
||||
yield InsnHandle(address=AbsoluteVirtualAddress(insn.getAddress().getOffset()), inner=insn)
|
||||
|
||||
|
||||
def get_file_imports() -> Dict[int, List[str]]:
|
||||
"""get all import names & addrs"""
|
||||
|
||||
import_dict: Dict[int, List[str]] = {}
|
||||
|
||||
for f in currentProgram().getFunctionManager().getExternalFunctions(): # type: ignore [name-defined] # noqa: F821
|
||||
for r in f.getSymbol().getReferences():
|
||||
if r.getReferenceType().isData():
|
||||
addr = r.getFromAddress().getOffset() # gets pointer to fake external addr
|
||||
|
||||
ex_loc = f.getExternalLocation().getAddress() # map external locations as well (offset into module files)
|
||||
|
||||
fstr = f.toString().split("::") # format: MODULE.dll::import / MODULE::Ordinal_* / <EXTERNAL>::import
|
||||
if "Ordinal_" in fstr[1]:
|
||||
fstr[1] = f"#{fstr[1].split('_')[1]}"
|
||||
|
||||
# <EXTERNAL> mostly shows up in ELF files, otherwise, strip '.dll' w/ [:-4]
|
||||
fstr[0] = "*" if "<EXTERNAL>" in fstr[0] else fstr[0][:-4]
|
||||
|
||||
for name in capa.features.extractors.helpers.generate_symbols(fstr[0], fstr[1]):
|
||||
import_dict.setdefault(addr, []).append(name)
|
||||
if ex_loc:
|
||||
import_dict.setdefault(ex_loc.getOffset(), []).append(name)
|
||||
|
||||
return import_dict
|
||||
|
||||
|
||||
def get_file_externs() -> Dict[int, List[str]]:
|
||||
"""
|
||||
Gets function names & addresses of statically-linked library functions
|
||||
|
||||
Ghidra's external namespace is mostly reserved for dynamically-linked
|
||||
imports. Statically-linked functions are part of the global namespace.
|
||||
Filtering on the type, source, and namespace of the symbols yield more
|
||||
statically-linked library functions.
|
||||
|
||||
Example: (PMA Lab 16-01.exe_) 7faafc7e4a5c736ebfee6abbbc812d80:0x407490
|
||||
- __aulldiv
|
||||
- Note: See Symbol Table labels
|
||||
"""
|
||||
|
||||
extern_dict: Dict[int, List[str]] = {}
|
||||
|
||||
for sym in currentProgram().getSymbolTable().getAllSymbols(True): # type: ignore [name-defined] # noqa: F821
|
||||
# .isExternal() misses more than this config for the function symbols
|
||||
if sym.getSymbolType() == SymbolType.FUNCTION and sym.getSource() == SourceType.ANALYSIS and sym.isGlobal():
|
||||
name = sym.getName() # starts to resolve names based on Ghidra's FidDB
|
||||
if name.startswith("FID_conflict:"): # format: FID_conflict:<function-name>
|
||||
name = name[13:]
|
||||
extern_dict.setdefault(sym.getAddress().getOffset(), []).append(name)
|
||||
if name.startswith("_"):
|
||||
# some linkers may prefix linked routines with a `_` to avoid name collisions.
|
||||
# extract features for both the mangled and un-mangled representations.
|
||||
# e.g. `_fwrite` -> `fwrite`
|
||||
# see: https://stackoverflow.com/a/2628384/87207
|
||||
extern_dict.setdefault(sym.getAddress().getOffset(), []).append(name[1:])
|
||||
|
||||
return extern_dict
|
||||
|
||||
|
||||
def map_fake_import_addrs() -> Dict[int, List[int]]:
|
||||
"""
|
||||
Map ghidra's fake import entrypoints to their
|
||||
real addresses
|
||||
|
||||
Helps as many Ghidra Scripting API calls end up returning
|
||||
these external (fake) addresses.
|
||||
|
||||
Undocumented but intended Ghidra behavior:
|
||||
- Import entryPoint fields are stored in the 'EXTERNAL:' AddressSpace.
|
||||
'getEntryPoint()' returns the entryPoint field, which is an offset
|
||||
from the beginning of the assigned AddressSpace. In the case of externals,
|
||||
they start from 1 and increment.
|
||||
https://github.com/NationalSecurityAgency/ghidra/blob/26d4bd9104809747c21f2528cab8aba9aef9acd5/Ghidra/Features/Base/src/test.slow/java/ghidra/program/database/function/ExternalFunctionDBTest.java#L90
|
||||
|
||||
Example: (mimikatz.exe_) 5f66b82558ca92e54e77f216ef4c066c:0x473090
|
||||
- 0x473090 -> PTR_CreateServiceW_00473090
|
||||
- 'EXTERNAL:00000025' -> External Address (ghidra.program.model.address.SpecialAddress)
|
||||
"""
|
||||
fake_dict: Dict[int, List[int]] = {}
|
||||
|
||||
for f in currentProgram().getFunctionManager().getExternalFunctions(): # type: ignore [name-defined] # noqa: F821
|
||||
for r in f.getSymbol().getReferences():
|
||||
if r.getReferenceType().isData():
|
||||
fake_dict.setdefault(f.getEntryPoint().getOffset(), []).append(r.getFromAddress().getOffset())
|
||||
|
||||
return fake_dict
|
||||
|
||||
|
||||
def check_addr_for_api(
|
||||
addr: ghidra.program.model.address.Address,
|
||||
fakes: Dict[int, List[int]],
|
||||
imports: Dict[int, List[str]],
|
||||
externs: Dict[int, List[str]],
|
||||
) -> bool:
|
||||
offset = addr.getOffset()
|
||||
|
||||
fake = fakes.get(offset)
|
||||
if fake:
|
||||
return True
|
||||
|
||||
imp = imports.get(offset)
|
||||
if imp:
|
||||
return True
|
||||
|
||||
extern = externs.get(offset)
|
||||
if extern:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def is_call_or_jmp(insn: ghidra.program.database.code.InstructionDB) -> bool:
|
||||
return any(mnem in insn.getMnemonicString() for mnem in ["CALL", "J"]) # JMP, JNE, JNZ, etc
|
||||
|
||||
|
||||
def is_sp_modified(insn: ghidra.program.database.code.InstructionDB) -> bool:
|
||||
for i in range(insn.getNumOperands()):
|
||||
if insn.getOperandType(i) == OperandType.REGISTER:
|
||||
return "SP" in insn.getRegister(i).getName() and insn.getOperandRefType(i).isWrite()
|
||||
return False
|
||||
|
||||
|
||||
def is_stack_referenced(insn: ghidra.program.database.code.InstructionDB) -> bool:
|
||||
"""generic catch-all for stack references"""
|
||||
for i in range(insn.getNumOperands()):
|
||||
if insn.getOperandType(i) == OperandType.REGISTER:
|
||||
if "BP" in insn.getRegister(i).getName():
|
||||
return True
|
||||
else:
|
||||
continue
|
||||
|
||||
return any(ref.isStackReference() for ref in insn.getReferencesFrom())
|
||||
|
||||
|
||||
def is_zxor(insn: ghidra.program.database.code.InstructionDB) -> bool:
|
||||
# assume XOR insn
|
||||
# XOR's against the same operand zero out
|
||||
ops = []
|
||||
operands = []
|
||||
for i in range(insn.getNumOperands()):
|
||||
ops.append(insn.getOpObjects(i))
|
||||
|
||||
# Operands stored in a 2D array
|
||||
for j in range(len(ops)):
|
||||
for k in range(len(ops[j])):
|
||||
operands.append(ops[j][k])
|
||||
|
||||
return all(n == operands[0] for n in operands)
|
||||
|
||||
|
||||
def handle_thunk(addr: ghidra.program.model.address.Address):
|
||||
"""Follow thunk chains down to a reasonable depth"""
|
||||
ref = addr
|
||||
for _ in range(THUNK_CHAIN_DEPTH_DELTA):
|
||||
thunk_jmp = getInstructionAt(ref) # type: ignore [name-defined] # noqa: F821
|
||||
if thunk_jmp and is_call_or_jmp(thunk_jmp):
|
||||
if OperandType.isAddress(thunk_jmp.getOperandType(0)):
|
||||
ref = thunk_jmp.getAddress(0)
|
||||
else:
|
||||
thunk_dat = getDataContaining(ref) # type: ignore [name-defined] # noqa: F821
|
||||
if thunk_dat and thunk_dat.isDefined() and thunk_dat.isPointer():
|
||||
ref = thunk_dat.getValue()
|
||||
break # end of thunk chain reached
|
||||
return ref
|
||||
|
||||
|
||||
def dereference_ptr(insn: ghidra.program.database.code.InstructionDB):
|
||||
addr_code = OperandType.ADDRESS | OperandType.CODE
|
||||
to_deref = insn.getAddress(0)
|
||||
dat = getDataContaining(to_deref) # type: ignore [name-defined] # noqa: F821
|
||||
|
||||
if insn.getOperandType(0) == addr_code:
|
||||
thfunc = getFunctionContaining(to_deref) # type: ignore [name-defined] # noqa: F821
|
||||
if thfunc and thfunc.isThunk():
|
||||
return handle_thunk(to_deref)
|
||||
else:
|
||||
# if it doesn't poin to a thunk, it's usually a jmp to a label
|
||||
return to_deref
|
||||
if not dat:
|
||||
return to_deref
|
||||
if dat.isDefined() and dat.isPointer():
|
||||
addr = dat.getValue()
|
||||
# now we need to check the addr space to see if it is truly resolvable
|
||||
# ghidra sometimes likes to hand us direct RAM addrs, which typically point
|
||||
# to api calls that we can't actually resolve as such
|
||||
if addr.getAddressSpace().getType() == AddressSpace.TYPE_RAM:
|
||||
return to_deref
|
||||
else:
|
||||
return addr
|
||||
else:
|
||||
return to_deref
|
||||
521
capa/features/extractors/ghidra/insn.py
Normal file
521
capa/features/extractors/ghidra/insn.py
Normal file
@@ -0,0 +1,521 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
from typing import Any, Dict, Tuple, Iterator
|
||||
|
||||
import ghidra
|
||||
from ghidra.program.model.lang import OperandType
|
||||
from ghidra.program.model.block import SimpleBlockModel
|
||||
|
||||
import capa.features.extractors.helpers
|
||||
import capa.features.extractors.ghidra.helpers
|
||||
from capa.features.insn import API, MAX_STRUCTURE_SIZE, Number, Offset, Mnemonic, OperandNumber, OperandOffset
|
||||
from capa.features.common import MAX_BYTES_FEATURE_SIZE, Bytes, String, Feature, Characteristic
|
||||
from capa.features.address import Address, AbsoluteVirtualAddress
|
||||
from capa.features.extractors.base_extractor import BBHandle, InsnHandle, FunctionHandle
|
||||
|
||||
# security cookie checks may perform non-zeroing XORs, these are expected within a certain
|
||||
# byte range within the first and returning basic blocks, this helps to reduce FP features
|
||||
SECURITY_COOKIE_BYTES_DELTA = 0x40
|
||||
|
||||
|
||||
def get_imports(ctx: Dict[str, Any]) -> Dict[int, Any]:
|
||||
"""Populate the import cache for this context"""
|
||||
if "imports_cache" not in ctx:
|
||||
ctx["imports_cache"] = capa.features.extractors.ghidra.helpers.get_file_imports()
|
||||
return ctx["imports_cache"]
|
||||
|
||||
|
||||
def get_externs(ctx: Dict[str, Any]) -> Dict[int, Any]:
|
||||
"""Populate the externs cache for this context"""
|
||||
if "externs_cache" not in ctx:
|
||||
ctx["externs_cache"] = capa.features.extractors.ghidra.helpers.get_file_externs()
|
||||
return ctx["externs_cache"]
|
||||
|
||||
|
||||
def get_fakes(ctx: Dict[str, Any]) -> Dict[int, Any]:
|
||||
"""Populate the fake import addrs cache for this context"""
|
||||
if "fakes_cache" not in ctx:
|
||||
ctx["fakes_cache"] = capa.features.extractors.ghidra.helpers.map_fake_import_addrs()
|
||||
return ctx["fakes_cache"]
|
||||
|
||||
|
||||
def check_for_api_call(
|
||||
insn, externs: Dict[int, Any], fakes: Dict[int, Any], imports: Dict[int, Any], imp_or_ex: bool
|
||||
) -> Iterator[Any]:
|
||||
"""check instruction for API call
|
||||
|
||||
params:
|
||||
externs - external library functions cache
|
||||
fakes - mapped fake import addresses cache
|
||||
imports - imported functions cache
|
||||
imp_or_ex - flag to check imports or externs
|
||||
|
||||
yields:
|
||||
matched api calls
|
||||
"""
|
||||
info = ()
|
||||
funcs = imports if imp_or_ex else externs
|
||||
|
||||
# assume only CALLs or JMPs are passed
|
||||
ref_type = insn.getOperandType(0)
|
||||
addr_data = OperandType.ADDRESS | OperandType.DATA # needs dereferencing
|
||||
addr_code = OperandType.ADDRESS | OperandType.CODE # needs dereferencing
|
||||
|
||||
if OperandType.isRegister(ref_type):
|
||||
if OperandType.isAddress(ref_type):
|
||||
# If it's an address in a register, check the mapped fake addrs
|
||||
# since they're dereferenced to their fake addrs
|
||||
op_ref = insn.getAddress(0).getOffset()
|
||||
ref = fakes.get(op_ref) # obtain the real addr
|
||||
if not ref:
|
||||
return
|
||||
else:
|
||||
return
|
||||
elif ref_type in (addr_data, addr_code) or (OperandType.isIndirect(ref_type) and OperandType.isAddress(ref_type)):
|
||||
# we must dereference and check if the addr is a pointer to an api function
|
||||
addr_ref = capa.features.extractors.ghidra.helpers.dereference_ptr(insn)
|
||||
if not capa.features.extractors.ghidra.helpers.check_addr_for_api(addr_ref, fakes, imports, externs):
|
||||
return
|
||||
ref = addr_ref.getOffset()
|
||||
elif ref_type == OperandType.DYNAMIC | OperandType.ADDRESS or ref_type == OperandType.DYNAMIC:
|
||||
return # cannot resolve dynamics statically
|
||||
else:
|
||||
# pure address does not need to get dereferenced/ handled
|
||||
addr_ref = insn.getAddress(0)
|
||||
if not addr_ref:
|
||||
# If it returned null, it was an indirect
|
||||
# that had no address reference.
|
||||
# This check is faster than checking for (indirect and not address)
|
||||
return
|
||||
if not capa.features.extractors.ghidra.helpers.check_addr_for_api(addr_ref, fakes, imports, externs):
|
||||
return
|
||||
ref = addr_ref.getOffset()
|
||||
|
||||
if isinstance(ref, list): # ref from REG | ADDR
|
||||
for r in ref:
|
||||
info = funcs.get(r) # type: ignore
|
||||
if info:
|
||||
yield info
|
||||
else:
|
||||
info = funcs.get(ref) # type: ignore
|
||||
if info:
|
||||
yield info
|
||||
|
||||
|
||||
def extract_insn_api_features(fh: FunctionHandle, bb: BBHandle, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
insn: ghidra.program.database.code.InstructionDB = ih.inner
|
||||
|
||||
if not capa.features.extractors.ghidra.helpers.is_call_or_jmp(insn):
|
||||
return
|
||||
|
||||
externs = get_externs(fh.ctx)
|
||||
fakes = get_fakes(fh.ctx)
|
||||
imports = get_imports(fh.ctx)
|
||||
|
||||
# check calls to imported functions
|
||||
for api in check_for_api_call(insn, externs, fakes, imports, True):
|
||||
for imp in api:
|
||||
yield API(imp), ih.address
|
||||
|
||||
# check calls to extern functions
|
||||
for api in check_for_api_call(insn, externs, fakes, imports, False):
|
||||
for ext in api:
|
||||
yield API(ext), ih.address
|
||||
|
||||
|
||||
def extract_insn_number_features(fh: FunctionHandle, bb: BBHandle, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
parse instruction number features
|
||||
example:
|
||||
push 3136B0h ; dwControlCode
|
||||
"""
|
||||
insn: ghidra.program.database.code.InstructionDB = ih.inner
|
||||
|
||||
if insn.getMnemonicString().startswith("RET"):
|
||||
# skip things like:
|
||||
# .text:0042250E retn 8
|
||||
return
|
||||
|
||||
if capa.features.extractors.ghidra.helpers.is_sp_modified(insn):
|
||||
# skip things like:
|
||||
# .text:00401145 add esp, 0Ch
|
||||
return
|
||||
|
||||
for i in range(insn.getNumOperands()):
|
||||
# Exceptions for LEA insn:
|
||||
# invalid operand encoding, considered numbers instead of offsets
|
||||
# see: mimikatz.exe_:0x4018C0
|
||||
if insn.getOperandType(i) == OperandType.DYNAMIC and insn.getMnemonicString().startswith("LEA"):
|
||||
# Additional check, avoid yielding "wide" values (ex. mimikatz.exe:0x471EE6 LEA EBX, [ECX + EAX*0x4])
|
||||
op_objs = insn.getOpObjects(i)
|
||||
if len(op_objs) == 3: # ECX, EAX, 0x4
|
||||
continue
|
||||
|
||||
if isinstance(op_objs[-1], ghidra.program.model.scalar.Scalar):
|
||||
const = op_objs[-1].getUnsignedValue()
|
||||
addr = ih.address
|
||||
|
||||
yield Number(const), addr
|
||||
yield OperandNumber(i, const), addr
|
||||
elif not OperandType.isScalar(insn.getOperandType(i)):
|
||||
# skip things like:
|
||||
# references, void types
|
||||
continue
|
||||
else:
|
||||
const = insn.getScalar(i).getUnsignedValue()
|
||||
addr = ih.address
|
||||
|
||||
yield Number(const), addr
|
||||
yield OperandNumber(i, const), addr
|
||||
|
||||
if insn.getMnemonicString().startswith("ADD") and 0 < const < MAX_STRUCTURE_SIZE:
|
||||
# for pattern like:
|
||||
#
|
||||
# add eax, 0x10
|
||||
#
|
||||
# assume 0x10 is also an offset (imagine eax is a pointer).
|
||||
yield Offset(const), addr
|
||||
yield OperandOffset(i, const), addr
|
||||
|
||||
|
||||
def extract_insn_offset_features(fh: FunctionHandle, bb: BBHandle, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
parse instruction structure offset features
|
||||
|
||||
example:
|
||||
.text:0040112F cmp [esi+4], ebx
|
||||
"""
|
||||
insn: ghidra.program.database.code.InstructionDB = ih.inner
|
||||
|
||||
if insn.getMnemonicString().startswith("LEA"):
|
||||
return
|
||||
|
||||
# ignore any stack references
|
||||
if not capa.features.extractors.ghidra.helpers.is_stack_referenced(insn):
|
||||
# Ghidra stores operands in 2D arrays if they contain offsets
|
||||
for i in range(insn.getNumOperands()):
|
||||
if insn.getOperandType(i) == OperandType.DYNAMIC: # e.g. [esi + 4]
|
||||
# manual extraction, since the default api calls only work on the 1st dimension of the array
|
||||
op_objs = insn.getOpObjects(i)
|
||||
if isinstance(op_objs[-1], ghidra.program.model.scalar.Scalar):
|
||||
op_off = op_objs[-1].getValue()
|
||||
yield Offset(op_off), ih.address
|
||||
yield OperandOffset(i, op_off), ih.address
|
||||
else:
|
||||
yield Offset(0), ih.address
|
||||
yield OperandOffset(i, 0), ih.address
|
||||
|
||||
|
||||
def extract_insn_bytes_features(fh: FunctionHandle, bb: BBHandle, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
parse referenced byte sequences
|
||||
example:
|
||||
push offset iid_004118d4_IShellLinkA ; riid
|
||||
"""
|
||||
insn: ghidra.program.database.code.InstructionDB = ih.inner
|
||||
|
||||
if capa.features.extractors.ghidra.helpers.is_call_or_jmp(insn):
|
||||
return
|
||||
|
||||
ref = insn.getAddress() # init to insn addr
|
||||
for i in range(insn.getNumOperands()):
|
||||
if OperandType.isAddress(insn.getOperandType(i)):
|
||||
ref = insn.getAddress(i) # pulls pointer if there is one
|
||||
|
||||
if ref != insn.getAddress(): # bail out if there's no pointer
|
||||
ghidra_dat = getDataAt(ref) # type: ignore [name-defined] # noqa: F821
|
||||
if (
|
||||
ghidra_dat and not ghidra_dat.hasStringValue() and not ghidra_dat.isPointer()
|
||||
): # avoid if the data itself is a pointer
|
||||
extracted_bytes = capa.features.extractors.ghidra.helpers.get_bytes(ref, MAX_BYTES_FEATURE_SIZE)
|
||||
if extracted_bytes and not capa.features.extractors.helpers.all_zeros(extracted_bytes):
|
||||
# don't extract byte features for obvious strings
|
||||
yield Bytes(extracted_bytes), ih.address
|
||||
|
||||
|
||||
def extract_insn_string_features(fh: FunctionHandle, bb: BBHandle, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
parse instruction string features
|
||||
|
||||
example:
|
||||
push offset aAcr ; "ACR > "
|
||||
"""
|
||||
insn: ghidra.program.database.code.InstructionDB = ih.inner
|
||||
dyn_addr = OperandType.DYNAMIC | OperandType.ADDRESS
|
||||
|
||||
ref = insn.getAddress()
|
||||
for i in range(insn.getNumOperands()):
|
||||
if OperandType.isScalarAsAddress(insn.getOperandType(i)):
|
||||
ref = insn.getAddress(i)
|
||||
# strings are also referenced dynamically via pointers & arrays, so we need to deref them
|
||||
if insn.getOperandType(i) == dyn_addr:
|
||||
ref = insn.getAddress(i)
|
||||
dat = getDataAt(ref) # type: ignore [name-defined] # noqa: F821
|
||||
if dat and dat.isPointer():
|
||||
ref = dat.getValue()
|
||||
|
||||
if ref != insn.getAddress():
|
||||
ghidra_dat = getDataAt(ref) # type: ignore [name-defined] # noqa: F821
|
||||
if ghidra_dat and ghidra_dat.hasStringValue():
|
||||
yield String(ghidra_dat.getValue()), ih.address
|
||||
|
||||
|
||||
def extract_insn_mnemonic_features(
|
||||
fh: FunctionHandle, bb: BBHandle, ih: InsnHandle
|
||||
) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""parse instruction mnemonic features"""
|
||||
insn: ghidra.program.database.code.InstructionDB = ih.inner
|
||||
|
||||
yield Mnemonic(insn.getMnemonicString().lower()), ih.address
|
||||
|
||||
|
||||
def extract_insn_obfs_call_plus_5_characteristic_features(
|
||||
fh: FunctionHandle, bb: BBHandle, ih: InsnHandle
|
||||
) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
parse call $+5 instruction from the given instruction.
|
||||
"""
|
||||
insn: ghidra.program.database.code.InstructionDB = ih.inner
|
||||
|
||||
if not capa.features.extractors.ghidra.helpers.is_call_or_jmp(insn):
|
||||
return
|
||||
|
||||
code_ref = OperandType.ADDRESS | OperandType.CODE
|
||||
ref = insn.getAddress()
|
||||
for i in range(insn.getNumOperands()):
|
||||
if insn.getOperandType(i) == code_ref:
|
||||
ref = insn.getAddress(i)
|
||||
|
||||
if insn.getAddress().add(5) == ref:
|
||||
yield Characteristic("call $+5"), ih.address
|
||||
|
||||
|
||||
def extract_insn_segment_access_features(
|
||||
fh: FunctionHandle, bb: BBHandle, ih: InsnHandle
|
||||
) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""parse instruction fs or gs access"""
|
||||
insn: ghidra.program.database.code.InstructionDB = ih.inner
|
||||
|
||||
insn_str = insn.toString()
|
||||
|
||||
if "FS:" in insn_str:
|
||||
yield Characteristic("fs access"), ih.address
|
||||
|
||||
if "GS:" in insn_str:
|
||||
yield Characteristic("gs access"), ih.address
|
||||
|
||||
|
||||
def extract_insn_peb_access_characteristic_features(
|
||||
fh: FunctionHandle, bb: BBHandle, ih: InsnHandle
|
||||
) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""parse instruction peb access
|
||||
|
||||
fs:[0x30] on x86, gs:[0x60] on x64
|
||||
|
||||
"""
|
||||
insn: ghidra.program.database.code.InstructionDB = ih.inner
|
||||
|
||||
insn_str = insn.toString()
|
||||
if insn_str.startswith(("PUSH", "MOV")):
|
||||
if "FS:[0x30]" in insn_str or "GS:[0x60]" in insn_str:
|
||||
yield Characteristic("peb access"), ih.address
|
||||
|
||||
|
||||
def extract_insn_cross_section_cflow(
|
||||
fh: FunctionHandle, bb: BBHandle, ih: InsnHandle
|
||||
) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""inspect the instruction for a CALL or JMP that crosses section boundaries"""
|
||||
insn: ghidra.program.database.code.InstructionDB = ih.inner
|
||||
|
||||
if not capa.features.extractors.ghidra.helpers.is_call_or_jmp(insn):
|
||||
return
|
||||
|
||||
externs = get_externs(fh.ctx)
|
||||
fakes = get_fakes(fh.ctx)
|
||||
imports = get_imports(fh.ctx)
|
||||
|
||||
# OperandType to dereference
|
||||
addr_data = OperandType.ADDRESS | OperandType.DATA
|
||||
addr_code = OperandType.ADDRESS | OperandType.CODE
|
||||
|
||||
ref_type = insn.getOperandType(0)
|
||||
|
||||
# both OperandType flags must be present
|
||||
# bail on REGISTER alone
|
||||
if OperandType.isRegister(ref_type):
|
||||
if OperandType.isAddress(ref_type):
|
||||
ref = insn.getAddress(0) # Ghidra dereferences REG | ADDR
|
||||
if capa.features.extractors.ghidra.helpers.check_addr_for_api(ref, fakes, imports, externs):
|
||||
return
|
||||
else:
|
||||
return
|
||||
elif ref_type in (addr_data, addr_code) or (OperandType.isIndirect(ref_type) and OperandType.isAddress(ref_type)):
|
||||
# we must dereference and check if the addr is a pointer to an api function
|
||||
ref = capa.features.extractors.ghidra.helpers.dereference_ptr(insn)
|
||||
if capa.features.extractors.ghidra.helpers.check_addr_for_api(ref, fakes, imports, externs):
|
||||
return
|
||||
elif ref_type == OperandType.DYNAMIC | OperandType.ADDRESS or ref_type == OperandType.DYNAMIC:
|
||||
return # cannot resolve dynamics statically
|
||||
else:
|
||||
# pure address does not need to get dereferenced/ handled
|
||||
ref = insn.getAddress(0)
|
||||
if not ref:
|
||||
# If it returned null, it was an indirect
|
||||
# that had no address reference.
|
||||
# This check is faster than checking for (indirect and not address)
|
||||
return
|
||||
if capa.features.extractors.ghidra.helpers.check_addr_for_api(ref, fakes, imports, externs):
|
||||
return
|
||||
|
||||
this_mem_block = getMemoryBlock(insn.getAddress()) # type: ignore [name-defined] # noqa: F821
|
||||
ref_block = getMemoryBlock(ref) # type: ignore [name-defined] # noqa: F821
|
||||
if ref_block != this_mem_block:
|
||||
yield Characteristic("cross section flow"), ih.address
|
||||
|
||||
|
||||
def extract_function_calls_from(
|
||||
fh: FunctionHandle,
|
||||
bb: BBHandle,
|
||||
ih: InsnHandle,
|
||||
) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""extract functions calls from features
|
||||
|
||||
most relevant at the function scope, however, its most efficient to extract at the instruction scope
|
||||
"""
|
||||
insn: ghidra.program.database.code.InstructionDB = ih.inner
|
||||
|
||||
if insn.getMnemonicString().startswith("CALL"):
|
||||
# This method of "dereferencing" addresses/ pointers
|
||||
# is not as robust as methods in other functions,
|
||||
# but works just fine for this one
|
||||
reference = 0
|
||||
for ref in insn.getReferencesFrom():
|
||||
addr = ref.getToAddress()
|
||||
|
||||
# avoid returning fake addrs
|
||||
if not addr.isExternalAddress():
|
||||
reference = addr.getOffset()
|
||||
|
||||
# if a reference is < 0, then ghidra pulled an offset from a DYNAMIC | ADDR (usually a stackvar)
|
||||
# these cannot be resolved to actual addrs
|
||||
if reference > 0:
|
||||
yield Characteristic("calls from"), AbsoluteVirtualAddress(reference)
|
||||
|
||||
|
||||
def extract_function_indirect_call_characteristic_features(
|
||||
fh: FunctionHandle,
|
||||
bb: BBHandle,
|
||||
ih: InsnHandle,
|
||||
) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""extract indirect function calls (e.g., call eax or call dword ptr [edx+4])
|
||||
does not include calls like => call ds:dword_ABD4974
|
||||
|
||||
most relevant at the function or basic block scope;
|
||||
however, its most efficient to extract at the instruction scope
|
||||
"""
|
||||
insn: ghidra.program.database.code.InstructionDB = ih.inner
|
||||
|
||||
if insn.getMnemonicString().startswith("CALL"):
|
||||
if OperandType.isRegister(insn.getOperandType(0)):
|
||||
yield Characteristic("indirect call"), ih.address
|
||||
if OperandType.isIndirect(insn.getOperandType(0)):
|
||||
yield Characteristic("indirect call"), ih.address
|
||||
|
||||
|
||||
def check_nzxor_security_cookie_delta(
|
||||
fh: ghidra.program.database.function.FunctionDB, insn: ghidra.program.database.code.InstructionDB
|
||||
):
|
||||
"""Get the function containing the insn
|
||||
Get the last block of the function that contains the insn
|
||||
|
||||
Check the bb containing the insn
|
||||
Check the last bb of the function containing the insn
|
||||
"""
|
||||
|
||||
model = SimpleBlockModel(currentProgram()) # type: ignore [name-defined] # noqa: F821
|
||||
insn_addr = insn.getAddress()
|
||||
func_asv = fh.getBody()
|
||||
first_addr = func_asv.getMinAddress()
|
||||
last_addr = func_asv.getMaxAddress()
|
||||
|
||||
if model.getFirstCodeBlockContaining(
|
||||
first_addr, monitor() # type: ignore [name-defined] # noqa: F821
|
||||
) == model.getFirstCodeBlockContaining(
|
||||
last_addr, monitor() # type: ignore [name-defined] # noqa: F821
|
||||
):
|
||||
if insn_addr < first_addr.add(SECURITY_COOKIE_BYTES_DELTA):
|
||||
return True
|
||||
else:
|
||||
return insn_addr > last_addr.add(SECURITY_COOKIE_BYTES_DELTA * -1)
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def extract_insn_nzxor_characteristic_features(
|
||||
fh: FunctionHandle,
|
||||
bb: BBHandle,
|
||||
ih: InsnHandle,
|
||||
) -> Iterator[Tuple[Feature, Address]]:
|
||||
f: ghidra.program.database.function.FunctionDB = fh.inner
|
||||
insn: ghidra.program.database.code.InstructionDB = ih.inner
|
||||
|
||||
if "XOR" not in insn.getMnemonicString():
|
||||
return
|
||||
if capa.features.extractors.ghidra.helpers.is_stack_referenced(insn):
|
||||
return
|
||||
if capa.features.extractors.ghidra.helpers.is_zxor(insn):
|
||||
return
|
||||
if check_nzxor_security_cookie_delta(f, insn):
|
||||
return
|
||||
yield Characteristic("nzxor"), ih.address
|
||||
|
||||
|
||||
def extract_features(
|
||||
fh: FunctionHandle,
|
||||
bb: BBHandle,
|
||||
insn: InsnHandle,
|
||||
) -> Iterator[Tuple[Feature, Address]]:
|
||||
for insn_handler in INSTRUCTION_HANDLERS:
|
||||
for feature, addr in insn_handler(fh, bb, insn):
|
||||
yield feature, addr
|
||||
|
||||
|
||||
INSTRUCTION_HANDLERS = (
|
||||
extract_insn_api_features,
|
||||
extract_insn_number_features,
|
||||
extract_insn_bytes_features,
|
||||
extract_insn_string_features,
|
||||
extract_insn_offset_features,
|
||||
extract_insn_nzxor_characteristic_features,
|
||||
extract_insn_mnemonic_features,
|
||||
extract_insn_obfs_call_plus_5_characteristic_features,
|
||||
extract_insn_peb_access_characteristic_features,
|
||||
extract_insn_cross_section_cflow,
|
||||
extract_insn_segment_access_features,
|
||||
extract_function_calls_from,
|
||||
extract_function_indirect_call_characteristic_features,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
""" """
|
||||
features = []
|
||||
from capa.features.extractors.ghidra.extractor import GhidraFeatureExtractor
|
||||
|
||||
for fh in GhidraFeatureExtractor().get_functions():
|
||||
for bb in capa.features.extractors.ghidra.helpers.get_function_blocks(fh):
|
||||
for insn in capa.features.extractors.ghidra.helpers.get_insn_in_range(bb):
|
||||
features.extend(list(extract_features(fh, bb, insn)))
|
||||
|
||||
import pprint
|
||||
|
||||
pprint.pprint(features) # noqa: T203
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
@@ -6,70 +6,104 @@
|
||||
# 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 True
|
||||
|
||||
|
||||
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, include_dll=False) -> Iterator[str]:
|
||||
"""
|
||||
for a given dll and symbol name, generate variants.
|
||||
we over-generate features to make matching easier.
|
||||
these include:
|
||||
- kernel32.CreateFileA
|
||||
- kernel32.CreateFile
|
||||
- CreateFileA
|
||||
- CreateFile
|
||||
- ws2_32.#1
|
||||
|
||||
note that since capa v7 only `import` features and APIs called via ordinal include DLL names:
|
||||
- kernel32.CreateFileA
|
||||
- kernel32.CreateFile
|
||||
- ws2_32.#1
|
||||
|
||||
for `api` features dll names are good for documentation but not used during matching
|
||||
"""
|
||||
# (kernel32.CreateFileA, 0x401000)
|
||||
yield API(apiname), va
|
||||
# normalize dll name
|
||||
dll = dll.lower()
|
||||
|
||||
if is_aw_function(apiname):
|
||||
# (kernel32.CreateFile, 0x401000)
|
||||
yield API(apiname[:-1]), va
|
||||
# trim extensions observed in dynamic traces
|
||||
dll = dll[0:-4] if dll.endswith(".dll") else dll
|
||||
dll = dll[0:-4] if dll.endswith(".drv") else dll
|
||||
|
||||
if "." in apiname:
|
||||
modname, impname = apiname.split(".")
|
||||
# strip modname to support importname-only matching
|
||||
# (CreateFileA, 0x401000)
|
||||
yield API(impname), va
|
||||
if include_dll or is_ordinal(symbol):
|
||||
# ws2_32.#1
|
||||
# kernel32.CreateFileA
|
||||
yield f"{dll}.{symbol}"
|
||||
|
||||
if is_aw_function(impname):
|
||||
# (CreateFile, 0x401000)
|
||||
yield API(impname[:-1]), va
|
||||
if not is_ordinal(symbol):
|
||||
# CreateFileA
|
||||
yield symbol
|
||||
|
||||
if is_aw_function(symbol):
|
||||
if include_dll:
|
||||
# kernel32.CreateFile
|
||||
yield f"{dll}.{symbol[:-1]}"
|
||||
|
||||
# CreateFile
|
||||
yield symbol[:-1]
|
||||
|
||||
|
||||
def all_zeros(bytez):
|
||||
def reformat_forwarded_export_name(forwarded_name: str) -> str:
|
||||
"""
|
||||
a forwarded export has a DLL name/path and symbol name.
|
||||
we want the former to be lowercase, and the latter to be verbatim.
|
||||
"""
|
||||
|
||||
# use rpartition so we can split on separator between dll and name.
|
||||
# the dll name can be a full path, like in the case of
|
||||
# ef64d6d7c34250af8e21a10feb931c9b
|
||||
# which i assume means the path can have embedded periods.
|
||||
# so we don't want the first period, we want the last.
|
||||
forwarded_dll, _, forwarded_symbol = forwarded_name.rpartition(".")
|
||||
forwarded_dll = forwarded_dll.lower()
|
||||
|
||||
return f"{forwarded_dll}.{forwarded_symbol}"
|
||||
|
||||
|
||||
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 +116,48 @@ 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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
@@ -6,25 +6,23 @@
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import sys
|
||||
import string
|
||||
import struct
|
||||
from typing import Tuple, Iterator
|
||||
|
||||
import idaapi
|
||||
|
||||
import capa.features.extractors.ida.helpers
|
||||
from capa.features import Characteristic
|
||||
from capa.features.common import Feature, Characteristic
|
||||
from capa.features.address import Address
|
||||
from capa.features.basicblock import BasicBlock
|
||||
from capa.features.extractors.ida import helpers
|
||||
from capa.features.extractors.helpers import MIN_STACKSTRING_LEN
|
||||
from capa.features.extractors.base_extractor import BBHandle, FunctionHandle
|
||||
|
||||
|
||||
def get_printable_len(op):
|
||||
"""Return string length if all operand bytes are ascii or utf16-le printable
|
||||
|
||||
args:
|
||||
op (IDA op_t)
|
||||
"""
|
||||
def get_printable_len(op: idaapi.op_t) -> int:
|
||||
"""Return string length if all operand bytes are ascii or utf16-le printable"""
|
||||
op_val = capa.features.extractors.ida.helpers.mask_op_val(op)
|
||||
|
||||
if op.dtype == idaapi.dt_byte:
|
||||
@@ -36,21 +34,14 @@ def get_printable_len(op):
|
||||
elif op.dtype == idaapi.dt_qword:
|
||||
chars = struct.pack("<Q", op_val)
|
||||
else:
|
||||
raise ValueError("Unhandled operand data type 0x%x." % op.dtype)
|
||||
raise ValueError(f"Unhandled operand data type 0x{op.dtype:x}.")
|
||||
|
||||
def is_printable_ascii(chars):
|
||||
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)
|
||||
def is_printable_ascii(chars_: bytes):
|
||||
return all(c < 127 and chr(c) in string.printable for c in chars_)
|
||||
|
||||
def is_printable_utf16le(chars):
|
||||
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])
|
||||
def is_printable_utf16le(chars_: bytes):
|
||||
if all(c == 0x00 for c in chars_[1::2]):
|
||||
return is_printable_ascii(chars_[::2])
|
||||
|
||||
if is_printable_ascii(chars):
|
||||
return idaapi.get_dtype_size(op.dtype)
|
||||
@@ -61,12 +52,8 @@ def get_printable_len(op):
|
||||
return 0
|
||||
|
||||
|
||||
def is_mov_imm_to_stack(insn):
|
||||
"""verify instruction moves immediate onto stack
|
||||
|
||||
args:
|
||||
insn (IDA insn_t)
|
||||
"""
|
||||
def is_mov_imm_to_stack(insn: idaapi.insn_t) -> bool:
|
||||
"""verify instruction moves immediate onto stack"""
|
||||
if insn.Op2.type != idaapi.o_imm:
|
||||
return False
|
||||
|
||||
@@ -79,14 +66,10 @@ def is_mov_imm_to_stack(insn):
|
||||
return True
|
||||
|
||||
|
||||
def bb_contains_stackstring(f, bb):
|
||||
def bb_contains_stackstring(f: idaapi.func_t, bb: idaapi.BasicBlock) -> bool:
|
||||
"""check basic block for stackstring indicators
|
||||
|
||||
true if basic block contains enough moves of constant bytes to the stack
|
||||
|
||||
args:
|
||||
f (IDA func_t)
|
||||
bb (IDA BasicBlock)
|
||||
"""
|
||||
count = 0
|
||||
for insn in capa.features.extractors.ida.helpers.get_instructions_in_range(bb.start_ea, bb.end_ea):
|
||||
@@ -97,57 +80,27 @@ def bb_contains_stackstring(f, bb):
|
||||
return False
|
||||
|
||||
|
||||
def extract_bb_stackstring(f, bb):
|
||||
"""extract stackstring indicators from basic block
|
||||
|
||||
args:
|
||||
f (IDA func_t)
|
||||
bb (IDA BasicBlock)
|
||||
"""
|
||||
if bb_contains_stackstring(f, bb):
|
||||
yield Characteristic("stack string"), bb.start_ea
|
||||
def extract_bb_stackstring(fh: FunctionHandle, bbh: BBHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""extract stackstring indicators from basic block"""
|
||||
if bb_contains_stackstring(fh.inner, bbh.inner):
|
||||
yield Characteristic("stack string"), bbh.address
|
||||
|
||||
|
||||
def extract_bb_tight_loop(f, bb):
|
||||
"""extract tight loop indicators from a basic block
|
||||
|
||||
args:
|
||||
f (IDA func_t)
|
||||
bb (IDA BasicBlock)
|
||||
"""
|
||||
if capa.features.extractors.ida.helpers.is_basic_block_tight_loop(bb):
|
||||
yield Characteristic("tight loop"), bb.start_ea
|
||||
def extract_bb_tight_loop(fh: FunctionHandle, bbh: BBHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""extract tight loop indicators from a basic block"""
|
||||
if capa.features.extractors.ida.helpers.is_basic_block_tight_loop(bbh.inner):
|
||||
yield Characteristic("tight loop"), bbh.address
|
||||
|
||||
|
||||
def extract_features(f, bb):
|
||||
"""extract basic block features
|
||||
|
||||
args:
|
||||
f (IDA func_t)
|
||||
bb (IDA BasicBlock)
|
||||
"""
|
||||
def extract_features(fh: FunctionHandle, bbh: BBHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""extract basic block features"""
|
||||
for bb_handler in BASIC_BLOCK_HANDLERS:
|
||||
for (feature, ea) in bb_handler(f, bb):
|
||||
yield feature, ea
|
||||
yield BasicBlock(), bb.start_ea
|
||||
for feature, addr in bb_handler(fh, bbh):
|
||||
yield feature, addr
|
||||
yield BasicBlock(), bbh.address
|
||||
|
||||
|
||||
BASIC_BLOCK_HANDLERS = (
|
||||
extract_bb_tight_loop,
|
||||
extract_bb_stackstring,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
features = []
|
||||
for f in helpers.get_functions(skip_thunks=True, skip_libs=True):
|
||||
for bb in idaapi.FlowChart(f, flags=idaapi.FC_PREDS):
|
||||
features.extend(list(extract_features(f, bb)))
|
||||
|
||||
import pprint
|
||||
|
||||
pprint.pprint(features)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
82
capa/features/extractors/ida/extractor.py
Normal file
82
capa/features/extractors/ida/extractor.py
Normal file
@@ -0,0 +1,82 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
from typing import List, Tuple, Iterator
|
||||
|
||||
import idaapi
|
||||
import ida_nalt
|
||||
|
||||
import capa.ida.helpers
|
||||
import capa.features.extractors.elf
|
||||
import capa.features.extractors.ida.file
|
||||
import capa.features.extractors.ida.insn
|
||||
import capa.features.extractors.ida.global_
|
||||
import capa.features.extractors.ida.function
|
||||
import capa.features.extractors.ida.basicblock
|
||||
from capa.features.common import Feature
|
||||
from capa.features.address import Address, AbsoluteVirtualAddress
|
||||
from capa.features.extractors.base_extractor import (
|
||||
BBHandle,
|
||||
InsnHandle,
|
||||
SampleHashes,
|
||||
FunctionHandle,
|
||||
StaticFeatureExtractor,
|
||||
)
|
||||
|
||||
|
||||
class IdaFeatureExtractor(StaticFeatureExtractor):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
hashes=SampleHashes(
|
||||
md5=ida_nalt.retrieve_input_file_md5(), sha1="(unknown)", sha256=ida_nalt.retrieve_input_file_sha256()
|
||||
)
|
||||
)
|
||||
self.global_features: List[Tuple[Feature, Address]] = []
|
||||
self.global_features.extend(capa.features.extractors.ida.file.extract_file_format())
|
||||
self.global_features.extend(capa.features.extractors.ida.global_.extract_os())
|
||||
self.global_features.extend(capa.features.extractors.ida.global_.extract_arch())
|
||||
|
||||
def get_base_address(self):
|
||||
return AbsoluteVirtualAddress(idaapi.get_imagebase())
|
||||
|
||||
def extract_global_features(self):
|
||||
yield from self.global_features
|
||||
|
||||
def extract_file_features(self):
|
||||
yield from capa.features.extractors.ida.file.extract_features()
|
||||
|
||||
def get_functions(self) -> Iterator[FunctionHandle]:
|
||||
import capa.features.extractors.ida.helpers as ida_helpers
|
||||
|
||||
# ignore library functions and thunk functions as identified by IDA
|
||||
yield from ida_helpers.get_functions(skip_thunks=True, skip_libs=True)
|
||||
|
||||
@staticmethod
|
||||
def get_function(ea: int) -> FunctionHandle:
|
||||
f = idaapi.get_func(ea)
|
||||
return FunctionHandle(address=AbsoluteVirtualAddress(f.start_ea), inner=f)
|
||||
|
||||
def extract_function_features(self, fh: FunctionHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
yield from capa.features.extractors.ida.function.extract_features(fh)
|
||||
|
||||
def get_basic_blocks(self, fh: FunctionHandle) -> Iterator[BBHandle]:
|
||||
import capa.features.extractors.ida.helpers as ida_helpers
|
||||
|
||||
for bb in ida_helpers.get_function_blocks(fh.inner):
|
||||
yield BBHandle(address=AbsoluteVirtualAddress(bb.start_ea), inner=bb)
|
||||
|
||||
def extract_basic_block_features(self, fh: FunctionHandle, bbh: BBHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
yield from capa.features.extractors.ida.basicblock.extract_features(fh, bbh)
|
||||
|
||||
def get_instructions(self, fh: FunctionHandle, bbh: BBHandle) -> Iterator[InsnHandle]:
|
||||
import capa.features.extractors.ida.helpers as ida_helpers
|
||||
|
||||
for insn in ida_helpers.get_instructions_in_range(bbh.inner.start_ea, bbh.inner.end_ea):
|
||||
yield InsnHandle(address=AbsoluteVirtualAddress(insn.ea), inner=insn)
|
||||
|
||||
def extract_insn_features(self, fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle):
|
||||
yield from capa.features.extractors.ida.insn.extract_features(fh, bbh, ih)
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
@@ -7,26 +7,29 @@
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import struct
|
||||
from typing import Tuple, Iterator
|
||||
|
||||
import idc
|
||||
import idaapi
|
||||
import idautils
|
||||
import ida_entry
|
||||
|
||||
import capa.features.extractors.common
|
||||
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 FORMAT_PE, FORMAT_ELF, Format, String, Feature, Characteristic
|
||||
from capa.features.address import NO_ADDRESS, Address, FileOffsetAddress, AbsoluteVirtualAddress
|
||||
|
||||
MAX_OFFSET_PE_AFTER_MZ = 0x200
|
||||
|
||||
|
||||
def check_segment_for_pe(seg):
|
||||
def check_segment_for_pe(seg: idaapi.segment_t) -> Iterator[Tuple[int, int]]:
|
||||
"""check segment for embedded PE
|
||||
|
||||
adapted for IDA from:
|
||||
https://github.com/vivisect/vivisect/blob/7be4037b1cecc4551b397f840405a1fc606f9b53/PE/carve.py#L19
|
||||
|
||||
args:
|
||||
seg (IDA segment_t)
|
||||
https://github.com/vivisect/vivisect/blob/91e8419a861f49779f18316f155311967e696836/PE/carve.py#L25
|
||||
"""
|
||||
seg_max = seg.end_ea
|
||||
mz_xor = [
|
||||
@@ -37,16 +40,17 @@ 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:
|
||||
# find all segment offsets containing XOR'd "MZ" bytes
|
||||
for off in capa.features.extractors.ida.helpers.find_byte_sequence(seg.start_ea, seg.end_ea, mzx):
|
||||
todo.append((off, mzx, pex, i))
|
||||
|
||||
while len(todo):
|
||||
off, mzx, pex, i = todo.pop()
|
||||
|
||||
# The MZ header has one field we will check e_lfanew is at 0x3c
|
||||
# MZ header has one field we will check e_lfanew is at 0x3c
|
||||
e_lfanew = off + 0x3C
|
||||
|
||||
if seg_max < (e_lfanew + 4):
|
||||
@@ -54,19 +58,19 @@ def check_segment_for_pe(seg):
|
||||
|
||||
newoff = struct.unpack("<I", capa.features.extractors.helpers.xor_static(idc.get_bytes(e_lfanew, 4), i))[0]
|
||||
|
||||
# assume XOR'd "PE" bytes exist within threshold
|
||||
if newoff > MAX_OFFSET_PE_AFTER_MZ:
|
||||
continue
|
||||
|
||||
peoff = off + newoff
|
||||
if seg_max < (peoff + 2):
|
||||
continue
|
||||
|
||||
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:
|
||||
todo.append((nextres, mzx, pex, i))
|
||||
yield off, i
|
||||
|
||||
|
||||
def extract_file_embedded_pe():
|
||||
def extract_file_embedded_pe() -> Iterator[Tuple[Feature, Address]]:
|
||||
"""extract embedded PE features
|
||||
|
||||
IDA must load resource sections for this to be complete
|
||||
@@ -74,17 +78,23 @@ def extract_file_embedded_pe():
|
||||
- Check 'Load resource sections' when opening binary in IDA manually
|
||||
"""
|
||||
for seg in capa.features.extractors.ida.helpers.get_segments(skip_header_segments=True):
|
||||
for (ea, _) in check_segment_for_pe(seg):
|
||||
yield Characteristic("embedded pe"), ea
|
||||
for ea, _ in check_segment_for_pe(seg):
|
||||
yield Characteristic("embedded pe"), FileOffsetAddress(ea)
|
||||
|
||||
|
||||
def extract_file_export_names():
|
||||
""" extract function exports """
|
||||
for (_, _, ea, name) in idautils.Entries():
|
||||
yield Export(name), ea
|
||||
def extract_file_export_names() -> Iterator[Tuple[Feature, Address]]:
|
||||
"""extract function exports"""
|
||||
for _, ordinal, ea, name in idautils.Entries():
|
||||
forwarded_name = ida_entry.get_entry_forwarder(ordinal)
|
||||
if forwarded_name is None:
|
||||
yield Export(name), AbsoluteVirtualAddress(ea)
|
||||
else:
|
||||
forwarded_name = capa.features.extractors.helpers.reformat_forwarded_export_name(forwarded_name)
|
||||
yield Export(forwarded_name), AbsoluteVirtualAddress(ea)
|
||||
yield Characteristic("forwarded export"), AbsoluteVirtualAddress(ea)
|
||||
|
||||
|
||||
def extract_file_import_names():
|
||||
def extract_file_import_names() -> Iterator[Tuple[Feature, Address]]:
|
||||
"""extract function imports
|
||||
|
||||
1. imports by ordinal:
|
||||
@@ -95,15 +105,32 @@ def extract_file_import_names():
|
||||
- modulename.importname
|
||||
- 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
|
||||
for ea, info in capa.features.extractors.ida.helpers.get_file_imports().items():
|
||||
addr = AbsoluteVirtualAddress(ea)
|
||||
if info[1] and info[2]:
|
||||
# e.g. in mimikatz: ('cabinet', 'FCIAddFile', 11L)
|
||||
# extract by name here and by ordinal below
|
||||
for name in capa.features.extractors.helpers.generate_symbols(info[0], info[1], include_dll=True):
|
||||
yield Import(name), addr
|
||||
dll = info[0]
|
||||
symbol = f"#{info[2]}"
|
||||
elif info[1]:
|
||||
dll = info[0]
|
||||
symbol = info[1]
|
||||
elif info[2]:
|
||||
dll = info[0]
|
||||
symbol = f"#{info[2]}"
|
||||
else:
|
||||
continue
|
||||
|
||||
for name in capa.features.extractors.helpers.generate_symbols(dll, symbol, include_dll=True):
|
||||
yield Import(name), addr
|
||||
|
||||
for ea, info in capa.features.extractors.ida.helpers.get_file_externs().items():
|
||||
yield Import(info[1]), AbsoluteVirtualAddress(ea)
|
||||
|
||||
|
||||
def extract_file_section_names():
|
||||
def extract_file_section_names() -> Iterator[Tuple[Feature, Address]]:
|
||||
"""extract section names
|
||||
|
||||
IDA must load resource sections for this to be complete
|
||||
@@ -111,10 +138,10 @@ def extract_file_section_names():
|
||||
- Check 'Load resource sections' when opening binary in IDA manually
|
||||
"""
|
||||
for seg in capa.features.extractors.ida.helpers.get_segments(skip_header_segments=True):
|
||||
yield Section(idaapi.get_segm_name(seg)), seg.start_ea
|
||||
yield Section(idaapi.get_segm_name(seg)), AbsoluteVirtualAddress(seg.start_ea)
|
||||
|
||||
|
||||
def extract_file_strings():
|
||||
def extract_file_strings() -> Iterator[Tuple[Feature, Address]]:
|
||||
"""extract ASCII and UTF-16 LE strings
|
||||
|
||||
IDA must load resource sections for this to be complete
|
||||
@@ -124,18 +151,50 @@ def extract_file_strings():
|
||||
for seg in capa.features.extractors.ida.helpers.get_segments():
|
||||
seg_buff = capa.features.extractors.ida.helpers.get_segment_buffer(seg)
|
||||
|
||||
# differing to common string extractor factor in segment offset here
|
||||
for s in capa.features.extractors.strings.extract_ascii_strings(seg_buff):
|
||||
yield String(s.s), (seg.start_ea + s.offset)
|
||||
yield String(s.s), FileOffsetAddress(seg.start_ea + s.offset)
|
||||
|
||||
for s in capa.features.extractors.strings.extract_unicode_strings(seg_buff):
|
||||
yield String(s.s), (seg.start_ea + s.offset)
|
||||
yield String(s.s), FileOffsetAddress(seg.start_ea + s.offset)
|
||||
|
||||
|
||||
def extract_features():
|
||||
""" extract file features """
|
||||
def extract_file_function_names() -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
extract the names of statically-linked library functions.
|
||||
"""
|
||||
for ea in idautils.Functions():
|
||||
addr = AbsoluteVirtualAddress(ea)
|
||||
if idaapi.get_func(ea).flags & idaapi.FUNC_LIB:
|
||||
name = idaapi.get_name(ea)
|
||||
yield FunctionName(name), addr
|
||||
if name.startswith("_"):
|
||||
# some linkers may prefix linked routines with a `_` to avoid name collisions.
|
||||
# extract features for both the mangled and un-mangled representations.
|
||||
# e.g. `_fwrite` -> `fwrite`
|
||||
# see: https://stackoverflow.com/a/2628384/87207
|
||||
yield FunctionName(name[1:]), addr
|
||||
|
||||
|
||||
def extract_file_format() -> Iterator[Tuple[Feature, Address]]:
|
||||
file_info = idaapi.get_inf_structure()
|
||||
|
||||
if file_info.filetype in (idaapi.f_PE, idaapi.f_COFF):
|
||||
yield Format(FORMAT_PE), NO_ADDRESS
|
||||
elif file_info.filetype == idaapi.f_ELF:
|
||||
yield Format(FORMAT_ELF), NO_ADDRESS
|
||||
elif file_info.filetype == idaapi.f_BIN:
|
||||
# no file type to return when processing a binary file, but we want to continue processing
|
||||
return
|
||||
else:
|
||||
raise NotImplementedError(f"unexpected file format: {file_info.filetype}")
|
||||
|
||||
|
||||
def extract_features() -> Iterator[Tuple[Feature, Address]]:
|
||||
"""extract file features"""
|
||||
for file_handler in FILE_HANDLERS:
|
||||
for feature, va in file_handler():
|
||||
yield feature, va
|
||||
for feature, addr in file_handler():
|
||||
yield feature, addr
|
||||
|
||||
|
||||
FILE_HANDLERS = (
|
||||
@@ -144,15 +203,6 @@ FILE_HANDLERS = (
|
||||
extract_file_strings,
|
||||
extract_file_section_names,
|
||||
extract_file_embedded_pe,
|
||||
extract_file_function_names,
|
||||
extract_file_format,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
""" """
|
||||
import pprint
|
||||
|
||||
pprint.pprint(list(extract_features()))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -1,35 +1,31 @@
|
||||
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
from typing import Tuple, Iterator
|
||||
|
||||
import idaapi
|
||||
import idautils
|
||||
|
||||
import capa.features.extractors.ida.helpers
|
||||
from capa.features import Characteristic
|
||||
from capa.features.common import Feature, Characteristic
|
||||
from capa.features.address import Address, AbsoluteVirtualAddress
|
||||
from capa.features.extractors import loops
|
||||
from capa.features.extractors.base_extractor import FunctionHandle
|
||||
|
||||
|
||||
def extract_function_calls_to(f):
|
||||
"""extract callers to a function
|
||||
|
||||
args:
|
||||
f (IDA func_t)
|
||||
"""
|
||||
for ea in idautils.CodeRefsTo(f.start_ea, True):
|
||||
yield Characteristic("calls to"), ea
|
||||
def extract_function_calls_to(fh: FunctionHandle):
|
||||
"""extract callers to a function"""
|
||||
for ea in idautils.CodeRefsTo(fh.inner.start_ea, True):
|
||||
yield Characteristic("calls to"), AbsoluteVirtualAddress(ea)
|
||||
|
||||
|
||||
def extract_function_loop(f):
|
||||
"""extract loop indicators from a function
|
||||
|
||||
args:
|
||||
f (IDA func_t)
|
||||
"""
|
||||
def extract_function_loop(fh: FunctionHandle):
|
||||
"""extract loop indicators from a function"""
|
||||
f: idaapi.func_t = fh.inner
|
||||
edges = []
|
||||
|
||||
# construct control flow graph
|
||||
@@ -38,43 +34,19 @@ def extract_function_loop(f):
|
||||
edges.append((bb.start_ea, succ.start_ea))
|
||||
|
||||
if loops.has_loop(edges):
|
||||
yield Characteristic("loop"), f.start_ea
|
||||
yield Characteristic("loop"), fh.address
|
||||
|
||||
|
||||
def extract_recursive_call(f):
|
||||
"""extract recursive function call
|
||||
|
||||
args:
|
||||
f (IDA func_t)
|
||||
"""
|
||||
if capa.features.extractors.ida.helpers.is_function_recursive(f):
|
||||
yield Characteristic("recursive call"), f.start_ea
|
||||
def extract_recursive_call(fh: FunctionHandle):
|
||||
"""extract recursive function call"""
|
||||
if capa.features.extractors.ida.helpers.is_function_recursive(fh.inner):
|
||||
yield Characteristic("recursive call"), fh.address
|
||||
|
||||
|
||||
def extract_features(f):
|
||||
"""extract function features
|
||||
|
||||
arg:
|
||||
f (IDA func_t)
|
||||
"""
|
||||
def extract_features(fh: FunctionHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
for func_handler in FUNCTION_HANDLERS:
|
||||
for (feature, ea) in func_handler(f):
|
||||
yield feature, ea
|
||||
for feature, addr in func_handler(fh):
|
||||
yield feature, addr
|
||||
|
||||
|
||||
FUNCTION_HANDLERS = (extract_function_calls_to, extract_function_loop, extract_recursive_call)
|
||||
|
||||
|
||||
def main():
|
||||
""" """
|
||||
features = []
|
||||
for f in capa.features.extractors.ida.get_functions(skip_thunks=True, skip_libs=True):
|
||||
features.extend(list(extract_features(f)))
|
||||
|
||||
import pprint
|
||||
|
||||
pprint.pprint(features)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
65
capa/features/extractors/ida/global_.py
Normal file
65
capa/features/extractors/ida/global_.py
Normal file
@@ -0,0 +1,65 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
import logging
|
||||
import contextlib
|
||||
from typing import Tuple, Iterator
|
||||
|
||||
import idaapi
|
||||
import ida_loader
|
||||
|
||||
import capa.ida.helpers
|
||||
import capa.features.extractors.elf
|
||||
from capa.features.common import OS, ARCH_I386, ARCH_AMD64, OS_WINDOWS, Arch, Feature
|
||||
from capa.features.address import NO_ADDRESS, Address
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def extract_os() -> Iterator[Tuple[Feature, Address]]:
|
||||
format_name: str = ida_loader.get_file_type_name()
|
||||
|
||||
if "PE" in format_name:
|
||||
yield OS(OS_WINDOWS), NO_ADDRESS
|
||||
|
||||
elif "ELF" in format_name:
|
||||
with contextlib.closing(capa.ida.helpers.IDAIO()) as f:
|
||||
os = capa.features.extractors.elf.detect_elf_os(f)
|
||||
|
||||
yield OS(os), NO_ADDRESS
|
||||
|
||||
else:
|
||||
# we likely end up here:
|
||||
# 1. handling shellcode, or
|
||||
# 2. handling a new file format (e.g. macho)
|
||||
#
|
||||
# for (1) we can't do much - its shellcode and all bets are off.
|
||||
# we could maybe accept a further CLI argument to specify the OS,
|
||||
# but i think this would be rarely used.
|
||||
# rules that rely on OS conditions will fail to match on shellcode.
|
||||
#
|
||||
# for (2), this logic will need to be updated as the format is implemented.
|
||||
logger.debug("unsupported file format: %s, will not guess OS", format_name)
|
||||
return
|
||||
|
||||
|
||||
def extract_arch() -> Iterator[Tuple[Feature, Address]]:
|
||||
info: idaapi.idainfo = idaapi.get_inf_structure()
|
||||
if info.procname == "metapc" and info.is_64bit():
|
||||
yield Arch(ARCH_AMD64), NO_ADDRESS
|
||||
elif info.procname == "metapc" and info.is_32bit():
|
||||
yield Arch(ARCH_I386), NO_ADDRESS
|
||||
elif info.procname == "metapc":
|
||||
logger.debug("unsupported architecture: non-32-bit nor non-64-bit intel")
|
||||
return
|
||||
else:
|
||||
# we likely end up here:
|
||||
# 1. handling a new architecture (e.g. aarch64)
|
||||
#
|
||||
# for (1), this logic will need to be updated as the format is implemented.
|
||||
logger.debug("unsupported architecture: %s", info.procname)
|
||||
return
|
||||
@@ -1,50 +1,58 @@
|
||||
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import sys
|
||||
import string
|
||||
import functools
|
||||
from typing import Any, Dict, Tuple, Iterator, Optional
|
||||
|
||||
import idc
|
||||
import idaapi
|
||||
import idautils
|
||||
import ida_bytes
|
||||
import ida_segment
|
||||
|
||||
from capa.features.address import AbsoluteVirtualAddress
|
||||
from capa.features.extractors.base_extractor import FunctionHandle
|
||||
|
||||
|
||||
def find_byte_sequence(start, end, seq):
|
||||
"""find byte sequence
|
||||
def find_byte_sequence(start: int, end: int, seq: bytes) -> Iterator[int]:
|
||||
"""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)
|
||||
seqstr = " ".join([f"{b:02x}" for b in seq])
|
||||
while True:
|
||||
# TODO(mike-hunhoff): find_binary is deprecated. Please use ida_bytes.bin_search() instead.
|
||||
# https://github.com/mandiant/capa/issues/1606
|
||||
ea = idaapi.find_binary(start, end, seqstr, 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):
|
||||
def get_functions(
|
||||
start: Optional[int] = None, end: Optional[int] = None, skip_thunks: bool = False, skip_libs: bool = False
|
||||
) -> Iterator[FunctionHandle]:
|
||||
"""get functions, range optional
|
||||
|
||||
args:
|
||||
start: min virtual address
|
||||
end: max virtual address
|
||||
|
||||
ret:
|
||||
yield func_t*
|
||||
"""
|
||||
for ea in idautils.Functions(start=start, end=end):
|
||||
f = idaapi.get_func(ea)
|
||||
if not (skip_thunks and (f.flags & idaapi.FUNC_THUNK) or skip_libs and (f.flags & idaapi.FUNC_LIB)):
|
||||
yield f
|
||||
yield FunctionHandle(address=AbsoluteVirtualAddress(ea), inner=f)
|
||||
|
||||
|
||||
def get_segments(skip_header_segments=False):
|
||||
def get_segments(skip_header_segments=False) -> Iterator[idaapi.segment_t]:
|
||||
"""get list of segments (sections) in the binary image
|
||||
|
||||
args:
|
||||
@@ -56,7 +64,7 @@ def get_segments(skip_header_segments=False):
|
||||
yield seg
|
||||
|
||||
|
||||
def get_segment_buffer(seg):
|
||||
def get_segment_buffer(seg: idaapi.segment_t) -> bytes:
|
||||
"""return bytes stored in a given segment
|
||||
|
||||
decrease buffer size until IDA is able to read bytes from the segment
|
||||
@@ -74,9 +82,22 @@ def get_segment_buffer(seg):
|
||||
return buff if buff else b""
|
||||
|
||||
|
||||
def get_file_imports():
|
||||
""" get file imports """
|
||||
imports = {}
|
||||
def inspect_import(imports, library, ea, function, ordinal):
|
||||
if function and function.startswith("__imp_"):
|
||||
# handle mangled PE imports
|
||||
function = function[len("__imp_") :]
|
||||
|
||||
if function and "@@" in function:
|
||||
# handle mangled ELF imports, like "fopen@@glibc_2.2.5"
|
||||
function, _, _ = function.partition("@@")
|
||||
|
||||
imports[ea] = (library.lower(), function, ordinal)
|
||||
return True
|
||||
|
||||
|
||||
def get_file_imports() -> Dict[int, Tuple[str, str, int]]:
|
||||
"""get file imports"""
|
||||
imports: Dict[int, Tuple[str, str, int]] = {}
|
||||
|
||||
for idx in range(idaapi.get_import_module_qty()):
|
||||
library = idaapi.get_import_module_name(idx)
|
||||
@@ -84,26 +105,38 @@ def get_file_imports():
|
||||
if not library:
|
||||
continue
|
||||
|
||||
def inspect_import(ea, function, ordinal):
|
||||
if function and function.startswith("__imp_"):
|
||||
# handle mangled names starting
|
||||
function = function[len("__imp_") :]
|
||||
imports[ea] = (library.lower(), function, ordinal)
|
||||
return True
|
||||
# IDA uses section names for the library of ELF imports, like ".dynsym".
|
||||
# These are not useful to us, we may need to expand this list over time
|
||||
# TODO(williballenthin): find all section names used by IDA
|
||||
# https://github.com/mandiant/capa/issues/1419
|
||||
if library == ".dynsym":
|
||||
library = ""
|
||||
|
||||
idaapi.enum_import_names(idx, inspect_import)
|
||||
cb = functools.partial(inspect_import, imports, library)
|
||||
idaapi.enum_import_names(idx, cb)
|
||||
|
||||
return imports
|
||||
|
||||
|
||||
def get_instructions_in_range(start, end):
|
||||
def get_file_externs() -> Dict[int, Tuple[str, str, int]]:
|
||||
externs = {}
|
||||
|
||||
for seg in get_segments(skip_header_segments=True):
|
||||
if seg.type != ida_segment.SEG_XTRN:
|
||||
continue
|
||||
|
||||
for ea in idautils.Functions(seg.start_ea, seg.end_ea):
|
||||
externs[ea] = ("", idaapi.get_func_name(ea), -1)
|
||||
|
||||
return externs
|
||||
|
||||
|
||||
def get_instructions_in_range(start: int, end: int) -> Iterator[idaapi.insn_t]:
|
||||
"""yield instructions in range
|
||||
|
||||
args:
|
||||
start: virtual address (inclusive)
|
||||
end: virtual address (exclusive)
|
||||
yield:
|
||||
(insn_t*)
|
||||
"""
|
||||
for head in idautils.Heads(start, end):
|
||||
insn = idautils.DecodeInstruction(head)
|
||||
@@ -111,8 +144,8 @@ def get_instructions_in_range(start, end):
|
||||
yield insn
|
||||
|
||||
|
||||
def is_operand_equal(op1, op2):
|
||||
""" compare two IDA op_t """
|
||||
def is_operand_equal(op1: idaapi.op_t, op2: idaapi.op_t) -> bool:
|
||||
"""compare two IDA op_t"""
|
||||
if op1.flags != op2.flags:
|
||||
return False
|
||||
|
||||
@@ -137,8 +170,8 @@ def is_operand_equal(op1, op2):
|
||||
return True
|
||||
|
||||
|
||||
def is_basic_block_equal(bb1, bb2):
|
||||
""" compare two IDA BasicBlock """
|
||||
def is_basic_block_equal(bb1: idaapi.BasicBlock, bb2: idaapi.BasicBlock) -> bool:
|
||||
"""compare two IDA BasicBlock"""
|
||||
if bb1.start_ea != bb2.start_ea:
|
||||
return False
|
||||
|
||||
@@ -151,13 +184,17 @@ def is_basic_block_equal(bb1, bb2):
|
||||
return True
|
||||
|
||||
|
||||
def basic_block_size(bb):
|
||||
""" calculate size of basic block """
|
||||
def basic_block_size(bb: idaapi.BasicBlock) -> int:
|
||||
"""calculate size of basic block"""
|
||||
return bb.end_ea - bb.start_ea
|
||||
|
||||
|
||||
def read_bytes_at(ea, count):
|
||||
def read_bytes_at(ea: int, count: int) -> bytes:
|
||||
""" """
|
||||
# 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)
|
||||
@@ -165,10 +202,10 @@ def read_bytes_at(ea, count):
|
||||
return idc.get_bytes(ea, count)
|
||||
|
||||
|
||||
def find_string_at(ea, min=4):
|
||||
""" check if ASCII string exists at a given virtual address """
|
||||
def find_string_at(ea: int, min_: int = 4) -> str:
|
||||
"""check if ASCII string exists at a given virtual address"""
|
||||
found = idaapi.get_strlit_contents(ea, -1, idaapi.STRTYPE_C)
|
||||
if found and len(found) > min:
|
||||
if found and len(found) >= min_:
|
||||
try:
|
||||
found = found.decode("ascii")
|
||||
# hacky check for IDA bug; get_strlit_contents also reads Unicode as
|
||||
@@ -182,7 +219,7 @@ def find_string_at(ea, min=4):
|
||||
return ""
|
||||
|
||||
|
||||
def get_op_phrase_info(op):
|
||||
def get_op_phrase_info(op: idaapi.op_t) -> Dict:
|
||||
"""parse phrase features from operand
|
||||
|
||||
Pretty much dup of sark's implementation:
|
||||
@@ -192,7 +229,8 @@ def get_op_phrase_info(op):
|
||||
return {}
|
||||
|
||||
scale = 1 << ((op.specflag2 & 0xC0) >> 6)
|
||||
offset = op.addr
|
||||
# IDA ea_t may be 32- or 64-bit; we assume displacement can only be 32-bit
|
||||
offset = op.addr & 0xFFFFFFFF
|
||||
|
||||
if op.specflag1 == 0:
|
||||
index = None
|
||||
@@ -219,41 +257,45 @@ def get_op_phrase_info(op):
|
||||
return {"base": base, "index": index, "scale": scale, "offset": offset}
|
||||
|
||||
|
||||
def is_op_write(insn, op):
|
||||
""" Check if an operand is written to (destination operand) """
|
||||
def is_op_write(insn: idaapi.insn_t, op: idaapi.op_t) -> bool:
|
||||
"""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) """
|
||||
def is_op_read(insn: idaapi.insn_t, op: idaapi.op_t) -> bool:
|
||||
"""Check if an operand is read from (source operand)"""
|
||||
return idaapi.has_cf_use(insn.get_canon_feature(), op.n)
|
||||
|
||||
|
||||
def is_sp_modified(insn):
|
||||
""" 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
|
||||
return True
|
||||
return False
|
||||
def is_op_offset(insn: idaapi.insn_t, op: idaapi.op_t) -> bool:
|
||||
"""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_bp_modified(insn):
|
||||
""" 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
|
||||
return True
|
||||
return False
|
||||
def is_sp_modified(insn: idaapi.insn_t) -> bool:
|
||||
"""determine if instruction modifies SP, ESP, RSP"""
|
||||
return any(
|
||||
op.reg == idautils.procregs.sp.reg and is_op_write(insn, op)
|
||||
for op in get_insn_ops(insn, target_ops=(idaapi.o_reg,))
|
||||
)
|
||||
|
||||
|
||||
def is_frame_register(reg):
|
||||
""" check if register is sp or bp """
|
||||
def is_bp_modified(insn: idaapi.insn_t) -> bool:
|
||||
"""check if instruction modifies BP, EBP, RBP"""
|
||||
return any(
|
||||
op.reg == idautils.procregs.bp.reg and is_op_write(insn, op)
|
||||
for op in get_insn_ops(insn, target_ops=(idaapi.o_reg,))
|
||||
)
|
||||
|
||||
|
||||
def is_frame_register(reg: int) -> bool:
|
||||
"""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 """
|
||||
def get_insn_ops(insn: idaapi.insn_t, target_ops: Optional[Tuple[Any]] = None) -> idaapi.op_t:
|
||||
"""yield op_t for instruction, filter on type if specified"""
|
||||
for op in insn.ops:
|
||||
if op.type == idaapi.o_void:
|
||||
# avoid looping all 6 ops if only subset exists
|
||||
@@ -263,12 +305,12 @@ def get_insn_ops(insn, target_ops=()):
|
||||
yield op
|
||||
|
||||
|
||||
def is_op_stack_var(ea, index):
|
||||
""" check if operand is a stack variable """
|
||||
def is_op_stack_var(ea: int, index: int) -> bool:
|
||||
"""check if operand is a stack variable"""
|
||||
return idaapi.is_stkvar(idaapi.get_flags(ea), index)
|
||||
|
||||
|
||||
def mask_op_val(op):
|
||||
def mask_op_val(op: idaapi.op_t) -> int:
|
||||
"""mask value by data type
|
||||
|
||||
necessary due to a bug in AMD64
|
||||
@@ -288,26 +330,15 @@ def mask_op_val(op):
|
||||
return masks.get(op.dtype, op.value) & op.value
|
||||
|
||||
|
||||
def is_function_recursive(f):
|
||||
"""check if function is recursive
|
||||
|
||||
args:
|
||||
f (IDA func_t)
|
||||
"""
|
||||
for ref in idautils.CodeRefsTo(f.start_ea, True):
|
||||
if f.contains(ref):
|
||||
return True
|
||||
return False
|
||||
def is_function_recursive(f: idaapi.func_t) -> bool:
|
||||
"""check if function is recursive"""
|
||||
return any(f.contains(ref) for ref in idautils.CodeRefsTo(f.start_ea, True))
|
||||
|
||||
|
||||
def is_basic_block_tight_loop(bb):
|
||||
def is_basic_block_tight_loop(bb: idaapi.BasicBlock) -> bool:
|
||||
"""check basic block loops to self
|
||||
|
||||
true if last instruction in basic block branches to basic block start
|
||||
|
||||
args:
|
||||
f (IDA func_t)
|
||||
bb (IDA BasicBlock)
|
||||
"""
|
||||
bb_end = idc.prev_head(bb.end_ea)
|
||||
if bb.start_ea < bb_end:
|
||||
@@ -317,8 +348,8 @@ def is_basic_block_tight_loop(bb):
|
||||
return False
|
||||
|
||||
|
||||
def find_data_reference_from_insn(insn, max_depth=10):
|
||||
""" search for data reference from instruction, return address of instruction if no reference exists """
|
||||
def find_data_reference_from_insn(insn: idaapi.insn_t, max_depth: int = 10) -> int:
|
||||
"""search for data reference from instruction, return address of instruction if no reference exists"""
|
||||
depth = 0
|
||||
ea = insn.ea
|
||||
|
||||
@@ -333,6 +364,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
|
||||
@@ -343,19 +378,17 @@ def find_data_reference_from_insn(insn, max_depth=10):
|
||||
return ea
|
||||
|
||||
|
||||
def get_function_blocks(f):
|
||||
"""yield basic blocks contained in specified function
|
||||
|
||||
args:
|
||||
f (IDA func_t)
|
||||
yield:
|
||||
block (IDA BasicBlock)
|
||||
"""
|
||||
def get_function_blocks(f: idaapi.func_t) -> Iterator[idaapi.BasicBlock]:
|
||||
"""yield basic blocks contained in specified function"""
|
||||
# leverage idaapi.FC_NOEXT flag to ignore useless external blocks referenced by the function
|
||||
for block in idaapi.FlowChart(f, flags=(idaapi.FC_PREDS | idaapi.FC_NOEXT)):
|
||||
yield block
|
||||
yield from idaapi.FlowChart(f, flags=(idaapi.FC_PREDS | idaapi.FC_NOEXT))
|
||||
|
||||
|
||||
def is_basic_block_return(bb):
|
||||
""" check if basic block is return block """
|
||||
def is_basic_block_return(bb: idaapi.BasicBlock) -> bool:
|
||||
"""check if basic block is return block"""
|
||||
return bb.type == idaapi.fcb_ret
|
||||
|
||||
|
||||
def has_sib(oper: idaapi.op_t) -> bool:
|
||||
# via: https://reverseengineering.stackexchange.com/a/14300
|
||||
return oper.specflag1 == 1
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
from typing import Any, Dict, Tuple, Iterator
|
||||
|
||||
import idc
|
||||
import idaapi
|
||||
@@ -12,86 +13,112 @@ 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, MAX_STRUCTURE_SIZE, Number, Offset, Mnemonic, OperandNumber, OperandOffset
|
||||
from capa.features.common import MAX_BYTES_FEATURE_SIZE, THUNK_CHAIN_DEPTH_DELTA, Bytes, String, Feature, Characteristic
|
||||
from capa.features.address import Address, AbsoluteVirtualAddress
|
||||
from capa.features.extractors.base_extractor import BBHandle, InsnHandle, FunctionHandle
|
||||
|
||||
# security cookie checks may perform non-zeroing XORs, these are expected within a certain
|
||||
# byte range within the first and returning basic blocks, this helps to reduce FP features
|
||||
SECURITY_COOKIE_BYTES_DELTA = 0x40
|
||||
|
||||
|
||||
def get_arch(ctx):
|
||||
"""
|
||||
fetch the ARCH_* constant for the currently open workspace.
|
||||
|
||||
via Tamir Bahar/@tmr232
|
||||
https://reverseengineering.stackexchange.com/a/11398/17194
|
||||
"""
|
||||
if "arch" not in ctx:
|
||||
info = idaapi.get_inf_structure()
|
||||
if info.is_64bit():
|
||||
ctx["arch"] = ARCH_X64
|
||||
elif info.is_32bit():
|
||||
ctx["arch"] = ARCH_X32
|
||||
else:
|
||||
raise ValueError("unexpected architecture")
|
||||
return ctx["arch"]
|
||||
|
||||
|
||||
def get_imports(ctx):
|
||||
def get_imports(ctx: Dict[str, Any]) -> Dict[int, Any]:
|
||||
if "imports_cache" not in ctx:
|
||||
ctx["imports_cache"] = capa.features.extractors.ida.helpers.get_file_imports()
|
||||
return ctx["imports_cache"]
|
||||
|
||||
|
||||
def check_for_api_call(ctx, insn):
|
||||
""" check instruction for API call """
|
||||
if not idaapi.is_call_insn(insn):
|
||||
return
|
||||
def get_externs(ctx: Dict[str, Any]) -> Dict[int, Any]:
|
||||
if "externs_cache" not in ctx:
|
||||
ctx["externs_cache"] = capa.features.extractors.ida.helpers.get_file_externs()
|
||||
return ctx["externs_cache"]
|
||||
|
||||
for ref in idautils.CodeRefsFrom(insn.ea, False):
|
||||
info = get_imports(ctx).get(ref, ())
|
||||
|
||||
def check_for_api_call(insn: idaapi.insn_t, funcs: Dict[int, Any]) -> Iterator[Any]:
|
||||
"""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
|
||||
|
||||
info = funcs.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 info
|
||||
|
||||
|
||||
def extract_insn_api_features(f, bb, insn):
|
||||
"""parse instruction API features
|
||||
|
||||
args:
|
||||
f (IDA func_t)
|
||||
bb (IDA BasicBlock)
|
||||
insn (IDA insn_t)
|
||||
def extract_insn_api_features(fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
parse instruction API features
|
||||
|
||||
example:
|
||||
call dword [0x00473038]
|
||||
call dword [0x00473038]
|
||||
"""
|
||||
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
|
||||
insn: idaapi.insn_t = ih.inner
|
||||
|
||||
if insn.get_canon_mnem() not in ("call", "jmp"):
|
||||
return
|
||||
|
||||
# check calls to imported functions
|
||||
for api in check_for_api_call(insn, get_imports(fh.ctx)):
|
||||
# tuple (<module>, <function>, <ordinal>)
|
||||
for name in capa.features.extractors.helpers.generate_symbols(api[0], api[1]):
|
||||
yield API(name), ih.address
|
||||
|
||||
# check calls to extern functions
|
||||
for api in check_for_api_call(insn, get_externs(fh.ctx)):
|
||||
# tuple (<module>, <function>, <ordinal>)
|
||||
yield API(api[1]), ih.address
|
||||
|
||||
# extract IDA/FLIRT recognized API functions
|
||||
targets = tuple(idautils.CodeRefsFrom(insn.ea, False))
|
||||
if not targets:
|
||||
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), ih.address
|
||||
if name.startswith("_"):
|
||||
# some linkers may prefix linked routines with a `_` to avoid name collisions.
|
||||
# extract features for both the mangled and un-mangled representations.
|
||||
# e.g. `_fwrite` -> `fwrite`
|
||||
# see: https://stackoverflow.com/a/2628384/87207
|
||||
yield API(name[1:]), ih.address
|
||||
|
||||
|
||||
def extract_insn_number_features(f, bb, insn):
|
||||
"""parse instruction number features
|
||||
|
||||
args:
|
||||
f (IDA func_t)
|
||||
bb (IDA BasicBlock)
|
||||
insn (IDA insn_t)
|
||||
|
||||
def extract_insn_number_features(
|
||||
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
|
||||
) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
parse instruction number features
|
||||
example:
|
||||
push 3136B0h ; dwControlCode
|
||||
"""
|
||||
insn: idaapi.insn_t = ih.inner
|
||||
|
||||
if idaapi.is_ret_insn(insn):
|
||||
# skip things like:
|
||||
# .text:0042250E retn 8
|
||||
@@ -102,68 +129,97 @@ def extract_insn_number_features(f, bb, insn):
|
||||
# .text:00401145 add esp, 0Ch
|
||||
return
|
||||
|
||||
for op in capa.features.extractors.ida.helpers.get_insn_ops(insn, target_ops=(idaapi.o_imm, idaapi.o_mem)):
|
||||
for i, op in enumerate(insn.ops):
|
||||
if op.type == idaapi.o_void:
|
||||
break
|
||||
if op.type not in (idaapi.o_imm, idaapi.o_mem):
|
||||
continue
|
||||
# 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), ih.address
|
||||
yield OperandNumber(i, const), ih.address
|
||||
|
||||
if insn.itype == idaapi.NN_add and 0 < const < MAX_STRUCTURE_SIZE and op.type == idaapi.o_imm:
|
||||
# for pattern like:
|
||||
#
|
||||
# add eax, 0x10
|
||||
#
|
||||
# assume 0x10 is also an offset (imagine eax is a pointer).
|
||||
yield Offset(const), ih.address
|
||||
yield OperandOffset(i, const), ih.address
|
||||
|
||||
|
||||
def extract_insn_bytes_features(f, bb, insn):
|
||||
"""parse referenced byte sequences
|
||||
|
||||
args:
|
||||
f (IDA func_t)
|
||||
bb (IDA BasicBlock)
|
||||
insn (IDA insn_t)
|
||||
|
||||
def extract_insn_bytes_features(fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
parse referenced byte sequences
|
||||
example:
|
||||
push offset iid_004118d4_IShellLinkA ; riid
|
||||
"""
|
||||
insn: idaapi.insn_t = ih.inner
|
||||
|
||||
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)
|
||||
if extracted_bytes and not capa.features.extractors.helpers.all_zeros(extracted_bytes):
|
||||
yield Bytes(extracted_bytes), insn.ea
|
||||
if not capa.features.extractors.ida.helpers.find_string_at(ref):
|
||||
# don't extract byte features for obvious strings
|
||||
yield Bytes(extracted_bytes), ih.address
|
||||
|
||||
|
||||
def extract_insn_string_features(f, bb, insn):
|
||||
"""parse instruction string features
|
||||
|
||||
args:
|
||||
f (IDA func_t)
|
||||
bb (IDA BasicBlock)
|
||||
insn (IDA insn_t)
|
||||
def extract_insn_string_features(
|
||||
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
|
||||
) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
parse instruction string features
|
||||
|
||||
example:
|
||||
push offset aAcr ; "ACR > "
|
||||
"""
|
||||
insn: idaapi.insn_t = ih.inner
|
||||
|
||||
ref = capa.features.extractors.ida.helpers.find_data_reference_from_insn(insn)
|
||||
if ref != insn.ea:
|
||||
found = capa.features.extractors.ida.helpers.find_string_at(ref)
|
||||
if found:
|
||||
yield String(found), insn.ea
|
||||
yield String(found), ih.address
|
||||
|
||||
|
||||
def extract_insn_offset_features(f, bb, insn):
|
||||
"""parse instruction structure offset features
|
||||
|
||||
args:
|
||||
f (IDA func_t)
|
||||
bb (IDA BasicBlock)
|
||||
insn (IDA insn_t)
|
||||
def extract_insn_offset_features(
|
||||
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
|
||||
) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
parse instruction structure offset features
|
||||
|
||||
example:
|
||||
.text:0040112F cmp [esi+4], ebx
|
||||
"""
|
||||
for op in capa.features.extractors.ida.helpers.get_insn_ops(insn, target_ops=(idaapi.o_phrase, idaapi.o_displ)):
|
||||
insn: idaapi.insn_t = ih.inner
|
||||
|
||||
for i, op in enumerate(insn.ops):
|
||||
if op.type == idaapi.o_void:
|
||||
break
|
||||
if op.type not in (idaapi.o_phrase, idaapi.o_displ):
|
||||
continue
|
||||
if capa.features.extractors.ida.helpers.is_op_stack_var(insn.ea, op.n):
|
||||
continue
|
||||
|
||||
p_info = capa.features.extractors.ida.helpers.get_op_phrase_info(op)
|
||||
op_off = p_info.get("offset", 0)
|
||||
|
||||
op_off = p_info.get("offset")
|
||||
if op_off is None:
|
||||
continue
|
||||
|
||||
if idaapi.is_mapped(op_off):
|
||||
# Ignore:
|
||||
# mov esi, dword_1005B148[esi]
|
||||
@@ -174,12 +230,32 @@ def extract_insn_offset_features(f, bb, insn):
|
||||
# https://stackoverflow.com/questions/31853189/x86-64-assembly-why-displacement-not-64-bits
|
||||
op_off = capa.features.extractors.helpers.twos_complement(op_off, 32)
|
||||
|
||||
yield Offset(op_off), insn.ea
|
||||
yield Offset(op_off, arch=get_arch(f.ctx)), insn.ea
|
||||
yield Offset(op_off), ih.address
|
||||
yield OperandOffset(i, op_off), ih.address
|
||||
|
||||
if (
|
||||
insn.itype == idaapi.NN_lea
|
||||
and i == 1
|
||||
# o_displ is used for both:
|
||||
# [eax+1]
|
||||
# [eax+ebx+2]
|
||||
and op.type == idaapi.o_displ
|
||||
# but the SIB is only present for [eax+ebx+2]
|
||||
# which we don't want
|
||||
and not capa.features.extractors.ida.helpers.has_sib(op)
|
||||
):
|
||||
# for pattern like:
|
||||
#
|
||||
# lea eax, [ebx + 1]
|
||||
#
|
||||
# assume 1 is also an offset (imagine ebx is a zero register).
|
||||
yield Number(op_off), ih.address
|
||||
yield OperandNumber(i, op_off), ih.address
|
||||
|
||||
|
||||
def contains_stack_cookie_keywords(s):
|
||||
"""check if string contains stack cookie keywords
|
||||
def contains_stack_cookie_keywords(s: str) -> bool:
|
||||
"""
|
||||
check if string contains stack cookie keywords
|
||||
|
||||
Examples:
|
||||
xor ecx, ebp ; StackCookie
|
||||
@@ -193,7 +269,7 @@ def contains_stack_cookie_keywords(s):
|
||||
return any(keyword in s for keyword in ("stack", "security"))
|
||||
|
||||
|
||||
def bb_stack_cookie_registers(bb):
|
||||
def bb_stack_cookie_registers(bb: idaapi.BasicBlock) -> Iterator[int]:
|
||||
"""scan basic block for stack cookie operations
|
||||
|
||||
yield registers ids that may have been used for stack cookie operations
|
||||
@@ -227,8 +303,8 @@ def bb_stack_cookie_registers(bb):
|
||||
yield op.reg
|
||||
|
||||
|
||||
def is_nzxor_stack_cookie_delta(f, bb, insn):
|
||||
""" check if nzxor exists within stack cookie delta """
|
||||
def is_nzxor_stack_cookie_delta(f: idaapi.func_t, bb: idaapi.BasicBlock, insn: idaapi.insn_t) -> bool:
|
||||
"""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
|
||||
@@ -250,8 +326,8 @@ def is_nzxor_stack_cookie_delta(f, bb, insn):
|
||||
return False
|
||||
|
||||
|
||||
def is_nzxor_stack_cookie(f, bb, insn):
|
||||
""" check if nzxor is related to stack cookie """
|
||||
def is_nzxor_stack_cookie(f: idaapi.func_t, bb: idaapi.BasicBlock, insn: idaapi.insn_t) -> bool:
|
||||
"""check if nzxor is related to stack cookie"""
|
||||
if contains_stack_cookie_keywords(idaapi.get_cmt(insn.ea, False)):
|
||||
# Example:
|
||||
# xor ecx, ebp ; StackCookie
|
||||
@@ -267,37 +343,49 @@ def is_nzxor_stack_cookie(f, bb, insn):
|
||||
return False
|
||||
|
||||
|
||||
def extract_insn_nzxor_characteristic_features(f, bb, insn):
|
||||
"""parse instruction non-zeroing XOR instruction
|
||||
|
||||
ignore expected non-zeroing XORs, e.g. security cookies
|
||||
|
||||
args:
|
||||
f (IDA func_t)
|
||||
bb (IDA BasicBlock)
|
||||
insn (IDA insn_t)
|
||||
def extract_insn_nzxor_characteristic_features(
|
||||
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
|
||||
) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
if insn.itype != idaapi.NN_xor:
|
||||
parse instruction non-zeroing XOR instruction
|
||||
ignore expected non-zeroing XORs, e.g. security cookies
|
||||
"""
|
||||
insn: idaapi.insn_t = ih.inner
|
||||
|
||||
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
|
||||
if is_nzxor_stack_cookie(f, bb, insn):
|
||||
if is_nzxor_stack_cookie(fh.inner, bbh.inner, insn):
|
||||
return
|
||||
yield Characteristic("nzxor"), insn.ea
|
||||
yield Characteristic("nzxor"), ih.address
|
||||
|
||||
|
||||
def extract_insn_mnemonic_features(f, bb, insn):
|
||||
"""parse instruction mnemonic features
|
||||
def extract_insn_mnemonic_features(
|
||||
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
|
||||
) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""parse instruction mnemonic features"""
|
||||
yield Mnemonic(idc.print_insn_mnem(ih.inner.ea)), ih.address
|
||||
|
||||
args:
|
||||
f (IDA func_t)
|
||||
bb (IDA BasicBlock)
|
||||
insn (IDA insn_t)
|
||||
|
||||
def extract_insn_obfs_call_plus_5_characteristic_features(
|
||||
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
|
||||
) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
yield Mnemonic(insn.get_canon_mnem()), insn.ea
|
||||
parse call $+5 instruction from the given instruction.
|
||||
"""
|
||||
insn: idaapi.insn_t = ih.inner
|
||||
|
||||
if not idaapi.is_call_insn(insn):
|
||||
return
|
||||
|
||||
if insn.ea + 5 == idc.get_operand_value(insn.ea, 0):
|
||||
yield Characteristic("call $+5"), ih.address
|
||||
|
||||
|
||||
def extract_insn_peb_access_characteristic_features(f, bb, insn):
|
||||
def extract_insn_peb_access_characteristic_features(
|
||||
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
|
||||
) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""parse instruction peb access
|
||||
|
||||
fs:[0x30] on x86, gs:[0x60] on x64
|
||||
@@ -305,51 +393,61 @@ def extract_insn_peb_access_characteristic_features(f, bb, insn):
|
||||
TODO:
|
||||
IDA should be able to do this..
|
||||
"""
|
||||
insn: idaapi.insn_t = ih.inner
|
||||
|
||||
if insn.itype not in (idaapi.NN_push, idaapi.NN_mov):
|
||||
return
|
||||
|
||||
if all(map(lambda op: op.type != idaapi.o_mem, insn.ops)):
|
||||
if all(op.type != idaapi.o_mem for op in insn.ops):
|
||||
# try to optimize for only memory references
|
||||
return
|
||||
|
||||
disasm = idc.GetDisasm(insn.ea)
|
||||
|
||||
if " fs:30h" in disasm or " gs:60h" in disasm:
|
||||
# TODO: replace above with proper IDA
|
||||
yield Characteristic("peb access"), insn.ea
|
||||
# TODO(mike-hunhoff): use proper IDA API for fetching segment access
|
||||
# scanning the disassembly text is a hack.
|
||||
# https://github.com/mandiant/capa/issues/1605
|
||||
yield Characteristic("peb access"), ih.address
|
||||
|
||||
|
||||
def extract_insn_segment_access_features(f, bb, insn):
|
||||
def extract_insn_segment_access_features(
|
||||
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
|
||||
) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""parse instruction fs or gs access
|
||||
|
||||
TODO:
|
||||
IDA should be able to do this...
|
||||
"""
|
||||
if all(map(lambda op: op.type != idaapi.o_mem, insn.ops)):
|
||||
insn: idaapi.insn_t = ih.inner
|
||||
|
||||
if all(op.type != idaapi.o_mem for op in insn.ops):
|
||||
# try to optimize for only memory references
|
||||
return
|
||||
|
||||
disasm = idc.GetDisasm(insn.ea)
|
||||
|
||||
if " fs:" in disasm:
|
||||
# TODO: replace above with proper IDA
|
||||
yield Characteristic("fs access"), insn.ea
|
||||
# TODO(mike-hunhoff): use proper IDA API for fetching segment access
|
||||
# scanning the disassembly text is a hack.
|
||||
# https://github.com/mandiant/capa/issues/1605
|
||||
yield Characteristic("fs access"), ih.address
|
||||
|
||||
if " gs:" in disasm:
|
||||
# TODO: replace above with proper IDA
|
||||
yield Characteristic("gs access"), insn.ea
|
||||
# TODO(mike-hunhoff): use proper IDA API for fetching segment access
|
||||
# scanning the disassembly text is a hack.
|
||||
# https://github.com/mandiant/capa/issues/1605
|
||||
yield Characteristic("gs access"), ih.address
|
||||
|
||||
|
||||
def extract_insn_cross_section_cflow(f, bb, insn):
|
||||
"""inspect the instruction for a CALL or JMP that crosses section boundaries
|
||||
def extract_insn_cross_section_cflow(
|
||||
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
|
||||
) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""inspect the instruction for a CALL or JMP that crosses section boundaries"""
|
||||
insn: idaapi.insn_t = ih.inner
|
||||
|
||||
args:
|
||||
f (IDA func_t)
|
||||
bb (IDA BasicBlock)
|
||||
insn (IDA insn_t)
|
||||
"""
|
||||
for ref in idautils.CodeRefsFrom(insn.ea, False):
|
||||
if ref in get_imports(f.ctx).keys():
|
||||
if ref in get_imports(fh.ctx):
|
||||
# ignore API calls
|
||||
continue
|
||||
if not idaapi.getseg(ref):
|
||||
@@ -357,50 +455,40 @@ def extract_insn_cross_section_cflow(f, bb, insn):
|
||||
continue
|
||||
if idaapi.getseg(ref) == idaapi.getseg(insn.ea):
|
||||
continue
|
||||
yield Characteristic("cross section flow"), insn.ea
|
||||
yield Characteristic("cross section flow"), ih.address
|
||||
|
||||
|
||||
def extract_function_calls_from(f, bb, insn):
|
||||
def extract_function_calls_from(fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""extract functions calls from features
|
||||
|
||||
most relevant at the function scope, however, its most efficient to extract at the instruction scope
|
||||
|
||||
args:
|
||||
f (IDA func_t)
|
||||
bb (IDA BasicBlock)
|
||||
insn (IDA insn_t)
|
||||
"""
|
||||
insn: idaapi.insn_t = ih.inner
|
||||
|
||||
if idaapi.is_call_insn(insn):
|
||||
for ref in idautils.CodeRefsFrom(insn.ea, False):
|
||||
yield Characteristic("calls from"), ref
|
||||
yield Characteristic("calls from"), AbsoluteVirtualAddress(ref)
|
||||
|
||||
|
||||
def extract_function_indirect_call_characteristic_features(f, bb, insn):
|
||||
def extract_function_indirect_call_characteristic_features(
|
||||
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
|
||||
) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""extract indirect function calls (e.g., call eax or call dword ptr [edx+4])
|
||||
does not include calls like => call ds:dword_ABD4974
|
||||
|
||||
most relevant at the function or basic block scope;
|
||||
however, its most efficient to extract at the instruction scope
|
||||
|
||||
args:
|
||||
f (IDA func_t)
|
||||
bb (IDA BasicBlock)
|
||||
insn (IDA insn_t)
|
||||
"""
|
||||
insn: idaapi.insn_t = ih.inner
|
||||
|
||||
if idaapi.is_call_insn(insn) and idc.get_operand_type(insn.ea, 0) in (idc.o_reg, idc.o_phrase, idc.o_displ):
|
||||
yield Characteristic("indirect call"), insn.ea
|
||||
yield Characteristic("indirect call"), ih.address
|
||||
|
||||
|
||||
def extract_features(f, bb, insn):
|
||||
"""extract instruction features
|
||||
|
||||
args:
|
||||
f (IDA func_t)
|
||||
bb (IDA BasicBlock)
|
||||
insn (IDA insn_t)
|
||||
"""
|
||||
def extract_features(f: FunctionHandle, bbh: BBHandle, insn: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""extract instruction features"""
|
||||
for inst_handler in INSTRUCTION_HANDLERS:
|
||||
for (feature, ea) in inst_handler(f, bb, insn):
|
||||
for feature, ea in inst_handler(f, bbh, insn):
|
||||
yield feature, ea
|
||||
|
||||
|
||||
@@ -412,26 +500,10 @@ INSTRUCTION_HANDLERS = (
|
||||
extract_insn_offset_features,
|
||||
extract_insn_nzxor_characteristic_features,
|
||||
extract_insn_mnemonic_features,
|
||||
extract_insn_obfs_call_plus_5_characteristic_features,
|
||||
extract_insn_peb_access_characteristic_features,
|
||||
extract_insn_cross_section_cflow,
|
||||
extract_insn_segment_access_features,
|
||||
extract_function_calls_from,
|
||||
extract_function_indirect_call_characteristic_features,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
""" """
|
||||
features = []
|
||||
for f in capa.features.extractors.ida.helpers.get_functions(skip_thunks=True, skip_libs=True):
|
||||
for bb in idaapi.FlowChart(f, flags=idaapi.FC_PREDS):
|
||||
for insn in capa.features.extractors.ida.helpers.get_instructions_in_range(bb.start_ea, bb.end_ea):
|
||||
features.extend(list(extract_features(f, bb, insn)))
|
||||
|
||||
import pprint
|
||||
|
||||
pprint.pprint(features)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
@@ -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 networkx import nx
|
||||
import networkx
|
||||
from networkx.algorithms.components import strongly_connected_components
|
||||
|
||||
|
||||
@@ -20,6 +20,6 @@ def has_loop(edges, threshold=2):
|
||||
returns:
|
||||
bool
|
||||
"""
|
||||
g = nx.DiGraph()
|
||||
g = networkx.DiGraph()
|
||||
g.add_edges_from(edges)
|
||||
return any(len(comp) >= threshold for comp in strongly_connected_components(g))
|
||||
|
||||
170
capa/features/extractors/null.py
Normal file
170
capa/features/extractors/null.py
Normal file
@@ -0,0 +1,170 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
from typing import Dict, List, Tuple, Union
|
||||
from dataclasses import dataclass
|
||||
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
from capa.features.common import Feature
|
||||
from capa.features.address import NO_ADDRESS, Address, ThreadAddress, ProcessAddress, DynamicCallAddress
|
||||
from capa.features.extractors.base_extractor import (
|
||||
BBHandle,
|
||||
CallHandle,
|
||||
InsnHandle,
|
||||
SampleHashes,
|
||||
ThreadHandle,
|
||||
ProcessHandle,
|
||||
FunctionHandle,
|
||||
StaticFeatureExtractor,
|
||||
DynamicFeatureExtractor,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class InstructionFeatures:
|
||||
features: List[Tuple[Address, Feature]]
|
||||
|
||||
|
||||
@dataclass
|
||||
class BasicBlockFeatures:
|
||||
features: List[Tuple[Address, Feature]]
|
||||
instructions: Dict[Address, InstructionFeatures]
|
||||
|
||||
|
||||
@dataclass
|
||||
class FunctionFeatures:
|
||||
features: List[Tuple[Address, Feature]]
|
||||
basic_blocks: Dict[Address, BasicBlockFeatures]
|
||||
|
||||
|
||||
@dataclass
|
||||
class NullStaticFeatureExtractor(StaticFeatureExtractor):
|
||||
"""
|
||||
An extractor that extracts some user-provided features.
|
||||
|
||||
This is useful for testing, as we can provide expected values and see if matching works.
|
||||
"""
|
||||
|
||||
base_address: Address
|
||||
sample_hashes: SampleHashes
|
||||
global_features: List[Feature]
|
||||
file_features: List[Tuple[Address, Feature]]
|
||||
functions: Dict[Address, FunctionFeatures]
|
||||
|
||||
def get_base_address(self):
|
||||
return self.base_address
|
||||
|
||||
def get_sample_hashes(self) -> SampleHashes:
|
||||
return self.sample_hashes
|
||||
|
||||
def extract_global_features(self):
|
||||
for feature in self.global_features:
|
||||
yield feature, NO_ADDRESS
|
||||
|
||||
def extract_file_features(self):
|
||||
for address, feature in self.file_features:
|
||||
yield feature, address
|
||||
|
||||
def get_functions(self):
|
||||
for address in sorted(self.functions.keys()):
|
||||
yield FunctionHandle(address, None)
|
||||
|
||||
def extract_function_features(self, f):
|
||||
for address, feature in self.functions[f.address].features:
|
||||
yield feature, address
|
||||
|
||||
def get_basic_blocks(self, f):
|
||||
for address in sorted(self.functions[f.address].basic_blocks.keys()):
|
||||
yield BBHandle(address, None)
|
||||
|
||||
def extract_basic_block_features(self, f, bb):
|
||||
for address, feature in self.functions[f.address].basic_blocks[bb.address].features:
|
||||
yield feature, address
|
||||
|
||||
def get_instructions(self, f, bb):
|
||||
for address in sorted(self.functions[f.address].basic_blocks[bb.address].instructions.keys()):
|
||||
yield InsnHandle(address, None)
|
||||
|
||||
def extract_insn_features(self, f, bb, insn):
|
||||
for address, feature in self.functions[f.address].basic_blocks[bb.address].instructions[insn.address].features:
|
||||
yield feature, address
|
||||
|
||||
|
||||
@dataclass
|
||||
class CallFeatures:
|
||||
name: str
|
||||
features: List[Tuple[Address, Feature]]
|
||||
|
||||
|
||||
@dataclass
|
||||
class ThreadFeatures:
|
||||
features: List[Tuple[Address, Feature]]
|
||||
calls: Dict[Address, CallFeatures]
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProcessFeatures:
|
||||
features: List[Tuple[Address, Feature]]
|
||||
threads: Dict[Address, ThreadFeatures]
|
||||
name: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class NullDynamicFeatureExtractor(DynamicFeatureExtractor):
|
||||
base_address: Address
|
||||
sample_hashes: SampleHashes
|
||||
global_features: List[Feature]
|
||||
file_features: List[Tuple[Address, Feature]]
|
||||
processes: Dict[Address, ProcessFeatures]
|
||||
|
||||
def extract_global_features(self):
|
||||
for feature in self.global_features:
|
||||
yield feature, NO_ADDRESS
|
||||
|
||||
def get_sample_hashes(self) -> SampleHashes:
|
||||
return self.sample_hashes
|
||||
|
||||
def extract_file_features(self):
|
||||
for address, feature in self.file_features:
|
||||
yield feature, address
|
||||
|
||||
def get_processes(self):
|
||||
for address in sorted(self.processes.keys()):
|
||||
assert isinstance(address, ProcessAddress)
|
||||
yield ProcessHandle(address=address, inner={})
|
||||
|
||||
def extract_process_features(self, ph):
|
||||
for addr, feature in self.processes[ph.address].features:
|
||||
yield feature, addr
|
||||
|
||||
def get_process_name(self, ph) -> str:
|
||||
return self.processes[ph.address].name
|
||||
|
||||
def get_threads(self, ph):
|
||||
for address in sorted(self.processes[ph.address].threads.keys()):
|
||||
assert isinstance(address, ThreadAddress)
|
||||
yield ThreadHandle(address=address, inner={})
|
||||
|
||||
def extract_thread_features(self, ph, th):
|
||||
for addr, feature in self.processes[ph.address].threads[th.address].features:
|
||||
yield feature, addr
|
||||
|
||||
def get_calls(self, ph, th):
|
||||
for address in sorted(self.processes[ph.address].threads[th.address].calls.keys()):
|
||||
assert isinstance(address, DynamicCallAddress)
|
||||
yield CallHandle(address=address, inner={})
|
||||
|
||||
def extract_call_features(self, ph, th, ch):
|
||||
for address, feature in self.processes[ph.address].threads[th.address].calls[ch.address].features:
|
||||
yield feature, address
|
||||
|
||||
def get_call_name(self, ph, th, ch) -> str:
|
||||
return self.processes[ph.address].threads[th.address].calls[ch.address].name
|
||||
|
||||
|
||||
NullFeatureExtractor: TypeAlias = Union[NullStaticFeatureExtractor, NullDynamicFeatureExtractor]
|
||||
229
capa/features/extractors/pefile.py
Normal file
229
capa/features/extractors/pefile.py
Normal file
@@ -0,0 +1,229 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
import pefile
|
||||
|
||||
import capa.features.common
|
||||
import capa.features.extractors
|
||||
import capa.features.extractors.common
|
||||
import capa.features.extractors.helpers
|
||||
import capa.features.extractors.strings
|
||||
from capa.features.file import Export, Import, Section
|
||||
from capa.features.common import OS, ARCH_I386, FORMAT_PE, ARCH_AMD64, OS_WINDOWS, Arch, Format, Characteristic
|
||||
from capa.features.address import NO_ADDRESS, FileOffsetAddress, AbsoluteVirtualAddress
|
||||
from capa.features.extractors.base_extractor import SampleHashes, StaticFeatureExtractor
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def extract_file_embedded_pe(buf, **kwargs):
|
||||
for offset, _ in capa.features.extractors.helpers.carve_pe(buf, 1):
|
||||
yield Characteristic("embedded pe"), FileOffsetAddress(offset)
|
||||
|
||||
|
||||
def extract_file_export_names(pe, **kwargs):
|
||||
base_address = pe.OPTIONAL_HEADER.ImageBase
|
||||
|
||||
if hasattr(pe, "DIRECTORY_ENTRY_EXPORT"):
|
||||
for export in pe.DIRECTORY_ENTRY_EXPORT.symbols:
|
||||
if not export.name:
|
||||
continue
|
||||
try:
|
||||
name = export.name.partition(b"\x00")[0].decode("ascii")
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
|
||||
if export.forwarder is None:
|
||||
va = base_address + export.address
|
||||
yield Export(name), AbsoluteVirtualAddress(va)
|
||||
|
||||
else:
|
||||
try:
|
||||
forwarded_name = export.forwarder.partition(b"\x00")[0].decode("ascii")
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
forwarded_name = capa.features.extractors.helpers.reformat_forwarded_export_name(forwarded_name)
|
||||
va = base_address + export.address
|
||||
yield Export(forwarded_name), AbsoluteVirtualAddress(va)
|
||||
yield Characteristic("forwarded export"), AbsoluteVirtualAddress(va)
|
||||
|
||||
|
||||
def extract_file_import_names(pe, **kwargs):
|
||||
"""
|
||||
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 = f"#{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, include_dll=True):
|
||||
yield Import(name), AbsoluteVirtualAddress(imp.address)
|
||||
|
||||
|
||||
def extract_file_section_names(pe, **kwargs):
|
||||
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), AbsoluteVirtualAddress(base_address + section.VirtualAddress)
|
||||
|
||||
|
||||
def extract_file_strings(buf, **kwargs):
|
||||
yield from capa.features.extractors.common.extract_file_strings(buf)
|
||||
|
||||
|
||||
def extract_file_function_names(**kwargs):
|
||||
"""
|
||||
extract the names of statically-linked library functions.
|
||||
"""
|
||||
if False:
|
||||
# using a `yield` here to force this to be a generator, not function.
|
||||
yield NotImplementedError("pefile doesn't have library matching")
|
||||
return
|
||||
|
||||
|
||||
def extract_file_os(**kwargs):
|
||||
# assuming PE -> Windows
|
||||
# though i suppose they're also used by UEFI
|
||||
yield OS(OS_WINDOWS), NO_ADDRESS
|
||||
|
||||
|
||||
def extract_file_format(**kwargs):
|
||||
yield Format(FORMAT_PE), NO_ADDRESS
|
||||
|
||||
|
||||
def extract_file_arch(pe, **kwargs):
|
||||
if pe.FILE_HEADER.Machine == pefile.MACHINE_TYPE["IMAGE_FILE_MACHINE_I386"]:
|
||||
yield Arch(ARCH_I386), NO_ADDRESS
|
||||
elif pe.FILE_HEADER.Machine == pefile.MACHINE_TYPE["IMAGE_FILE_MACHINE_AMD64"]:
|
||||
yield Arch(ARCH_AMD64), NO_ADDRESS
|
||||
else:
|
||||
logger.warning("unsupported architecture: %s", pefile.MACHINE_TYPE[pe.FILE_HEADER.Machine])
|
||||
|
||||
|
||||
def extract_file_features(pe, buf):
|
||||
"""
|
||||
extract file features from given workspace
|
||||
|
||||
args:
|
||||
pe (pefile.PE): the parsed PE
|
||||
buf: the raw sample bytes
|
||||
|
||||
yields:
|
||||
Tuple[Feature, VA]: a feature and its location.
|
||||
"""
|
||||
|
||||
for file_handler in FILE_HANDLERS:
|
||||
# file_handler: type: (pe, bytes) -> Iterable[Tuple[Feature, Address]]
|
||||
for feature, va in file_handler(pe=pe, buf=buf): # type: ignore
|
||||
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,
|
||||
extract_file_format,
|
||||
)
|
||||
|
||||
|
||||
def extract_global_features(pe, buf):
|
||||
"""
|
||||
extract global features from given workspace
|
||||
|
||||
args:
|
||||
pe (pefile.PE): the parsed PE
|
||||
buf: the raw sample bytes
|
||||
|
||||
yields:
|
||||
Tuple[Feature, VA]: a feature and its location.
|
||||
"""
|
||||
for handler in GLOBAL_HANDLERS:
|
||||
# file_handler: type: (pe, bytes) -> Iterable[Tuple[Feature, Address]]
|
||||
for feature, va in handler(pe=pe, buf=buf): # type: ignore
|
||||
yield feature, va
|
||||
|
||||
|
||||
GLOBAL_HANDLERS = (
|
||||
extract_file_os,
|
||||
extract_file_arch,
|
||||
)
|
||||
|
||||
|
||||
class PefileFeatureExtractor(StaticFeatureExtractor):
|
||||
def __init__(self, path: Path):
|
||||
super().__init__(hashes=SampleHashes.from_bytes(path.read_bytes()))
|
||||
self.path: Path = path
|
||||
self.pe = pefile.PE(str(path))
|
||||
|
||||
def get_base_address(self):
|
||||
return AbsoluteVirtualAddress(self.pe.OPTIONAL_HEADER.ImageBase)
|
||||
|
||||
def extract_global_features(self):
|
||||
buf = Path(self.path).read_bytes()
|
||||
|
||||
yield from extract_global_features(self.pe, buf)
|
||||
|
||||
def extract_file_features(self):
|
||||
buf = Path(self.path).read_bytes()
|
||||
|
||||
yield from extract_file_features(self.pe, buf)
|
||||
|
||||
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")
|
||||
@@ -1,6 +1,6 @@
|
||||
# strings code from FLOSS, https://github.com/fireeye/flare-floss
|
||||
# strings code from FLOSS, https://github.com/mandiant/flare-floss
|
||||
#
|
||||
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
@@ -9,6 +9,7 @@
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import re
|
||||
import contextlib
|
||||
from collections import namedtuple
|
||||
|
||||
ASCII_BYTE = r" !\"#\$%&\'\(\)\*\+,-\./0123456789:;<=>\?@ABCDEFGHIJKLMNOPQRSTUVWXYZ\[\]\^_`abcdefghijklmnopqrstuvwxyz\{\|\}\\\~\t".encode(
|
||||
@@ -81,24 +82,5 @@ def extract_unicode_strings(buf, n=4):
|
||||
reg = b"((?:[%s]\x00){%d,})" % (ASCII_BYTE, n)
|
||||
r = re.compile(reg)
|
||||
for match in r.finditer(buf):
|
||||
try:
|
||||
with contextlib.suppress(UnicodeDecodeError):
|
||||
yield String(match.group().decode("utf-16"), match.start())
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
|
||||
|
||||
def main():
|
||||
import sys
|
||||
|
||||
with open(sys.argv[1], "rb") as f:
|
||||
b = f.read()
|
||||
|
||||
for s in extract_ascii_strings(b):
|
||||
print("0x{:x}: {:s}".format(s.offset, s.s))
|
||||
|
||||
for s in extract_unicode_strings(b):
|
||||
print("0x{:x}: {:s}".format(s.offset, s.s))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
@@ -8,27 +8,30 @@
|
||||
|
||||
import string
|
||||
import struct
|
||||
from typing import Tuple, Iterator
|
||||
|
||||
import envi
|
||||
import vivisect.const
|
||||
import envi.archs.i386.disasm
|
||||
|
||||
from capa.features import Characteristic
|
||||
from capa.features.common import Feature, Characteristic
|
||||
from capa.features.address import Address, AbsoluteVirtualAddress
|
||||
from capa.features.basicblock import BasicBlock
|
||||
from capa.features.extractors.helpers import MIN_STACKSTRING_LEN
|
||||
from capa.features.extractors.base_extractor import BBHandle, FunctionHandle
|
||||
|
||||
|
||||
def interface_extract_basic_block_XXX(f, bb):
|
||||
def interface_extract_basic_block_XXX(f: FunctionHandle, bb: BBHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
parse features from the given basic block.
|
||||
|
||||
args:
|
||||
f (viv_utils.Function): the function to process.
|
||||
bb (viv_utils.BasicBlock): the basic block to process.
|
||||
f: the function to process.
|
||||
bb: the basic block to process.
|
||||
|
||||
yields:
|
||||
(Feature, int): the feature and the address at which its found.
|
||||
(Feature, Address): the feature and the address at which its found.
|
||||
"""
|
||||
yield NotImplementedError("feature"), NotImplementedError("virtual address")
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def _bb_has_tight_loop(f, bb):
|
||||
@@ -37,17 +40,17 @@ 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
|
||||
|
||||
return 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.va
|
||||
def extract_bb_tight_loop(f: FunctionHandle, bb: BBHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""check basic block for tight loop indicators"""
|
||||
if _bb_has_tight_loop(f, bb.inner):
|
||||
yield Characteristic("tight loop"), bb.address
|
||||
|
||||
|
||||
def _bb_has_stackstring(f, bb):
|
||||
@@ -67,13 +70,13 @@ def _bb_has_stackstring(f, bb):
|
||||
return False
|
||||
|
||||
|
||||
def extract_stackstring(f, bb):
|
||||
""" check basic block for stackstring indicators """
|
||||
if _bb_has_stackstring(f, bb):
|
||||
yield Characteristic("stack string"), bb.va
|
||||
def extract_stackstring(f: FunctionHandle, bb: BBHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""check basic block for stackstring indicators"""
|
||||
if _bb_has_stackstring(f, bb.inner):
|
||||
yield Characteristic("stack string"), bb.address
|
||||
|
||||
|
||||
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
|
||||
"""
|
||||
@@ -89,7 +92,6 @@ def is_mov_imm_to_stack(instr):
|
||||
if not src.isImmed():
|
||||
return False
|
||||
|
||||
# TODO what about 64-bit operands?
|
||||
if not isinstance(dst, envi.archs.i386.disasm.i386SibOper) and not isinstance(
|
||||
dst, envi.archs.i386.disasm.i386RegMemOper
|
||||
):
|
||||
@@ -105,7 +107,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,23 +119,33 @@ def get_printable_len(oper):
|
||||
chars = struct.pack("<I", oper.imm)
|
||||
elif oper.tsize == 8:
|
||||
chars = struct.pack("<Q", oper.imm)
|
||||
else:
|
||||
raise ValueError(f"unexpected oper.tsize: {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 == 0x0 for c in chars[1::2]):
|
||||
return is_printable_ascii(chars[::2])
|
||||
return False
|
||||
|
||||
|
||||
def extract_features(f, bb):
|
||||
def extract_features(f: FunctionHandle, bb: BBHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
extract features from the given basic block.
|
||||
|
||||
@@ -142,12 +154,12 @@ 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
|
||||
yield BasicBlock(), AbsoluteVirtualAddress(bb.inner.va)
|
||||
for bb_handler in BASIC_BLOCK_HANDLERS:
|
||||
for feature, va in bb_handler(f, bb):
|
||||
yield feature, va
|
||||
for feature, addr in bb_handler(f, bb):
|
||||
yield feature, addr
|
||||
|
||||
|
||||
BASIC_BLOCK_HANDLERS = (
|
||||
|
||||
89
capa/features/extractors/viv/extractor.py
Normal file
89
capa/features/extractors/viv/extractor.py
Normal file
@@ -0,0 +1,89 @@
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
import logging
|
||||
from typing import Any, Dict, List, Tuple, Iterator
|
||||
from pathlib import Path
|
||||
|
||||
import viv_utils
|
||||
import viv_utils.flirt
|
||||
|
||||
import capa.features.extractors.common
|
||||
import capa.features.extractors.viv.file
|
||||
import capa.features.extractors.viv.insn
|
||||
import capa.features.extractors.viv.global_
|
||||
import capa.features.extractors.viv.function
|
||||
import capa.features.extractors.viv.basicblock
|
||||
from capa.features.common import Feature
|
||||
from capa.features.address import Address, AbsoluteVirtualAddress
|
||||
from capa.features.extractors.base_extractor import (
|
||||
BBHandle,
|
||||
InsnHandle,
|
||||
SampleHashes,
|
||||
FunctionHandle,
|
||||
StaticFeatureExtractor,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class VivisectFeatureExtractor(StaticFeatureExtractor):
|
||||
def __init__(self, vw, path: Path, os):
|
||||
self.vw = vw
|
||||
self.path = path
|
||||
self.buf = path.read_bytes()
|
||||
super().__init__(hashes=SampleHashes.from_bytes(self.buf))
|
||||
|
||||
# pre-compute these because we'll yield them at *every* scope.
|
||||
self.global_features: List[Tuple[Feature, Address]] = []
|
||||
self.global_features.extend(capa.features.extractors.viv.file.extract_file_format(self.buf))
|
||||
self.global_features.extend(capa.features.extractors.common.extract_os(self.buf, os))
|
||||
self.global_features.extend(capa.features.extractors.viv.global_.extract_arch(self.vw))
|
||||
|
||||
def get_base_address(self):
|
||||
# assume there is only one file loaded into the vw
|
||||
return AbsoluteVirtualAddress(list(self.vw.filemeta.values())[0]["imagebase"])
|
||||
|
||||
def extract_global_features(self):
|
||||
yield from self.global_features
|
||||
|
||||
def extract_file_features(self):
|
||||
yield from capa.features.extractors.viv.file.extract_features(self.vw, self.buf)
|
||||
|
||||
def get_functions(self) -> Iterator[FunctionHandle]:
|
||||
cache: Dict[str, Any] = {}
|
||||
for va in sorted(self.vw.getFunctions()):
|
||||
yield FunctionHandle(
|
||||
address=AbsoluteVirtualAddress(va), inner=viv_utils.Function(self.vw, va), ctx={"cache": cache}
|
||||
)
|
||||
|
||||
def extract_function_features(self, fh: FunctionHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
yield from capa.features.extractors.viv.function.extract_features(fh)
|
||||
|
||||
def get_basic_blocks(self, fh: FunctionHandle) -> Iterator[BBHandle]:
|
||||
f: viv_utils.Function = fh.inner
|
||||
for bb in f.basic_blocks:
|
||||
yield BBHandle(address=AbsoluteVirtualAddress(bb.va), inner=bb)
|
||||
|
||||
def extract_basic_block_features(self, fh: FunctionHandle, bbh) -> Iterator[Tuple[Feature, Address]]:
|
||||
yield from capa.features.extractors.viv.basicblock.extract_features(fh, bbh)
|
||||
|
||||
def get_instructions(self, fh: FunctionHandle, bbh: BBHandle) -> Iterator[InsnHandle]:
|
||||
bb: viv_utils.BasicBlock = bbh.inner
|
||||
for insn in bb.instructions:
|
||||
yield InsnHandle(address=AbsoluteVirtualAddress(insn.va), inner=insn)
|
||||
|
||||
def extract_insn_features(
|
||||
self, fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
|
||||
) -> Iterator[Tuple[Feature, Address]]:
|
||||
yield from capa.features.extractors.viv.insn.extract_features(fh, bbh, ih)
|
||||
|
||||
def is_library_function(self, addr):
|
||||
return viv_utils.flirt.is_library_function(self.vw, addr)
|
||||
|
||||
def get_function_name(self, addr):
|
||||
return viv_utils.get_function_name(self.vw, addr)
|
||||
@@ -1,32 +1,62 @@
|
||||
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
from typing import Tuple, Iterator
|
||||
|
||||
import PE.carve as pe_carve # vivisect PE
|
||||
import vivisect
|
||||
import viv_utils
|
||||
import viv_utils.flirt
|
||||
|
||||
import capa.features.insn
|
||||
import capa.features.extractors.common
|
||||
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 Feature, Characteristic
|
||||
from capa.features.address import Address, FileOffsetAddress, AbsoluteVirtualAddress
|
||||
|
||||
|
||||
def extract_file_embedded_pe(vw, file_path):
|
||||
with open(file_path, "rb") as f:
|
||||
fbytes = f.read()
|
||||
|
||||
for offset, i in pe_carve.carve(fbytes, 1):
|
||||
yield Characteristic("embedded pe"), offset
|
||||
def extract_file_embedded_pe(buf, **kwargs) -> Iterator[Tuple[Feature, Address]]:
|
||||
for offset, _ in pe_carve.carve(buf, 1):
|
||||
yield Characteristic("embedded pe"), FileOffsetAddress(offset)
|
||||
|
||||
|
||||
def extract_file_export_names(vw, file_path):
|
||||
for va, etype, name, _ in vw.getExports():
|
||||
yield Export(name), va
|
||||
def get_first_vw_filename(vw: vivisect.VivWorkspace):
|
||||
# vivisect associates metadata with each file that its loaded into the workspace.
|
||||
# capa only loads a single file into each workspace.
|
||||
# so to access the metadata for the file in question, we can just take the first one.
|
||||
# otherwise, we'd have to pass around the module name of the file we're analyzing,
|
||||
# which is a pain.
|
||||
#
|
||||
# so this is a simplifying assumption.
|
||||
return next(iter(vw.filemeta.keys()))
|
||||
|
||||
|
||||
def extract_file_import_names(vw, file_path):
|
||||
def extract_file_export_names(vw: vivisect.VivWorkspace, **kwargs) -> Iterator[Tuple[Feature, Address]]:
|
||||
for va, _, name, _ in vw.getExports():
|
||||
yield Export(name), AbsoluteVirtualAddress(va)
|
||||
|
||||
if vw.getMeta("Format") == "pe":
|
||||
pe = vw.parsedbin
|
||||
baseaddr = pe.IMAGE_NT_HEADERS.OptionalHeader.ImageBase
|
||||
for rva, _, forwarded_name in vw.getFileMeta(get_first_vw_filename(vw), "forwarders"):
|
||||
try:
|
||||
forwarded_name = forwarded_name.partition(b"\x00")[0].decode("ascii")
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
|
||||
forwarded_name = capa.features.extractors.helpers.reformat_forwarded_export_name(forwarded_name)
|
||||
va = baseaddr + rva
|
||||
yield Export(forwarded_name), AbsoluteVirtualAddress(va)
|
||||
yield Characteristic("forwarded export"), AbsoluteVirtualAddress(va)
|
||||
|
||||
|
||||
def extract_file_import_names(vw, **kwargs) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
extract imported function names
|
||||
1. imports by ordinal:
|
||||
@@ -37,18 +67,17 @@ def extract_file_import_names(vw, file_path):
|
||||
"""
|
||||
for va, _, _, tinfo in vw.getImports():
|
||||
# vivisect source: tinfo = "%s.%s" % (libname, impname)
|
||||
modname, impname = tinfo.split(".")
|
||||
modname, impname = tinfo.split(".", 1)
|
||||
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
|
||||
impname = "#" + impname[len("ord") :]
|
||||
|
||||
addr = AbsoluteVirtualAddress(va)
|
||||
for name in capa.features.extractors.helpers.generate_symbols(modname, impname, include_dll=True):
|
||||
yield Import(name), addr
|
||||
|
||||
|
||||
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`
|
||||
"""
|
||||
@@ -62,40 +91,51 @@ def is_viv_ord_impname(impname):
|
||||
return True
|
||||
|
||||
|
||||
def extract_file_section_names(vw, file_path):
|
||||
def extract_file_section_names(vw, **kwargs) -> Iterator[Tuple[Feature, Address]]:
|
||||
for va, _, segname, _ in vw.getSegments():
|
||||
yield Section(segname), va
|
||||
yield Section(segname), AbsoluteVirtualAddress(va)
|
||||
|
||||
|
||||
def extract_file_strings(vw, file_path):
|
||||
def extract_file_strings(buf, **kwargs) -> Iterator[Tuple[Feature, Address]]:
|
||||
yield from capa.features.extractors.common.extract_file_strings(buf)
|
||||
|
||||
|
||||
def extract_file_function_names(vw, **kwargs) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
extract ASCII and UTF-16 LE strings from file
|
||||
extract the names of statically-linked library functions.
|
||||
"""
|
||||
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
|
||||
for va in sorted(vw.getFunctions()):
|
||||
addr = AbsoluteVirtualAddress(va)
|
||||
if viv_utils.flirt.is_library_function(vw, va):
|
||||
name = viv_utils.get_function_name(vw, va)
|
||||
yield FunctionName(name), addr
|
||||
if name.startswith("_"):
|
||||
# some linkers may prefix linked routines with a `_` to avoid name collisions.
|
||||
# extract features for both the mangled and un-mangled representations.
|
||||
# e.g. `_fwrite` -> `fwrite`
|
||||
# see: https://stackoverflow.com/a/2628384/87207
|
||||
yield FunctionName(name[1:]), addr
|
||||
|
||||
|
||||
def extract_features(vw, file_path):
|
||||
def extract_file_format(buf, **kwargs) -> Iterator[Tuple[Feature, Address]]:
|
||||
yield from capa.features.extractors.common.extract_format(buf)
|
||||
|
||||
|
||||
def extract_features(vw, buf: bytes) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
extract file features from given workspace
|
||||
|
||||
args:
|
||||
vw (vivisect.VivWorkspace): the vivisect workspace
|
||||
file_path: path to the input file
|
||||
buf: the raw input file bytes
|
||||
|
||||
yields:
|
||||
Tuple[Feature, VA]: a feature and its location.
|
||||
Tuple[Feature, Address]: a feature and its location.
|
||||
"""
|
||||
|
||||
for file_handler in FILE_HANDLERS:
|
||||
for feature, va in file_handler(vw, file_path):
|
||||
yield feature, va
|
||||
for feature, addr in file_handler(vw=vw, buf=buf): # type: ignore
|
||||
yield feature, addr
|
||||
|
||||
|
||||
FILE_HANDLERS = (
|
||||
@@ -104,4 +144,6 @@ FILE_HANDLERS = (
|
||||
extract_file_import_names,
|
||||
extract_file_section_names,
|
||||
extract_file_strings,
|
||||
extract_file_function_names,
|
||||
extract_file_format,
|
||||
)
|
||||
|
||||
@@ -1,70 +1,110 @@
|
||||
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
|
||||
# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
from typing import Tuple, Iterator
|
||||
|
||||
import envi
|
||||
import viv_utils
|
||||
import vivisect.const
|
||||
|
||||
from capa.features import Characteristic
|
||||
from capa.features.file import FunctionName
|
||||
from capa.features.common import Feature, Characteristic
|
||||
from capa.features.address import Address, AbsoluteVirtualAddress
|
||||
from capa.features.extractors import loops
|
||||
from capa.features.extractors.elf import SymTab
|
||||
from capa.features.extractors.base_extractor import FunctionHandle
|
||||
|
||||
|
||||
def interface_extract_function_XXX(f):
|
||||
def interface_extract_function_XXX(fh: FunctionHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
parse features from the given function.
|
||||
|
||||
args:
|
||||
f (viv_utils.Function): the function to process.
|
||||
f: the function to process.
|
||||
|
||||
yields:
|
||||
(Feature, int): the feature and the address at which its found.
|
||||
(Feature, Address): the feature and the address at which its found.
|
||||
"""
|
||||
yield NotImplementedError("feature"), NotImplementedError("virtual address")
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def extract_function_calls_to(f):
|
||||
def extract_function_symtab_names(fh: FunctionHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
if fh.inner.vw.metadata["Format"] == "elf":
|
||||
# the file's symbol table gets added to the metadata of the vivisect workspace.
|
||||
# this is in order to eliminate the computational overhead of refetching symtab each time.
|
||||
if "symtab" not in fh.ctx["cache"]:
|
||||
try:
|
||||
fh.ctx["cache"]["symtab"] = SymTab.from_viv(fh.inner.vw.parsedbin)
|
||||
except Exception:
|
||||
fh.ctx["cache"]["symtab"] = None
|
||||
|
||||
symtab = fh.ctx["cache"]["symtab"]
|
||||
if symtab:
|
||||
for symbol in symtab.get_symbols():
|
||||
sym_name = symtab.get_name(symbol)
|
||||
sym_value = symbol.value
|
||||
sym_info = symbol.info
|
||||
|
||||
STT_FUNC = 0x2
|
||||
if sym_value == fh.address and sym_info & STT_FUNC != 0:
|
||||
yield FunctionName(sym_name), fh.address
|
||||
|
||||
|
||||
def extract_function_calls_to(fhandle: FunctionHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
f: viv_utils.Function = fhandle.inner
|
||||
for src, _, _, _ in f.vw.getXrefsTo(f.va, rtype=vivisect.const.REF_CODE):
|
||||
yield Characteristic("calls to"), src
|
||||
yield Characteristic("calls to"), AbsoluteVirtualAddress(src)
|
||||
|
||||
|
||||
def extract_function_loop(f):
|
||||
def extract_function_loop(fhandle: FunctionHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
parse if a function has a loop
|
||||
"""
|
||||
f: viv_utils.Function = fhandle.inner
|
||||
|
||||
edges = []
|
||||
|
||||
for bb in f.basic_blocks:
|
||||
if len(bb.instructions) > 0:
|
||||
for bva, bflags in bb.instructions[-1].getBranches():
|
||||
if bva is None:
|
||||
# vivisect may be unable to recover the call target, e.g. on dynamic calls like `call esi`
|
||||
# for this bva is None, and we don't want to add it for loop detection, ref: vivisect#574
|
||||
continue
|
||||
# vivisect does not set branch flags for non-conditional jmp so add explicit check
|
||||
if (
|
||||
bflags & 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))
|
||||
|
||||
if edges and loops.has_loop(edges):
|
||||
yield Characteristic("loop"), f.va
|
||||
yield Characteristic("loop"), fhandle.address
|
||||
|
||||
|
||||
def extract_features(f):
|
||||
def extract_features(fh: FunctionHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
extract features from the given function.
|
||||
|
||||
args:
|
||||
f (viv_utils.Function): the function from which to extract features
|
||||
fh: the function handle 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):
|
||||
yield feature, va
|
||||
for feature, addr in func_handler(fh):
|
||||
yield feature, addr
|
||||
|
||||
|
||||
FUNCTION_HANDLERS = (extract_function_calls_to, extract_function_loop)
|
||||
FUNCTION_HANDLERS = (
|
||||
extract_function_symtab_names,
|
||||
extract_function_calls_to,
|
||||
extract_function_loop,
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user