mirror of
https://github.com/mandiant/capa.git
synced 2025-12-13 08:00:44 -08:00
Compare commits
517 Commits
add-cfg-in
...
v4.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
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]",
|
||||
|
||||
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
|
||||
"remoteUser": "vscode",
|
||||
"features": {
|
||||
"git": "latest"
|
||||
}
|
||||
}
|
||||
3
.github/mypy/mypy.ini
vendored
3
.github/mypy/mypy.ini
vendored
@@ -74,3 +74,6 @@ ignore_missing_imports = True
|
||||
|
||||
[mypy-elftools.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-dncil.*]
|
||||
ignore_missing_imports = True
|
||||
72
.github/pyinstaller/pyinstaller.spec
vendored
72
.github/pyinstaller/pyinstaller.spec
vendored
@@ -6,51 +6,59 @@ 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"])
|
||||
# git output will look like:
|
||||
#
|
||||
# tags/v1.0.0-0-g3af38dc
|
||||
# ------- tag
|
||||
# - commits since
|
||||
# g------- git hash fragment
|
||||
version = (
|
||||
subprocess.check_output(["git", "describe", "--always", "--tags", "--long"])
|
||||
.decode("utf-8")
|
||||
.strip()
|
||||
.replace("tags/", ""))
|
||||
f.write(("__version__ = '%s'" % version).encode("utf-8"))
|
||||
.replace("tags/", "")
|
||||
)
|
||||
# when invoking pyinstaller from the project root, this gets run from the project root.
|
||||
with open("./capa/version.py", "r", encoding="utf-8") as f:
|
||||
lines = f.read()
|
||||
# version.py contains the version string and other helper functions
|
||||
# here we manually replace the version value substring with the result of the above git output
|
||||
VERSION_DEF = "__version__ = "
|
||||
s = lines.index(VERSION_DEF)
|
||||
e = s + len(VERSION_DEF)
|
||||
off_rest_file = e + lines[e:].index("\n")
|
||||
lines = lines[s:e] + f'"{version}"' + lines[off_rest_file:]
|
||||
with open("./capa/version.py", "w", encoding="utf-8") as f:
|
||||
f.write(lines)
|
||||
|
||||
a = Analysis(
|
||||
# when invoking pyinstaller from the project root,
|
||||
# this gets invoked from the directory of the spec file,
|
||||
# i.e. ./.github/pyinstaller
|
||||
['../../capa/main.py'],
|
||||
pathex=['capa'],
|
||||
["../../capa/main.py"],
|
||||
pathex=["capa"],
|
||||
binaries=None,
|
||||
datas=[
|
||||
# when invoking pyinstaller from the project root,
|
||||
# this gets invoked from the directory of the spec file,
|
||||
# i.e. ./.github/pyinstaller
|
||||
('../../rules', 'rules'),
|
||||
('../../sigs', 'sigs'),
|
||||
|
||||
("../../rules", "rules"),
|
||||
("../../sigs", "sigs"),
|
||||
# capa.render.default uses tabulate that depends on wcwidth.
|
||||
# it seems wcwidth uses a json file `version.json`
|
||||
# and this doesn't get picked up by pyinstaller automatically.
|
||||
# so we manually embed the wcwidth resources here.
|
||||
#
|
||||
# ref: https://stackoverflow.com/a/62278462/87207
|
||||
(os.path.dirname(wcwidth.__file__), 'wcwidth')
|
||||
(os.path.dirname(wcwidth.__file__), "wcwidth"),
|
||||
],
|
||||
# when invoking pyinstaller from the project root,
|
||||
# this gets run from the project root.
|
||||
hookspath=['.github/pyinstaller/hooks'],
|
||||
hookspath=[".github/pyinstaller/hooks"],
|
||||
runtime_hooks=None,
|
||||
excludes=[
|
||||
# ignore packages that would otherwise be bundled with the .exe.
|
||||
# review: build/pyinstaller/xref-pyinstaller.html
|
||||
|
||||
# we don't do any GUI stuff, so ignore these modules
|
||||
"tkinter",
|
||||
"_tkinter",
|
||||
@@ -60,7 +68,6 @@ a = Analysis(
|
||||
# since we don't spawn a notebook, we can safely remove these.
|
||||
"IPython",
|
||||
"ipywidgets",
|
||||
|
||||
# these are pulled in by networkx
|
||||
# but we don't need to compute the strongly connected components.
|
||||
"numpy",
|
||||
@@ -68,7 +75,6 @@ a = Analysis(
|
||||
"matplotlib",
|
||||
"pandas",
|
||||
"pytest",
|
||||
|
||||
# deps from viv that we don't use.
|
||||
# this duplicates the entries in `hook-vivisect`,
|
||||
# but works better this way.
|
||||
@@ -78,32 +84,32 @@ a = Analysis(
|
||||
"PyQt5",
|
||||
"qt5",
|
||||
"pyqtwebengine",
|
||||
"pyasn1"
|
||||
])
|
||||
"pyasn1",
|
||||
],
|
||||
)
|
||||
|
||||
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,
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
exclude_binaries=False,
|
||||
name='capa',
|
||||
icon='logo.ico',
|
||||
name="capa",
|
||||
icon="logo.ico",
|
||||
debug=False,
|
||||
strip=None,
|
||||
upx=True,
|
||||
console=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,
|
||||
|
||||
25
.github/workflows/build.yml
vendored
25
.github/workflows/build.yml
vendored
@@ -1,8 +1,8 @@
|
||||
name: build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
release:
|
||||
types: [edited, published]
|
||||
|
||||
@@ -11,16 +11,19 @@ jobs:
|
||||
name: PyInstaller for ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
# set to false for debugging
|
||||
fail-fast: true
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-18.04
|
||||
# use old linux so that the shared library versioning is more portable
|
||||
artifact_name: capa
|
||||
asset_name: linux
|
||||
- os: windows-2019
|
||||
- os: windows-2022
|
||||
artifact_name: capa.exe
|
||||
asset_name: windows
|
||||
- os: macos-10.15
|
||||
# use older macOS for assumed better portability
|
||||
artifact_name: capa
|
||||
asset_name: macos
|
||||
steps:
|
||||
@@ -35,12 +38,12 @@ jobs:
|
||||
python-version: 3.8
|
||||
- if: matrix.os == 'ubuntu-18.04'
|
||||
run: sudo apt-get install -y libyaml-dev
|
||||
- name: Install PyInstaller
|
||||
run: pip install 'pyinstaller==4.2'
|
||||
- name: Install capa
|
||||
run: pip install -e .
|
||||
- name: Upgrade pip, setuptools
|
||||
run: python -m pip install --upgrade pip setuptools
|
||||
- name: Install capa with build requirements
|
||||
run: pip install -e .[build]
|
||||
- name: Build standalone executable
|
||||
run: pyinstaller .github/pyinstaller/pyinstaller.spec
|
||||
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)?
|
||||
@@ -53,8 +56,6 @@ jobs:
|
||||
path: dist/${{ matrix.artifact_name }}
|
||||
|
||||
test_run:
|
||||
# test that binaries run on push to master
|
||||
if: github.event_name == 'push'
|
||||
name: Test run on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: [build]
|
||||
@@ -68,7 +69,7 @@ jobs:
|
||||
- os: ubuntu-20.04
|
||||
artifact_name: capa
|
||||
asset_name: linux
|
||||
- os: windows-2016
|
||||
- os: windows-2022
|
||||
artifact_name: capa.exe
|
||||
asset_name: windows
|
||||
steps:
|
||||
@@ -77,7 +78,7 @@ jobs:
|
||||
with:
|
||||
name: ${{ matrix.asset_name }}
|
||||
- name: Set executable flag
|
||||
if: matrix.os != 'windows-2016'
|
||||
if: matrix.os != 'windows-2022'
|
||||
run: chmod +x ${{ matrix.artifact_name }}
|
||||
- name: Run capa
|
||||
run: ./${{ matrix.artifact_name }} -h
|
||||
|
||||
2
.github/workflows/publish.yml
vendored
2
.github/workflows/publish.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.6'
|
||||
python-version: '3.7'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
|
||||
1
.github/workflows/tag.yml
vendored
1
.github/workflows/tag.yml
vendored
@@ -21,6 +21,7 @@ jobs:
|
||||
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@master
|
||||
with:
|
||||
|
||||
12
.github/workflows/tests.yml
vendored
12
.github/workflows/tests.yml
vendored
@@ -37,6 +37,8 @@ jobs:
|
||||
run: isort --profile black --length-sort --line-width 120 -c .
|
||||
- name: Lint with black
|
||||
run: black -l 120 --check .
|
||||
- name: Lint with pycodestyle
|
||||
run: pycodestyle --show-source capa/ scripts/ tests/
|
||||
- name: Check types with mypy
|
||||
run: mypy --config-file .github/mypy/mypy.ini capa/ scripts/ tests/
|
||||
|
||||
@@ -46,7 +48,7 @@ jobs:
|
||||
- name: Checkout capa with submodules
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: true
|
||||
submodules: recursive
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
@@ -63,13 +65,11 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-20.04, windows-2019, macos-10.15]
|
||||
os: [ubuntu-20.04, windows-2019, macos-11]
|
||||
# across all operating systems
|
||||
python-version: ["3.6", "3.10"]
|
||||
python-version: ["3.7", "3.10"]
|
||||
include:
|
||||
# on Ubuntu run these as well
|
||||
- os: ubuntu-20.04
|
||||
python-version: "3.7"
|
||||
- os: ubuntu-20.04
|
||||
python-version: "3.8"
|
||||
- os: ubuntu-20.04
|
||||
@@ -78,7 +78,7 @@ jobs:
|
||||
- name: Checkout capa with submodules
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: true
|
||||
submodules: recursive
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -118,3 +118,7 @@ rule-linter-output.log
|
||||
scripts/perf/*.txt
|
||||
scripts/perf/*.svg
|
||||
scripts/perf/*.zip
|
||||
.direnv
|
||||
.envrc
|
||||
.DS_Store
|
||||
*/.DS_Store
|
||||
|
||||
146
CHANGELOG.md
146
CHANGELOG.md
@@ -12,15 +12,155 @@
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- elf: fix OS detection for Linux kernel modules #867 @williballenthin
|
||||
|
||||
### capa explorer IDA Pro plugin
|
||||
|
||||
### Development
|
||||
|
||||
### Raw diffs
|
||||
- [capa v3.1.0...master](https://github.com/mandiant/capa/compare/v3.1.0...master)
|
||||
- [capa-rules v3.1.0...master](https://github.com/mandiant/capa-rules/compare/v3.1.0...master)
|
||||
- [capa v4.0.1...master](https://github.com/mandiant/capa/compare/v4.0.1...master)
|
||||
- [capa-rules v4.0.1...master](https://github.com/mandiant/capa-rules/compare/v4.0.1...master)
|
||||
|
||||
|
||||
## v4.0.1 (2022-08-15)
|
||||
Some rules contained invalid metadata fields that caused an error when rendering rule hits. We've updated all rules and enhanced the rule linter to catch such issues.
|
||||
|
||||
### New Rules (1)
|
||||
|
||||
- anti-analysis/obfuscation/obfuscated-with-vs-obfuscation jakub.jozwiak@mandiant.com
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
- linter: use pydantic to validate rule metadata #1141 @mike-hunhoff
|
||||
- build binaries using PyInstaller no longer overwrites functions in version.py #1136 @mr-tz
|
||||
|
||||
### Raw diffs
|
||||
- [capa v4.0.0...v4.0.1](https://github.com/mandiant/capa/compare/v4.0.0...v4.0.1)
|
||||
- [capa-rules v4.0.0...v4.0.1](https://github.com/mandiant/capa-rules/compare/v4.0.0...v4.0.1)
|
||||
|
||||
## v4.0.0 (2022-08-10)
|
||||
Version 4 adds support for analyzing .NET executables. capa will autodetect .NET modules, or you can explicitly invoke the new feature extractor via `--format dotnet`. We've also extended the rule syntax for .NET features including `namespace` and `class`.
|
||||
|
||||
Additionally, new `instruction` scope and `operand` features enable users to create more explicit rules. These features are not backwards compatible. We removed the previously used `/x32` and `/x64` flavors of number and operand features.
|
||||
|
||||
We updated 49 existing rules and added 22 new rules leveraging these new features and characteristics to detect capabilities seen in .NET malware.
|
||||
|
||||
More breaking changes include updates to the JSON results document, freeze file format schema (now format version v2), and the internal handling of addresses.
|
||||
|
||||
Thanks for all the support, especially to @htnhan, @jtothej, @sara-rn, @anushkavirgaonkar, and @_re_fox!
|
||||
|
||||
*Deprecation warning: v4.0 will be the last capa version to support the SMDA backend.*
|
||||
|
||||
### New Features
|
||||
|
||||
- add new scope "instruction" for matching mnemonics and operands #767 @williballenthin
|
||||
- add new feature "operand[{0, 1, 2}].number" for matching instruction operand immediate values #767 @williballenthin
|
||||
- add new feature "operand[{0, 1, 2}].offset" for matching instruction operand offsets #767 @williballenthin
|
||||
- extract additional offset/number features in certain circumstances #320 @williballenthin
|
||||
- add detection and basic feature extraction for dotnet #987 @mr-tz, @mike-hunhoff, @williballenthin
|
||||
- add file string extraction for dotnet files #1012 @mike-hunhoff
|
||||
- add file function-name extraction for dotnet files #1015 @mike-hunhoff
|
||||
- add unmanaged call characteristic for dotnet files #1023 @mike-hunhoff
|
||||
- add mixed mode characteristic feature extraction for dotnet files #1024 @mike-hunhoff
|
||||
- emit class and namespace features for dotnet files #1030 @mike-hunhoff
|
||||
- render: support Addresses that aren't simple integers, like .NET token+offset #981 @williballenthin
|
||||
- document rule tags and branches #1006 @williballenthin, @mr-tz
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- instruction scope and operand feature are new and are not backwards compatible with older versions of capa
|
||||
- Python 3.7 is now the minimum supported Python version #866 @williballenthin
|
||||
- remove /x32 and /x64 flavors of number and operand features #932 @williballenthin
|
||||
- the tool now accepts multiple paths to rules, and JSON doc updated accordingly @williballenthin
|
||||
- extractors must use handles to identify functions/basic blocks/instructions #981 @williballenthin
|
||||
- the freeze file format schema was updated, including format version bump to v2 #986 @williballenthin
|
||||
|
||||
Deprecation notice: as described in [#937](https://github.com/mandiant/capa/issues/937), we plan to remove the SMDA backend for v5. If you rely on this backend, please reach out so we can discuss extending the support for SMDA or transitioning your workflow to use vivisect.
|
||||
|
||||
### New Rules (30)
|
||||
|
||||
- data-manipulation/encryption/aes/manually-build-aes-constants huynh.t.nhan@gmail.com
|
||||
- nursery/get-process-image-filename michael.hunhoff@mandiant.com
|
||||
- compiler/v/compiled-with-v jakub.jozwiak@mandiant.com
|
||||
- compiler/zig/compiled-with-zig jakub.jozwiak@mandiant.com
|
||||
- anti-analysis/packer/huan/packed-with-huan jakub.jozwiak@mandiant.com
|
||||
- internal/limitation/file/internal-dotnet-file-limitation william.ballenthin@mandiant.com
|
||||
- nursery/get-os-information-via-kuser_shared_data @mr-tz
|
||||
- load-code/pe/resolve-function-by-parsing-PE-exports @sara-rn
|
||||
- anti-analysis/packer/huan/packed-with-huan jakub.jozwiak@mandiant.com
|
||||
- nursery/execute-dotnet-assembly anushka.virgaonkar@mandiant.com
|
||||
- nursery/invoke-dotnet-assembly-method anushka.virgaonkar@mandiant.com
|
||||
- collection/screenshot/capture-screenshot-via-keybd-event @_re_fox
|
||||
- collection/browser/gather-chrome-based-browser-login-information @_re_fox
|
||||
- nursery/power-down-monitor michael.hunhoff@mandiant.com
|
||||
- nursery/hash-data-using-aphash @_re_fox
|
||||
- nursery/hash-data-using-jshash @_re_fox
|
||||
- host-interaction/file-system/files/list/enumerate-files-on-windows moritz.raabe@mandiant.com anushka.virgaonkar@mandiant.com
|
||||
- nursery/check-clipboard-data anushka.virgaonkar@mandiant.com
|
||||
- nursery/clear-clipboard-data anushka.virgaonkar@mandiant.com
|
||||
- nursery/compile-dotnet-assembly anushka.virgaonkar@mandiant.com
|
||||
- nursery/create-process-via-wmi anushka.virgaonkar@mandiant.com
|
||||
- nursery/display-service-notification-message-box anushka.virgaonkar@mandiant.com
|
||||
- nursery/find-process-by-name anushka.virgaonkar@mandiant.com
|
||||
- nursery/generate-random-numbers-in-dotnet anushka.virgaonkar@mandiant.com
|
||||
- nursery/send-keystrokes anushka.virgaonkar@mandiant.com
|
||||
- nursery/send-request-in-dotnet anushka.virgaonakr@mandiant.com
|
||||
- nursery/terminate-process-by-name-in-dotnet anushka.virgaonkar@mandiant.com
|
||||
- nursery/hash-data-using-rshash @_re_fox
|
||||
- persistence/authentication-process/act-as-credential-manager-dll jakub.jozwiak@mandiant.com
|
||||
- persistence/authentication-process/act-as-password-filter-dll jakub.jozwiak@mandiant.com
|
||||
|
||||
### Bug Fixes
|
||||
- improve handling _ prefix compile/link artifact #924 @mike-hunhoff
|
||||
- better detect OS in ELF samples #988 @williballenthin
|
||||
- display number feature zero in vverbose #1097 @mike-hunhoff
|
||||
|
||||
### capa explorer IDA Pro plugin
|
||||
- improve file format extraction #918 @mike-hunhoff
|
||||
- remove decorators added by IDA to ELF imports #919 @mike-hunhoff
|
||||
- bug fixes for Address abstraction #1091 @mike-hunhoff
|
||||
|
||||
### Development
|
||||
|
||||
### Raw diffs
|
||||
- [capa v3.2.0...v4.0.0](https://github.com/mandiant/capa/compare/v3.2.0...master)
|
||||
- [capa-rules v3.2.0...v4.0.0](https://github.com/mandiant/capa-rules/compare/v3.2.0...master)
|
||||
|
||||
## v3.2.1 (2022-06-06)
|
||||
This out-of-band release bumps the SMDA dependency version to enable installation on Python 3.10.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- update SMDA dependency @mike-hunhoff #922
|
||||
|
||||
### Raw diffs
|
||||
- [capa v3.2.0...v3.2.1](https://github.com/mandiant/capa/compare/v3.2.0...v3.2.1)
|
||||
- [capa-rules v3.2.0...v3.2.1](https://github.com/mandiant/capa-rules/compare/v3.2.0...v3.2.1)
|
||||
|
||||
## v3.2.0 (2022-03-03)
|
||||
This release adds a new characteristic `characteristic: call $+5` enabling users to create more explicit rules. The linter now also validates ATT&CK and MBC categories. Additionally, many dependencies, including the vivisect backend, have been updated.
|
||||
|
||||
One rule has been added and many more have been improved.
|
||||
|
||||
Thanks for all the support, especially to @kn0wl3dge and first time contributor @uckelman-sf!
|
||||
|
||||
### New Features
|
||||
|
||||
- linter: validate ATT&CK/MBC categories and IDs #103 @kn0wl3dge
|
||||
- extractor: add characteristic "call $+5" feature #366 @kn0wl3dge
|
||||
|
||||
### New Rules (1)
|
||||
|
||||
- anti-analysis/obfuscation/obfuscated-with-advobfuscator jakub.jozwiak@mandiant.com
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- remove typing package as a requirement for Python 3.7+ compatibility #901 @uckelman-sf
|
||||
- elf: fix OS detection for Linux kernel modules #867 @williballenthin
|
||||
|
||||
### Raw diffs
|
||||
- [capa v3.1.0...v3.2.0](https://github.com/mandiant/capa/compare/v3.1.0...v3.2.0)
|
||||
- [capa-rules v3.1.0...v3.2.0](https://github.com/mandiant/capa-rules/compare/v3.1.0...v3.2.0)
|
||||
|
||||
## v3.1.0 (2022-01-10)
|
||||
This release improves the performance of capa while also adding 23 new rules and many code quality enhancements. We profiled capa's CPU usage and optimized the way that it matches rules, such as by short circuiting when appropriate. According to our testing, the matching phase is approximately 66% faster than v3.0.3! We also added support for Python 3.10, aarch64 builds, and additional MAEC metadata in the rule headers.
|
||||
|
||||
52
README.md
52
README.md
@@ -2,19 +2,20 @@
|
||||
|
||||
[](https://pypi.org/project/flare-capa)
|
||||
[](https://github.com/mandiant/capa/releases)
|
||||
[](https://github.com/mandiant/capa-rules)
|
||||
[](https://github.com/mandiant/capa-rules)
|
||||
[](https://github.com/mandiant/capa/actions?query=workflow%3ACI+event%3Apush+branch%3Amaster)
|
||||
[](https://github.com/mandiant/capa/releases)
|
||||
[](LICENSE.txt)
|
||||
|
||||
capa detects capabilities in executable files.
|
||||
You run it against a PE, ELF, or shellcode file and it tells you what it thinks the program can do.
|
||||
You run it against a PE, ELF, .NET module, or shellcode file 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)
|
||||
- the major version 2.0 updates described in our [second blog post](https://www.fireeye.com/blog/threat-research/2021/07/capa-2-better-stronger-faster.html)
|
||||
- the major version 3.0 (ELF support) described in the [third blog post](https://www.fireeye.com/blog/threat-research/2021/09/elfant-in-the-room-capa-v3.html)
|
||||
- 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
|
||||
@@ -95,23 +96,32 @@ 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
|
||||
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:
|
||||
number: 0x44 @ 0x10003ADC
|
||||
api: kernel32.CreatePipe @ 0x40126F, 0x401280
|
||||
optional:
|
||||
api: kernel32.GetStartupInfo @ 0x10003AE4
|
||||
match: create pipe @ 0x10003A13
|
||||
match: create thread @ 0x40136A, 0x4013BA
|
||||
or:
|
||||
api: kernel32.CreatePipe @ 0x10003ACB
|
||||
and:
|
||||
os: windows
|
||||
or:
|
||||
string: cmd.exe /c @ 0x10003AED
|
||||
api: kernel32.CreateThread @ 0x4013D7
|
||||
or:
|
||||
and:
|
||||
os: windows
|
||||
or:
|
||||
api: kernel32.CreateThread @ 0x401395
|
||||
or:
|
||||
string: "cmd.exe" @ 0x4012FD
|
||||
...
|
||||
```
|
||||
|
||||
@@ -127,18 +137,28 @@ rule:
|
||||
meta:
|
||||
name: hash data with CRC32
|
||||
namespace: data-manipulation/checksum/crc32
|
||||
author: moritz.raabe@mandiant.com
|
||||
authors:
|
||||
- moritz.raabe@mandiant.com
|
||||
scope: function
|
||||
mbc:
|
||||
- Data::Checksum::CRC32 [C0032.001]
|
||||
examples:
|
||||
- 2D3EDC218A90F03089CC01715A9F047F:0x403CBD
|
||||
- 7D28CB106CB54876B2A5C111724A07CD:0x402350 # RtlComputeCrc32
|
||||
- 7EFF498DE13CC734262F87E6B3EF38AB:0x100084A6
|
||||
features:
|
||||
- or:
|
||||
- and:
|
||||
- mnemonic: shr
|
||||
- or:
|
||||
- number: 0xEDB88320
|
||||
- bytes: 00 00 00 00 96 30 07 77 2C 61 0E EE BA 51 09 99 19 C4 6D 07 8F F4 6A 70 35 A5 63 E9 A3 95 64 9E = crc32_tab
|
||||
- number: 8
|
||||
- characteristic: nzxor
|
||||
- and:
|
||||
- number: 0x8320
|
||||
- number: 0xEDB8
|
||||
- characteristic: nzxor
|
||||
- api: RtlComputeCrc32
|
||||
```
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ from typing import TYPE_CHECKING, Set, Dict, List, Tuple, Mapping, Iterable
|
||||
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
|
||||
@@ -26,7 +27,7 @@ if TYPE_CHECKING:
|
||||
# to collect the locations of a feature, do: `features[Number(0x10)]`
|
||||
#
|
||||
# aliased here so that the type can be documented and xref'd.
|
||||
FeatureSet = Dict[Feature, Set[int]]
|
||||
FeatureSet = Dict[Feature, Set[Address]]
|
||||
|
||||
|
||||
class Statement:
|
||||
@@ -235,8 +236,8 @@ 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(Subscope, self).__init__(description=description)
|
||||
self.scope = scope
|
||||
self.child = child
|
||||
|
||||
@@ -257,10 +258,10 @@ class Subscope(Statement):
|
||||
# inspect(match_details)
|
||||
#
|
||||
# aliased here so that the type can be documented and xref'd.
|
||||
MatchResults = Mapping[str, List[Tuple[int, Result]]]
|
||||
MatchResults = Mapping[str, List[Tuple[Address, Result]]]
|
||||
|
||||
|
||||
def index_rule_matches(features: FeatureSet, rule: "capa.rules.Rule", locations: Iterable[int]):
|
||||
def index_rule_matches(features: FeatureSet, rule: "capa.rules.Rule", locations: Iterable[Address]):
|
||||
"""
|
||||
record into the given featureset that the given rule matched at the given locations.
|
||||
|
||||
@@ -277,7 +278,7 @@ def index_rule_matches(features: FeatureSet, rule: "capa.rules.Rule", locations:
|
||||
namespace, _, _ = namespace.rpartition("/")
|
||||
|
||||
|
||||
def match(rules: List["capa.rules.Rule"], features: FeatureSet, va: int) -> Tuple[FeatureSet, MatchResults]:
|
||||
def match(rules: List["capa.rules.Rule"], features: FeatureSet, addr: Address) -> Tuple[FeatureSet, MatchResults]:
|
||||
"""
|
||||
match the given rules against the given features,
|
||||
returning an updated set of features and the matches.
|
||||
@@ -315,10 +316,10 @@ def match(rules: List["capa.rules.Rule"], features: FeatureSet, va: int) -> Tupl
|
||||
# sanity check
|
||||
assert bool(res) is True
|
||||
|
||||
results[rule.name].append((va, res))
|
||||
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, [va])
|
||||
index_rule_matches(features, rule, [addr])
|
||||
|
||||
return (features, results)
|
||||
|
||||
14
capa/exceptions.py
Normal file
14
capa/exceptions.py
Normal file
@@ -0,0 +1,14 @@
|
||||
class UnsupportedRuntimeError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
class UnsupportedFormatError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class UnsupportedArchError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class UnsupportedOSError(ValueError):
|
||||
pass
|
||||
117
capa/features/address.py
Normal file
117
capa/features/address.py
Normal file
@@ -0,0 +1,117 @@
|
||||
import abc
|
||||
|
||||
from dncil.clr.token import Token
|
||||
|
||||
|
||||
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})"
|
||||
|
||||
|
||||
class RelativeVirtualAddress(int, Address):
|
||||
"""a memory address relative to a base address"""
|
||||
|
||||
def __repr__(self):
|
||||
return f"relative(0x{self:x})"
|
||||
|
||||
|
||||
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})"
|
||||
|
||||
|
||||
class DNTokenAddress(Address):
|
||||
"""a .NET token"""
|
||||
|
||||
def __init__(self, token: Token):
|
||||
self.token = token
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.token.value == other.token.value
|
||||
|
||||
def __lt__(self, other):
|
||||
return self.token.value < other.token.value
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.token.value)
|
||||
|
||||
def __repr__(self):
|
||||
return f"token(0x{self.token.value:x})"
|
||||
|
||||
def __index__(self):
|
||||
# returns the object converted to an integer
|
||||
return self.token.value
|
||||
|
||||
|
||||
class DNTokenOffsetAddress(Address):
|
||||
"""an offset into an object specified by a .NET token"""
|
||||
|
||||
def __init__(self, token: Token, offset: int):
|
||||
assert offset >= 0
|
||||
self.token = token
|
||||
self.offset = offset
|
||||
|
||||
def __eq__(self, other):
|
||||
return (self.token.value, self.offset) == (other.token.value, other.offset)
|
||||
|
||||
def __lt__(self, other):
|
||||
return (self.token.value, self.offset) < (other.token.value, other.offset)
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.token.value, self.offset))
|
||||
|
||||
def __repr__(self):
|
||||
return f"token(0x{self.token.value:x})+(0x{self.offset:x})"
|
||||
|
||||
def __index__(self):
|
||||
return self.token.value + self.offset
|
||||
|
||||
|
||||
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()
|
||||
@@ -10,18 +10,11 @@ from capa.features.common import Feature
|
||||
|
||||
|
||||
class BasicBlock(Feature):
|
||||
def __init__(self):
|
||||
super(BasicBlock, self).__init__(None)
|
||||
def __init__(self, description=None):
|
||||
super(BasicBlock, self).__init__(None, 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()
|
||||
|
||||
@@ -7,10 +7,11 @@
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import re
|
||||
import abc
|
||||
import codecs
|
||||
import logging
|
||||
import collections
|
||||
from typing import TYPE_CHECKING, Set, Dict, List, Union
|
||||
from typing import TYPE_CHECKING, Set, Dict, List, Union, Optional, Sequence
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# circular import, otherwise
|
||||
@@ -19,6 +20,7 @@ if TYPE_CHECKING:
|
||||
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
|
||||
@@ -69,20 +71,13 @@ class Result:
|
||||
success: bool,
|
||||
statement: Union["capa.engine.Statement", "Feature"],
|
||||
children: List["Result"],
|
||||
locations=None,
|
||||
locations: Optional[Set[Address]] = 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 ()
|
||||
self.locations = locations if locations is not None else set()
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, bool):
|
||||
@@ -96,34 +91,33 @@ class Result:
|
||||
return self.success
|
||||
|
||||
|
||||
class Feature:
|
||||
def __init__(self, value: Union[str, int, bytes], bitness=None, description=None):
|
||||
class Feature(abc.ABC):
|
||||
def __init__(self, value: Union[str, int, float, bytes], description=None):
|
||||
"""
|
||||
Args:
|
||||
value (any): the value of the feature, such as the number or string.
|
||||
bitness (str): one of the VALID_BITNESS values, or None.
|
||||
When None, then the feature applies to any bitness.
|
||||
Modifies the feature name from `feature` to `feature/bitness`, like `offset/x32`.
|
||||
description (str): a human-readable description that explains the feature value.
|
||||
"""
|
||||
super(Feature, self).__init__()
|
||||
|
||||
if bitness is not None:
|
||||
if bitness not in VALID_BITNESS:
|
||||
raise ValueError("bitness '%s' must be one of %s" % (bitness, VALID_BITNESS))
|
||||
self.name = self.__class__.__name__.lower() + "/" + bitness
|
||||
else:
|
||||
self.name = self.__class__.__name__.lower()
|
||||
|
||||
self.value = value
|
||||
self.bitness = bitness
|
||||
self.description = description
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.name, self.value, self.bitness))
|
||||
return hash((self.name, self.value))
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.name == other.name and self.value == other.value and self.bitness == other.bitness
|
||||
return self.name == other.name and self.value == other.value
|
||||
|
||||
def __lt__(self, other):
|
||||
# TODO: this is a huge hack!
|
||||
import capa.features.freeze.features
|
||||
|
||||
return (
|
||||
capa.features.freeze.features.feature_from_capa(self).json()
|
||||
< capa.features.freeze.features.feature_from_capa(other).json()
|
||||
)
|
||||
|
||||
def get_value_str(self) -> str:
|
||||
"""
|
||||
@@ -146,28 +140,10 @@ class Feature:
|
||||
def __repr__(self):
|
||||
return str(self)
|
||||
|
||||
def evaluate(self, ctx: Dict["Feature", Set[int]], **kwargs) -> Result:
|
||||
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, []))
|
||||
|
||||
def freeze_serialize(self):
|
||||
if self.bitness is not None:
|
||||
return (self.__class__.__name__, [self.value, {"bitness": self.bitness}])
|
||||
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)
|
||||
return Result(self in ctx, self, [], locations=ctx.get(self, set()))
|
||||
|
||||
|
||||
class MatchedRule(Feature):
|
||||
@@ -178,7 +154,6 @@ class MatchedRule(Feature):
|
||||
|
||||
class Characteristic(Feature):
|
||||
def __init__(self, value: str, description=None):
|
||||
|
||||
super(Characteristic, self).__init__(value, description=description)
|
||||
|
||||
|
||||
@@ -187,6 +162,16 @@ class String(Feature):
|
||||
super(String, self).__init__(value, description=description)
|
||||
|
||||
|
||||
class Class(Feature):
|
||||
def __init__(self, value: str, description=None):
|
||||
super(Class, self).__init__(value, description=description)
|
||||
|
||||
|
||||
class Namespace(Feature):
|
||||
def __init__(self, value: str, description=None):
|
||||
super(Namespace, self).__init__(value, description=description)
|
||||
|
||||
|
||||
class Substring(String):
|
||||
def __init__(self, value: str, description=None):
|
||||
super(Substring, self).__init__(value, description=description)
|
||||
@@ -231,7 +216,7 @@ class Substring(String):
|
||||
# instead, return a new instance that has a reference to both the substring and the matched values.
|
||||
return Result(True, _MatchedSubstring(self, matches), [], locations=locations)
|
||||
else:
|
||||
return Result(False, _MatchedSubstring(self, None), [])
|
||||
return Result(False, _MatchedSubstring(self, {}), [])
|
||||
|
||||
def __str__(self):
|
||||
return "substring(%s)" % self.value
|
||||
@@ -245,11 +230,11 @@ class _MatchedSubstring(Substring):
|
||||
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):
|
||||
def __init__(self, substring: Substring, matches: Dict[str, Set[Address]]):
|
||||
"""
|
||||
args:
|
||||
substring (Substring): the substring feature that matches.
|
||||
match (Dict[string, List[int]]|None): mapping from matching string to its locations.
|
||||
substring: the substring feature that matches.
|
||||
match: mapping from matching string to its locations.
|
||||
"""
|
||||
super(_MatchedSubstring, self).__init__(str(substring.value), description=substring.description)
|
||||
# we want this to collide with the name of `Substring` above,
|
||||
@@ -328,7 +313,7 @@ class Regex(String):
|
||||
# see #262.
|
||||
return Result(True, _MatchedRegex(self, matches), [], locations=locations)
|
||||
else:
|
||||
return Result(False, _MatchedRegex(self, None), [])
|
||||
return Result(False, _MatchedRegex(self, {}), [])
|
||||
|
||||
def __str__(self):
|
||||
return "regex(string =~ %s)" % self.value
|
||||
@@ -342,11 +327,11 @@ class _MatchedRegex(Regex):
|
||||
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):
|
||||
def __init__(self, regex: Regex, matches: Dict[str, Set[Address]]):
|
||||
"""
|
||||
args:
|
||||
regex (Regex): the regex feature that matches.
|
||||
match (Dict[string, List[int]]|None): mapping from matching string to its locations.
|
||||
regex: the regex feature that matches.
|
||||
matches: mapping from matching string to its locations.
|
||||
"""
|
||||
super(_MatchedRegex, self).__init__(str(regex.value), description=regex.description)
|
||||
# we want this to collide with the name of `Regex` above,
|
||||
@@ -390,25 +375,13 @@ class Bytes(Feature):
|
||||
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])
|
||||
|
||||
|
||||
# identifiers for supported bitness names that tweak a feature
|
||||
# for example, offset/x32
|
||||
BITNESS_X32 = "x32"
|
||||
BITNESS_X64 = "x64"
|
||||
VALID_BITNESS = (BITNESS_X32, BITNESS_X64)
|
||||
|
||||
|
||||
# other candidates here: https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#machine-types
|
||||
ARCH_I386 = "i386"
|
||||
ARCH_AMD64 = "amd64"
|
||||
VALID_ARCH = (ARCH_I386, ARCH_AMD64)
|
||||
# dotnet
|
||||
ARCH_ANY = "any"
|
||||
VALID_ARCH = (ARCH_I386, ARCH_AMD64, ARCH_ANY)
|
||||
|
||||
|
||||
class Arch(Feature):
|
||||
@@ -420,8 +393,10 @@ class Arch(Feature):
|
||||
OS_WINDOWS = "windows"
|
||||
OS_LINUX = "linux"
|
||||
OS_MACOS = "macos"
|
||||
# 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})
|
||||
VALID_OS.update({OS_WINDOWS, OS_LINUX, OS_MACOS, OS_ANY})
|
||||
|
||||
|
||||
class OS(Feature):
|
||||
@@ -432,7 +407,14 @@ class OS(Feature):
|
||||
|
||||
FORMAT_PE = "pe"
|
||||
FORMAT_ELF = "elf"
|
||||
VALID_FORMAT = (FORMAT_PE, FORMAT_ELF)
|
||||
FORMAT_DOTNET = "dotnet"
|
||||
VALID_FORMAT = (FORMAT_PE, FORMAT_ELF, FORMAT_DOTNET)
|
||||
# internal only, not to be used in rules
|
||||
FORMAT_AUTO = "auto"
|
||||
FORMAT_SC32 = "sc32"
|
||||
FORMAT_SC64 = "sc64"
|
||||
FORMAT_FREEZE = "freeze"
|
||||
FORMAT_UNKNOWN = "unknown"
|
||||
|
||||
|
||||
class Format(Feature):
|
||||
|
||||
@@ -7,23 +7,60 @@
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import abc
|
||||
from typing import Tuple, Iterator, SupportsInt
|
||||
import dataclasses
|
||||
from typing import Any, Dict, Tuple, Union, Iterator
|
||||
from dataclasses import dataclass
|
||||
|
||||
import capa.features.address
|
||||
from capa.features.common import Feature
|
||||
from capa.features.address import Address, AbsoluteVirtualAddress
|
||||
|
||||
# feature extractors may reference functions, BBs, insns by opaque handle values.
|
||||
# the only requirement of these handles are that they support `__int__`,
|
||||
# so that they can be rendered as addresses.
|
||||
# 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.
|
||||
#
|
||||
# int(FunctionHandle) -> function start address
|
||||
# int(BBHandle) -> BasicBlock start address
|
||||
# int(InsnHandle) -> instruction address
|
||||
FunctionHandle = SupportsInt
|
||||
BBHandle = SupportsInt
|
||||
InsnHandle = SupportsInt
|
||||
|
||||
|
||||
@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 FeatureExtractor:
|
||||
@@ -53,14 +90,18 @@ class FeatureExtractor:
|
||||
super(FeatureExtractor, self).__init__()
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_base_address(self) -> int:
|
||||
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()
|
||||
|
||||
@abc.abstractmethod
|
||||
def extract_global_features(self) -> Iterator[Tuple[Feature, int]]:
|
||||
def extract_global_features(self) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
extract features found at every scope ("global").
|
||||
|
||||
@@ -71,12 +112,12 @@ class FeatureExtractor:
|
||||
print('0x%x: %s', va, feature)
|
||||
|
||||
yields:
|
||||
Tuple[Feature, int]: feature and its location
|
||||
Tuple[Feature, Address]: feature and its location
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
def extract_file_features(self) -> Iterator[Tuple[Feature, int]]:
|
||||
def extract_file_features(self) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
extract file-scope features.
|
||||
|
||||
@@ -87,7 +128,7 @@ class FeatureExtractor:
|
||||
print('0x%x: %s', va, feature)
|
||||
|
||||
yields:
|
||||
Tuple[Feature, int]: feature and its location
|
||||
Tuple[Feature, Address]: feature and its location
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@@ -99,32 +140,33 @@ class FeatureExtractor:
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def is_library_function(self, va: int) -> bool:
|
||||
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 a VA here, rather than function object, to handle addresses identified in instructions.
|
||||
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:
|
||||
va (int): the virtual address of a function.
|
||||
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, va: int) -> str:
|
||||
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:
|
||||
va (int): the virtual address of a function.
|
||||
addr (Address): the address of a function.
|
||||
|
||||
returns:
|
||||
str: the function name
|
||||
@@ -132,10 +174,10 @@ class FeatureExtractor:
|
||||
raises:
|
||||
KeyError: when the given function does not have a name.
|
||||
"""
|
||||
raise KeyError(va)
|
||||
raise KeyError(addr)
|
||||
|
||||
@abc.abstractmethod
|
||||
def extract_function_features(self, f: FunctionHandle) -> Iterator[Tuple[Feature, int]]:
|
||||
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.
|
||||
@@ -144,14 +186,14 @@ class FeatureExtractor:
|
||||
|
||||
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)
|
||||
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, int]: feature and its location
|
||||
Tuple[Feature, Address]: feature and its location
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@@ -164,7 +206,7 @@ class FeatureExtractor:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
def extract_basic_block_features(self, f: FunctionHandle, bb: BBHandle) -> Iterator[Tuple[Feature, int]]:
|
||||
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.
|
||||
@@ -174,15 +216,15 @@ class FeatureExtractor:
|
||||
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)
|
||||
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, int]: feature and its location
|
||||
Tuple[Feature, Address]: feature and its location
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@@ -195,7 +237,9 @@ class FeatureExtractor:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
def extract_insn_features(self, f: FunctionHandle, bb: BBHandle, insn: InsnHandle) -> Iterator[Tuple[Feature, int]]:
|
||||
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.
|
||||
@@ -206,8 +250,8 @@ class FeatureExtractor:
|
||||
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)
|
||||
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()`.
|
||||
@@ -215,123 +259,6 @@ class FeatureExtractor:
|
||||
insn [InsnHandle]: an opaque value previously fetched from `.get_instructions()`.
|
||||
|
||||
yields:
|
||||
Tuple[Feature, int]: feature and its location
|
||||
Tuple[Feature, Address]: feature and its location
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
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,
|
||||
'global features': [
|
||||
(0x0, capa.features.Arch('i386')),
|
||||
(0x0, capa.features.OS('linux')),
|
||||
],
|
||||
'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_global_features(self):
|
||||
for p in self.features.get("global features", []):
|
||||
va, feature = p
|
||||
yield feature, va
|
||||
|
||||
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
|
||||
|
||||
@@ -2,33 +2,38 @@ import io
|
||||
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
|
||||
from capa.features.common import OS, FORMAT_PE, FORMAT_ELF, OS_WINDOWS, Arch, Format, String
|
||||
from capa.features.common import OS, FORMAT_PE, FORMAT_ELF, OS_WINDOWS, FORMAT_FREEZE, Arch, Format, String, Feature
|
||||
from capa.features.freeze import is_freeze
|
||||
from capa.features.address import NO_ADDRESS, Address, FileOffsetAddress
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def extract_file_strings(buf, **kwargs):
|
||||
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), s.offset
|
||||
yield String(s.s), FileOffsetAddress(s.offset)
|
||||
|
||||
for s in capa.features.extractors.strings.extract_unicode_strings(buf):
|
||||
yield String(s.s), s.offset
|
||||
yield String(s.s), FileOffsetAddress(s.offset)
|
||||
|
||||
|
||||
def extract_format(buf):
|
||||
def extract_format(buf) -> Iterator[Tuple[Feature, Address]]:
|
||||
if buf.startswith(b"MZ"):
|
||||
yield Format(FORMAT_PE), 0x0
|
||||
yield Format(FORMAT_PE), NO_ADDRESS
|
||||
elif buf.startswith(b"\x7fELF"):
|
||||
yield Format(FORMAT_ELF), 0x0
|
||||
yield Format(FORMAT_ELF), NO_ADDRESS
|
||||
elif is_freeze(buf):
|
||||
yield Format(FORMAT_FREEZE), NO_ADDRESS
|
||||
else:
|
||||
# we likely end up here:
|
||||
# 1. handling a file format (e.g. macho)
|
||||
@@ -38,7 +43,7 @@ def extract_format(buf):
|
||||
return
|
||||
|
||||
|
||||
def extract_arch(buf):
|
||||
def extract_arch(buf) -> Iterator[Tuple[Feature, Address]]:
|
||||
if buf.startswith(b"MZ"):
|
||||
yield from capa.features.extractors.pefile.extract_file_arch(pe=pefile.PE(data=buf))
|
||||
|
||||
@@ -50,7 +55,7 @@ def extract_arch(buf):
|
||||
logger.debug("unsupported arch: %s", arch)
|
||||
return
|
||||
|
||||
yield Arch(arch), 0x0
|
||||
yield Arch(arch), NO_ADDRESS
|
||||
|
||||
else:
|
||||
# we likely end up here:
|
||||
@@ -67,9 +72,9 @@ def extract_arch(buf):
|
||||
return
|
||||
|
||||
|
||||
def extract_os(buf):
|
||||
def extract_os(buf) -> Iterator[Tuple[Feature, Address]]:
|
||||
if buf.startswith(b"MZ"):
|
||||
yield OS(OS_WINDOWS), 0x0
|
||||
yield OS(OS_WINDOWS), NO_ADDRESS
|
||||
elif buf.startswith(b"\x7fELF"):
|
||||
with contextlib.closing(io.BytesIO(buf)) as f:
|
||||
os = capa.features.extractors.elf.detect_elf_os(f)
|
||||
@@ -78,7 +83,7 @@ def extract_os(buf):
|
||||
logger.debug("unsupported os: %s", os)
|
||||
return
|
||||
|
||||
yield OS(os), 0x0
|
||||
yield OS(os), NO_ADDRESS
|
||||
|
||||
else:
|
||||
# we likely end up here:
|
||||
|
||||
0
capa/features/extractors/dnfile/__init__.py
Normal file
0
capa/features/extractors/dnfile/__init__.py
Normal file
71
capa/features/extractors/dnfile/extractor.py
Normal file
71
capa/features/extractors/dnfile/extractor.py
Normal file
@@ -0,0 +1,71 @@
|
||||
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, Tuple, Iterator
|
||||
|
||||
import dnfile
|
||||
from dncil.clr.token import Token
|
||||
|
||||
import capa.features.extractors
|
||||
import capa.features.extractors.dnfile.file
|
||||
import capa.features.extractors.dnfile.insn
|
||||
from capa.features.common import Feature
|
||||
from capa.features.address import NO_ADDRESS, Address, DNTokenAddress, DNTokenOffsetAddress, AbsoluteVirtualAddress
|
||||
from capa.features.extractors.base_extractor import BBHandle, InsnHandle, FunctionHandle, FeatureExtractor
|
||||
from capa.features.extractors.dnfile.helpers import get_dotnet_managed_method_bodies
|
||||
|
||||
|
||||
class DnfileFeatureExtractor(FeatureExtractor):
|
||||
def __init__(self, path: str):
|
||||
super(DnfileFeatureExtractor, self).__init__()
|
||||
self.pe: dnfile.dnPE = dnfile.dnPE(path)
|
||||
|
||||
# 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_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]:
|
||||
for token, f in get_dotnet_managed_method_bodies(self.pe):
|
||||
yield FunctionHandle(address=DNTokenAddress(Token(token)), inner=f, ctx={"pe": self.pe})
|
||||
|
||||
def extract_function_features(self, f):
|
||||
# TODO
|
||||
yield from []
|
||||
|
||||
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.token, 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) 2020 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
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,
|
||||
)
|
||||
261
capa/features/extractors/dnfile/helpers.py
Normal file
261
capa/features/extractors/dnfile/helpers.py
Normal file
@@ -0,0 +1,261 @@
|
||||
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any, Tuple, 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
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# key indexes to dotnet metadata tables
|
||||
DOTNET_META_TABLES_BY_INDEX = {table.value: table.name for table in dnfile.enums.MetadataTables}
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
class DnClass(object):
|
||||
def __init__(self, token: int, namespace: str, classname: str):
|
||||
self.token: int = token
|
||||
self.namespace: str = namespace
|
||||
self.classname: str = classname
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.token,))
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.token == other.token
|
||||
|
||||
def __str__(self):
|
||||
return DnClass.format_name(self.namespace, self.classname)
|
||||
|
||||
def __repr__(self):
|
||||
return str(self)
|
||||
|
||||
@staticmethod
|
||||
def format_name(namespace: str, classname: str):
|
||||
name: str = classname
|
||||
if namespace:
|
||||
# like System.IO.File::OpenRead
|
||||
name = f"{namespace}.{name}"
|
||||
return name
|
||||
|
||||
|
||||
class DnMethod(DnClass):
|
||||
def __init__(self, token: int, namespace: str, classname: str, methodname: str):
|
||||
super(DnMethod, self).__init__(token, namespace, classname)
|
||||
self.methodname: str = methodname
|
||||
|
||||
def __str__(self):
|
||||
return DnMethod.format_name(self.namespace, self.classname, self.methodname)
|
||||
|
||||
@staticmethod
|
||||
def format_name(namespace: str, classname: str, methodname: str): # type: ignore
|
||||
# like File::OpenRead
|
||||
name: str = f"{classname}::{methodname}"
|
||||
if namespace:
|
||||
# like System.IO.File::OpenRead
|
||||
name = f"{namespace}.{name}"
|
||||
return name
|
||||
|
||||
|
||||
class DnUnmanagedMethod:
|
||||
def __init__(self, token: int, modulename: str, methodname: str):
|
||||
self.token: int = token
|
||||
self.modulename: str = modulename
|
||||
self.methodname: str = methodname
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.token,))
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.token == other.token
|
||||
|
||||
def __str__(self):
|
||||
return DnUnmanagedMethod.format_name(self.modulename, self.methodname)
|
||||
|
||||
def __repr__(self):
|
||||
return str(self)
|
||||
|
||||
@staticmethod
|
||||
def format_name(modulename, methodname):
|
||||
return f"{modulename}.{methodname}"
|
||||
|
||||
|
||||
def resolve_dotnet_token(pe: dnfile.dnPE, token: Token) -> Any:
|
||||
"""map generic token to string or table row"""
|
||||
if isinstance(token, StringToken):
|
||||
user_string: Optional[str] = read_dotnet_user_string(pe, token)
|
||||
if user_string is None:
|
||||
return InvalidToken(token.value)
|
||||
return user_string
|
||||
|
||||
table_name: str = DOTNET_META_TABLES_BY_INDEX.get(token.table, "")
|
||||
if not table_name:
|
||||
# table_index is not valid
|
||||
return InvalidToken(token.value)
|
||||
|
||||
table: Any = getattr(pe.net.mdtables, table_name, None)
|
||||
if table is None:
|
||||
# table index is valid but table is not present
|
||||
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.warn("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"""
|
||||
try:
|
||||
user_string: Optional[dnfile.stream.UserString] = pe.net.user_strings.get_us(token.rid)
|
||||
except UnicodeDecodeError as e:
|
||||
logger.warn("failed to decode #US stream index 0x%08x (%s)" % (token.rid, e))
|
||||
return None
|
||||
|
||||
if user_string is None:
|
||||
return None
|
||||
|
||||
return user_string.value
|
||||
|
||||
|
||||
def get_dotnet_managed_imports(pe: dnfile.dnPE) -> Iterator[DnMethod]:
|
||||
"""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, row) in enumerate(iter_dotnet_table(pe, "MemberRef")):
|
||||
if not isinstance(row.Class.row, dnfile.mdtable.TypeRefRow):
|
||||
continue
|
||||
|
||||
token: int = calculate_dotnet_token_value(pe.net.mdtables.MemberRef.number, rid + 1)
|
||||
yield DnMethod(token, row.Class.row.TypeNamespace, row.Class.row.TypeName, row.Name)
|
||||
|
||||
|
||||
def get_dotnet_managed_methods(pe: dnfile.dnPE) -> Iterator[DnMethod]:
|
||||
"""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 continguous run of Methods owned by this Type)
|
||||
"""
|
||||
for row in iter_dotnet_table(pe, "TypeDef"):
|
||||
for index in row.MethodList:
|
||||
token = calculate_dotnet_token_value(index.table.number, index.row_index)
|
||||
yield DnMethod(token, row.TypeNamespace, row.TypeName, index.row.Name)
|
||||
|
||||
|
||||
def get_dotnet_managed_method_bodies(pe: dnfile.dnPE) -> Iterator[Tuple[int, CilMethodBody]]:
|
||||
"""get managed methods from MethodDef table"""
|
||||
if not hasattr(pe.net.mdtables, "MethodDef"):
|
||||
return
|
||||
|
||||
for (rid, row) in enumerate(pe.net.mdtables.MethodDef):
|
||||
if not row.ImplFlags.miIL or any((row.Flags.mdAbstract, row.Flags.mdPinvokeImpl)):
|
||||
# skip methods that do not have a method body
|
||||
continue
|
||||
|
||||
body: Optional[CilMethodBody] = read_dotnet_method_body(pe, row)
|
||||
if body is None:
|
||||
continue
|
||||
|
||||
token: int = calculate_dotnet_token_value(dnfile.enums.MetadataTables.MethodDef.value, rid + 1)
|
||||
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 row in iter_dotnet_table(pe, "ImplMap"):
|
||||
modulename: str = row.ImportScope.row.Name
|
||||
methodname: str = row.ImportName
|
||||
|
||||
# ECMA says "Each row of the ImplMap table associates a row in the MethodDef table (MemberForwarded) with the
|
||||
# name of a routine (ImportName) in some unmanaged DLL (ImportScope)"; so we calculate and map the MemberForwarded
|
||||
# MethodDef table token to help us later record native import method calls made from CIL
|
||||
token: int = calculate_dotnet_token_value(row.MemberForwarded.table.number, row.MemberForwarded.row_index)
|
||||
|
||||
# like Kernel32.dll
|
||||
if modulename and "." in modulename:
|
||||
modulename = modulename.split(".")[0]
|
||||
|
||||
# like kernel32.CreateFileA
|
||||
yield DnUnmanagedMethod(token, modulename, methodname)
|
||||
|
||||
|
||||
def calculate_dotnet_token_value(table: int, rid: int) -> int:
|
||||
return ((table & 0xFF) << Token.TABLE_SHIFT) | (rid & Token.RID_MASK)
|
||||
|
||||
|
||||
def is_dotnet_table_valid(pe: dnfile.dnPE, table_name: str) -> bool:
|
||||
return bool(getattr(pe.net.mdtables, table_name, None))
|
||||
|
||||
|
||||
def is_dotnet_mixed_mode(pe: dnfile.dnPE) -> bool:
|
||||
return not bool(pe.net.Flags.CLR_ILONLY)
|
||||
|
||||
|
||||
def iter_dotnet_table(pe: dnfile.dnPE, name: str) -> Iterator[Any]:
|
||||
if not is_dotnet_table_valid(pe, name):
|
||||
return
|
||||
for row in getattr(pe.net.mdtables, name):
|
||||
yield row
|
||||
182
capa/features/extractors/dnfile/insn.py
Normal file
182
capa/features/extractors/dnfile/insn.py
Normal file
@@ -0,0 +1,182 @@
|
||||
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, Tuple, Union, Iterator, Optional
|
||||
|
||||
import dnfile
|
||||
from dncil.cil.body import CilMethodBody
|
||||
from dncil.clr.token import Token, StringToken, InvalidToken
|
||||
from dncil.cil.opcode import OpCodes
|
||||
from dncil.cil.instruction import Instruction
|
||||
|
||||
import capa.features.extractors.helpers
|
||||
from capa.features.insn import API, Number
|
||||
from capa.features.common import Class, String, Feature, Namespace, Characteristic
|
||||
from capa.features.address import Address
|
||||
from capa.features.extractors.base_extractor import BBHandle, InsnHandle, FunctionHandle
|
||||
from capa.features.extractors.dnfile.helpers import (
|
||||
DnClass,
|
||||
DnMethod,
|
||||
DnUnmanagedMethod,
|
||||
resolve_dotnet_token,
|
||||
read_dotnet_user_string,
|
||||
get_dotnet_managed_imports,
|
||||
get_dotnet_managed_methods,
|
||||
get_dotnet_unmanaged_imports,
|
||||
)
|
||||
|
||||
|
||||
def get_managed_imports(ctx: Dict) -> Dict:
|
||||
if "managed_imports_cache" not in ctx:
|
||||
ctx["managed_imports_cache"] = {}
|
||||
for method in get_dotnet_managed_imports(ctx["pe"]):
|
||||
ctx["managed_imports_cache"][method.token] = method
|
||||
return ctx["managed_imports_cache"]
|
||||
|
||||
|
||||
def get_unmanaged_imports(ctx: Dict) -> Dict:
|
||||
if "unmanaged_imports_cache" not in ctx:
|
||||
ctx["unmanaged_imports_cache"] = {}
|
||||
for imp in get_dotnet_unmanaged_imports(ctx["pe"]):
|
||||
ctx["unmanaged_imports_cache"][imp.token] = imp
|
||||
return ctx["unmanaged_imports_cache"]
|
||||
|
||||
|
||||
def get_methods(ctx: Dict) -> Dict:
|
||||
if "methods_cache" not in ctx:
|
||||
ctx["methods_cache"] = {}
|
||||
for method in get_dotnet_managed_methods(ctx["pe"]):
|
||||
ctx["methods_cache"][method.token] = method
|
||||
return ctx["methods_cache"]
|
||||
|
||||
|
||||
def get_callee(ctx: Dict, token: int) -> Union[DnMethod, DnUnmanagedMethod, None]:
|
||||
"""map dotnet token to un/managed method"""
|
||||
callee: Union[DnMethod, DnUnmanagedMethod, None] = get_managed_imports(ctx).get(token, None)
|
||||
if not callee:
|
||||
# we must check unmanaged imports before managed methods because we map forwarded managed methods
|
||||
# to their unmanaged imports; we prefer a forwarded managed method be mapped to its unmanaged import for analysis
|
||||
callee = get_unmanaged_imports(ctx).get(token, None)
|
||||
if not callee:
|
||||
callee = get_methods(ctx).get(token, None)
|
||||
return callee
|
||||
|
||||
|
||||
def extract_insn_api_features(fh: FunctionHandle, bh, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""parse instruction API features"""
|
||||
insn: Instruction = ih.inner
|
||||
|
||||
if insn.opcode not in (OpCodes.Call, OpCodes.Callvirt, OpCodes.Jmp, OpCodes.Calli):
|
||||
return
|
||||
|
||||
callee: Union[DnMethod, DnUnmanagedMethod, None] = get_callee(fh.ctx, insn.operand.value)
|
||||
if callee is None:
|
||||
return
|
||||
|
||||
if isinstance(callee, DnUnmanagedMethod):
|
||||
# like kernel32.CreateFileA
|
||||
for name in capa.features.extractors.helpers.generate_symbols(callee.modulename, callee.methodname):
|
||||
yield API(name), ih.address
|
||||
else:
|
||||
# like System.IO.File::Delete
|
||||
yield API(str(callee)), ih.address
|
||||
|
||||
|
||||
def extract_insn_class_features(fh: FunctionHandle, bh, ih: InsnHandle) -> Iterator[Tuple[Class, Address]]:
|
||||
"""parse instruction class features"""
|
||||
if ih.inner.opcode not in (OpCodes.Call, OpCodes.Callvirt, OpCodes.Jmp, OpCodes.Calli):
|
||||
return
|
||||
|
||||
row: Any = resolve_dotnet_token(fh.ctx["pe"], Token(ih.inner.operand.value))
|
||||
|
||||
if not isinstance(row, dnfile.mdtable.MemberRefRow):
|
||||
return
|
||||
if not isinstance(row.Class.row, (dnfile.mdtable.TypeRefRow, dnfile.mdtable.TypeDefRow)):
|
||||
return
|
||||
|
||||
yield Class(DnClass.format_name(row.Class.row.TypeNamespace, row.Class.row.TypeName)), ih.address
|
||||
|
||||
|
||||
def extract_insn_namespace_features(fh: FunctionHandle, bh, ih: InsnHandle) -> Iterator[Tuple[Namespace, Address]]:
|
||||
"""parse instruction namespace features"""
|
||||
if ih.inner.opcode not in (OpCodes.Call, OpCodes.Callvirt, OpCodes.Jmp, OpCodes.Calli):
|
||||
return
|
||||
|
||||
row: Any = resolve_dotnet_token(fh.ctx["pe"], Token(ih.inner.operand.value))
|
||||
|
||||
if not isinstance(row, dnfile.mdtable.MemberRefRow):
|
||||
return
|
||||
if not isinstance(row.Class.row, (dnfile.mdtable.TypeRefRow, dnfile.mdtable.TypeDefRow)):
|
||||
return
|
||||
if not row.Class.row.TypeNamespace:
|
||||
return
|
||||
|
||||
yield Namespace(row.Class.row.TypeNamespace), ih.address
|
||||
|
||||
|
||||
def extract_insn_number_features(fh, bh, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""parse instruction number features"""
|
||||
insn: Instruction = ih.inner
|
||||
|
||||
if insn.is_ldc():
|
||||
yield Number(insn.get_ldc()), ih.address
|
||||
|
||||
|
||||
def extract_insn_string_features(fh: FunctionHandle, bh, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""parse instruction string features"""
|
||||
f: CilMethodBody = fh.inner
|
||||
insn: Instruction = ih.inner
|
||||
|
||||
if not insn.is_ldstr():
|
||||
return
|
||||
|
||||
if not isinstance(insn.operand, StringToken):
|
||||
return
|
||||
|
||||
user_string: Optional[str] = read_dotnet_user_string(fh.ctx["pe"], insn.operand)
|
||||
if user_string is None:
|
||||
return
|
||||
|
||||
yield String(user_string), ih.address
|
||||
|
||||
|
||||
def extract_unmanaged_call_characteristic_features(
|
||||
fh: FunctionHandle, bb: BBHandle, ih: InsnHandle
|
||||
) -> Iterator[Tuple[Characteristic, Address]]:
|
||||
insn: Instruction = ih.inner
|
||||
if insn.opcode not in (OpCodes.Call, OpCodes.Callvirt, OpCodes.Jmp, OpCodes.Calli):
|
||||
return
|
||||
|
||||
token: Any = resolve_dotnet_token(fh.ctx["pe"], insn.operand)
|
||||
if isinstance(token, InvalidToken):
|
||||
return
|
||||
if not isinstance(token, dnfile.mdtable.MethodDefRow):
|
||||
return
|
||||
|
||||
if any((token.Flags.mdPinvokeImpl, token.ImplFlags.miUnmanaged, token.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_number_features,
|
||||
extract_insn_string_features,
|
||||
extract_insn_namespace_features,
|
||||
extract_insn_class_features,
|
||||
extract_unmanaged_call_characteristic_features,
|
||||
)
|
||||
116
capa/features/extractors/dnfile_.py
Normal file
116
capa/features/extractors/dnfile_.py
Normal file
@@ -0,0 +1,116 @@
|
||||
import logging
|
||||
from typing import Tuple, Iterator
|
||||
|
||||
import dnfile
|
||||
import pefile
|
||||
|
||||
from capa.features.common import OS, OS_ANY, ARCH_ANY, ARCH_I386, ARCH_AMD64, FORMAT_DOTNET, Arch, Format, Feature
|
||||
from capa.features.address import NO_ADDRESS, Address, AbsoluteVirtualAddress
|
||||
from capa.features.extractors.base_extractor import FeatureExtractor
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def extract_file_format(**kwargs) -> Iterator[Tuple[Feature, Address]]:
|
||||
yield Format(FORMAT_DOTNET), NO_ADDRESS
|
||||
|
||||
|
||||
def extract_file_os(**kwargs) -> Iterator[Tuple[Feature, Address]]:
|
||||
yield OS(OS_ANY), NO_ADDRESS
|
||||
|
||||
|
||||
def extract_file_arch(pe, **kwargs) -> Iterator[Tuple[Feature, Address]]:
|
||||
# to distinguish in more detail, see https://stackoverflow.com/a/23614024/10548020
|
||||
# .NET 4.5 added option: any CPU, 32-bit preferred
|
||||
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_features(pe: dnfile.dnPE) -> Iterator[Tuple[Feature, Address]]:
|
||||
for file_handler in FILE_HANDLERS:
|
||||
for feature, address in file_handler(pe=pe): # type: ignore
|
||||
yield feature, address
|
||||
|
||||
|
||||
FILE_HANDLERS = (
|
||||
# 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: dnfile.dnPE) -> Iterator[Tuple[Feature, Address]]:
|
||||
for handler in GLOBAL_HANDLERS:
|
||||
for feature, addr in handler(pe=pe): # type: ignore
|
||||
yield feature, addr
|
||||
|
||||
|
||||
GLOBAL_HANDLERS = (
|
||||
extract_file_os,
|
||||
extract_file_arch,
|
||||
)
|
||||
|
||||
|
||||
class DnfileFeatureExtractor(FeatureExtractor):
|
||||
def __init__(self, path: str):
|
||||
super(DnfileFeatureExtractor, self).__init__()
|
||||
self.path: str = path
|
||||
self.pe: dnfile.dnPE = dnfile.dnPE(path)
|
||||
|
||||
def get_base_address(self) -> AbsoluteVirtualAddress:
|
||||
return AbsoluteVirtualAddress(0x0)
|
||||
|
||||
def get_entry_point(self) -> int:
|
||||
# self.pe.net.Flags.CLT_NATIVE_ENTRYPOINT
|
||||
# True: native EP: Token
|
||||
# False: managed EP: RVA
|
||||
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 not bool(self.pe.net.Flags.CLR_ILONLY)
|
||||
|
||||
def get_runtime_version(self) -> Tuple[int, int]:
|
||||
return self.pe.net.struct.MajorRuntimeVersion, self.pe.net.struct.MinorRuntimeVersion
|
||||
|
||||
def get_meta_version_string(self) -> str:
|
||||
return self.pe.net.metadata.struct.Version.rstrip(b"\x00").decode("utf-8")
|
||||
|
||||
def get_functions(self):
|
||||
raise NotImplementedError("DnfileFeatureExtractor can only be used to extract file features")
|
||||
|
||||
def extract_function_features(self, f):
|
||||
raise NotImplementedError("DnfileFeatureExtractor can only be used to extract file features")
|
||||
|
||||
def get_basic_blocks(self, f):
|
||||
raise NotImplementedError("DnfileFeatureExtractor can only be used to extract file features")
|
||||
|
||||
def extract_basic_block_features(self, f, bb):
|
||||
raise NotImplementedError("DnfileFeatureExtractor can only be used to extract file features")
|
||||
|
||||
def get_instructions(self, f, bb):
|
||||
raise NotImplementedError("DnfileFeatureExtractor can only be used to extract file features")
|
||||
|
||||
def extract_insn_features(self, f, bb, insn):
|
||||
raise NotImplementedError("DnfileFeatureExtractor can only be used to extract file features")
|
||||
|
||||
def is_library_function(self, va):
|
||||
raise NotImplementedError("DnfileFeatureExtractor can only be used to extract file features")
|
||||
|
||||
def get_function_name(self, va):
|
||||
raise NotImplementedError("DnfileFeatureExtractor can only be used to extract file features")
|
||||
203
capa/features/extractors/dotnetfile.py
Normal file
203
capa/features/extractors/dotnetfile.py
Normal file
@@ -0,0 +1,203 @@
|
||||
import logging
|
||||
import itertools
|
||||
from typing import Tuple, Iterator
|
||||
|
||||
import dnfile
|
||||
import pefile
|
||||
from dncil.clr.token import Token
|
||||
|
||||
import capa.features.extractors.helpers
|
||||
from capa.features.file import Import, FunctionName
|
||||
from capa.features.common import (
|
||||
OS,
|
||||
OS_ANY,
|
||||
ARCH_ANY,
|
||||
ARCH_I386,
|
||||
ARCH_AMD64,
|
||||
FORMAT_DOTNET,
|
||||
Arch,
|
||||
Class,
|
||||
Format,
|
||||
String,
|
||||
Feature,
|
||||
Namespace,
|
||||
Characteristic,
|
||||
)
|
||||
from capa.features.address import NO_ADDRESS, Address, DNTokenAddress, DNTokenOffsetAddress, AbsoluteVirtualAddress
|
||||
from capa.features.extractors.base_extractor import FeatureExtractor
|
||||
from capa.features.extractors.dnfile.helpers import (
|
||||
DnClass,
|
||||
DnMethod,
|
||||
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_DOTNET), NO_ADDRESS
|
||||
|
||||
|
||||
def extract_file_import_names(pe: dnfile.dnPE, **kwargs) -> Iterator[Tuple[Import, Address]]:
|
||||
for method in get_dotnet_managed_imports(pe):
|
||||
# like System.IO.File::OpenRead
|
||||
yield Import(str(method)), DNTokenAddress(Token(method.token))
|
||||
|
||||
for imp in get_dotnet_unmanaged_imports(pe):
|
||||
# like kernel32.CreateFileA
|
||||
for name in capa.features.extractors.helpers.generate_symbols(imp.modulename, imp.methodname):
|
||||
yield Import(name), DNTokenAddress(Token(imp.token))
|
||||
|
||||
|
||||
def extract_file_function_names(pe: dnfile.dnPE, **kwargs) -> Iterator[Tuple[FunctionName, Address]]:
|
||||
for method in get_dotnet_managed_methods(pe):
|
||||
yield FunctionName(str(method)), DNTokenAddress(Token(method.token))
|
||||
|
||||
|
||||
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 row in iter_dotnet_table(pe, "TypeDef"):
|
||||
namespaces.add(row.TypeNamespace)
|
||||
|
||||
for row in iter_dotnet_table(pe, "TypeRef"):
|
||||
namespaces.add(row.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, row) in enumerate(iter_dotnet_table(pe, "TypeDef")):
|
||||
token = calculate_dotnet_token_value(pe.net.mdtables.TypeDef.number, rid + 1)
|
||||
yield Class(DnClass.format_name(row.TypeNamespace, row.TypeName)), DNTokenAddress(Token(token))
|
||||
|
||||
for (rid, row) in enumerate(iter_dotnet_table(pe, "TypeRef")):
|
||||
token = calculate_dotnet_token_value(pe.net.mdtables.TypeRef.number, rid + 1)
|
||||
yield Class(DnClass.format_name(row.TypeNamespace, row.TypeName)), DNTokenAddress(Token(token))
|
||||
|
||||
|
||||
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
|
||||
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(FeatureExtractor):
|
||||
def __init__(self, path: str):
|
||||
super(DotnetFileFeatureExtractor, self).__init__()
|
||||
self.path: str = path
|
||||
self.pe: dnfile.dnPE = dnfile.dnPE(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
|
||||
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]:
|
||||
return self.pe.net.struct.MajorRuntimeVersion, self.pe.net.struct.MinorRuntimeVersion
|
||||
|
||||
def get_meta_version_string(self) -> str:
|
||||
return self.pe.net.metadata.struct.Version.rstrip(b"\x00").decode("utf-8")
|
||||
|
||||
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")
|
||||
@@ -47,7 +47,23 @@ class OS(str, Enum):
|
||||
NACL = "nacl"
|
||||
|
||||
|
||||
def detect_elf_os(f: BinaryIO) -> str:
|
||||
# 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,
|
||||
}
|
||||
|
||||
|
||||
def detect_elf_os(f) -> str:
|
||||
"""
|
||||
f: type Union[BinaryIO, IDAIO]
|
||||
"""
|
||||
f.seek(0x0)
|
||||
file_header = f.read(0x40)
|
||||
|
||||
@@ -141,7 +157,7 @@ def detect_elf_os(f: BinaryIO) -> str:
|
||||
PT_NOTE = 0x4
|
||||
|
||||
(p_type,) = struct.unpack_from(endian + "I", phent, 0x0)
|
||||
logger.debug("p_type: 0x%04x", p_type)
|
||||
logger.debug("ph:p_type: 0x%04x", p_type)
|
||||
if p_type != PT_NOTE:
|
||||
continue
|
||||
|
||||
@@ -152,7 +168,7 @@ def detect_elf_os(f: BinaryIO) -> str:
|
||||
else:
|
||||
raise NotImplementedError()
|
||||
|
||||
logger.debug("p_offset: 0x%02x p_filesz: 0x%04x", p_offset, p_filesz)
|
||||
logger.debug("ph:p_offset: 0x%02x p_filesz: 0x%04x", p_offset, p_filesz)
|
||||
|
||||
f.seek(p_offset)
|
||||
note = f.read(p_filesz)
|
||||
@@ -164,7 +180,7 @@ def detect_elf_os(f: BinaryIO) -> str:
|
||||
name_offset = 0xC
|
||||
desc_offset = name_offset + align(namesz, 0x4)
|
||||
|
||||
logger.debug("namesz: 0x%02x descsz: 0x%02x type: 0x%04x", namesz, descsz, type_)
|
||||
logger.debug("ph:namesz: 0x%02x descsz: 0x%02x type: 0x%04x", namesz, descsz, type_)
|
||||
|
||||
name = note[name_offset : name_offset + namesz].partition(b"\x00")[0].decode("ascii")
|
||||
logger.debug("name: %s", name)
|
||||
@@ -178,17 +194,6 @@ def detect_elf_os(f: BinaryIO) -> str:
|
||||
|
||||
desc = note[desc_offset : desc_offset + descsz]
|
||||
abi_tag, kmajor, kminor, kpatch = struct.unpack_from(endian + "IIII", desc, 0x0)
|
||||
# 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,
|
||||
}
|
||||
logger.debug("GNU_ABI_TAG: 0x%02x", abi_tag)
|
||||
|
||||
if abi_tag in GNU_ABI_TAG:
|
||||
@@ -262,7 +267,7 @@ def detect_elf_os(f: BinaryIO) -> str:
|
||||
if sh_type != SHT_NOTE:
|
||||
continue
|
||||
|
||||
logger.debug("sh_offset: 0x%02x sh_size: 0x%04x", sh_offset, sh_size)
|
||||
logger.debug("sh:sh_offset: 0x%02x sh_size: 0x%04x", sh_offset, sh_size)
|
||||
|
||||
f.seek(sh_offset)
|
||||
note = f.read(sh_size)
|
||||
@@ -274,7 +279,7 @@ def detect_elf_os(f: BinaryIO) -> str:
|
||||
name_offset = 0xC
|
||||
desc_offset = name_offset + align(namesz, 0x4)
|
||||
|
||||
logger.debug("namesz: 0x%02x descsz: 0x%02x type: 0x%04x", namesz, descsz, type_)
|
||||
logger.debug("sh:namesz: 0x%02x descsz: 0x%02x type: 0x%04x", namesz, descsz, type_)
|
||||
|
||||
name = note[name_offset : name_offset + namesz].partition(b"\x00")[0].decode("ascii")
|
||||
logger.debug("name: %s", name)
|
||||
@@ -282,6 +287,28 @@ def detect_elf_os(f: BinaryIO) -> str:
|
||||
if name == "Linux":
|
||||
logger.debug("note owner: %s", "LINUX")
|
||||
ret = OS.LINUX if not ret else ret
|
||||
elif name == "OpenBSD":
|
||||
logger.debug("note owner: %s", "OPENBSD")
|
||||
ret = OS.OPENBSD if not ret else ret
|
||||
elif name == "NetBSD":
|
||||
logger.debug("note owner: %s", "NETBSD")
|
||||
ret = OS.NETBSD if not ret else ret
|
||||
elif name == "FreeBSD":
|
||||
logger.debug("note owner: %s", "FREEBSD")
|
||||
ret = OS.FREEBSD if not ret else ret
|
||||
elif name == "GNU":
|
||||
if descsz < 16:
|
||||
continue
|
||||
|
||||
desc = note[desc_offset : desc_offset + descsz]
|
||||
abi_tag, kmajor, kminor, kpatch = struct.unpack_from(endian + "IIII", desc, 0x0)
|
||||
logger.debug("GNU_ABI_TAG: 0x%02x", abi_tag)
|
||||
|
||||
if abi_tag in GNU_ABI_TAG:
|
||||
# update only if not set
|
||||
# so we can get the debugging output of subsequent strategies
|
||||
ret = GNU_ABI_TAG[abi_tag] if not ret else ret
|
||||
logger.debug("abi tag: %s earliest compatible kernel: %d.%d.%d", ret, kmajor, kminor, kpatch)
|
||||
|
||||
return ret.value if ret is not None else "unknown"
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ from elftools.elf.elffile import ELFFile, SymbolTableSection
|
||||
import capa.features.extractors.common
|
||||
from capa.features.file import Import, Section
|
||||
from capa.features.common import OS, FORMAT_ELF, Arch, Format, Feature
|
||||
from capa.features.address import NO_ADDRESS, FileOffsetAddress, AbsoluteVirtualAddress
|
||||
from capa.features.extractors.elf import Arch as ElfArch
|
||||
from capa.features.extractors.base_extractor import FeatureExtractor
|
||||
|
||||
@@ -39,15 +40,15 @@ def extract_file_import_names(elf, **kwargs):
|
||||
if symbol.name and symbol.entry.st_info.type == "STT_FUNC":
|
||||
# TODO symbol address
|
||||
# TODO symbol version info?
|
||||
yield Import(symbol.name), 0x0
|
||||
yield Import(symbol.name), FileOffsetAddress(0x0)
|
||||
|
||||
|
||||
def extract_file_section_names(elf, **kwargs):
|
||||
for section in elf.iter_sections():
|
||||
if section.name:
|
||||
yield Section(section.name), section.header.sh_addr
|
||||
yield Section(section.name), AbsoluteVirtualAddress(section.header.sh_addr)
|
||||
elif section.is_null():
|
||||
yield Section("NULL"), section.header.sh_addr
|
||||
yield Section("NULL"), AbsoluteVirtualAddress(section.header.sh_addr)
|
||||
|
||||
|
||||
def extract_file_strings(buf, **kwargs):
|
||||
@@ -58,31 +59,31 @@ def extract_file_os(elf, 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 = next(capa.features.extractors.common.extract_os(buf))
|
||||
yield os
|
||||
os_tuple = next(capa.features.extractors.common.extract_os(buf))
|
||||
yield os_tuple
|
||||
except StopIteration:
|
||||
yield OS("unknown"), 0x0
|
||||
yield OS("unknown"), NO_ADDRESS
|
||||
|
||||
|
||||
def extract_file_format(**kwargs):
|
||||
yield Format(FORMAT_ELF), 0x0
|
||||
yield Format(FORMAT_ELF), NO_ADDRESS
|
||||
|
||||
|
||||
def extract_file_arch(elf, **kwargs):
|
||||
# TODO merge with capa.features.extractors.elf.detect_elf_arch()
|
||||
arch = elf.get_machine_arch()
|
||||
if arch == "x86":
|
||||
yield Arch(ElfArch.I386), 0x0
|
||||
yield Arch(ElfArch.I386), NO_ADDRESS
|
||||
elif arch == "x64":
|
||||
yield Arch(ElfArch.AMD64), 0x0
|
||||
yield Arch(ElfArch.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, va in file_handler(elf=elf, buf=buf): # type: ignore
|
||||
yield feature, va
|
||||
for feature, addr in file_handler(elf=elf, buf=buf): # type: ignore
|
||||
yield feature, addr
|
||||
|
||||
|
||||
FILE_HANDLERS = (
|
||||
@@ -97,8 +98,8 @@ FILE_HANDLERS = (
|
||||
|
||||
def extract_global_features(elf: ELFFile, buf: bytes) -> Iterator[Tuple[Feature, int]]:
|
||||
for global_handler in GLOBAL_HANDLERS:
|
||||
for feature, va in global_handler(elf=elf, buf=buf): # type: ignore
|
||||
yield feature, va
|
||||
for feature, addr in global_handler(elf=elf, buf=buf): # type: ignore
|
||||
yield feature, addr
|
||||
|
||||
|
||||
GLOBAL_HANDLERS = (
|
||||
@@ -118,21 +119,21 @@ class ElfFeatureExtractor(FeatureExtractor):
|
||||
# virtual address of the first segment with type LOAD
|
||||
for segment in self.elf.iter_segments():
|
||||
if segment.header.p_type == "PT_LOAD":
|
||||
return segment.header.p_vaddr
|
||||
return AbsoluteVirtualAddress(segment.header.p_vaddr)
|
||||
|
||||
def extract_global_features(self):
|
||||
with open(self.path, "rb") as f:
|
||||
buf = f.read()
|
||||
|
||||
for feature, va in extract_global_features(self.elf, buf):
|
||||
yield feature, va
|
||||
for feature, addr in extract_global_features(self.elf, buf):
|
||||
yield feature, addr
|
||||
|
||||
def extract_file_features(self):
|
||||
with open(self.path, "rb") as f:
|
||||
buf = f.read()
|
||||
|
||||
for feature, va in extract_file_features(self.elf, buf):
|
||||
yield feature, va
|
||||
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")
|
||||
|
||||
@@ -29,8 +29,7 @@ def is_aw_function(symbol: str) -> bool:
|
||||
if symbol[-1] not in ("A", "W"):
|
||||
return False
|
||||
|
||||
# second to last character should be lowercase letter
|
||||
return "a" <= symbol[-2] <= "z" or "0" <= symbol[-2] <= "9"
|
||||
return True
|
||||
|
||||
|
||||
def is_ordinal(symbol: str) -> bool:
|
||||
@@ -52,6 +51,9 @@ def generate_symbols(dll: str, symbol: str) -> Iterator[str]:
|
||||
- CreateFileA
|
||||
- CreateFile
|
||||
"""
|
||||
# normalize dll name
|
||||
dll = dll.lower()
|
||||
|
||||
# kernel32.CreateFileA
|
||||
yield "%s.%s" % (dll, symbol)
|
||||
|
||||
|
||||
@@ -8,22 +8,21 @@
|
||||
|
||||
import string
|
||||
import struct
|
||||
from typing import Tuple, Iterator
|
||||
|
||||
import idaapi
|
||||
|
||||
import capa.features.extractors.ida.helpers
|
||||
from capa.features.common 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:
|
||||
@@ -37,12 +36,12 @@ def get_printable_len(op):
|
||||
else:
|
||||
raise ValueError("Unhandled operand data type 0x%x." % op.dtype)
|
||||
|
||||
def is_printable_ascii(chars):
|
||||
return all(c < 127 and chr(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 all(c == 0x00 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)
|
||||
@@ -53,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
|
||||
|
||||
@@ -71,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):
|
||||
@@ -89,39 +80,24 @@ 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 = (
|
||||
@@ -132,9 +108,10 @@ BASIC_BLOCK_HANDLERS = (
|
||||
|
||||
def main():
|
||||
features = []
|
||||
for f in helpers.get_functions(skip_thunks=True, skip_libs=True):
|
||||
for fhandle in helpers.get_functions(skip_thunks=True, skip_libs=True):
|
||||
f: idaapi.func_t = fhandle.inner
|
||||
for bb in idaapi.FlowChart(f, flags=idaapi.FC_PREDS):
|
||||
features.extend(list(extract_features(f, bb)))
|
||||
features.extend(list(extract_features(fhandle, bb)))
|
||||
|
||||
import pprint
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
# 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 capa.ida.helpers
|
||||
@@ -14,57 +16,20 @@ 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.extractors.base_extractor import FeatureExtractor
|
||||
|
||||
|
||||
class FunctionHandle:
|
||||
"""this acts like an idaapi.func_t but with __int__()"""
|
||||
|
||||
def __init__(self, inner):
|
||||
self._inner = inner
|
||||
|
||||
def __int__(self):
|
||||
return self.start_ea
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._inner, name)
|
||||
|
||||
|
||||
class BasicBlockHandle:
|
||||
"""this acts like an idaapi.BasicBlock but with __int__()"""
|
||||
|
||||
def __init__(self, inner):
|
||||
self._inner = inner
|
||||
|
||||
def __int__(self):
|
||||
return self.start_ea
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._inner, name)
|
||||
|
||||
|
||||
class InstructionHandle:
|
||||
"""this acts like an idaapi.insn_t but with __int__()"""
|
||||
|
||||
def __init__(self, inner):
|
||||
self._inner = inner
|
||||
|
||||
def __int__(self):
|
||||
return self.ea
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._inner, name)
|
||||
from capa.features.common import Feature
|
||||
from capa.features.address import Address, AbsoluteVirtualAddress
|
||||
from capa.features.extractors.base_extractor import BBHandle, InsnHandle, FunctionHandle, FeatureExtractor
|
||||
|
||||
|
||||
class IdaFeatureExtractor(FeatureExtractor):
|
||||
def __init__(self):
|
||||
super(IdaFeatureExtractor, self).__init__()
|
||||
self.global_features = []
|
||||
self.global_features: List[Tuple[Feature, Address]] = []
|
||||
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 idaapi.get_imagebase()
|
||||
return AbsoluteVirtualAddress(idaapi.get_imagebase())
|
||||
|
||||
def extract_global_features(self):
|
||||
yield from self.global_features
|
||||
@@ -72,41 +37,34 @@ class IdaFeatureExtractor(FeatureExtractor):
|
||||
def extract_file_features(self):
|
||||
yield from capa.features.extractors.ida.file.extract_features()
|
||||
|
||||
def get_functions(self):
|
||||
def get_functions(self) -> Iterator[FunctionHandle]:
|
||||
import capa.features.extractors.ida.helpers as ida_helpers
|
||||
|
||||
# data structure shared across functions yielded here.
|
||||
# useful for caching analysis relevant across a single workspace.
|
||||
ctx = {}
|
||||
|
||||
# ignore library functions and thunk functions as identified by IDA
|
||||
for f in ida_helpers.get_functions(skip_thunks=True, skip_libs=True):
|
||||
setattr(f, "ctx", ctx)
|
||||
yield FunctionHandle(f)
|
||||
yield from ida_helpers.get_functions(skip_thunks=True, skip_libs=True)
|
||||
|
||||
@staticmethod
|
||||
def get_function(ea):
|
||||
def get_function(ea: int) -> FunctionHandle:
|
||||
f = idaapi.get_func(ea)
|
||||
setattr(f, "ctx", {})
|
||||
return FunctionHandle(f)
|
||||
return FunctionHandle(address=AbsoluteVirtualAddress(f.start_ea), inner=f)
|
||||
|
||||
def extract_function_features(self, f):
|
||||
yield from capa.features.extractors.ida.function.extract_features(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, f):
|
||||
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(f):
|
||||
yield BasicBlockHandle(bb)
|
||||
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, f, bb):
|
||||
yield from capa.features.extractors.ida.basicblock.extract_features(f, 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, f, bb):
|
||||
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(bb.start_ea, bb.end_ea):
|
||||
yield InstructionHandle(insn)
|
||||
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, f, bb, insn):
|
||||
yield from capa.features.extractors.ida.insn.extract_features(f, bb, insn)
|
||||
def extract_insn_features(self, fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle):
|
||||
yield from capa.features.extractors.ida.insn.extract_features(fh, bbh, ih)
|
||||
|
||||
@@ -7,27 +7,26 @@
|
||||
# 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_loader
|
||||
|
||||
import capa.features.extractors.common
|
||||
import capa.features.extractors.helpers
|
||||
import capa.features.extractors.strings
|
||||
import capa.features.extractors.ida.helpers
|
||||
from capa.features.file import Export, Import, Section, FunctionName
|
||||
from capa.features.common import OS, FORMAT_PE, FORMAT_ELF, OS_WINDOWS, Format, String, Characteristic
|
||||
from capa.features.common import FORMAT_PE, FORMAT_ELF, Format, String, Feature, Characteristic
|
||||
from capa.features.address import NO_ADDRESS, Address, FileOffsetAddress, AbsoluteVirtualAddress
|
||||
|
||||
|
||||
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)
|
||||
"""
|
||||
seg_max = seg.end_ea
|
||||
mz_xor = [
|
||||
@@ -60,13 +59,13 @@ def check_segment_for_pe(seg):
|
||||
continue
|
||||
|
||||
if idc.get_bytes(peoff, 2) == pex:
|
||||
yield (off, i)
|
||||
yield off, i
|
||||
|
||||
for nextres in capa.features.extractors.ida.helpers.find_byte_sequence(off + 1, seg.end_ea, mzx):
|
||||
todo.append((nextres, mzx, pex, i))
|
||||
|
||||
|
||||
def extract_file_embedded_pe():
|
||||
def extract_file_embedded_pe() -> Iterator[Tuple[Feature, Address]]:
|
||||
"""extract embedded PE features
|
||||
|
||||
IDA must load resource sections for this to be complete
|
||||
@@ -75,16 +74,16 @@ def extract_file_embedded_pe():
|
||||
"""
|
||||
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
|
||||
yield Characteristic("embedded pe"), FileOffsetAddress(ea)
|
||||
|
||||
|
||||
def extract_file_export_names():
|
||||
def extract_file_export_names() -> Iterator[Tuple[Feature, Address]]:
|
||||
"""extract function exports"""
|
||||
for (_, _, ea, name) in idautils.Entries():
|
||||
yield Export(name), ea
|
||||
yield Export(name), AbsoluteVirtualAddress(ea)
|
||||
|
||||
|
||||
def extract_file_import_names():
|
||||
def extract_file_import_names() -> Iterator[Tuple[Feature, Address]]:
|
||||
"""extract function imports
|
||||
|
||||
1. imports by ordinal:
|
||||
@@ -96,11 +95,12 @@ def extract_file_import_names():
|
||||
- importname
|
||||
"""
|
||||
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]):
|
||||
yield Import(name), ea
|
||||
yield Import(name), addr
|
||||
dll = info[0]
|
||||
symbol = "#%d" % (info[2])
|
||||
elif info[1]:
|
||||
@@ -113,10 +113,10 @@ def extract_file_import_names():
|
||||
continue
|
||||
|
||||
for name in capa.features.extractors.helpers.generate_symbols(dll, symbol):
|
||||
yield Import(name), ea
|
||||
yield Import(name), addr
|
||||
|
||||
|
||||
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
|
||||
@@ -124,10 +124,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
|
||||
@@ -137,41 +137,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_file_function_names():
|
||||
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), 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():
|
||||
format_name = ida_loader.get_file_type_name()
|
||||
def extract_file_format() -> Iterator[Tuple[Feature, Address]]:
|
||||
file_info = idaapi.get_inf_structure()
|
||||
|
||||
if "PE" in format_name:
|
||||
yield Format(FORMAT_PE), 0x0
|
||||
elif "ELF64" in format_name:
|
||||
yield Format(FORMAT_ELF), 0x0
|
||||
elif "ELF32" in format_name:
|
||||
yield Format(FORMAT_ELF), 0x0
|
||||
if file_info.filetype == idaapi.f_PE:
|
||||
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("file format: %s", format_name)
|
||||
raise NotImplementedError("file format: %d" % file_info.filetype)
|
||||
|
||||
|
||||
def extract_features():
|
||||
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 = (
|
||||
|
||||
@@ -5,31 +5,27 @@
|
||||
# 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.common 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,28 +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)
|
||||
@@ -68,8 +55,8 @@ FUNCTION_HANDLERS = (extract_function_calls_to, extract_function_loop, extract_r
|
||||
def main():
|
||||
""" """
|
||||
features = []
|
||||
for f in capa.features.extractors.ida.get_functions(skip_thunks=True, skip_libs=True):
|
||||
features.extend(list(extract_features(f)))
|
||||
for fhandle in capa.features.extractors.ida.helpers.get_functions(skip_thunks=True, skip_libs=True):
|
||||
features.extend(list(extract_features(fhandle)))
|
||||
|
||||
import pprint
|
||||
|
||||
|
||||
@@ -1,27 +1,29 @@
|
||||
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
|
||||
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():
|
||||
format_name = ida_loader.get_file_type_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), 0x0
|
||||
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), 0x0
|
||||
yield OS(os), NO_ADDRESS
|
||||
|
||||
else:
|
||||
# we likely end up here:
|
||||
@@ -38,12 +40,12 @@ def extract_os():
|
||||
return
|
||||
|
||||
|
||||
def extract_arch():
|
||||
info = idaapi.get_inf_structure()
|
||||
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), 0x0
|
||||
yield Arch(ARCH_AMD64), NO_ADDRESS
|
||||
elif info.procname == "metapc" and info.is_32bit():
|
||||
yield Arch(ARCH_I386), 0x0
|
||||
yield Arch(ARCH_I386), NO_ADDRESS
|
||||
elif info.procname == "metapc":
|
||||
logger.debug("unsupported architecture: non-32-bit nor non-64-bit intel")
|
||||
return
|
||||
|
||||
@@ -5,14 +5,18 @@
|
||||
# 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
|
||||
import idautils
|
||||
import ida_bytes
|
||||
|
||||
from capa.features.address import AbsoluteVirtualAddress
|
||||
from capa.features.extractors.base_extractor import FunctionHandle
|
||||
|
||||
def find_byte_sequence(start, end, seq):
|
||||
|
||||
def find_byte_sequence(start: int, end: int, seq: bytes) -> Iterator[int]:
|
||||
"""yield all ea of a given byte sequence
|
||||
|
||||
args:
|
||||
@@ -20,32 +24,32 @@ def find_byte_sequence(start, end, seq):
|
||||
end: max virtual address
|
||||
seq: bytes to search e.g. b"\x01\x03"
|
||||
"""
|
||||
seq = " ".join(["%02x" % b for b in seq])
|
||||
seqstr = " ".join(["%02x" % b for b in seq])
|
||||
while True:
|
||||
ea = idaapi.find_binary(start, end, seq, 0, idaapi.SEARCH_DOWN)
|
||||
# TODO find_binary: Deprecated. Please use ida_bytes.bin_search() instead.
|
||||
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: int = None, end: 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:
|
||||
@@ -57,7 +61,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
|
||||
@@ -75,7 +79,7 @@ def get_segment_buffer(seg):
|
||||
return buff if buff else b""
|
||||
|
||||
|
||||
def get_file_imports():
|
||||
def get_file_imports() -> Dict[int, Tuple[str, str, int]]:
|
||||
"""get file imports"""
|
||||
imports = {}
|
||||
|
||||
@@ -85,10 +89,18 @@ def get_file_imports():
|
||||
if not library:
|
||||
continue
|
||||
|
||||
# IDA uses section names for the library of ELF imports, like ".dynsym"
|
||||
library = library.lstrip(".")
|
||||
|
||||
def inspect_import(ea, function, ordinal):
|
||||
if function and function.startswith("__imp_"):
|
||||
# handle mangled names starting
|
||||
# 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
|
||||
|
||||
@@ -97,14 +109,12 @@ def get_file_imports():
|
||||
return imports
|
||||
|
||||
|
||||
def get_instructions_in_range(start, end):
|
||||
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)
|
||||
@@ -112,7 +122,7 @@ def get_instructions_in_range(start, end):
|
||||
yield insn
|
||||
|
||||
|
||||
def is_operand_equal(op1, op2):
|
||||
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
|
||||
@@ -138,7 +148,7 @@ def is_operand_equal(op1, op2):
|
||||
return True
|
||||
|
||||
|
||||
def is_basic_block_equal(bb1, bb2):
|
||||
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
|
||||
@@ -152,12 +162,12 @@ def is_basic_block_equal(bb1, bb2):
|
||||
return True
|
||||
|
||||
|
||||
def basic_block_size(bb):
|
||||
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):
|
||||
@@ -170,10 +180,10 @@ def read_bytes_at(ea, count):
|
||||
return idc.get_bytes(ea, count)
|
||||
|
||||
|
||||
def find_string_at(ea, min=4):
|
||||
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
|
||||
@@ -187,7 +197,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:
|
||||
@@ -224,23 +234,23 @@ def get_op_phrase_info(op):
|
||||
return {"base": base, "index": index, "scale": scale, "offset": offset}
|
||||
|
||||
|
||||
def is_op_write(insn, op):
|
||||
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):
|
||||
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_op_offset(insn, op):
|
||||
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_sp_modified(insn):
|
||||
def is_sp_modified(insn: idaapi.insn_t) -> bool:
|
||||
"""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):
|
||||
@@ -249,7 +259,7 @@ def is_sp_modified(insn):
|
||||
return False
|
||||
|
||||
|
||||
def is_bp_modified(insn):
|
||||
def is_bp_modified(insn: idaapi.insn_t) -> bool:
|
||||
"""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):
|
||||
@@ -258,12 +268,12 @@ def is_bp_modified(insn):
|
||||
return False
|
||||
|
||||
|
||||
def is_frame_register(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=()):
|
||||
def get_insn_ops(insn: idaapi.insn_t, target_ops: 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:
|
||||
@@ -274,12 +284,12 @@ def get_insn_ops(insn, target_ops=()):
|
||||
yield op
|
||||
|
||||
|
||||
def is_op_stack_var(ea, index):
|
||||
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
|
||||
@@ -299,26 +309,18 @@ 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)
|
||||
"""
|
||||
def is_function_recursive(f: idaapi.func_t) -> bool:
|
||||
"""check if function is recursive"""
|
||||
for ref in idautils.CodeRefsTo(f.start_ea, True):
|
||||
if f.contains(ref):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
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:
|
||||
@@ -328,7 +330,7 @@ def is_basic_block_tight_loop(bb):
|
||||
return False
|
||||
|
||||
|
||||
def find_data_reference_from_insn(insn, max_depth=10):
|
||||
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
|
||||
@@ -358,19 +360,18 @@ 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
|
||||
|
||||
|
||||
def is_basic_block_return(bb):
|
||||
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
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
# 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,47 +13,23 @@ import idautils
|
||||
|
||||
import capa.features.extractors.helpers
|
||||
import capa.features.extractors.ida.helpers
|
||||
from capa.features.insn import API, Number, Offset, Mnemonic
|
||||
from capa.features.common import (
|
||||
BITNESS_X32,
|
||||
BITNESS_X64,
|
||||
MAX_BYTES_FEATURE_SIZE,
|
||||
THUNK_CHAIN_DEPTH_DELTA,
|
||||
Bytes,
|
||||
String,
|
||||
Characteristic,
|
||||
)
|
||||
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_bitness(ctx):
|
||||
"""
|
||||
fetch the BITNESS_* constant for the currently open workspace.
|
||||
|
||||
via Tamir Bahar/@tmr232
|
||||
https://reverseengineering.stackexchange.com/a/11398/17194
|
||||
"""
|
||||
if "bitness" not in ctx:
|
||||
info = idaapi.get_inf_structure()
|
||||
if info.is_64bit():
|
||||
ctx["bitness"] = BITNESS_X64
|
||||
elif info.is_32bit():
|
||||
ctx["bitness"] = BITNESS_X32
|
||||
else:
|
||||
raise ValueError("unexpected bitness")
|
||||
return ctx["bitness"]
|
||||
|
||||
|
||||
def get_imports(ctx):
|
||||
def get_imports(ctx: Dict[str, Any]) -> Dict[str, 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):
|
||||
def check_for_api_call(ctx: Dict[str, Any], insn: idaapi.insn_t) -> Iterator[str]:
|
||||
"""check instruction for API call"""
|
||||
info = ()
|
||||
ref = insn.ea
|
||||
@@ -81,24 +58,22 @@ def check_for_api_call(ctx, insn):
|
||||
yield "%s.%s" % (info[0], info[1])
|
||||
|
||||
|
||||
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]
|
||||
"""
|
||||
insn: idaapi.insn_t = ih.inner
|
||||
|
||||
if not insn.get_canon_mnem() in ("call", "jmp"):
|
||||
return
|
||||
|
||||
for api in check_for_api_call(f.ctx, insn):
|
||||
for api in check_for_api_call(fh.ctx, insn):
|
||||
dll, _, symbol = api.rpartition(".")
|
||||
for name in capa.features.extractors.helpers.generate_symbols(dll, symbol):
|
||||
yield API(name), insn.ea
|
||||
yield API(name), ih.address
|
||||
|
||||
# extract IDA/FLIRT recognized API functions
|
||||
targets = tuple(idautils.CodeRefsFrom(insn.ea, False))
|
||||
@@ -113,20 +88,25 @@ def extract_insn_api_features(f, bb, insn):
|
||||
|
||||
if target_func.flags & idaapi.FUNC_LIB:
|
||||
name = idaapi.get_name(target_func.start_ea)
|
||||
yield API(name), insn.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
|
||||
@@ -137,7 +117,11 @@ 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):
|
||||
@@ -148,21 +132,27 @@ def extract_insn_number_features(f, bb, insn):
|
||||
else:
|
||||
const = op.addr
|
||||
|
||||
yield Number(const), insn.ea
|
||||
yield Number(const, bitness=get_bitness(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
|
||||
|
||||
@@ -170,41 +160,46 @@ def extract_insn_bytes_features(f, bb, 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
|
||||
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)
|
||||
if idaapi.is_mapped(op_off):
|
||||
@@ -217,12 +212,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, bitness=get_bitness(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
|
||||
@@ -236,7 +251,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
|
||||
@@ -270,7 +285,7 @@ def bb_stack_cookie_registers(bb):
|
||||
yield op.reg
|
||||
|
||||
|
||||
def is_nzxor_stack_cookie_delta(f, bb, insn):
|
||||
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):
|
||||
@@ -293,7 +308,7 @@ def is_nzxor_stack_cookie_delta(f, bb, insn):
|
||||
return False
|
||||
|
||||
|
||||
def is_nzxor_stack_cookie(f, bb, insn):
|
||||
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:
|
||||
@@ -310,37 +325,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]]:
|
||||
"""
|
||||
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(idc.print_insn_mnem(insn.ea)), 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
|
||||
@@ -348,6 +375,8 @@ 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
|
||||
|
||||
@@ -359,15 +388,19 @@ def extract_insn_peb_access_characteristic_features(f, bb, insn):
|
||||
|
||||
if " fs:30h" in disasm or " gs:60h" in disasm:
|
||||
# TODO: replace above with proper IDA
|
||||
yield Characteristic("peb access"), insn.ea
|
||||
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...
|
||||
"""
|
||||
insn: idaapi.insn_t = ih.inner
|
||||
|
||||
if all(map(lambda op: op.type != idaapi.o_mem, insn.ops)):
|
||||
# try to optimize for only memory references
|
||||
return
|
||||
@@ -376,23 +409,21 @@ def extract_insn_segment_access_features(f, bb, insn):
|
||||
|
||||
if " fs:" in disasm:
|
||||
# TODO: replace above with proper IDA
|
||||
yield Characteristic("fs access"), insn.ea
|
||||
yield Characteristic("fs access"), ih.address
|
||||
|
||||
if " gs:" in disasm:
|
||||
# TODO: replace above with proper IDA
|
||||
yield Characteristic("gs access"), insn.ea
|
||||
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).keys():
|
||||
# ignore API calls
|
||||
continue
|
||||
if not idaapi.getseg(ref):
|
||||
@@ -400,50 +431,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
|
||||
|
||||
|
||||
@@ -455,6 +476,7 @@ 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,
|
||||
|
||||
77
capa/features/extractors/null.py
Normal file
77
capa/features/extractors/null.py
Normal file
@@ -0,0 +1,77 @@
|
||||
from typing import Dict, List, Tuple
|
||||
from dataclasses import dataclass
|
||||
|
||||
from capa.features.common import Feature
|
||||
from capa.features.address import NO_ADDRESS, Address
|
||||
from capa.features.extractors.base_extractor import BBHandle, InsnHandle, FunctionHandle, FeatureExtractor
|
||||
|
||||
|
||||
@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 NullFeatureExtractor(FeatureExtractor):
|
||||
"""
|
||||
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
|
||||
global_features: List[Feature]
|
||||
file_features: List[Tuple[Address, Feature]]
|
||||
functions: Dict[Address, FunctionFeatures]
|
||||
|
||||
def get_base_address(self):
|
||||
return self.base_address
|
||||
|
||||
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.get(f.address, {}).features:
|
||||
yield feature, address
|
||||
|
||||
def get_basic_blocks(self, f):
|
||||
for address in sorted(self.functions.get(f.address, {}).basic_blocks.keys()):
|
||||
yield BBHandle(address, None)
|
||||
|
||||
def extract_basic_block_features(self, f, bb):
|
||||
for address, feature in self.functions.get(f.address, {}).basic_blocks.get(bb.address, {}).features:
|
||||
yield feature, address
|
||||
|
||||
def get_instructions(self, f, bb):
|
||||
for address in sorted(self.functions.get(f.address, {}).basic_blocks.get(bb.address, {}).instructions.keys()):
|
||||
yield InsnHandle(address, None)
|
||||
|
||||
def extract_insn_features(self, f, bb, insn):
|
||||
for address, feature in (
|
||||
self.functions.get(f.address, {})
|
||||
.basic_blocks.get(bb.address, {})
|
||||
.instructions.get(insn.address, {})
|
||||
.features
|
||||
):
|
||||
yield feature, address
|
||||
@@ -17,6 +17,7 @@ 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 FeatureExtractor
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -24,7 +25,7 @@ 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"), offset
|
||||
yield Characteristic("embedded pe"), FileOffsetAddress(offset)
|
||||
|
||||
|
||||
def extract_file_export_names(pe, **kwargs):
|
||||
@@ -39,7 +40,7 @@ def extract_file_export_names(pe, **kwargs):
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
va = base_address + export.address
|
||||
yield Export(name), va
|
||||
yield Export(name), AbsoluteVirtualAddress(va)
|
||||
|
||||
|
||||
def extract_file_import_names(pe, **kwargs):
|
||||
@@ -71,7 +72,7 @@ def extract_file_import_names(pe, **kwargs):
|
||||
continue
|
||||
|
||||
for name in capa.features.extractors.helpers.generate_symbols(modname, impname):
|
||||
yield Import(name), imp.address
|
||||
yield Import(name), AbsoluteVirtualAddress(imp.address)
|
||||
|
||||
|
||||
def extract_file_section_names(pe, **kwargs):
|
||||
@@ -83,7 +84,7 @@ def extract_file_section_names(pe, **kwargs):
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
|
||||
yield Section(name), base_address + section.VirtualAddress
|
||||
yield Section(name), AbsoluteVirtualAddress(base_address + section.VirtualAddress)
|
||||
|
||||
|
||||
def extract_file_strings(buf, **kwargs):
|
||||
@@ -103,18 +104,18 @@ def extract_file_function_names(**kwargs):
|
||||
def extract_file_os(**kwargs):
|
||||
# assuming PE -> Windows
|
||||
# though i suppose they're also used by UEFI
|
||||
yield OS(OS_WINDOWS), 0x0
|
||||
yield OS(OS_WINDOWS), NO_ADDRESS
|
||||
|
||||
|
||||
def extract_file_format(**kwargs):
|
||||
yield Format(FORMAT_PE), 0x0
|
||||
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), 0x0
|
||||
yield Arch(ARCH_I386), NO_ADDRESS
|
||||
elif pe.FILE_HEADER.Machine == pefile.MACHINE_TYPE["IMAGE_FILE_MACHINE_AMD64"]:
|
||||
yield Arch(ARCH_AMD64), 0x0
|
||||
yield Arch(ARCH_AMD64), NO_ADDRESS
|
||||
else:
|
||||
logger.warning("unsupported architecture: %s", pefile.MACHINE_TYPE[pe.FILE_HEADER.Machine])
|
||||
|
||||
@@ -176,7 +177,7 @@ class PefileFeatureExtractor(FeatureExtractor):
|
||||
self.pe = pefile.PE(path)
|
||||
|
||||
def get_base_address(self):
|
||||
return self.pe.OPTIONAL_HEADER.ImageBase
|
||||
return AbsoluteVirtualAddress(self.pe.OPTIONAL_HEADER.ImageBase)
|
||||
|
||||
def extract_global_features(self):
|
||||
with open(self.path, "rb") as f:
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import string
|
||||
import struct
|
||||
from typing import Tuple, Iterator
|
||||
|
||||
from capa.features.common 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.helpers import MIN_STACKSTRING_LEN
|
||||
from capa.features.extractors.base_extractor import BBHandle, FunctionHandle
|
||||
|
||||
|
||||
def _bb_has_tight_loop(f, bb):
|
||||
@@ -13,10 +16,10 @@ def _bb_has_tight_loop(f, bb):
|
||||
return bb.offset in f.blockrefs[bb.offset] if bb.offset in f.blockrefs else False
|
||||
|
||||
|
||||
def extract_bb_tight_loop(f, bb):
|
||||
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):
|
||||
yield Characteristic("tight loop"), bb.offset
|
||||
if _bb_has_tight_loop(f.inner, bb.inner):
|
||||
yield Characteristic("tight loop"), bb.address
|
||||
|
||||
|
||||
def _bb_has_stackstring(f, bb):
|
||||
@@ -37,10 +40,10 @@ def get_operands(smda_ins):
|
||||
return [o.strip() for o in smda_ins.operands.split(",")]
|
||||
|
||||
|
||||
def extract_stackstring(f, bb):
|
||||
def extract_stackstring(f: FunctionHandle, bb: BBHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""check basic block for stackstring indicators"""
|
||||
if _bb_has_stackstring(f, bb):
|
||||
yield Characteristic("stack string"), bb.offset
|
||||
if _bb_has_stackstring(f.inner, bb.inner):
|
||||
yield Characteristic("stack string"), bb.address
|
||||
|
||||
|
||||
def is_mov_imm_to_stack(smda_ins):
|
||||
@@ -107,21 +110,21 @@ def get_printable_len(instr):
|
||||
return 0
|
||||
|
||||
|
||||
def extract_features(f, bb):
|
||||
def extract_features(f: FunctionHandle, bb: BBHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
extract features from the given basic block.
|
||||
|
||||
args:
|
||||
f (smda.common.SmdaFunction): the function from which to extract features
|
||||
bb (smda.common.SmdaBasicBlock): the basic block to process.
|
||||
f: the function from which to extract features
|
||||
bb: the basic block to process.
|
||||
|
||||
yields:
|
||||
Tuple[Feature, int]: the features and their location found in this basic block.
|
||||
Tuple[Feature, Address]: the features and their location found in this basic block.
|
||||
"""
|
||||
yield BasicBlock(), bb.offset
|
||||
yield BasicBlock(), bb.address
|
||||
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 = (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from typing import List, Tuple
|
||||
|
||||
from smda.common.SmdaReport import SmdaReport
|
||||
|
||||
import capa.features.extractors.common
|
||||
@@ -6,7 +8,9 @@ import capa.features.extractors.smda.insn
|
||||
import capa.features.extractors.smda.global_
|
||||
import capa.features.extractors.smda.function
|
||||
import capa.features.extractors.smda.basicblock
|
||||
from capa.features.extractors.base_extractor import FeatureExtractor
|
||||
from capa.features.common import Feature
|
||||
from capa.features.address import Address, AbsoluteVirtualAddress
|
||||
from capa.features.extractors.base_extractor import BBHandle, InsnHandle, FunctionHandle, FeatureExtractor
|
||||
|
||||
|
||||
class SmdaFeatureExtractor(FeatureExtractor):
|
||||
@@ -18,12 +22,12 @@ class SmdaFeatureExtractor(FeatureExtractor):
|
||||
self.buf = f.read()
|
||||
|
||||
# pre-compute these because we'll yield them at *every* scope.
|
||||
self.global_features = []
|
||||
self.global_features: List[Tuple[Feature, Address]] = []
|
||||
self.global_features.extend(capa.features.extractors.common.extract_os(self.buf))
|
||||
self.global_features.extend(capa.features.extractors.smda.global_.extract_arch(self.smda_report))
|
||||
|
||||
def get_base_address(self):
|
||||
return self.smda_report.base_addr
|
||||
return AbsoluteVirtualAddress(self.smda_report.base_addr)
|
||||
|
||||
def extract_global_features(self):
|
||||
yield from self.global_features
|
||||
@@ -33,21 +37,21 @@ class SmdaFeatureExtractor(FeatureExtractor):
|
||||
|
||||
def get_functions(self):
|
||||
for function in self.smda_report.getFunctions():
|
||||
yield function
|
||||
yield FunctionHandle(address=AbsoluteVirtualAddress(function.offset), inner=function)
|
||||
|
||||
def extract_function_features(self, f):
|
||||
yield from capa.features.extractors.smda.function.extract_features(f)
|
||||
def extract_function_features(self, fh):
|
||||
yield from capa.features.extractors.smda.function.extract_features(fh)
|
||||
|
||||
def get_basic_blocks(self, f):
|
||||
for bb in f.getBlocks():
|
||||
yield bb
|
||||
def get_basic_blocks(self, fh):
|
||||
for bb in fh.inner.getBlocks():
|
||||
yield BBHandle(address=AbsoluteVirtualAddress(bb.offset), inner=bb)
|
||||
|
||||
def extract_basic_block_features(self, f, bb):
|
||||
yield from capa.features.extractors.smda.basicblock.extract_features(f, bb)
|
||||
def extract_basic_block_features(self, fh, bbh):
|
||||
yield from capa.features.extractors.smda.basicblock.extract_features(fh, bbh)
|
||||
|
||||
def get_instructions(self, f, bb):
|
||||
for smda_ins in bb.getInstructions():
|
||||
yield smda_ins
|
||||
def get_instructions(self, fh, bbh):
|
||||
for smda_ins in bbh.inner.getInstructions():
|
||||
yield InsnHandle(address=AbsoluteVirtualAddress(smda_ins.offset), inner=smda_ins)
|
||||
|
||||
def extract_insn_features(self, f, bb, insn):
|
||||
yield from capa.features.extractors.smda.insn.extract_features(f, bb, insn)
|
||||
def extract_insn_features(self, fh, bbh, ih):
|
||||
yield from capa.features.extractors.smda.insn.extract_features(fh, bbh, ih)
|
||||
|
||||
@@ -6,11 +6,12 @@ import capa.features.extractors.helpers
|
||||
import capa.features.extractors.strings
|
||||
from capa.features.file import Export, Import, Section
|
||||
from capa.features.common import String, Characteristic
|
||||
from capa.features.address import FileOffsetAddress, AbsoluteVirtualAddress
|
||||
|
||||
|
||||
def extract_file_embedded_pe(buf, **kwargs):
|
||||
for offset, _ in capa.features.extractors.helpers.carve_pe(buf, 1):
|
||||
yield Characteristic("embedded pe"), offset
|
||||
yield Characteristic("embedded pe"), FileOffsetAddress(offset)
|
||||
|
||||
|
||||
def extract_file_export_names(buf, **kwargs):
|
||||
@@ -18,7 +19,7 @@ def extract_file_export_names(buf, **kwargs):
|
||||
|
||||
if lief_binary is not None:
|
||||
for function in lief_binary.exported_functions:
|
||||
yield Export(function.name), function.address
|
||||
yield Export(function.name), AbsoluteVirtualAddress(function.address)
|
||||
|
||||
|
||||
def extract_file_import_names(smda_report, buf):
|
||||
@@ -33,10 +34,10 @@ def extract_file_import_names(smda_report, buf):
|
||||
va = func.iat_address + smda_report.base_addr
|
||||
if func.name:
|
||||
for name in capa.features.extractors.helpers.generate_symbols(library_name, func.name):
|
||||
yield Import(name), va
|
||||
yield Import(name), AbsoluteVirtualAddress(va)
|
||||
elif func.is_ordinal:
|
||||
for name in capa.features.extractors.helpers.generate_symbols(library_name, "#%s" % func.ordinal):
|
||||
yield Import(name), va
|
||||
yield Import(name), AbsoluteVirtualAddress(va)
|
||||
|
||||
|
||||
def extract_file_section_names(buf, **kwargs):
|
||||
@@ -46,7 +47,7 @@ def extract_file_section_names(buf, **kwargs):
|
||||
if lief_binary and lief_binary.sections:
|
||||
base_address = lief_binary.optional_header.imagebase
|
||||
for section in lief_binary.sections:
|
||||
yield Section(section.name), base_address + section.virtual_address
|
||||
yield Section(section.name), AbsoluteVirtualAddress(base_address + section.virtual_address)
|
||||
|
||||
|
||||
def extract_file_strings(buf, **kwargs):
|
||||
@@ -54,10 +55,10 @@ def extract_file_strings(buf, **kwargs):
|
||||
extract ASCII and UTF-16 LE strings from file
|
||||
"""
|
||||
for s in capa.features.extractors.strings.extract_ascii_strings(buf):
|
||||
yield String(s.s), s.offset
|
||||
yield String(s.s), FileOffsetAddress(s.offset)
|
||||
|
||||
for s in capa.features.extractors.strings.extract_unicode_strings(buf):
|
||||
yield String(s.s), s.offset
|
||||
yield String(s.s), FileOffsetAddress(s.offset)
|
||||
|
||||
|
||||
def extract_file_function_names(smda_report, **kwargs):
|
||||
@@ -87,8 +88,8 @@ def extract_features(smda_report, buf):
|
||||
"""
|
||||
|
||||
for file_handler in FILE_HANDLERS:
|
||||
for feature, va in file_handler(smda_report=smda_report, buf=buf):
|
||||
yield feature, va
|
||||
for feature, addr in file_handler(smda_report=smda_report, buf=buf):
|
||||
yield feature, addr
|
||||
|
||||
|
||||
FILE_HANDLERS = (
|
||||
|
||||
@@ -1,38 +1,42 @@
|
||||
from capa.features.common import Characteristic
|
||||
from typing import Tuple, Iterator
|
||||
|
||||
from capa.features.common import Feature, Characteristic
|
||||
from capa.features.address import Address, AbsoluteVirtualAddress
|
||||
from capa.features.extractors import loops
|
||||
from capa.features.extractors.base_extractor import FunctionHandle
|
||||
|
||||
|
||||
def extract_function_calls_to(f):
|
||||
for inref in f.inrefs:
|
||||
yield Characteristic("calls to"), inref
|
||||
def extract_function_calls_to(f: FunctionHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
for inref in f.inner.inrefs:
|
||||
yield Characteristic("calls to"), AbsoluteVirtualAddress(inref)
|
||||
|
||||
|
||||
def extract_function_loop(f):
|
||||
def extract_function_loop(f: FunctionHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
parse if a function has a loop
|
||||
"""
|
||||
edges = []
|
||||
for bb_from, bb_tos in f.blockrefs.items():
|
||||
for bb_from, bb_tos in f.inner.blockrefs.items():
|
||||
for bb_to in bb_tos:
|
||||
edges.append((bb_from, bb_to))
|
||||
|
||||
if edges and loops.has_loop(edges):
|
||||
yield Characteristic("loop"), f.offset
|
||||
yield Characteristic("loop"), f.address
|
||||
|
||||
|
||||
def extract_features(f):
|
||||
def extract_features(f: FunctionHandle):
|
||||
"""
|
||||
extract features from the given function.
|
||||
|
||||
args:
|
||||
f (smda.common.SmdaFunction): the function from which to extract features
|
||||
f: the function from which to extract features
|
||||
|
||||
yields:
|
||||
Tuple[Feature, int]: the features and their location found in this function.
|
||||
Tuple[Feature, Address]: 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(f):
|
||||
yield feature, addr
|
||||
|
||||
|
||||
FUNCTION_HANDLERS = (extract_function_calls_to, extract_function_loop)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import logging
|
||||
|
||||
from capa.features.common import ARCH_I386, ARCH_AMD64, Arch
|
||||
from capa.features.address import NO_ADDRESS
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -8,9 +9,9 @@ logger = logging.getLogger(__name__)
|
||||
def extract_arch(smda_report):
|
||||
if smda_report.architecture == "intel":
|
||||
if smda_report.bitness == 32:
|
||||
yield Arch(ARCH_I386), 0x0
|
||||
yield Arch(ARCH_I386), NO_ADDRESS
|
||||
elif smda_report.bitness == 64:
|
||||
yield Arch(ARCH_AMD64), 0x0
|
||||
yield Arch(ARCH_AMD64), NO_ADDRESS
|
||||
else:
|
||||
# we likely end up here:
|
||||
# 1. handling a new architecture (e.g. aarch64)
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
import re
|
||||
import string
|
||||
import struct
|
||||
from typing import Tuple, Iterator
|
||||
|
||||
from smda.common.SmdaReport import SmdaReport
|
||||
import smda
|
||||
|
||||
import capa.features.extractors.helpers
|
||||
from capa.features.insn import API, Number, Offset, Mnemonic
|
||||
from capa.features.common import (
|
||||
BITNESS_X32,
|
||||
BITNESS_X64,
|
||||
MAX_BYTES_FEATURE_SIZE,
|
||||
THUNK_CHAIN_DEPTH_DELTA,
|
||||
Bytes,
|
||||
String,
|
||||
Characteristic,
|
||||
)
|
||||
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
|
||||
@@ -23,27 +18,20 @@ PATTERN_HEXNUM = re.compile(r"[+\-] (?P<num>0x[a-fA-F0-9]+)")
|
||||
PATTERN_SINGLENUM = re.compile(r"[+\-] (?P<num>[0-9])")
|
||||
|
||||
|
||||
def get_bitness(smda_report):
|
||||
if smda_report.architecture == "intel":
|
||||
if smda_report.bitness == 32:
|
||||
return BITNESS_X32
|
||||
elif smda_report.bitness == 64:
|
||||
return BITNESS_X64
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def extract_insn_api_features(f, bb, insn):
|
||||
def extract_insn_api_features(fh: FunctionHandle, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""parse API features from the given instruction."""
|
||||
if insn.offset in f.apirefs:
|
||||
api_entry = f.apirefs[insn.offset]
|
||||
f: smda.Function = fh.inner
|
||||
insn: smda.Insn = ih.inner
|
||||
|
||||
if ih.address in f.apirefs:
|
||||
api_entry = f.apirefs[ih.address]
|
||||
# reformat
|
||||
dll_name, api_name = api_entry.split("!")
|
||||
dll_name = dll_name.split(".")[0]
|
||||
dll_name = dll_name.lower()
|
||||
for name in capa.features.extractors.helpers.generate_symbols(dll_name, api_name):
|
||||
yield API(name), insn.offset
|
||||
elif insn.offset in f.outrefs:
|
||||
yield API(name), ih.address
|
||||
elif ih.address in f.outrefs:
|
||||
current_function = f
|
||||
current_instruction = insn
|
||||
for index in range(THUNK_CHAIN_DEPTH_DELTA):
|
||||
@@ -62,7 +50,7 @@ def extract_insn_api_features(f, bb, insn):
|
||||
dll_name = dll_name.split(".")[0]
|
||||
dll_name = dll_name.lower()
|
||||
for name in capa.features.extractors.helpers.generate_symbols(dll_name, api_name):
|
||||
yield API(name), insn.offset
|
||||
yield API(name), ih.address
|
||||
elif referenced_function.num_instructions == 1 and referenced_function.num_outrefs == 1:
|
||||
current_function = referenced_function
|
||||
current_instruction = [i for i in referenced_function.getInstructions()][0]
|
||||
@@ -70,11 +58,14 @@ def extract_insn_api_features(f, bb, insn):
|
||||
return
|
||||
|
||||
|
||||
def extract_insn_number_features(f, bb, insn):
|
||||
def extract_insn_number_features(fh: FunctionHandle, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""parse number features from the given instruction."""
|
||||
# example:
|
||||
#
|
||||
# push 3136B0h ; dwControlCode
|
||||
f: smda.Function = fh.inner
|
||||
insn: smda.Insn = ih.inner
|
||||
|
||||
operands = [o.strip() for o in insn.operands.split(",")]
|
||||
if insn.mnemonic == "add" and operands[0] in ["esp", "rsp"]:
|
||||
# skip things like:
|
||||
@@ -82,16 +73,25 @@ def extract_insn_number_features(f, bb, insn):
|
||||
# .text:00401140 call sub_407E2B
|
||||
# .text:00401145 add esp, 0Ch
|
||||
return
|
||||
for operand in operands:
|
||||
for i, operand in enumerate(operands):
|
||||
try:
|
||||
# The result of bitwise operations is calculated as though carried out
|
||||
# in two’s complement with an infinite number of sign bits
|
||||
value = int(operand, 16) & ((1 << f.smda_report.bitness) - 1)
|
||||
|
||||
yield Number(value), insn.offset
|
||||
yield Number(value, bitness=get_bitness(f.smda_report)), insn.offset
|
||||
except:
|
||||
except ValueError:
|
||||
continue
|
||||
else:
|
||||
yield Number(value), ih.address
|
||||
yield OperandNumber(i, value), ih.address
|
||||
|
||||
if insn.mnemonic == "add" and 0 < value < MAX_STRUCTURE_SIZE:
|
||||
# for pattern like:
|
||||
#
|
||||
# add eax, 0x10
|
||||
#
|
||||
# assume 0x10 is also an offset (imagine eax is a pointer).
|
||||
yield Offset(value), ih.address
|
||||
yield OperandOffset(i, value), ih.address
|
||||
|
||||
|
||||
def read_bytes(smda_report, va, num_bytes=None):
|
||||
@@ -140,12 +140,15 @@ def derefs(smda_report, p):
|
||||
p = val
|
||||
|
||||
|
||||
def extract_insn_bytes_features(f, bb, insn):
|
||||
def extract_insn_bytes_features(fh: FunctionHandle, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
parse byte sequence features from the given instruction.
|
||||
example:
|
||||
# push offset iid_004118d4_IShellLinkA ; riid
|
||||
"""
|
||||
f: smda.Function = fh.inner
|
||||
insn: smda.Insn = ih.inner
|
||||
|
||||
for data_ref in insn.getDataRefs():
|
||||
for v in derefs(f.smda_report, data_ref):
|
||||
bytes_read = read_bytes(f.smda_report, v)
|
||||
@@ -154,7 +157,7 @@ def extract_insn_bytes_features(f, bb, insn):
|
||||
if capa.features.extractors.helpers.all_zeros(bytes_read):
|
||||
continue
|
||||
|
||||
yield Bytes(bytes_read), insn.offset
|
||||
yield Bytes(bytes_read), ih.address
|
||||
|
||||
|
||||
def detect_ascii_len(smda_report, offset):
|
||||
@@ -198,30 +201,34 @@ def read_string(smda_report, offset):
|
||||
return read_bytes(smda_report, offset, ulen).decode("utf-16")
|
||||
|
||||
|
||||
def extract_insn_string_features(f, bb, insn):
|
||||
def extract_insn_string_features(fh: FunctionHandle, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""parse string features from the given instruction."""
|
||||
# example:
|
||||
#
|
||||
# push offset aAcr ; "ACR > "
|
||||
f: smda.Function = fh.inner
|
||||
insn: smda.Insn = ih.inner
|
||||
|
||||
for data_ref in insn.getDataRefs():
|
||||
for v in derefs(f.smda_report, data_ref):
|
||||
string_read = read_string(f.smda_report, v)
|
||||
if string_read:
|
||||
yield String(string_read.rstrip("\x00")), insn.offset
|
||||
yield String(string_read.rstrip("\x00")), ih.address
|
||||
|
||||
|
||||
def extract_insn_offset_features(f, bb, insn):
|
||||
def extract_insn_offset_features(f, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""parse structure offset features from the given instruction."""
|
||||
# examples:
|
||||
#
|
||||
# mov eax, [esi + 4]
|
||||
# mov eax, [esi + ecx + 16384]
|
||||
insn: smda.Insn = ih.inner
|
||||
|
||||
operands = [o.strip() for o in insn.operands.split(",")]
|
||||
for operand in operands:
|
||||
if not "ptr" in operand:
|
||||
continue
|
||||
for i, operand in enumerate(operands):
|
||||
if "esp" in operand or "ebp" in operand or "rbp" in operand:
|
||||
continue
|
||||
|
||||
number = 0
|
||||
number_hex = re.search(PATTERN_HEXNUM, operand)
|
||||
number_int = re.search(PATTERN_SINGLENUM, operand)
|
||||
@@ -231,8 +238,26 @@ def extract_insn_offset_features(f, bb, insn):
|
||||
elif number_int:
|
||||
number = int(number_int.group("num"))
|
||||
number = -1 * number if number_int.group().startswith("-") else number
|
||||
yield Offset(number), insn.offset
|
||||
yield Offset(number, bitness=get_bitness(f.smda_report)), insn.offset
|
||||
|
||||
if "ptr" not in operand:
|
||||
if (
|
||||
insn.mnemonic == "lea"
|
||||
and i == 1
|
||||
and (operand.count("+") + operand.count("-")) == 1
|
||||
and operand.count("*") == 0
|
||||
):
|
||||
# for pattern like:
|
||||
#
|
||||
# lea eax, [ebx + 1]
|
||||
#
|
||||
# assume 1 is also an offset (imagine ebx is a zero register).
|
||||
yield Number(number), ih.address
|
||||
yield OperandNumber(i, number), ih.address
|
||||
|
||||
continue
|
||||
|
||||
yield Offset(number), ih.address
|
||||
yield OperandOffset(i, number), ih.address
|
||||
|
||||
|
||||
def is_security_cookie(f, bb, insn):
|
||||
@@ -256,11 +281,16 @@ def is_security_cookie(f, bb, insn):
|
||||
return False
|
||||
|
||||
|
||||
def extract_insn_nzxor_characteristic_features(f, bb, insn):
|
||||
def extract_insn_nzxor_characteristic_features(
|
||||
fh: FunctionHandle, bh: BBHandle, ih: InsnHandle
|
||||
) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
parse non-zeroing XOR instruction from the given instruction.
|
||||
ignore expected non-zeroing XORs, e.g. security cookies.
|
||||
"""
|
||||
f: smda.Function = fh.inner
|
||||
bb: smda.BasicBlock = bh.inner
|
||||
insn: smda.Insn = ih.inner
|
||||
|
||||
if insn.mnemonic not in ("xor", "xorpd", "xorps", "pxor"):
|
||||
return
|
||||
@@ -272,18 +302,35 @@ def extract_insn_nzxor_characteristic_features(f, bb, insn):
|
||||
if is_security_cookie(f, bb, insn):
|
||||
return
|
||||
|
||||
yield Characteristic("nzxor"), insn.offset
|
||||
yield Characteristic("nzxor"), ih.address
|
||||
|
||||
|
||||
def extract_insn_mnemonic_features(f, bb, insn):
|
||||
def extract_insn_mnemonic_features(f, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""parse mnemonic features from the given instruction."""
|
||||
yield Mnemonic(insn.mnemonic), insn.offset
|
||||
yield Mnemonic(ih.inner.mnemonic), ih.address
|
||||
|
||||
|
||||
def extract_insn_peb_access_characteristic_features(f, bb, insn):
|
||||
def extract_insn_obfs_call_plus_5_characteristic_features(f, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
parse call $+5 instruction from the given instruction.
|
||||
"""
|
||||
insn: smda.Insn = ih.inner
|
||||
|
||||
if insn.mnemonic != "call":
|
||||
return
|
||||
|
||||
if not insn.operands.startswith("0x"):
|
||||
return
|
||||
|
||||
if int(insn.operands, 16) == insn.offset + 5:
|
||||
yield Characteristic("call $+5"), ih.address
|
||||
|
||||
|
||||
def extract_insn_peb_access_characteristic_features(f, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
parse peb access from the given function. fs:[0x30] on x86, gs:[0x60] on x64
|
||||
"""
|
||||
insn: smda.Insn = ih.inner
|
||||
|
||||
if insn.mnemonic not in ["push", "mov"]:
|
||||
return
|
||||
@@ -291,65 +338,75 @@ def extract_insn_peb_access_characteristic_features(f, bb, insn):
|
||||
operands = [o.strip() for o in insn.operands.split(",")]
|
||||
for operand in operands:
|
||||
if "fs:" in operand and "0x30" in operand:
|
||||
yield Characteristic("peb access"), insn.offset
|
||||
yield Characteristic("peb access"), ih.address
|
||||
elif "gs:" in operand and "0x60" in operand:
|
||||
yield Characteristic("peb access"), insn.offset
|
||||
yield Characteristic("peb access"), ih.address
|
||||
|
||||
|
||||
def extract_insn_segment_access_features(f, bb, insn):
|
||||
def extract_insn_segment_access_features(f, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""parse the instruction for access to fs or gs"""
|
||||
insn: smda.Insn = ih.inner
|
||||
|
||||
operands = [o.strip() for o in insn.operands.split(",")]
|
||||
for operand in operands:
|
||||
if "fs:" in operand:
|
||||
yield Characteristic("fs access"), insn.offset
|
||||
yield Characteristic("fs access"), ih.address
|
||||
elif "gs:" in operand:
|
||||
yield Characteristic("gs access"), insn.offset
|
||||
yield Characteristic("gs access"), ih.address
|
||||
|
||||
|
||||
def extract_insn_cross_section_cflow(f, bb, insn):
|
||||
def extract_insn_cross_section_cflow(fh: FunctionHandle, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
inspect the instruction for a CALL or JMP that crosses section boundaries.
|
||||
"""
|
||||
f: smda.Function = fh.inner
|
||||
insn: smda.Insn = ih.inner
|
||||
|
||||
if insn.mnemonic in ["call", "jmp"]:
|
||||
if insn.offset in f.apirefs:
|
||||
if ih.address in f.apirefs:
|
||||
return
|
||||
|
||||
smda_report = insn.smda_function.smda_report
|
||||
if insn.offset in f.outrefs:
|
||||
for target in f.outrefs[insn.offset]:
|
||||
if smda_report.getSection(insn.offset) != smda_report.getSection(target):
|
||||
yield Characteristic("cross section flow"), insn.offset
|
||||
if ih.address in f.outrefs:
|
||||
for target in f.outrefs[ih.address]:
|
||||
if smda_report.getSection(ih.address) != smda_report.getSection(target):
|
||||
yield Characteristic("cross section flow"), ih.address
|
||||
elif insn.operands.startswith("0x"):
|
||||
target = int(insn.operands, 16)
|
||||
if smda_report.getSection(insn.offset) != smda_report.getSection(target):
|
||||
yield Characteristic("cross section flow"), insn.offset
|
||||
if smda_report.getSection(ih.address) != smda_report.getSection(target):
|
||||
yield Characteristic("cross section flow"), ih.address
|
||||
|
||||
|
||||
# this is a feature that's most relevant at the function scope,
|
||||
# however, its most efficient to extract at the instruction scope.
|
||||
def extract_function_calls_from(f, bb, insn):
|
||||
def extract_function_calls_from(fh: FunctionHandle, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
f: smda.Function = fh.inner
|
||||
insn: smda.Insn = ih.inner
|
||||
|
||||
if insn.mnemonic != "call":
|
||||
return
|
||||
|
||||
if insn.offset in f.outrefs:
|
||||
for outref in f.outrefs[insn.offset]:
|
||||
yield Characteristic("calls from"), outref
|
||||
if ih.address in f.outrefs:
|
||||
for outref in f.outrefs[ih.address]:
|
||||
yield Characteristic("calls from"), AbsoluteVirtualAddress(outref)
|
||||
|
||||
if outref == f.offset:
|
||||
# if we found a jump target and it's the function address
|
||||
# mark as recursive
|
||||
yield Characteristic("recursive call"), outref
|
||||
if insn.offset in f.apirefs:
|
||||
yield Characteristic("calls from"), insn.offset
|
||||
yield Characteristic("recursive call"), AbsoluteVirtualAddress(outref)
|
||||
if ih.address in f.apirefs:
|
||||
yield Characteristic("calls from"), ih.address
|
||||
|
||||
|
||||
# this is a feature that's most relevant at the function or basic block scope,
|
||||
# however, its most efficient to extract at the instruction scope.
|
||||
def extract_function_indirect_call_characteristic_features(f, bb, insn):
|
||||
def extract_function_indirect_call_characteristic_features(f, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
extract indirect function call characteristic (e.g., call eax or call dword ptr [edx+4])
|
||||
does not include calls like => call ds:dword_ABD4974
|
||||
"""
|
||||
insn: smda.Insn = ih.inner
|
||||
|
||||
if insn.mnemonic != "call":
|
||||
return
|
||||
if insn.operands.startswith("0x"):
|
||||
@@ -361,7 +418,7 @@ def extract_function_indirect_call_characteristic_features(f, bb, insn):
|
||||
# call edx
|
||||
# call dword ptr [eax+50h]
|
||||
# call qword ptr [rsp+78h]
|
||||
yield Characteristic("indirect call"), insn.offset
|
||||
yield Characteristic("indirect call"), ih.address
|
||||
|
||||
|
||||
def extract_features(f, bb, insn):
|
||||
@@ -369,16 +426,16 @@ def extract_features(f, bb, insn):
|
||||
extract features from the given insn.
|
||||
|
||||
args:
|
||||
f (smda.common.SmdaFunction): the function to process.
|
||||
bb (smda.common.SmdaBasicBlock): the basic block to process.
|
||||
insn (smda.common.SmdaInstruction): the instruction to process.
|
||||
f: the function to process.
|
||||
bb: the basic block to process.
|
||||
insn: the instruction to process.
|
||||
|
||||
yields:
|
||||
Tuple[Feature, int]: the features and their location found in this insn.
|
||||
Tuple[Feature, Address]: the features and their location found in this insn.
|
||||
"""
|
||||
for insn_handler in INSTRUCTION_HANDLERS:
|
||||
for feature, va in insn_handler(f, bb, insn):
|
||||
yield feature, va
|
||||
for feature, addr in insn_handler(f, bb, insn):
|
||||
yield feature, addr
|
||||
|
||||
|
||||
INSTRUCTION_HANDLERS = (
|
||||
@@ -389,6 +446,7 @@ 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,
|
||||
|
||||
@@ -8,27 +8,30 @@
|
||||
|
||||
import string
|
||||
import struct
|
||||
from typing import Tuple, Iterator
|
||||
|
||||
import envi
|
||||
import envi.archs.i386.disasm
|
||||
|
||||
from capa.features.common 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")
|
||||
...
|
||||
|
||||
|
||||
def _bb_has_tight_loop(f, bb):
|
||||
@@ -44,10 +47,10 @@ def _bb_has_tight_loop(f, bb):
|
||||
return False
|
||||
|
||||
|
||||
def extract_bb_tight_loop(f, bb):
|
||||
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):
|
||||
yield Characteristic("tight loop"), bb.va
|
||||
if _bb_has_tight_loop(f, bb.inner):
|
||||
yield Characteristic("tight loop"), bb.address
|
||||
|
||||
|
||||
def _bb_has_stackstring(f, bb):
|
||||
@@ -67,10 +70,10 @@ def _bb_has_stackstring(f, bb):
|
||||
return False
|
||||
|
||||
|
||||
def extract_stackstring(f, bb):
|
||||
def extract_stackstring(f: FunctionHandle, bb: BBHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""check basic block for stackstring indicators"""
|
||||
if _bb_has_stackstring(f, bb):
|
||||
yield Characteristic("stack string"), bb.va
|
||||
if _bb_has_stackstring(f, bb.inner):
|
||||
yield Characteristic("stack string"), bb.address
|
||||
|
||||
|
||||
def is_mov_imm_to_stack(instr: envi.archs.i386.disasm.i386Opcode) -> bool:
|
||||
@@ -143,7 +146,7 @@ def is_printable_utf16le(chars: bytes) -> bool:
|
||||
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.
|
||||
|
||||
@@ -154,10 +157,10 @@ def extract_features(f, bb):
|
||||
yields:
|
||||
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 = (
|
||||
|
||||
@@ -6,6 +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.
|
||||
import logging
|
||||
from typing import List, Tuple, Iterator
|
||||
|
||||
import viv_utils
|
||||
import viv_utils.flirt
|
||||
@@ -16,24 +17,13 @@ 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.extractors.base_extractor import FeatureExtractor
|
||||
from capa.features.common import Feature
|
||||
from capa.features.address import Address, AbsoluteVirtualAddress
|
||||
from capa.features.extractors.base_extractor import BBHandle, InsnHandle, FunctionHandle, FeatureExtractor
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class InstructionHandle:
|
||||
"""this acts like a vivisect.Opcode but with an __int__() method"""
|
||||
|
||||
def __init__(self, inner):
|
||||
self._inner = inner
|
||||
|
||||
def __int__(self):
|
||||
return self.va
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._inner, name)
|
||||
|
||||
|
||||
class VivisectFeatureExtractor(FeatureExtractor):
|
||||
def __init__(self, vw, path):
|
||||
super(VivisectFeatureExtractor, self).__init__()
|
||||
@@ -43,13 +33,13 @@ class VivisectFeatureExtractor(FeatureExtractor):
|
||||
self.buf = f.read()
|
||||
|
||||
# pre-compute these because we'll yield them at *every* scope.
|
||||
self.global_features = []
|
||||
self.global_features: List[Tuple[Feature, Address]] = []
|
||||
self.global_features.extend(capa.features.extractors.common.extract_os(self.buf))
|
||||
self.global_features.extend(capa.features.extractors.viv.global_.extract_arch(self.vw))
|
||||
|
||||
def get_base_address(self):
|
||||
# assume there is only one file loaded into the vw
|
||||
return list(self.vw.filemeta.values())[0]["imagebase"]
|
||||
return AbsoluteVirtualAddress(list(self.vw.filemeta.values())[0]["imagebase"])
|
||||
|
||||
def extract_global_features(self):
|
||||
yield from self.global_features
|
||||
@@ -57,28 +47,33 @@ class VivisectFeatureExtractor(FeatureExtractor):
|
||||
def extract_file_features(self):
|
||||
yield from capa.features.extractors.viv.file.extract_features(self.vw, self.buf)
|
||||
|
||||
def get_functions(self):
|
||||
def get_functions(self) -> Iterator[FunctionHandle]:
|
||||
for va in sorted(self.vw.getFunctions()):
|
||||
yield viv_utils.Function(self.vw, va)
|
||||
yield FunctionHandle(address=AbsoluteVirtualAddress(va), inner=viv_utils.Function(self.vw, va))
|
||||
|
||||
def extract_function_features(self, f):
|
||||
yield from capa.features.extractors.viv.function.extract_features(f)
|
||||
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, f):
|
||||
return f.basic_blocks
|
||||
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, f, bb):
|
||||
yield from capa.features.extractors.viv.basicblock.extract_features(f, 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, f, bb):
|
||||
def get_instructions(self, fh: FunctionHandle, bbh: BBHandle) -> Iterator[InsnHandle]:
|
||||
bb: viv_utils.BasicBlock = bbh.inner
|
||||
for insn in bb.instructions:
|
||||
yield InstructionHandle(insn)
|
||||
yield InsnHandle(address=AbsoluteVirtualAddress(insn.va), inner=insn)
|
||||
|
||||
def extract_insn_features(self, f, bb, insn):
|
||||
yield from capa.features.extractors.viv.insn.extract_features(f, bb, 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, va):
|
||||
return viv_utils.flirt.is_library_function(self.vw, va)
|
||||
def is_library_function(self, addr):
|
||||
return viv_utils.flirt.is_library_function(self.vw, addr)
|
||||
|
||||
def get_function_name(self, va):
|
||||
return viv_utils.get_function_name(self.vw, va)
|
||||
def get_function_name(self, addr):
|
||||
return viv_utils.get_function_name(self.vw, addr)
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
# 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 viv_utils
|
||||
@@ -15,20 +16,21 @@ 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 String, Characteristic
|
||||
from capa.features.common import String, Feature, Characteristic
|
||||
from capa.features.address import Address, FileOffsetAddress, AbsoluteVirtualAddress
|
||||
|
||||
|
||||
def extract_file_embedded_pe(buf, **kwargs):
|
||||
def extract_file_embedded_pe(buf, **kwargs) -> Iterator[Tuple[Feature, Address]]:
|
||||
for offset, _ in pe_carve.carve(buf, 1):
|
||||
yield Characteristic("embedded pe"), offset
|
||||
yield Characteristic("embedded pe"), FileOffsetAddress(offset)
|
||||
|
||||
|
||||
def extract_file_export_names(vw, **kwargs):
|
||||
def extract_file_export_names(vw, **kwargs) -> Iterator[Tuple[Feature, Address]]:
|
||||
for va, _, name, _ in vw.getExports():
|
||||
yield Export(name), va
|
||||
yield Export(name), AbsoluteVirtualAddress(va)
|
||||
|
||||
|
||||
def extract_file_import_names(vw, **kwargs):
|
||||
def extract_file_import_names(vw, **kwargs) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
extract imported function names
|
||||
1. imports by ordinal:
|
||||
@@ -44,8 +46,9 @@ def extract_file_import_names(vw, **kwargs):
|
||||
# replace ord prefix with #
|
||||
impname = "#%s" % impname[len("ord") :]
|
||||
|
||||
addr = AbsoluteVirtualAddress(va)
|
||||
for name in capa.features.extractors.helpers.generate_symbols(modname, impname):
|
||||
yield Import(name), va
|
||||
yield Import(name), addr
|
||||
|
||||
|
||||
def is_viv_ord_impname(impname: str) -> bool:
|
||||
@@ -62,30 +65,37 @@ def is_viv_ord_impname(impname: str) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def extract_file_section_names(vw, **kwargs):
|
||||
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(buf, **kwargs):
|
||||
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):
|
||||
def extract_file_function_names(vw, **kwargs) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
extract the names of statically-linked library functions.
|
||||
"""
|
||||
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), 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_file_format(buf, **kwargs):
|
||||
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):
|
||||
def extract_features(vw, buf: bytes) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
extract file features from given workspace
|
||||
|
||||
@@ -94,12 +104,12 @@ def extract_features(vw, buf: bytes):
|
||||
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=vw, buf=buf): # type: ignore
|
||||
yield feature, va
|
||||
for feature, addr in file_handler(vw=vw, buf=buf): # type: ignore
|
||||
yield feature, addr
|
||||
|
||||
|
||||
FILE_HANDLERS = (
|
||||
|
||||
@@ -5,36 +5,43 @@
|
||||
# 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.common 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 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")
|
||||
...
|
||||
|
||||
|
||||
def extract_function_calls_to(f):
|
||||
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:
|
||||
@@ -50,22 +57,22 @@ def extract_function_loop(f):
|
||||
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:
|
||||
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)
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import logging
|
||||
from typing import Tuple, Iterator
|
||||
|
||||
import envi.archs.i386
|
||||
import envi.archs.amd64
|
||||
|
||||
from capa.features.common import ARCH_I386, ARCH_AMD64, Arch
|
||||
from capa.features.common import ARCH_I386, ARCH_AMD64, Arch, Feature
|
||||
from capa.features.address import NO_ADDRESS, Address
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def extract_arch(vw):
|
||||
def extract_arch(vw) -> Iterator[Tuple[Feature, Address]]:
|
||||
if isinstance(vw.arch, envi.archs.amd64.Amd64Module):
|
||||
yield Arch(ARCH_AMD64), 0x0
|
||||
yield Arch(ARCH_AMD64), NO_ADDRESS
|
||||
|
||||
elif isinstance(vw.arch, envi.archs.i386.i386Module):
|
||||
yield Arch(ARCH_I386), 0x0
|
||||
yield Arch(ARCH_I386), NO_ADDRESS
|
||||
|
||||
else:
|
||||
# we likely end up here:
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import collections
|
||||
from typing import TYPE_CHECKING, Set, List, Deque, Tuple, Union, Optional
|
||||
from typing import Set, List, Deque, Tuple, Union, Optional
|
||||
|
||||
import envi
|
||||
import vivisect.const
|
||||
@@ -15,9 +15,6 @@ import envi.archs.i386.disasm
|
||||
import envi.archs.amd64.disasm
|
||||
from vivisect import VivWorkspace
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from capa.features.extractors.viv.extractor import InstructionHandle
|
||||
|
||||
# pull out consts for lookup performance
|
||||
i386RegOper = envi.archs.i386.disasm.i386RegOper
|
||||
i386ImmOper = envi.archs.i386.disasm.i386ImmOper
|
||||
@@ -135,16 +132,14 @@ def find_definition(vw: VivWorkspace, va: int, reg: int) -> Tuple[int, Union[int
|
||||
raise NotFoundError()
|
||||
|
||||
|
||||
def is_indirect_call(vw: VivWorkspace, va: int, insn: Optional["InstructionHandle"] = None) -> bool:
|
||||
def is_indirect_call(vw: VivWorkspace, va: int, insn: envi.Opcode) -> bool:
|
||||
if insn is None:
|
||||
insn = vw.parseOpcode(va)
|
||||
|
||||
return insn.mnem in ("call", "jmp") and isinstance(insn.opers[0], envi.archs.i386.disasm.i386RegOper)
|
||||
|
||||
|
||||
def resolve_indirect_call(
|
||||
vw: VivWorkspace, va: int, insn: Optional["InstructionHandle"] = None
|
||||
) -> Tuple[int, Optional[int]]:
|
||||
def resolve_indirect_call(vw: VivWorkspace, va: int, insn: envi.Opcode) -> Tuple[int, Optional[int]]:
|
||||
"""
|
||||
inspect the given indirect call instruction and attempt to resolve the target address.
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
# 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, Callable, Iterator
|
||||
|
||||
import envi
|
||||
import envi.exc
|
||||
import viv_utils
|
||||
@@ -17,16 +19,10 @@ import envi.archs.amd64.disasm
|
||||
|
||||
import capa.features.extractors.helpers
|
||||
import capa.features.extractors.viv.helpers
|
||||
from capa.features.insn import API, Number, Offset, Mnemonic
|
||||
from capa.features.common import (
|
||||
BITNESS_X32,
|
||||
BITNESS_X64,
|
||||
MAX_BYTES_FEATURE_SIZE,
|
||||
THUNK_CHAIN_DEPTH_DELTA,
|
||||
Bytes,
|
||||
String,
|
||||
Characteristic,
|
||||
)
|
||||
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
|
||||
from capa.features.extractors.viv.indirect_calls import NotFoundError, resolve_indirect_call
|
||||
|
||||
# security cookie checks may perform non-zeroing XORs, these are expected within a certain
|
||||
@@ -34,27 +30,21 @@ from capa.features.extractors.viv.indirect_calls import NotFoundError, resolve_i
|
||||
SECURITY_COOKIE_BYTES_DELTA = 0x40
|
||||
|
||||
|
||||
def get_bitness(vw):
|
||||
bitness = vw.getMeta("Architecture")
|
||||
if bitness == "i386":
|
||||
return BITNESS_X32
|
||||
elif bitness == "amd64":
|
||||
return BITNESS_X64
|
||||
|
||||
|
||||
def interface_extract_instruction_XXX(f, bb, insn):
|
||||
def interface_extract_instruction_XXX(
|
||||
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle
|
||||
) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
parse features from the given instruction.
|
||||
|
||||
args:
|
||||
f (viv_utils.Function): the function to process.
|
||||
bb (viv_utils.BasicBlock): the basic block to process.
|
||||
insn (vivisect...Instruction): the instruction to process.
|
||||
fh: the function handle to process.
|
||||
bbh: the basic block handle to process.
|
||||
ih: the instruction handle 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")
|
||||
...
|
||||
|
||||
|
||||
def get_imports(vw):
|
||||
@@ -74,12 +64,15 @@ def get_imports(vw):
|
||||
return imports
|
||||
|
||||
|
||||
def extract_insn_api_features(f, bb, insn):
|
||||
"""parse API features from the given instruction."""
|
||||
def extract_insn_api_features(fh: FunctionHandle, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
parse API features from the given instruction.
|
||||
|
||||
# example:
|
||||
#
|
||||
# call dword [0x00473038]
|
||||
example:
|
||||
call dword [0x00473038]
|
||||
"""
|
||||
insn: envi.Opcode = ih.inner
|
||||
f: viv_utils.Function = fh.inner
|
||||
if insn.mnem not in ("call", "jmp"):
|
||||
return
|
||||
|
||||
@@ -96,7 +89,7 @@ def extract_insn_api_features(f, bb, insn):
|
||||
if target in imports:
|
||||
dll, symbol = imports[target]
|
||||
for name in capa.features.extractors.helpers.generate_symbols(dll, symbol):
|
||||
yield API(name), insn.va
|
||||
yield API(name), ih.address
|
||||
|
||||
# call via thunk on x86,
|
||||
# see 9324d1a8ae37a36ae560c37448c9705a at 0x407985
|
||||
@@ -118,14 +111,20 @@ def extract_insn_api_features(f, bb, insn):
|
||||
|
||||
if viv_utils.flirt.is_library_function(f.vw, target):
|
||||
name = viv_utils.get_function_name(f.vw, target)
|
||||
yield API(name), insn.va
|
||||
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
|
||||
return
|
||||
|
||||
for _ in range(THUNK_CHAIN_DEPTH_DELTA):
|
||||
if target in imports:
|
||||
dll, symbol = imports[target]
|
||||
for name in capa.features.extractors.helpers.generate_symbols(dll, symbol):
|
||||
yield API(name), insn.va
|
||||
yield API(name), ih.address
|
||||
|
||||
# if jump leads to an ENDBRANCH instruction, skip it
|
||||
if f.vw.getByteDef(target)[1].startswith(b"\xf3\x0f\x1e"):
|
||||
@@ -145,7 +144,7 @@ def extract_insn_api_features(f, bb, insn):
|
||||
if target in imports:
|
||||
dll, symbol = imports[target]
|
||||
for name in capa.features.extractors.helpers.generate_symbols(dll, symbol):
|
||||
yield API(name), insn.va
|
||||
yield API(name), ih.address
|
||||
|
||||
elif isinstance(insn.opers[0], envi.archs.i386.disasm.i386RegOper):
|
||||
try:
|
||||
@@ -162,38 +161,7 @@ def extract_insn_api_features(f, bb, insn):
|
||||
if target in imports:
|
||||
dll, symbol = imports[target]
|
||||
for name in capa.features.extractors.helpers.generate_symbols(dll, symbol):
|
||||
yield API(name), insn.va
|
||||
|
||||
|
||||
def extract_insn_number_features(f, bb, insn):
|
||||
"""parse number features from the given instruction."""
|
||||
# example:
|
||||
#
|
||||
# push 3136B0h ; dwControlCode
|
||||
for oper in insn.opers:
|
||||
# this is for both x32 and x64
|
||||
if not isinstance(oper, (envi.archs.i386.disasm.i386ImmOper, envi.archs.i386.disasm.i386ImmMemOper)):
|
||||
continue
|
||||
|
||||
if isinstance(oper, envi.archs.i386.disasm.i386ImmOper):
|
||||
v = oper.getOperValue(oper)
|
||||
else:
|
||||
v = oper.getOperAddr(oper)
|
||||
|
||||
if f.vw.probeMemory(v, 1, envi.memory.MM_READ):
|
||||
# this is a valid address
|
||||
# assume its not also a constant.
|
||||
continue
|
||||
|
||||
if insn.mnem == "add" and insn.opers[0].isReg() and insn.opers[0].reg == envi.archs.i386.regs.REG_ESP:
|
||||
# skip things like:
|
||||
#
|
||||
# .text:00401140 call sub_407E2B
|
||||
# .text:00401145 add esp, 0Ch
|
||||
return
|
||||
|
||||
yield Number(v), insn.va
|
||||
yield Number(v, bitness=get_bitness(f.vw)), insn.va
|
||||
yield API(name), ih.address
|
||||
|
||||
|
||||
def derefs(vw, p):
|
||||
@@ -266,12 +234,15 @@ def read_bytes(vw, va: int) -> bytes:
|
||||
raise
|
||||
|
||||
|
||||
def extract_insn_bytes_features(f, bb, insn):
|
||||
def extract_insn_bytes_features(fh: FunctionHandle, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
parse byte sequence features from the given instruction.
|
||||
example:
|
||||
# push offset iid_004118d4_IShellLinkA ; riid
|
||||
"""
|
||||
insn: envi.Opcode = ih.inner
|
||||
f: viv_utils.Function = fh.inner
|
||||
|
||||
if insn.mnem == "call":
|
||||
return
|
||||
|
||||
@@ -300,7 +271,7 @@ def extract_insn_bytes_features(f, bb, insn):
|
||||
if capa.features.extractors.helpers.all_zeros(buf):
|
||||
continue
|
||||
|
||||
yield Bytes(buf), insn.va
|
||||
yield Bytes(buf), ih.address
|
||||
|
||||
|
||||
def read_string(vw, offset: int) -> str:
|
||||
@@ -334,75 +305,6 @@ def read_string(vw, offset: int) -> str:
|
||||
raise ValueError("not a string", offset)
|
||||
|
||||
|
||||
def extract_insn_string_features(f, bb, insn):
|
||||
"""parse string features from the given instruction."""
|
||||
# example:
|
||||
#
|
||||
# push offset aAcr ; "ACR > "
|
||||
|
||||
for oper in insn.opers:
|
||||
if isinstance(oper, envi.archs.i386.disasm.i386ImmOper):
|
||||
v = oper.getOperValue(oper)
|
||||
elif isinstance(oper, envi.archs.i386.disasm.i386ImmMemOper):
|
||||
# like 0x10056CB4 in `lea eax, dword [0x10056CB4]`
|
||||
v = oper.imm
|
||||
elif isinstance(oper, envi.archs.i386.disasm.i386SibOper):
|
||||
# like 0x401000 in `mov eax, 0x401000[2 * ebx]`
|
||||
v = oper.imm
|
||||
elif isinstance(oper, envi.archs.amd64.disasm.Amd64RipRelOper):
|
||||
v = oper.getOperAddr(insn)
|
||||
else:
|
||||
continue
|
||||
|
||||
for v in derefs(f.vw, v):
|
||||
try:
|
||||
s = read_string(f.vw, v)
|
||||
except ValueError:
|
||||
continue
|
||||
else:
|
||||
yield String(s.rstrip("\x00")), insn.va
|
||||
|
||||
|
||||
def extract_insn_offset_features(f, bb, insn):
|
||||
"""parse structure offset features from the given instruction."""
|
||||
# example:
|
||||
#
|
||||
# .text:0040112F cmp [esi+4], ebx
|
||||
for oper in insn.opers:
|
||||
|
||||
# this is for both x32 and x64
|
||||
# like [esi + 4]
|
||||
# reg ^
|
||||
# disp
|
||||
if isinstance(oper, envi.archs.i386.disasm.i386RegMemOper):
|
||||
if oper.reg == envi.archs.i386.regs.REG_ESP:
|
||||
continue
|
||||
|
||||
if oper.reg == envi.archs.i386.regs.REG_EBP:
|
||||
continue
|
||||
|
||||
# TODO: do x64 support for real.
|
||||
if oper.reg == envi.archs.amd64.regs.REG_RBP:
|
||||
continue
|
||||
|
||||
# viv already decodes offsets as signed
|
||||
v = oper.disp
|
||||
|
||||
yield Offset(v), insn.va
|
||||
yield Offset(v, bitness=get_bitness(f.vw)), insn.va
|
||||
|
||||
# like: [esi + ecx + 16384]
|
||||
# reg ^ ^
|
||||
# index ^
|
||||
# disp
|
||||
elif isinstance(oper, envi.archs.i386.disasm.i386SibOper):
|
||||
# viv already decodes offsets as signed
|
||||
v = oper.disp
|
||||
|
||||
yield Offset(v), insn.va
|
||||
yield Offset(v, bitness=get_bitness(f.vw)), insn.va
|
||||
|
||||
|
||||
def is_security_cookie(f, bb, insn) -> bool:
|
||||
"""
|
||||
check if an instruction is related to security cookie checks
|
||||
@@ -431,11 +333,17 @@ def is_security_cookie(f, bb, insn) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def extract_insn_nzxor_characteristic_features(f, bb, insn):
|
||||
def extract_insn_nzxor_characteristic_features(
|
||||
fh: FunctionHandle, bbhandle: BBHandle, ih: InsnHandle
|
||||
) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
parse non-zeroing XOR instruction from the given instruction.
|
||||
ignore expected non-zeroing XORs, e.g. security cookies.
|
||||
"""
|
||||
insn: envi.Opcode = ih.inner
|
||||
bb: viv_utils.BasicBlock = bbhandle.inner
|
||||
f: viv_utils.Function = fh.inner
|
||||
|
||||
if insn.mnem not in ("xor", "xorpd", "xorps", "pxor"):
|
||||
return
|
||||
|
||||
@@ -445,19 +353,40 @@ def extract_insn_nzxor_characteristic_features(f, bb, insn):
|
||||
if is_security_cookie(f, bb, insn):
|
||||
return
|
||||
|
||||
yield Characteristic("nzxor"), insn.va
|
||||
yield Characteristic("nzxor"), ih.address
|
||||
|
||||
|
||||
def extract_insn_mnemonic_features(f, bb, insn):
|
||||
def extract_insn_mnemonic_features(f, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""parse mnemonic features from the given instruction."""
|
||||
yield Mnemonic(insn.mnem), insn.va
|
||||
yield Mnemonic(ih.inner.mnem), ih.address
|
||||
|
||||
|
||||
def extract_insn_peb_access_characteristic_features(f, bb, insn):
|
||||
def extract_insn_obfs_call_plus_5_characteristic_features(f, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
parse call $+5 instruction from the given instruction.
|
||||
"""
|
||||
insn: envi.Opcode = ih.inner
|
||||
|
||||
if insn.mnem != "call":
|
||||
return
|
||||
|
||||
if isinstance(insn.opers[0], envi.archs.i386.disasm.i386PcRelOper):
|
||||
if insn.va + 5 == insn.opers[0].getOperValue(insn):
|
||||
yield Characteristic("call $+5"), ih.address
|
||||
|
||||
if isinstance(insn.opers[0], envi.archs.i386.disasm.i386ImmMemOper) or isinstance(
|
||||
insn.opers[0], envi.archs.amd64.disasm.Amd64RipRelOper
|
||||
):
|
||||
if insn.va + 5 == insn.opers[0].getOperAddr(insn):
|
||||
yield Characteristic("call $+5"), ih.address
|
||||
|
||||
|
||||
def extract_insn_peb_access_characteristic_features(f, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
parse peb access from the given function. fs:[0x30] on x86, gs:[0x60] on x64
|
||||
"""
|
||||
# TODO handle where fs/gs are loaded into a register or onto the stack and used later
|
||||
insn: envi.Opcode = ih.inner
|
||||
|
||||
if insn.mnem not in ["push", "mov"]:
|
||||
return
|
||||
@@ -476,7 +405,7 @@ def extract_insn_peb_access_characteristic_features(f, bb, insn):
|
||||
if (isinstance(oper, envi.archs.i386.disasm.i386RegMemOper) and oper.disp == 0x30) or (
|
||||
isinstance(oper, envi.archs.i386.disasm.i386ImmMemOper) and oper.imm == 0x30
|
||||
):
|
||||
yield Characteristic("peb access"), insn.va
|
||||
yield Characteristic("peb access"), ih.address
|
||||
elif "gs" in prefix:
|
||||
for oper in insn.opers:
|
||||
if (
|
||||
@@ -484,20 +413,22 @@ def extract_insn_peb_access_characteristic_features(f, bb, insn):
|
||||
or (isinstance(oper, envi.archs.amd64.disasm.i386SibOper) and oper.imm == 0x60)
|
||||
or (isinstance(oper, envi.archs.amd64.disasm.i386ImmMemOper) and oper.imm == 0x60)
|
||||
):
|
||||
yield Characteristic("peb access"), insn.va
|
||||
yield Characteristic("peb access"), ih.address
|
||||
else:
|
||||
pass
|
||||
|
||||
|
||||
def extract_insn_segment_access_features(f, bb, insn):
|
||||
def extract_insn_segment_access_features(f, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""parse the instruction for access to fs or gs"""
|
||||
insn: envi.Opcode = ih.inner
|
||||
|
||||
prefix = insn.getPrefixName()
|
||||
|
||||
if prefix == "fs":
|
||||
yield Characteristic("fs access"), insn.va
|
||||
yield Characteristic("fs access"), ih.address
|
||||
|
||||
if prefix == "gs":
|
||||
yield Characteristic("gs access"), insn.va
|
||||
yield Characteristic("gs access"), ih.address
|
||||
|
||||
|
||||
def get_section(vw, va: int):
|
||||
@@ -508,10 +439,13 @@ def get_section(vw, va: int):
|
||||
raise KeyError(va)
|
||||
|
||||
|
||||
def extract_insn_cross_section_cflow(f, bb, insn):
|
||||
def extract_insn_cross_section_cflow(fh: FunctionHandle, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
inspect the instruction for a CALL or JMP that crosses section boundaries.
|
||||
"""
|
||||
insn: envi.Opcode = ih.inner
|
||||
f: viv_utils.Function = fh.inner
|
||||
|
||||
for va, flags in insn.getBranches():
|
||||
if va is None:
|
||||
# va may be none for dynamic branches that haven't been resolved, such as `jmp eax`.
|
||||
@@ -538,7 +472,7 @@ def extract_insn_cross_section_cflow(f, bb, insn):
|
||||
continue
|
||||
|
||||
if get_section(f.vw, insn.va) != get_section(f.vw, va):
|
||||
yield Characteristic("cross section flow"), insn.va
|
||||
yield Characteristic("cross section flow"), ih.address
|
||||
|
||||
except KeyError:
|
||||
continue
|
||||
@@ -546,7 +480,10 @@ def extract_insn_cross_section_cflow(f, bb, insn):
|
||||
|
||||
# this is a feature that's most relevant at the function scope,
|
||||
# however, its most efficient to extract at the instruction scope.
|
||||
def extract_function_calls_from(f, bb, insn):
|
||||
def extract_function_calls_from(fh: FunctionHandle, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
insn: envi.Opcode = ih.inner
|
||||
f: viv_utils.Function = fh.inner
|
||||
|
||||
if insn.mnem != "call":
|
||||
return
|
||||
|
||||
@@ -556,7 +493,7 @@ def extract_function_calls_from(f, bb, insn):
|
||||
if isinstance(insn.opers[0], envi.archs.i386.disasm.i386ImmMemOper):
|
||||
oper = insn.opers[0]
|
||||
target = oper.getOperAddr(insn)
|
||||
yield Characteristic("calls from"), target
|
||||
yield Characteristic("calls from"), AbsoluteVirtualAddress(target)
|
||||
|
||||
# call via thunk on x86,
|
||||
# see 9324d1a8ae37a36ae560c37448c9705a at 0x407985
|
||||
@@ -565,43 +502,191 @@ def extract_function_calls_from(f, bb, insn):
|
||||
# see Lab21-01.exe_:0x140001178
|
||||
elif isinstance(insn.opers[0], envi.archs.i386.disasm.i386PcRelOper):
|
||||
target = insn.opers[0].getOperValue(insn)
|
||||
yield Characteristic("calls from"), target
|
||||
if target >= 0:
|
||||
yield Characteristic("calls from"), AbsoluteVirtualAddress(target)
|
||||
|
||||
# call via IAT, x64
|
||||
elif isinstance(insn.opers[0], envi.archs.amd64.disasm.Amd64RipRelOper):
|
||||
op = insn.opers[0]
|
||||
target = op.getOperAddr(insn)
|
||||
yield Characteristic("calls from"), target
|
||||
yield Characteristic("calls from"), AbsoluteVirtualAddress(target)
|
||||
|
||||
if target and target == f.va:
|
||||
# if we found a jump target and it's the function address
|
||||
# mark as recursive
|
||||
yield Characteristic("recursive call"), target
|
||||
yield Characteristic("recursive call"), AbsoluteVirtualAddress(target)
|
||||
|
||||
|
||||
# this is a feature that's most relevant at the function or basic block scope,
|
||||
# however, its most efficient to extract at the instruction scope.
|
||||
def extract_function_indirect_call_characteristic_features(f, bb, insn):
|
||||
def extract_function_indirect_call_characteristic_features(f, bb, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
extract indirect function call characteristic (e.g., call eax or call dword ptr [edx+4])
|
||||
does not include calls like => call ds:dword_ABD4974
|
||||
"""
|
||||
insn: envi.Opcode = ih.inner
|
||||
|
||||
if insn.mnem != "call":
|
||||
return
|
||||
|
||||
# Checks below work for x86 and x64
|
||||
if isinstance(insn.opers[0], envi.archs.i386.disasm.i386RegOper):
|
||||
# call edx
|
||||
yield Characteristic("indirect call"), insn.va
|
||||
yield Characteristic("indirect call"), ih.address
|
||||
elif isinstance(insn.opers[0], envi.archs.i386.disasm.i386RegMemOper):
|
||||
# call dword ptr [eax+50h]
|
||||
yield Characteristic("indirect call"), insn.va
|
||||
yield Characteristic("indirect call"), ih.address
|
||||
elif isinstance(insn.opers[0], envi.archs.i386.disasm.i386SibOper):
|
||||
# call qword ptr [rsp+78h]
|
||||
yield Characteristic("indirect call"), insn.va
|
||||
yield Characteristic("indirect call"), ih.address
|
||||
|
||||
|
||||
def extract_features(f, bb, insn):
|
||||
def extract_op_number_features(
|
||||
fh: FunctionHandle, bb, ih: InsnHandle, i, oper: envi.Operand
|
||||
) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""parse number features from the given operand.
|
||||
|
||||
example:
|
||||
push 3136B0h ; dwControlCode
|
||||
"""
|
||||
insn: envi.Opcode = ih.inner
|
||||
f: viv_utils.Function = fh.inner
|
||||
|
||||
# this is for both x32 and x64
|
||||
if not isinstance(oper, (envi.archs.i386.disasm.i386ImmOper, envi.archs.i386.disasm.i386ImmMemOper)):
|
||||
return
|
||||
|
||||
if isinstance(oper, envi.archs.i386.disasm.i386ImmOper):
|
||||
v = oper.getOperValue(oper)
|
||||
else:
|
||||
v = oper.getOperAddr(oper)
|
||||
|
||||
if f.vw.probeMemory(v, 1, envi.memory.MM_READ):
|
||||
# this is a valid address
|
||||
# assume its not also a constant.
|
||||
return
|
||||
|
||||
if insn.mnem == "add" and insn.opers[0].isReg() and insn.opers[0].reg == envi.archs.i386.regs.REG_ESP:
|
||||
# skip things like:
|
||||
#
|
||||
# .text:00401140 call sub_407E2B
|
||||
# .text:00401145 add esp, 0Ch
|
||||
return
|
||||
|
||||
yield Number(v), ih.address
|
||||
yield OperandNumber(i, v), ih.address
|
||||
|
||||
if insn.mnem == "add" and 0 < v < MAX_STRUCTURE_SIZE and isinstance(oper, envi.archs.i386.disasm.i386ImmOper):
|
||||
# for pattern like:
|
||||
#
|
||||
# add eax, 0x10
|
||||
#
|
||||
# assume 0x10 is also an offset (imagine eax is a pointer).
|
||||
yield Offset(v), ih.address
|
||||
yield OperandOffset(i, v), ih.address
|
||||
|
||||
|
||||
def extract_op_offset_features(
|
||||
fh: FunctionHandle, bb, ih: InsnHandle, i, oper: envi.Operand
|
||||
) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""parse structure offset features from the given operand."""
|
||||
# example:
|
||||
#
|
||||
# .text:0040112F cmp [esi+4], ebx
|
||||
insn: envi.Opcode = ih.inner
|
||||
f: viv_utils.Function = fh.inner
|
||||
|
||||
# this is for both x32 and x64
|
||||
# like [esi + 4]
|
||||
# reg ^
|
||||
# disp
|
||||
if isinstance(oper, envi.archs.i386.disasm.i386RegMemOper):
|
||||
if oper.reg == envi.archs.i386.regs.REG_ESP:
|
||||
return
|
||||
|
||||
if oper.reg == envi.archs.i386.regs.REG_EBP:
|
||||
return
|
||||
|
||||
# TODO: do x64 support for real.
|
||||
if oper.reg == envi.archs.amd64.regs.REG_RBP:
|
||||
return
|
||||
|
||||
# viv already decodes offsets as signed
|
||||
v = oper.disp
|
||||
|
||||
yield Offset(v), ih.address
|
||||
yield OperandOffset(i, v), ih.address
|
||||
|
||||
if insn.mnem == "lea" and i == 1 and not f.vw.probeMemory(v, 1, envi.memory.MM_READ):
|
||||
# for pattern like:
|
||||
#
|
||||
# lea eax, [ebx + 1]
|
||||
#
|
||||
# assume 1 is also an offset (imagine ebx is a zero register).
|
||||
yield Number(v), ih.address
|
||||
yield OperandNumber(i, v), ih.address
|
||||
|
||||
# like: [esi + ecx + 16384]
|
||||
# reg ^ ^
|
||||
# index ^
|
||||
# disp
|
||||
elif isinstance(oper, envi.archs.i386.disasm.i386SibOper):
|
||||
# viv already decodes offsets as signed
|
||||
v = oper.disp
|
||||
|
||||
yield Offset(v), ih.address
|
||||
yield OperandOffset(i, v), ih.address
|
||||
|
||||
|
||||
def extract_op_string_features(
|
||||
fh: FunctionHandle, bb, ih: InsnHandle, i, oper: envi.Operand
|
||||
) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""parse string features from the given operand."""
|
||||
# example:
|
||||
#
|
||||
# push offset aAcr ; "ACR > "
|
||||
insn: envi.Opcode = ih.inner
|
||||
f: viv_utils.Function = fh.inner
|
||||
|
||||
if isinstance(oper, envi.archs.i386.disasm.i386ImmOper):
|
||||
v = oper.getOperValue(oper)
|
||||
elif isinstance(oper, envi.archs.i386.disasm.i386ImmMemOper):
|
||||
# like 0x10056CB4 in `lea eax, dword [0x10056CB4]`
|
||||
v = oper.imm
|
||||
elif isinstance(oper, envi.archs.i386.disasm.i386SibOper):
|
||||
# like 0x401000 in `mov eax, 0x401000[2 * ebx]`
|
||||
v = oper.imm
|
||||
elif isinstance(oper, envi.archs.amd64.disasm.Amd64RipRelOper):
|
||||
v = oper.getOperAddr(insn)
|
||||
else:
|
||||
return
|
||||
|
||||
for v in derefs(f.vw, v):
|
||||
try:
|
||||
s = read_string(f.vw, v)
|
||||
except ValueError:
|
||||
continue
|
||||
else:
|
||||
yield String(s.rstrip("\x00")), ih.address
|
||||
|
||||
|
||||
def extract_operand_features(f: FunctionHandle, bb, insn: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
|
||||
for i, oper in enumerate(insn.inner.opers):
|
||||
for op_handler in OPERAND_HANDLERS:
|
||||
for feature, addr in op_handler(f, bb, insn, i, oper):
|
||||
yield feature, addr
|
||||
|
||||
|
||||
OPERAND_HANDLERS: List[
|
||||
Callable[[FunctionHandle, BBHandle, InsnHandle, int, envi.Operand], Iterator[Tuple[Feature, Address]]]
|
||||
] = [
|
||||
extract_op_number_features,
|
||||
extract_op_offset_features,
|
||||
extract_op_string_features,
|
||||
]
|
||||
|
||||
|
||||
def extract_features(f, bb, insn) -> Iterator[Tuple[Feature, Address]]:
|
||||
"""
|
||||
extract features from the given insn.
|
||||
|
||||
@@ -611,24 +696,23 @@ def extract_features(f, bb, insn):
|
||||
insn (vivisect...Instruction): the instruction to process.
|
||||
|
||||
yields:
|
||||
Tuple[Feature, int]: the features and their location found in this insn.
|
||||
Tuple[Feature, Address]: the features and their location found in this insn.
|
||||
"""
|
||||
for insn_handler in INSTRUCTION_HANDLERS:
|
||||
for feature, va in insn_handler(f, bb, insn):
|
||||
yield feature, va
|
||||
for feature, addr in insn_handler(f, bb, insn):
|
||||
yield feature, addr
|
||||
|
||||
|
||||
INSTRUCTION_HANDLERS = (
|
||||
INSTRUCTION_HANDLERS: List[Callable[[FunctionHandle, BBHandle, InsnHandle], Iterator[Tuple[Feature, Address]]]] = [
|
||||
extract_insn_api_features,
|
||||
extract_insn_number_features,
|
||||
extract_insn_string_features,
|
||||
extract_insn_bytes_features,
|
||||
extract_insn_offset_features,
|
||||
extract_insn_nzxor_characteristic_features,
|
||||
extract_insn_mnemonic_features,
|
||||
extract_insn_obfs_call_plus_5_characteristic_features,
|
||||
extract_insn_peb_access_characteristic_features,
|
||||
extract_insn_cross_section_cflow,
|
||||
extract_insn_segment_access_features,
|
||||
extract_function_calls_from,
|
||||
extract_function_indirect_call_characteristic_features,
|
||||
)
|
||||
extract_operand_features,
|
||||
]
|
||||
|
||||
@@ -1,279 +0,0 @@
|
||||
"""
|
||||
capa freeze file format: `| capa0000 | + zlib(utf-8(json(...)))`
|
||||
|
||||
json format:
|
||||
|
||||
{
|
||||
'version': 1,
|
||||
'base address': int(base address),
|
||||
'functions': {
|
||||
int(function va): {
|
||||
int(basic block va): [int(instruction va), ...]
|
||||
...
|
||||
},
|
||||
...
|
||||
},
|
||||
'scopes': {
|
||||
'global': [
|
||||
(str(name), [any(arg), ...], int(va), ()),
|
||||
...
|
||||
},
|
||||
'file': [
|
||||
(str(name), [any(arg), ...], int(va), ()),
|
||||
...
|
||||
},
|
||||
'function': [
|
||||
(str(name), [any(arg), ...], int(va), (int(function va), )),
|
||||
...
|
||||
],
|
||||
'basic block': [
|
||||
(str(name), [any(arg), ...], int(va), (int(function va),
|
||||
int(basic block va))),
|
||||
...
|
||||
],
|
||||
'instruction': [
|
||||
(str(name), [any(arg), ...], int(va), (int(function va),
|
||||
int(basic block va),
|
||||
int(instruction va))),
|
||||
...
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and limitations under the License.
|
||||
"""
|
||||
import json
|
||||
import zlib
|
||||
import logging
|
||||
|
||||
import capa.features.file
|
||||
import capa.features.insn
|
||||
import capa.features.common
|
||||
import capa.features.basicblock
|
||||
import capa.features.extractors.base_extractor
|
||||
from capa.helpers import hex
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def serialize_feature(feature):
|
||||
return feature.freeze_serialize()
|
||||
|
||||
|
||||
KNOWN_FEATURES = {F.__name__: F for F in capa.features.common.Feature.__subclasses__()}
|
||||
|
||||
|
||||
def deserialize_feature(doc):
|
||||
F = KNOWN_FEATURES[doc[0]]
|
||||
return F.freeze_deserialize(doc[1])
|
||||
|
||||
|
||||
def dumps(extractor):
|
||||
"""
|
||||
serialize the given extractor to a string
|
||||
|
||||
args:
|
||||
extractor: capa.features.extractors.base_extractor.FeatureExtractor:
|
||||
|
||||
returns:
|
||||
str: the serialized features.
|
||||
"""
|
||||
ret = {
|
||||
"version": 1,
|
||||
"base address": extractor.get_base_address(),
|
||||
"functions": {},
|
||||
"scopes": {
|
||||
"global": [],
|
||||
"file": [],
|
||||
"function": [],
|
||||
"basic block": [],
|
||||
"instruction": [],
|
||||
},
|
||||
}
|
||||
for feature, va in extractor.extract_global_features():
|
||||
ret["scopes"]["global"].append(serialize_feature(feature) + (hex(va), ()))
|
||||
|
||||
for feature, va in extractor.extract_file_features():
|
||||
ret["scopes"]["file"].append(serialize_feature(feature) + (hex(va), ()))
|
||||
|
||||
for f in extractor.get_functions():
|
||||
ret["functions"][hex(f)] = {}
|
||||
|
||||
for feature, va in extractor.extract_function_features(f):
|
||||
ret["scopes"]["function"].append(serialize_feature(feature) + (hex(va), (hex(f),)))
|
||||
|
||||
for bb in extractor.get_basic_blocks(f):
|
||||
ret["functions"][hex(f)][hex(bb)] = []
|
||||
|
||||
for feature, va in extractor.extract_basic_block_features(f, bb):
|
||||
ret["scopes"]["basic block"].append(
|
||||
serialize_feature(feature)
|
||||
+ (
|
||||
hex(va),
|
||||
(
|
||||
hex(f),
|
||||
hex(bb),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
for insnva, insn in sorted(
|
||||
[(int(insn), insn) for insn in extractor.get_instructions(f, bb)], key=lambda p: p[0]
|
||||
):
|
||||
ret["functions"][hex(f)][hex(bb)].append(hex(insnva))
|
||||
|
||||
for feature, va in extractor.extract_insn_features(f, bb, insn):
|
||||
ret["scopes"]["instruction"].append(
|
||||
serialize_feature(feature)
|
||||
+ (
|
||||
hex(va),
|
||||
(
|
||||
hex(f),
|
||||
hex(bb),
|
||||
hex(insnva),
|
||||
),
|
||||
)
|
||||
)
|
||||
return json.dumps(ret)
|
||||
|
||||
|
||||
def loads(s):
|
||||
"""deserialize a set of features (as a NullFeatureExtractor) from a string."""
|
||||
doc = json.loads(s)
|
||||
|
||||
if doc.get("version") != 1:
|
||||
raise ValueError("unsupported freeze format version: %d" % (doc.get("version")))
|
||||
|
||||
features = {
|
||||
"base address": doc.get("base address"),
|
||||
"global features": [],
|
||||
"file features": [],
|
||||
"functions": {},
|
||||
}
|
||||
|
||||
for fva, function in doc.get("functions", {}).items():
|
||||
fva = int(fva, 0x10)
|
||||
features["functions"][fva] = {
|
||||
"features": [],
|
||||
"basic blocks": {},
|
||||
}
|
||||
|
||||
for bbva, bb in function.items():
|
||||
bbva = int(bbva, 0x10)
|
||||
features["functions"][fva]["basic blocks"][bbva] = {
|
||||
"features": [],
|
||||
"instructions": {},
|
||||
}
|
||||
|
||||
for insnva in bb:
|
||||
insnva = int(insnva, 0x10)
|
||||
features["functions"][fva]["basic blocks"][bbva]["instructions"][insnva] = {
|
||||
"features": [],
|
||||
}
|
||||
|
||||
# in the following blocks, each entry looks like:
|
||||
#
|
||||
# ('MatchedRule', ('foo', ), '0x401000', ('0x401000', ))
|
||||
# ^^^^^^^^^^^^^ ^^^^^^^^^ ^^^^^^^^^^ ^^^^^^^^^^^^^^
|
||||
# feature name args addr func/bb/insn
|
||||
for feature in doc.get("scopes", {}).get("global", []):
|
||||
va, loc = feature[2:]
|
||||
va = int(va, 0x10)
|
||||
feature = deserialize_feature(feature[:2])
|
||||
features["global features"].append((va, feature))
|
||||
|
||||
for feature in doc.get("scopes", {}).get("file", []):
|
||||
va, loc = feature[2:]
|
||||
va = int(va, 0x10)
|
||||
feature = deserialize_feature(feature[:2])
|
||||
features["file features"].append((va, feature))
|
||||
|
||||
for feature in doc.get("scopes", {}).get("function", []):
|
||||
# fetch the pair like:
|
||||
#
|
||||
# ('0x401000', ('0x401000', ))
|
||||
# ^^^^^^^^^^ ^^^^^^^^^^^^^^
|
||||
# addr func/bb/insn
|
||||
va, loc = feature[2:]
|
||||
va = int(va, 0x10)
|
||||
loc = [int(lo, 0x10) for lo in loc]
|
||||
|
||||
# decode the feature from the pair like:
|
||||
#
|
||||
# ('MatchedRule', ('foo', ))
|
||||
# ^^^^^^^^^^^^^ ^^^^^^^^^
|
||||
# feature name args
|
||||
feature = deserialize_feature(feature[:2])
|
||||
features["functions"][loc[0]]["features"].append((va, feature))
|
||||
|
||||
for feature in doc.get("scopes", {}).get("basic block", []):
|
||||
va, loc = feature[2:]
|
||||
va = int(va, 0x10)
|
||||
loc = [int(lo, 0x10) for lo in loc]
|
||||
feature = deserialize_feature(feature[:2])
|
||||
features["functions"][loc[0]]["basic blocks"][loc[1]]["features"].append((va, feature))
|
||||
|
||||
for feature in doc.get("scopes", {}).get("instruction", []):
|
||||
va, loc = feature[2:]
|
||||
va = int(va, 0x10)
|
||||
loc = [int(lo, 0x10) for lo in loc]
|
||||
feature = deserialize_feature(feature[:2])
|
||||
features["functions"][loc[0]]["basic blocks"][loc[1]]["instructions"][loc[2]]["features"].append((va, feature))
|
||||
|
||||
return capa.features.extractors.base_extractor.NullFeatureExtractor(features)
|
||||
|
||||
|
||||
MAGIC = "capa0000".encode("ascii")
|
||||
|
||||
|
||||
def dump(extractor):
|
||||
"""serialize the given extractor to a byte array."""
|
||||
return MAGIC + zlib.compress(dumps(extractor).encode("utf-8"))
|
||||
|
||||
|
||||
def is_freeze(buf: bytes) -> bool:
|
||||
return buf[: len(MAGIC)] == MAGIC
|
||||
|
||||
|
||||
def load(buf):
|
||||
"""deserialize a set of features (as a NullFeatureExtractor) from a byte array."""
|
||||
if not is_freeze(buf):
|
||||
raise ValueError("missing magic header")
|
||||
return loads(zlib.decompress(buf[len(MAGIC) :]).decode("utf-8"))
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
import sys
|
||||
import argparse
|
||||
|
||||
import capa.main
|
||||
|
||||
if argv is None:
|
||||
argv = sys.argv[1:]
|
||||
|
||||
parser = argparse.ArgumentParser(description="save capa features to a file")
|
||||
capa.main.install_common_args(parser, {"sample", "format", "backend", "signatures"})
|
||||
parser.add_argument("output", type=str, help="Path to output file")
|
||||
args = parser.parse_args(args=argv)
|
||||
capa.main.handle_common_args(args)
|
||||
|
||||
sigpaths = capa.main.get_signatures(args.signatures)
|
||||
|
||||
extractor = capa.main.get_extractor(args.sample, args.format, args.backend, sigpaths, False)
|
||||
|
||||
with open(args.output, "wb") as f:
|
||||
f.write(dump(extractor))
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
sys.exit(main())
|
||||
393
capa/features/freeze/__init__.py
Normal file
393
capa/features/freeze/__init__.py
Normal file
@@ -0,0 +1,393 @@
|
||||
"""
|
||||
capa freeze file format: `| capa0000 | + zlib(utf-8(json(...)))`
|
||||
|
||||
Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and limitations under the License.
|
||||
"""
|
||||
import zlib
|
||||
import logging
|
||||
from enum import Enum
|
||||
from typing import Any, List, Tuple
|
||||
|
||||
import dncil.clr.token
|
||||
from pydantic import Field, BaseModel
|
||||
|
||||
import capa.helpers
|
||||
import capa.version
|
||||
import capa.features.file
|
||||
import capa.features.insn
|
||||
import capa.features.common
|
||||
import capa.features.address
|
||||
import capa.features.basicblock
|
||||
import capa.features.extractors.base_extractor
|
||||
from capa.helpers import assert_never
|
||||
from capa.features.freeze.features import Feature, feature_from_capa
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HashableModel(BaseModel):
|
||||
class Config:
|
||||
frozen = True
|
||||
|
||||
|
||||
class AddressType(str, Enum):
|
||||
ABSOLUTE = "absolute"
|
||||
RELATIVE = "relative"
|
||||
FILE = "file"
|
||||
DN_TOKEN = "dn token"
|
||||
DN_TOKEN_OFFSET = "dn token offset"
|
||||
NO_ADDRESS = "no address"
|
||||
|
||||
|
||||
class Address(HashableModel):
|
||||
type: AddressType
|
||||
value: Any
|
||||
|
||||
@classmethod
|
||||
def from_capa(cls, a: capa.features.address.Address) -> "Address":
|
||||
if isinstance(a, capa.features.address.AbsoluteVirtualAddress):
|
||||
return cls(type=AddressType.ABSOLUTE, value=int(a))
|
||||
|
||||
elif isinstance(a, capa.features.address.RelativeVirtualAddress):
|
||||
return cls(type=AddressType.RELATIVE, value=int(a))
|
||||
|
||||
elif isinstance(a, capa.features.address.FileOffsetAddress):
|
||||
return cls(type=AddressType.FILE, value=int(a))
|
||||
|
||||
elif isinstance(a, capa.features.address.DNTokenAddress):
|
||||
return cls(type=AddressType.DN_TOKEN, value=a.token.value)
|
||||
|
||||
elif isinstance(a, capa.features.address.DNTokenOffsetAddress):
|
||||
return cls(type=AddressType.DN_TOKEN_OFFSET, value=(a.token.value, a.offset))
|
||||
|
||||
elif a == capa.features.address.NO_ADDRESS or isinstance(a, capa.features.address._NoAddress):
|
||||
return cls(type=AddressType.NO_ADDRESS, value=None)
|
||||
|
||||
elif isinstance(a, capa.features.address.Address) and not issubclass(type(a), capa.features.address.Address):
|
||||
raise ValueError("don't use an Address instance directly")
|
||||
|
||||
elif isinstance(a, capa.features.address.Address):
|
||||
raise ValueError("don't use an Address instance directly")
|
||||
|
||||
else:
|
||||
assert_never(a)
|
||||
|
||||
def to_capa(self) -> capa.features.address.Address:
|
||||
if self.type is AddressType.ABSOLUTE:
|
||||
return capa.features.address.AbsoluteVirtualAddress(self.value)
|
||||
|
||||
elif self.type is AddressType.RELATIVE:
|
||||
return capa.features.address.RelativeVirtualAddress(self.value)
|
||||
|
||||
elif self.type is AddressType.FILE:
|
||||
return capa.features.address.FileOffsetAddress(self.value)
|
||||
|
||||
elif self.type is AddressType.DN_TOKEN:
|
||||
return capa.features.address.DNTokenAddress(dncil.clr.token.Token(self.value))
|
||||
|
||||
elif self.type is AddressType.DN_TOKEN_OFFSET:
|
||||
token, offset = self.value
|
||||
return capa.features.address.DNTokenOffsetAddress(dncil.clr.token.Token(token), offset)
|
||||
|
||||
elif self.type is AddressType.NO_ADDRESS:
|
||||
return capa.features.address.NO_ADDRESS
|
||||
|
||||
else:
|
||||
assert_never(self.type)
|
||||
|
||||
def __lt__(self, other: "Address") -> bool:
|
||||
if self.type != other.type:
|
||||
return self.type < other.type
|
||||
|
||||
if self.type is AddressType.NO_ADDRESS:
|
||||
return True
|
||||
|
||||
else:
|
||||
return self.value < other.value
|
||||
|
||||
|
||||
class GlobalFeature(HashableModel):
|
||||
feature: Feature
|
||||
|
||||
|
||||
class FileFeature(HashableModel):
|
||||
address: Address
|
||||
feature: Feature
|
||||
|
||||
|
||||
class FunctionFeature(HashableModel):
|
||||
"""
|
||||
args:
|
||||
function: the address of the function to which this feature belongs.
|
||||
address: the address at which this feature is found.
|
||||
|
||||
function != address because, e.g., the feature may be found *within* the scope (function).
|
||||
versus right at its starting address.
|
||||
"""
|
||||
|
||||
function: Address
|
||||
address: Address
|
||||
feature: Feature
|
||||
|
||||
|
||||
class BasicBlockFeature(HashableModel):
|
||||
"""
|
||||
args:
|
||||
basic_block: the address of the basic block to which this feature belongs.
|
||||
address: the address at which this feature is found.
|
||||
|
||||
basic_block != address because, e.g., the feature may be found *within* the scope (basic block).
|
||||
versus right at its starting address.
|
||||
"""
|
||||
|
||||
basic_block: Address
|
||||
address: Address
|
||||
feature: Feature
|
||||
|
||||
|
||||
class InstructionFeature(HashableModel):
|
||||
"""
|
||||
args:
|
||||
instruction: the address of the instruction to which this feature belongs.
|
||||
address: the address at which this feature is found.
|
||||
|
||||
instruction != address because, e.g., the feature may be found *within* the scope (basic block),
|
||||
versus right at its starting address.
|
||||
"""
|
||||
|
||||
instruction: Address
|
||||
address: Address
|
||||
feature: Feature
|
||||
|
||||
|
||||
class InstructionFeatures(BaseModel):
|
||||
address: Address
|
||||
features: Tuple[InstructionFeature, ...]
|
||||
|
||||
|
||||
class BasicBlockFeatures(BaseModel):
|
||||
address: Address
|
||||
features: Tuple[BasicBlockFeature, ...]
|
||||
instructions: Tuple[InstructionFeatures, ...]
|
||||
|
||||
|
||||
class FunctionFeatures(BaseModel):
|
||||
address: Address
|
||||
features: Tuple[FunctionFeature, ...]
|
||||
basic_blocks: Tuple[BasicBlockFeatures, ...] = Field(alias="basic block")
|
||||
|
||||
class Config:
|
||||
allow_population_by_field_name = True
|
||||
|
||||
|
||||
class Features(BaseModel):
|
||||
global_: Tuple[GlobalFeature, ...] = Field(alias="global")
|
||||
file: Tuple[FileFeature, ...]
|
||||
functions: Tuple[FunctionFeatures, ...]
|
||||
|
||||
class Config:
|
||||
allow_population_by_field_name = True
|
||||
|
||||
|
||||
class Extractor(BaseModel):
|
||||
name: str
|
||||
version: str = capa.version.__version__
|
||||
|
||||
class Config:
|
||||
allow_population_by_field_name = True
|
||||
|
||||
|
||||
class Freeze(BaseModel):
|
||||
version: int = 2
|
||||
base_address: Address = Field(alias="base address")
|
||||
extractor: Extractor
|
||||
features: Features
|
||||
|
||||
class Config:
|
||||
allow_population_by_field_name = True
|
||||
|
||||
|
||||
def dumps(extractor: capa.features.extractors.base_extractor.FeatureExtractor) -> str:
|
||||
"""
|
||||
serialize the given extractor to a string
|
||||
"""
|
||||
|
||||
global_features: List[GlobalFeature] = []
|
||||
for feature, _ in extractor.extract_global_features():
|
||||
global_features.append(
|
||||
GlobalFeature(
|
||||
feature=feature_from_capa(feature),
|
||||
)
|
||||
)
|
||||
|
||||
file_features: List[FileFeature] = []
|
||||
for feature, address in extractor.extract_file_features():
|
||||
file_features.append(
|
||||
FileFeature(
|
||||
feature=feature_from_capa(feature),
|
||||
address=Address.from_capa(address),
|
||||
)
|
||||
)
|
||||
|
||||
function_features: List[FunctionFeatures] = []
|
||||
for f in extractor.get_functions():
|
||||
faddr = Address.from_capa(f.address)
|
||||
ffeatures = [
|
||||
FunctionFeature(
|
||||
function=faddr,
|
||||
address=Address.from_capa(addr),
|
||||
feature=feature_from_capa(feature),
|
||||
)
|
||||
for feature, addr in extractor.extract_function_features(f)
|
||||
]
|
||||
|
||||
basic_blocks = []
|
||||
for bb in extractor.get_basic_blocks(f):
|
||||
bbaddr = Address.from_capa(bb.address)
|
||||
bbfeatures = [
|
||||
BasicBlockFeature(
|
||||
basic_block=bbaddr,
|
||||
address=Address.from_capa(addr),
|
||||
feature=feature_from_capa(feature),
|
||||
)
|
||||
for feature, addr in extractor.extract_basic_block_features(f, bb)
|
||||
]
|
||||
|
||||
instructions = []
|
||||
for insn in extractor.get_instructions(f, bb):
|
||||
iaddr = Address.from_capa(insn.address)
|
||||
ifeatures = [
|
||||
InstructionFeature(
|
||||
instruction=iaddr,
|
||||
address=Address.from_capa(addr),
|
||||
feature=feature_from_capa(feature),
|
||||
)
|
||||
for feature, addr in extractor.extract_insn_features(f, bb, insn)
|
||||
]
|
||||
|
||||
instructions.append(
|
||||
InstructionFeatures(
|
||||
address=iaddr,
|
||||
features=ifeatures,
|
||||
)
|
||||
)
|
||||
|
||||
basic_blocks.append(
|
||||
BasicBlockFeatures(
|
||||
address=bbaddr,
|
||||
features=bbfeatures,
|
||||
instructions=instructions,
|
||||
)
|
||||
)
|
||||
|
||||
function_features.append(
|
||||
FunctionFeatures(
|
||||
address=faddr,
|
||||
features=ffeatures,
|
||||
basic_blocks=basic_blocks,
|
||||
)
|
||||
)
|
||||
|
||||
features = Features(
|
||||
global_=global_features,
|
||||
file=file_features,
|
||||
functions=function_features,
|
||||
)
|
||||
|
||||
freeze = Freeze(
|
||||
version=2,
|
||||
base_address=Address.from_capa(extractor.get_base_address()),
|
||||
extractor=Extractor(name=extractor.__class__.__name__),
|
||||
features=features,
|
||||
)
|
||||
|
||||
return freeze.json()
|
||||
|
||||
|
||||
def loads(s: str) -> capa.features.extractors.base_extractor.FeatureExtractor:
|
||||
"""deserialize a set of features (as a NullFeatureExtractor) from a string."""
|
||||
import capa.features.extractors.null as null
|
||||
|
||||
freeze = Freeze.parse_raw(s)
|
||||
if freeze.version != 2:
|
||||
raise ValueError("unsupported freeze format version: %d", freeze.version)
|
||||
|
||||
return null.NullFeatureExtractor(
|
||||
base_address=freeze.base_address.to_capa(),
|
||||
global_features=[f.feature.to_capa() for f in freeze.features.global_],
|
||||
file_features=[(f.address.to_capa(), f.feature.to_capa()) for f in freeze.features.file],
|
||||
functions={
|
||||
f.address.to_capa(): null.FunctionFeatures(
|
||||
features=[(fe.address.to_capa(), fe.feature.to_capa()) for fe in f.features],
|
||||
basic_blocks={
|
||||
bb.address.to_capa(): null.BasicBlockFeatures(
|
||||
features=[(fe.address.to_capa(), fe.feature.to_capa()) for fe in bb.features],
|
||||
instructions={
|
||||
i.address.to_capa(): null.InstructionFeatures(
|
||||
features=[(fe.address.to_capa(), fe.feature.to_capa()) for fe in i.features]
|
||||
)
|
||||
for i in bb.instructions
|
||||
},
|
||||
)
|
||||
for bb in f.basic_blocks
|
||||
},
|
||||
)
|
||||
for f in freeze.features.functions
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
MAGIC = "capa0000".encode("ascii")
|
||||
|
||||
|
||||
def dump(extractor: capa.features.extractors.base_extractor.FeatureExtractor) -> bytes:
|
||||
"""serialize the given extractor to a byte array."""
|
||||
return MAGIC + zlib.compress(dumps(extractor).encode("utf-8"))
|
||||
|
||||
|
||||
def is_freeze(buf: bytes) -> bool:
|
||||
return buf[: len(MAGIC)] == MAGIC
|
||||
|
||||
|
||||
def load(buf: bytes) -> capa.features.extractors.base_extractor.FeatureExtractor:
|
||||
"""deserialize a set of features (as a NullFeatureExtractor) from a byte array."""
|
||||
if not is_freeze(buf):
|
||||
raise ValueError("missing magic header")
|
||||
return loads(zlib.decompress(buf[len(MAGIC) :]).decode("utf-8"))
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
import sys
|
||||
import argparse
|
||||
|
||||
import capa.main
|
||||
|
||||
if argv is None:
|
||||
argv = sys.argv[1:]
|
||||
|
||||
parser = argparse.ArgumentParser(description="save capa features to a file")
|
||||
capa.main.install_common_args(parser, {"sample", "format", "backend", "signatures"})
|
||||
parser.add_argument("output", type=str, help="Path to output file")
|
||||
args = parser.parse_args(args=argv)
|
||||
capa.main.handle_common_args(args)
|
||||
|
||||
sigpaths = capa.main.get_signatures(args.signatures)
|
||||
|
||||
extractor = capa.main.get_extractor(args.sample, args.format, args.backend, sigpaths, False)
|
||||
|
||||
with open(args.output, "wb") as f:
|
||||
f.write(dump(extractor))
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
sys.exit(main())
|
||||
332
capa/features/freeze/features.py
Normal file
332
capa/features/freeze/features.py
Normal file
@@ -0,0 +1,332 @@
|
||||
import binascii
|
||||
from typing import Union, Optional
|
||||
|
||||
from pydantic import Field, BaseModel
|
||||
|
||||
import capa.features.file
|
||||
import capa.features.insn
|
||||
import capa.features.common
|
||||
import capa.features.basicblock
|
||||
|
||||
|
||||
class FeatureModel(BaseModel):
|
||||
class Config:
|
||||
frozen = True
|
||||
allow_population_by_field_name = True
|
||||
|
||||
def to_capa(self) -> capa.features.common.Feature:
|
||||
if isinstance(self, OSFeature):
|
||||
return capa.features.common.OS(self.os, description=self.description)
|
||||
|
||||
elif isinstance(self, ArchFeature):
|
||||
return capa.features.common.Arch(self.arch, description=self.description)
|
||||
|
||||
elif isinstance(self, FormatFeature):
|
||||
return capa.features.common.Format(self.format, description=self.description)
|
||||
|
||||
elif isinstance(self, MatchFeature):
|
||||
return capa.features.common.MatchedRule(self.match, description=self.description)
|
||||
|
||||
elif isinstance(
|
||||
self,
|
||||
CharacteristicFeature,
|
||||
):
|
||||
return capa.features.common.Characteristic(self.characteristic, description=self.description)
|
||||
|
||||
elif isinstance(self, ExportFeature):
|
||||
return capa.features.file.Export(self.export, description=self.description)
|
||||
|
||||
elif isinstance(self, ImportFeature):
|
||||
return capa.features.file.Import(self.import_, description=self.description)
|
||||
|
||||
elif isinstance(self, SectionFeature):
|
||||
return capa.features.file.Section(self.section, description=self.description)
|
||||
|
||||
elif isinstance(self, FunctionNameFeature):
|
||||
return capa.features.file.FunctionName(self.function_name, description=self.description)
|
||||
|
||||
elif isinstance(self, SubstringFeature):
|
||||
return capa.features.common.Substring(self.substring, description=self.description)
|
||||
|
||||
elif isinstance(self, RegexFeature):
|
||||
return capa.features.common.Regex(self.regex, description=self.description)
|
||||
|
||||
elif isinstance(self, StringFeature):
|
||||
return capa.features.common.String(self.string, description=self.description)
|
||||
|
||||
elif isinstance(self, ClassFeature):
|
||||
return capa.features.common.Class(self.class_, description=self.description)
|
||||
|
||||
elif isinstance(self, NamespaceFeature):
|
||||
return capa.features.common.Namespace(self.namespace, description=self.description)
|
||||
|
||||
elif isinstance(self, BasicBlockFeature):
|
||||
return capa.features.basicblock.BasicBlock(description=self.description)
|
||||
|
||||
elif isinstance(self, APIFeature):
|
||||
return capa.features.insn.API(self.api, description=self.description)
|
||||
|
||||
elif isinstance(self, NumberFeature):
|
||||
return capa.features.insn.Number(self.number, description=self.description)
|
||||
|
||||
elif isinstance(self, BytesFeature):
|
||||
return capa.features.common.Bytes(binascii.unhexlify(self.bytes), description=self.description)
|
||||
|
||||
elif isinstance(self, OffsetFeature):
|
||||
return capa.features.insn.Offset(self.offset, description=self.description)
|
||||
|
||||
elif isinstance(self, MnemonicFeature):
|
||||
return capa.features.insn.Mnemonic(self.mnemonic, description=self.description)
|
||||
|
||||
elif isinstance(self, OperandNumberFeature):
|
||||
return capa.features.insn.OperandNumber(
|
||||
self.index,
|
||||
self.operand_number,
|
||||
description=self.description,
|
||||
)
|
||||
|
||||
elif isinstance(self, OperandOffsetFeature):
|
||||
return capa.features.insn.OperandOffset(
|
||||
self.index,
|
||||
self.operand_offset,
|
||||
description=self.description,
|
||||
)
|
||||
|
||||
else:
|
||||
raise NotImplementedError(f"Feature.to_capa({type(self)}) not implemented")
|
||||
|
||||
|
||||
def feature_from_capa(f: capa.features.common.Feature) -> "Feature":
|
||||
if isinstance(f, capa.features.common.OS):
|
||||
return OSFeature(os=f.value, description=f.description)
|
||||
|
||||
elif isinstance(f, capa.features.common.Arch):
|
||||
return ArchFeature(arch=f.value, description=f.description)
|
||||
|
||||
elif isinstance(f, capa.features.common.Format):
|
||||
return FormatFeature(format=f.value, description=f.description)
|
||||
|
||||
elif isinstance(f, capa.features.common.MatchedRule):
|
||||
return MatchFeature(match=f.value, description=f.description)
|
||||
|
||||
elif isinstance(f, capa.features.common.Characteristic):
|
||||
return CharacteristicFeature(characteristic=f.value, description=f.description)
|
||||
|
||||
elif isinstance(f, capa.features.file.Export):
|
||||
return ExportFeature(export=f.value, description=f.description)
|
||||
|
||||
elif isinstance(f, capa.features.file.Import):
|
||||
return ImportFeature(import_=f.value, description=f.description)
|
||||
|
||||
elif isinstance(f, capa.features.file.Section):
|
||||
return SectionFeature(section=f.value, description=f.description)
|
||||
|
||||
elif isinstance(f, capa.features.file.FunctionName):
|
||||
return FunctionNameFeature(function_name=f.value, description=f.description)
|
||||
|
||||
# must come before check for String due to inheritance
|
||||
elif isinstance(f, capa.features.common.Substring):
|
||||
return SubstringFeature(substring=f.value, description=f.description)
|
||||
|
||||
# must come before check for String due to inheritance
|
||||
elif isinstance(f, capa.features.common.Regex):
|
||||
return RegexFeature(regex=f.value, description=f.description)
|
||||
|
||||
elif isinstance(f, capa.features.common.String):
|
||||
return StringFeature(string=f.value, description=f.description)
|
||||
|
||||
elif isinstance(f, capa.features.common.Class):
|
||||
return ClassFeature(class_=f.value, description=f.description)
|
||||
|
||||
elif isinstance(f, capa.features.common.Namespace):
|
||||
return NamespaceFeature(namespace=f.value, description=f.description)
|
||||
|
||||
elif isinstance(f, capa.features.basicblock.BasicBlock):
|
||||
return BasicBlockFeature(description=f.description)
|
||||
|
||||
elif isinstance(f, capa.features.insn.API):
|
||||
return APIFeature(api=f.value, description=f.description)
|
||||
|
||||
elif isinstance(f, capa.features.insn.Number):
|
||||
return NumberFeature(number=f.value, description=f.description)
|
||||
|
||||
elif isinstance(f, capa.features.common.Bytes):
|
||||
buf = f.value
|
||||
assert isinstance(buf, bytes)
|
||||
return BytesFeature(bytes=binascii.hexlify(buf).decode("ascii"), description=f.description)
|
||||
|
||||
elif isinstance(f, capa.features.insn.Offset):
|
||||
return OffsetFeature(offset=f.value, description=f.description)
|
||||
|
||||
elif isinstance(f, capa.features.insn.Mnemonic):
|
||||
return MnemonicFeature(mnemonic=f.value, description=f.description)
|
||||
|
||||
elif isinstance(f, capa.features.insn.OperandNumber):
|
||||
return OperandNumberFeature(index=f.index, operand_number=f.value, description=f.description)
|
||||
|
||||
elif isinstance(f, capa.features.insn.OperandOffset):
|
||||
return OperandOffsetFeature(index=f.index, operand_offset=f.value, description=f.description)
|
||||
|
||||
else:
|
||||
raise NotImplementedError(f"feature_from_capa({type(f)}) not implemented")
|
||||
|
||||
|
||||
class OSFeature(FeatureModel):
|
||||
type: str = "os"
|
||||
os: str
|
||||
description: Optional[str]
|
||||
|
||||
|
||||
class ArchFeature(FeatureModel):
|
||||
type: str = "arch"
|
||||
arch: str
|
||||
description: Optional[str]
|
||||
|
||||
|
||||
class FormatFeature(FeatureModel):
|
||||
type: str = "format"
|
||||
format: str
|
||||
description: Optional[str]
|
||||
|
||||
|
||||
class MatchFeature(FeatureModel):
|
||||
type: str = "match"
|
||||
match: str
|
||||
description: Optional[str]
|
||||
|
||||
|
||||
class CharacteristicFeature(FeatureModel):
|
||||
type: str = "characteristic"
|
||||
characteristic: str
|
||||
description: Optional[str]
|
||||
|
||||
|
||||
class ExportFeature(FeatureModel):
|
||||
type: str = "export"
|
||||
export: str
|
||||
description: Optional[str]
|
||||
|
||||
|
||||
class ImportFeature(FeatureModel):
|
||||
type: str = "import"
|
||||
import_: str = Field(alias="import")
|
||||
description: Optional[str]
|
||||
|
||||
|
||||
class SectionFeature(FeatureModel):
|
||||
type: str = "section"
|
||||
section: str
|
||||
description: Optional[str]
|
||||
|
||||
|
||||
class FunctionNameFeature(FeatureModel):
|
||||
type: str = "function name"
|
||||
function_name: str = Field(alias="function name")
|
||||
description: Optional[str]
|
||||
|
||||
|
||||
class SubstringFeature(FeatureModel):
|
||||
type: str = "substring"
|
||||
substring: str
|
||||
description: Optional[str]
|
||||
|
||||
|
||||
class RegexFeature(FeatureModel):
|
||||
type: str = "regex"
|
||||
regex: str
|
||||
description: Optional[str]
|
||||
|
||||
|
||||
class StringFeature(FeatureModel):
|
||||
type: str = "string"
|
||||
string: str
|
||||
description: Optional[str]
|
||||
|
||||
|
||||
class ClassFeature(FeatureModel):
|
||||
type: str = "class"
|
||||
class_: str = Field(alias="class")
|
||||
description: Optional[str]
|
||||
|
||||
|
||||
class NamespaceFeature(FeatureModel):
|
||||
type: str = "namespace"
|
||||
namespace: str
|
||||
description: Optional[str]
|
||||
|
||||
|
||||
class BasicBlockFeature(FeatureModel):
|
||||
type: str = "basic block"
|
||||
description: Optional[str]
|
||||
|
||||
|
||||
class APIFeature(FeatureModel):
|
||||
type: str = "api"
|
||||
api: str
|
||||
description: Optional[str]
|
||||
|
||||
|
||||
class NumberFeature(FeatureModel):
|
||||
type: str = "number"
|
||||
number: Union[int, float]
|
||||
description: Optional[str]
|
||||
|
||||
|
||||
class BytesFeature(FeatureModel):
|
||||
type: str = "bytes"
|
||||
bytes: str
|
||||
description: Optional[str]
|
||||
|
||||
|
||||
class OffsetFeature(FeatureModel):
|
||||
type: str = "offset"
|
||||
offset: int
|
||||
description: Optional[str]
|
||||
|
||||
|
||||
class MnemonicFeature(FeatureModel):
|
||||
type: str = "mnemonic"
|
||||
mnemonic: str
|
||||
description: Optional[str]
|
||||
|
||||
|
||||
class OperandNumberFeature(FeatureModel):
|
||||
type: str = "operand number"
|
||||
index: int
|
||||
operand_number: int = Field(alias="operand number")
|
||||
description: Optional[str]
|
||||
|
||||
|
||||
class OperandOffsetFeature(FeatureModel):
|
||||
type: str = "operand offset"
|
||||
index: int
|
||||
operand_offset: int = Field(alias="operand offset")
|
||||
description: Optional[str]
|
||||
|
||||
|
||||
Feature = Union[
|
||||
OSFeature,
|
||||
ArchFeature,
|
||||
FormatFeature,
|
||||
MatchFeature,
|
||||
CharacteristicFeature,
|
||||
ExportFeature,
|
||||
ImportFeature,
|
||||
SectionFeature,
|
||||
FunctionNameFeature,
|
||||
SubstringFeature,
|
||||
RegexFeature,
|
||||
StringFeature,
|
||||
ClassFeature,
|
||||
NamespaceFeature,
|
||||
APIFeature,
|
||||
NumberFeature,
|
||||
BytesFeature,
|
||||
OffsetFeature,
|
||||
MnemonicFeature,
|
||||
OperandNumberFeature,
|
||||
OperandOffsetFeature,
|
||||
# this has to go last because...? pydantic fails to serialize correctly otherwise.
|
||||
# possibly because this feature has no associated value?
|
||||
BasicBlockFeature,
|
||||
]
|
||||
@@ -5,37 +5,99 @@
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
import abc
|
||||
from typing import Union
|
||||
|
||||
import capa.render.utils
|
||||
from capa.features.common import Feature
|
||||
|
||||
|
||||
def hex(n: int) -> str:
|
||||
"""render the given number using upper case hex, like: 0x123ABC"""
|
||||
if n < 0:
|
||||
return "-0x%X" % (-n)
|
||||
else:
|
||||
return "0x%X" % n
|
||||
|
||||
|
||||
class API(Feature):
|
||||
def __init__(self, name: str, description=None):
|
||||
# Downcase library name if given
|
||||
if "." in name:
|
||||
modname, _, impname = name.rpartition(".")
|
||||
name = modname.lower() + "." + impname
|
||||
|
||||
super(API, self).__init__(name, description=description)
|
||||
|
||||
|
||||
class Number(Feature):
|
||||
def __init__(self, value: int, bitness=None, description=None):
|
||||
super(Number, self).__init__(value, bitness=bitness, description=description)
|
||||
def __init__(self, value: Union[int, float], description=None):
|
||||
super(Number, self).__init__(value, description=description)
|
||||
|
||||
def get_value_str(self):
|
||||
return capa.render.utils.hex(self.value)
|
||||
if isinstance(self.value, int):
|
||||
return hex(self.value)
|
||||
elif isinstance(self.value, float):
|
||||
return str(self.value)
|
||||
else:
|
||||
raise ValueError("invalid value type")
|
||||
|
||||
|
||||
# max recognized structure size (and therefore, offset size)
|
||||
MAX_STRUCTURE_SIZE = 0x10000
|
||||
|
||||
|
||||
class Offset(Feature):
|
||||
def __init__(self, value: int, bitness=None, description=None):
|
||||
super(Offset, self).__init__(value, bitness=bitness, description=description)
|
||||
def __init__(self, value: int, description=None):
|
||||
super(Offset, self).__init__(value, description=description)
|
||||
|
||||
def get_value_str(self):
|
||||
return capa.render.utils.hex(self.value)
|
||||
return hex(self.value)
|
||||
|
||||
|
||||
class Mnemonic(Feature):
|
||||
def __init__(self, value: str, description=None):
|
||||
super(Mnemonic, self).__init__(value, description=description)
|
||||
|
||||
|
||||
# max number of operands to consider for a given instrucion.
|
||||
# since we only support Intel and .NET, we can assume this is 3
|
||||
# which covers cases up to e.g. "vinserti128 ymm0,ymm0,ymm5,1"
|
||||
MAX_OPERAND_COUNT = 4
|
||||
MAX_OPERAND_INDEX = MAX_OPERAND_COUNT - 1
|
||||
|
||||
|
||||
class _Operand(Feature, abc.ABC):
|
||||
# superclass: don't use directly
|
||||
# subclasses should set self.name and provide the value string formatter
|
||||
def __init__(self, index: int, value: int, description=None):
|
||||
super(_Operand, self).__init__(value, description=description)
|
||||
self.index = index
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.name, self.value))
|
||||
|
||||
def __eq__(self, other):
|
||||
return super().__eq__(other) and self.index == other.index
|
||||
|
||||
|
||||
class OperandNumber(_Operand):
|
||||
# cached names so we don't do extra string formatting every ctor
|
||||
NAMES = ["operand[%d].number" % i for i in range(MAX_OPERAND_COUNT)]
|
||||
|
||||
# operand[i].number: 0x12
|
||||
def __init__(self, index: int, value: int, description=None):
|
||||
super(OperandNumber, self).__init__(index, value, description=description)
|
||||
self.name = self.NAMES[index]
|
||||
|
||||
def get_value_str(self) -> str:
|
||||
assert isinstance(self.value, int)
|
||||
return hex(self.value)
|
||||
|
||||
|
||||
class OperandOffset(_Operand):
|
||||
# cached names so we don't do extra string formatting every ctor
|
||||
NAMES = ["operand[%d].offset" % i for i in range(MAX_OPERAND_COUNT)]
|
||||
|
||||
# operand[i].offset: 0x12
|
||||
def __init__(self, index: int, value: int, description=None):
|
||||
super(OperandOffset, self).__init__(index, value, description=description)
|
||||
self.name = self.NAMES[index]
|
||||
|
||||
def get_value_str(self) -> str:
|
||||
assert isinstance(self.value, int)
|
||||
return hex(self.value)
|
||||
|
||||
@@ -5,10 +5,19 @@
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import os
|
||||
import logging
|
||||
from typing import NoReturn
|
||||
|
||||
from capa.exceptions import UnsupportedFormatError
|
||||
from capa.features.common import FORMAT_SC32, FORMAT_SC64, FORMAT_UNKNOWN
|
||||
|
||||
EXTENSIONS_SHELLCODE_32 = ("sc32", "raw32")
|
||||
EXTENSIONS_SHELLCODE_64 = ("sc64", "raw64")
|
||||
EXTENSIONS_ELF = "elf_"
|
||||
|
||||
logger = logging.getLogger("capa")
|
||||
|
||||
_hex = hex
|
||||
|
||||
|
||||
@@ -35,3 +44,75 @@ def is_runtime_ida():
|
||||
|
||||
def assert_never(value: NoReturn) -> NoReturn:
|
||||
assert False, f"Unhandled value: {value} ({type(value).__name__})"
|
||||
|
||||
|
||||
def get_format_from_extension(sample: str) -> str:
|
||||
if sample.endswith(EXTENSIONS_SHELLCODE_32):
|
||||
return FORMAT_SC32
|
||||
elif sample.endswith(EXTENSIONS_SHELLCODE_64):
|
||||
return FORMAT_SC64
|
||||
return FORMAT_UNKNOWN
|
||||
|
||||
|
||||
def get_auto_format(path: str) -> str:
|
||||
format_ = get_format(path)
|
||||
if format_ == FORMAT_UNKNOWN:
|
||||
format_ = get_format_from_extension(path)
|
||||
if format_ == FORMAT_UNKNOWN:
|
||||
raise UnsupportedFormatError()
|
||||
return format_
|
||||
|
||||
|
||||
def get_format(sample: str) -> str:
|
||||
# imported locally to avoid import cycle
|
||||
from capa.features.extractors.common import extract_format
|
||||
|
||||
with open(sample, "rb") as f:
|
||||
buf = f.read()
|
||||
|
||||
for feature, _ in extract_format(buf):
|
||||
assert isinstance(feature.value, str)
|
||||
return feature.value
|
||||
|
||||
return FORMAT_UNKNOWN
|
||||
|
||||
|
||||
def log_unsupported_format_error():
|
||||
logger.error("-" * 80)
|
||||
logger.error(" Input file does not appear to be a PE or ELF file.")
|
||||
logger.error(" ")
|
||||
logger.error(
|
||||
" capa currently only supports analyzing PE and ELF files (or shellcode, when using --format sc32|sc64)."
|
||||
)
|
||||
logger.error(" If you don't know the input file type, you can try using the `file` utility to guess it.")
|
||||
logger.error("-" * 80)
|
||||
|
||||
|
||||
def log_unsupported_os_error():
|
||||
logger.error("-" * 80)
|
||||
logger.error(" Input file does not appear to target a supported OS.")
|
||||
logger.error(" ")
|
||||
logger.error(
|
||||
" capa currently only supports analyzing executables for some operating systems (including Windows and Linux)."
|
||||
)
|
||||
logger.error("-" * 80)
|
||||
|
||||
|
||||
def log_unsupported_arch_error():
|
||||
logger.error("-" * 80)
|
||||
logger.error(" Input file does not appear to target a supported architecture.")
|
||||
logger.error(" ")
|
||||
logger.error(" capa currently only supports analyzing x86 (32- and 64-bit).")
|
||||
logger.error("-" * 80)
|
||||
|
||||
|
||||
def log_unsupported_runtime_error():
|
||||
logger.error("-" * 80)
|
||||
logger.error(" Unsupported runtime or Python interpreter.")
|
||||
logger.error(" ")
|
||||
logger.error(" capa supports running under Python 3.7 and higher.")
|
||||
logger.error(" ")
|
||||
logger.error(
|
||||
" If you're seeing this message on the command line, please ensure you're running a supported Python version."
|
||||
)
|
||||
logger.error("-" * 80)
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import logging
|
||||
import datetime
|
||||
import contextlib
|
||||
|
||||
import idc
|
||||
import idaapi
|
||||
@@ -39,10 +40,10 @@ def inform_user_ida_ui(message):
|
||||
|
||||
def is_supported_ida_version():
|
||||
version = float(idaapi.get_kernel_version())
|
||||
if version < 7.4 or version >= 8:
|
||||
if version < 7.4 or version >= 9:
|
||||
warning_msg = "This plugin does not support your IDA Pro version"
|
||||
logger.warning(warning_msg)
|
||||
logger.warning("Your IDA Pro version is: %s. Supported versions are: IDA >= 7.4 and IDA < 8.0." % version)
|
||||
logger.warning("Your IDA Pro version is: %s. Supported versions are: IDA >= 7.4 and IDA < 9.0." % version)
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -107,14 +108,31 @@ def get_file_sha256():
|
||||
return sha256
|
||||
|
||||
|
||||
def collect_metadata():
|
||||
def collect_metadata(rules):
|
||||
""" """
|
||||
md5 = get_file_md5()
|
||||
sha256 = get_file_sha256()
|
||||
|
||||
info: idaapi.idainfo = idaapi.get_inf_structure()
|
||||
if info.procname == "metapc" and info.is_64bit():
|
||||
arch = "x86_64"
|
||||
elif info.procname == "metapc" and info.is_32bit():
|
||||
arch = "x86"
|
||||
else:
|
||||
arch = "unknown arch"
|
||||
|
||||
format_name: str = ida_loader.get_file_type_name()
|
||||
if "PE" in format_name:
|
||||
os = "windows"
|
||||
elif "ELF" in format_name:
|
||||
with contextlib.closing(capa.ida.helpers.IDAIO()) as f:
|
||||
os = capa.features.extractors.elf.detect_elf_os(f)
|
||||
else:
|
||||
os = "unknown os"
|
||||
|
||||
return {
|
||||
"timestamp": datetime.datetime.now().isoformat(),
|
||||
# "argv" is not relevant here
|
||||
"argv": [],
|
||||
"sample": {
|
||||
"md5": md5,
|
||||
"sha1": "", # not easily accessible
|
||||
@@ -123,7 +141,10 @@ def collect_metadata():
|
||||
},
|
||||
"analysis": {
|
||||
"format": idaapi.get_file_type_name(),
|
||||
"arch": arch,
|
||||
"os": os,
|
||||
"extractor": "ida",
|
||||
"rules": rules,
|
||||
"base_address": idaapi.get_imagebase(),
|
||||
"layout": {
|
||||
# this is updated after capabilities have been collected.
|
||||
@@ -131,6 +152,12 @@ def collect_metadata():
|
||||
#
|
||||
# "functions": { 0x401000: { "matched_basic_blocks": [ 0x401000, 0x401005, ... ] }, ... }
|
||||
},
|
||||
# ignore these for now - not used by IDA plugin.
|
||||
"feature_counts": {
|
||||
"file": {},
|
||||
"functions": {},
|
||||
},
|
||||
"library_functions": {},
|
||||
},
|
||||
"version": capa.version.__version__,
|
||||
}
|
||||
|
||||
@@ -28,25 +28,24 @@ to modify the rule text directly and the `Editor` pane to construct and rearrang
|
||||
|
||||

|
||||
|
||||
For more information on the FLARE team's open-source framework, capa, check out the overview in our first [blog](https://www.fireeye.com/blog/threat-research/2020/07/capa-automatically-identify-malware-capabilities.html).
|
||||
For more information on the FLARE team's open-source framework, capa, check out the overview in our first [blog](https://www.mandiant.com/resources/capa-automatically-identify-malware-capabilities).
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Requirements
|
||||
|
||||
capa explorer supports Python versions >= 3.6.x and the following IDA Pro versions:
|
||||
capa explorer supports Python versions >= 3.7.x and the following IDA Pro versions:
|
||||
|
||||
* IDA 7.4
|
||||
* IDA 7.5
|
||||
* IDA 7.6 (caveat below)
|
||||
* IDA 7.7
|
||||
|
||||
capa explorer is however limited to the Python versions supported by your IDA installation (which may not include all Python versions >= 3.6.x). Based on our testing the following matrix shows the Python versions supported
|
||||
capa explorer is however limited to the Python versions supported by your IDA installation (which may not include all Python versions >= 3.7.x). Based on our testing the following matrix shows the Python versions supported
|
||||
by each supported IDA version:
|
||||
|
||||
| | IDA 7.4 | IDA 7.5 | IDA 7.6 |
|
||||
| --- | --- | --- | --- |
|
||||
| Python 3.6.x | Yes | Yes | Yes |
|
||||
| Python 3.7.x | Yes | Yes | Yes |
|
||||
| Python 3.8.x | Partial (see below) | Yes | Yes |
|
||||
| Python 3.9.x | No | Partial (see below) | Yes |
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import logging
|
||||
|
||||
import idaapi
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
|
||||
import os
|
||||
import copy
|
||||
import json
|
||||
import logging
|
||||
import itertools
|
||||
import collections
|
||||
from typing import Set, Dict, Optional
|
||||
|
||||
import idaapi
|
||||
import ida_kernwin
|
||||
@@ -26,16 +26,20 @@ import capa.render.json
|
||||
import capa.features.common
|
||||
import capa.render.result_document
|
||||
import capa.features.extractors.ida.extractor
|
||||
from capa.engine import FeatureSet
|
||||
from capa.features.common import Feature
|
||||
from capa.ida.plugin.icon import QICON
|
||||
from capa.ida.plugin.view import (
|
||||
CapaExplorerQtreeView,
|
||||
CapaExplorerRulgenEditor,
|
||||
CapaExplorerRulgenPreview,
|
||||
CapaExplorerRulegenEditor,
|
||||
CapaExplorerRulegenPreview,
|
||||
CapaExplorerRulegenFeatures,
|
||||
)
|
||||
from capa.features.address import NO_ADDRESS, Address
|
||||
from capa.ida.plugin.hooks import CapaExplorerIdaHooks
|
||||
from capa.ida.plugin.model import CapaExplorerDataModel
|
||||
from capa.ida.plugin.proxy import CapaExplorerRangeProxyModel, CapaExplorerSearchProxyModel
|
||||
from capa.features.extractors.base_extractor import FunctionHandle
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
settings = ida_settings.IDASettings("capa")
|
||||
@@ -66,32 +70,32 @@ def trim_function_name(f, max_length=25):
|
||||
return n
|
||||
|
||||
|
||||
def find_func_features(f, extractor):
|
||||
def find_func_features(fh: FunctionHandle, extractor):
|
||||
""" """
|
||||
func_features = collections.defaultdict(set)
|
||||
bb_features = collections.defaultdict(dict)
|
||||
func_features: Dict[Feature, Set] = collections.defaultdict(set)
|
||||
bb_features: Dict[Address, Dict] = collections.defaultdict(dict)
|
||||
|
||||
for (feature, ea) in extractor.extract_function_features(f):
|
||||
func_features[feature].add(ea)
|
||||
for (feature, addr) in extractor.extract_function_features(fh):
|
||||
func_features[feature].add(addr)
|
||||
|
||||
for bb in extractor.get_basic_blocks(f):
|
||||
for bbh in extractor.get_basic_blocks(fh):
|
||||
_bb_features = collections.defaultdict(set)
|
||||
|
||||
for (feature, ea) in extractor.extract_basic_block_features(f, bb):
|
||||
_bb_features[feature].add(ea)
|
||||
func_features[feature].add(ea)
|
||||
for (feature, addr) in extractor.extract_basic_block_features(fh, bbh):
|
||||
_bb_features[feature].add(addr)
|
||||
func_features[feature].add(addr)
|
||||
|
||||
for insn in extractor.get_instructions(f, bb):
|
||||
for (feature, ea) in extractor.extract_insn_features(f, bb, insn):
|
||||
_bb_features[feature].add(ea)
|
||||
func_features[feature].add(ea)
|
||||
for insn in extractor.get_instructions(fh, bbh):
|
||||
for (feature, addr) in extractor.extract_insn_features(fh, bbh, insn):
|
||||
_bb_features[feature].add(addr)
|
||||
func_features[feature].add(addr)
|
||||
|
||||
bb_features[int(bb)] = _bb_features
|
||||
bb_features[bbh.address] = _bb_features
|
||||
|
||||
return func_features, bb_features
|
||||
|
||||
|
||||
def find_func_matches(f, ruleset, func_features, bb_features):
|
||||
def find_func_matches(f: FunctionHandle, ruleset, func_features, bb_features):
|
||||
""" """
|
||||
func_matches = collections.defaultdict(list)
|
||||
bb_matches = collections.defaultdict(list)
|
||||
@@ -108,7 +112,7 @@ def find_func_matches(f, ruleset, func_features, bb_features):
|
||||
func_features[capa.features.common.MatchedRule(name)].add(ea)
|
||||
|
||||
# find rule matches for function, function features include rule matches for basic blocks
|
||||
_, matches = capa.engine.match(ruleset.function_rules, func_features, int(f))
|
||||
_, matches = capa.engine.match(ruleset.function_rules, func_features, f.address)
|
||||
for (name, res) in matches.items():
|
||||
func_matches[name].extend(res)
|
||||
|
||||
@@ -117,19 +121,19 @@ def find_func_matches(f, ruleset, func_features, bb_features):
|
||||
|
||||
def find_file_features(extractor):
|
||||
""" """
|
||||
file_features = collections.defaultdict(set)
|
||||
for (feature, ea) in extractor.extract_file_features():
|
||||
if ea:
|
||||
file_features[feature].add(ea)
|
||||
file_features = collections.defaultdict(set) # type: FeatureSet
|
||||
for (feature, addr) in extractor.extract_file_features():
|
||||
if addr:
|
||||
file_features[feature].add(addr)
|
||||
else:
|
||||
if feature not in file_features:
|
||||
file_features[feature] = set()
|
||||
return file_features
|
||||
|
||||
|
||||
def find_file_matches(ruleset, file_features):
|
||||
def find_file_matches(ruleset, file_features: FeatureSet):
|
||||
""" """
|
||||
_, matches = capa.engine.match(ruleset.file_rules, file_features, 0x0)
|
||||
_, matches = capa.engine.match(ruleset.file_rules, file_features, NO_ADDRESS)
|
||||
return matches
|
||||
|
||||
|
||||
@@ -173,9 +177,9 @@ class CapaExplorerFeatureExtractor(capa.features.extractors.ida.extractor.IdaFea
|
||||
super(CapaExplorerFeatureExtractor, self).__init__()
|
||||
self.indicator = CapaExplorerProgressIndicator()
|
||||
|
||||
def extract_function_features(self, f):
|
||||
self.indicator.update("function at 0x%X" % f.start_ea)
|
||||
return super(CapaExplorerFeatureExtractor, self).extract_function_features(f)
|
||||
def extract_function_features(self, fh: FunctionHandle):
|
||||
self.indicator.update("function at 0x%X" % fh.inner.start_ea)
|
||||
return super(CapaExplorerFeatureExtractor, self).extract_function_features(fh)
|
||||
|
||||
|
||||
class QLineEditClicked(QtWidgets.QLineEdit):
|
||||
@@ -245,8 +249,9 @@ class CapaExplorerForm(idaapi.PluginForm):
|
||||
|
||||
self.parent = None
|
||||
self.ida_hooks = None
|
||||
self.doc = None
|
||||
self.doc: Optional[capa.render.result_document.ResultDocument] = None
|
||||
|
||||
self.rule_paths = None
|
||||
self.rules_cache = None
|
||||
self.ruleset_cache = None
|
||||
|
||||
@@ -484,8 +489,8 @@ class CapaExplorerForm(idaapi.PluginForm):
|
||||
self.view_rulegen_header_label.setText("Features")
|
||||
self.view_rulegen_header_label.setFont(font)
|
||||
|
||||
self.view_rulegen_preview = CapaExplorerRulgenPreview(parent=self.parent)
|
||||
self.view_rulegen_editor = CapaExplorerRulgenEditor(self.view_rulegen_preview, parent=self.parent)
|
||||
self.view_rulegen_preview = CapaExplorerRulegenPreview(parent=self.parent)
|
||||
self.view_rulegen_editor = CapaExplorerRulegenEditor(self.view_rulegen_preview, parent=self.parent)
|
||||
self.view_rulegen_features = CapaExplorerRulegenFeatures(self.view_rulegen_editor, parent=self.parent)
|
||||
|
||||
self.view_rulegen_preview.textChanged.connect(self.slot_rulegen_preview_update)
|
||||
@@ -617,6 +622,7 @@ class CapaExplorerForm(idaapi.PluginForm):
|
||||
|
||||
def load_capa_rules(self):
|
||||
""" """
|
||||
self.rule_paths = None
|
||||
self.ruleset_cache = None
|
||||
self.rules_cache = None
|
||||
|
||||
@@ -650,10 +656,12 @@ class CapaExplorerForm(idaapi.PluginForm):
|
||||
rule_paths.append(rule_path)
|
||||
elif os.path.isdir(rule_path):
|
||||
for root, dirs, files in os.walk(rule_path):
|
||||
if ".github" in root:
|
||||
if ".git" in root:
|
||||
# the .github directory contains CI config in capa-rules
|
||||
# this includes some .yml files
|
||||
# these are not rules
|
||||
# additionally, .git has files that are not .yml and generate the warning
|
||||
# skip those too
|
||||
continue
|
||||
for file in files:
|
||||
if not file.endswith(".yml"):
|
||||
@@ -696,9 +704,22 @@ class CapaExplorerForm(idaapi.PluginForm):
|
||||
logger.error(
|
||||
"Make sure your file directory contains properly formatted capa rules. You can download the standard collection of capa rules from https://github.com/mandiant/capa-rules."
|
||||
)
|
||||
logger.error(
|
||||
"Please ensure you're using the rules that correspond to your major version of capa (%s)",
|
||||
capa.version.get_major_version(),
|
||||
)
|
||||
logger.error(
|
||||
"You can check out these rules with the following command:\n %s",
|
||||
capa.version.get_rules_checkout_command(),
|
||||
)
|
||||
logger.error(
|
||||
"Or, for more details, see the rule set documentation here: %s",
|
||||
"https://github.com/mandiant/capa/blob/master/doc/rules.md",
|
||||
)
|
||||
settings.user[CAPA_SETTINGS_RULE_PATH] = ""
|
||||
return False
|
||||
|
||||
self.rule_paths = rule_paths
|
||||
self.ruleset_cache = ruleset
|
||||
self.rules_cache = rules
|
||||
|
||||
@@ -748,7 +769,7 @@ class CapaExplorerForm(idaapi.PluginForm):
|
||||
update_wait_box("extracting features")
|
||||
|
||||
try:
|
||||
meta = capa.ida.helpers.collect_metadata()
|
||||
meta = capa.ida.helpers.collect_metadata(self.rule_paths)
|
||||
capabilities, counts = capa.main.find_capabilities(self.ruleset_cache, extractor, disable_progress=True)
|
||||
meta["analysis"].update(counts)
|
||||
meta["analysis"]["layout"] = capa.main.compute_layout(self.ruleset_cache, extractor, capabilities)
|
||||
@@ -795,11 +816,9 @@ class CapaExplorerForm(idaapi.PluginForm):
|
||||
update_wait_box("rendering results")
|
||||
|
||||
try:
|
||||
self.doc = capa.render.result_document.convert_capabilities_to_result_document(
|
||||
meta, self.ruleset_cache, capabilities
|
||||
)
|
||||
self.doc = capa.render.result_document.ResultDocument.from_capa(meta, self.ruleset_cache, capabilities)
|
||||
except Exception as e:
|
||||
logger.error("Failed to render results (error: %s)", e)
|
||||
logger.error("Failed to collect results (error: %s)", e, exc_info=True)
|
||||
return False
|
||||
|
||||
try:
|
||||
@@ -808,7 +827,7 @@ class CapaExplorerForm(idaapi.PluginForm):
|
||||
"capa rules directory: %s (%d rules)" % (settings.user[CAPA_SETTINGS_RULE_PATH], len(self.rules_cache))
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error("Failed to render results (error: %s)", e)
|
||||
logger.error("Failed to render results (error: %s)", e, exc_info=True)
|
||||
return False
|
||||
|
||||
return True
|
||||
@@ -861,7 +880,7 @@ class CapaExplorerForm(idaapi.PluginForm):
|
||||
# must use extractor to get function, as capa analysis requires casted object
|
||||
extractor = CapaExplorerFeatureExtractor()
|
||||
except Exception as e:
|
||||
logger.error("Failed to load IDA feature extractor (error: %s)" % e)
|
||||
logger.error("Failed to load IDA feature extractor (error: %s)", e)
|
||||
return False
|
||||
|
||||
if ida_kernwin.user_cancelled():
|
||||
@@ -872,10 +891,10 @@ class CapaExplorerForm(idaapi.PluginForm):
|
||||
try:
|
||||
f = idaapi.get_func(idaapi.get_screen_ea())
|
||||
if f:
|
||||
f = extractor.get_function(f.start_ea)
|
||||
self.rulegen_current_function = f
|
||||
fh: FunctionHandle = extractor.get_function(f.start_ea)
|
||||
self.rulegen_current_function = fh
|
||||
|
||||
func_features, bb_features = find_func_features(f, extractor)
|
||||
func_features, bb_features = find_func_features(fh, extractor)
|
||||
self.rulegen_func_features_cache = collections.defaultdict(set, copy.copy(func_features))
|
||||
self.rulegen_bb_features_cache = collections.defaultdict(dict, copy.copy(bb_features))
|
||||
|
||||
@@ -886,15 +905,15 @@ class CapaExplorerForm(idaapi.PluginForm):
|
||||
|
||||
try:
|
||||
# add function and bb rule matches to function features, for display purposes
|
||||
func_matches, bb_matches = find_func_matches(f, self.ruleset_cache, func_features, bb_features)
|
||||
for (name, res) in itertools.chain(func_matches.items(), bb_matches.items()):
|
||||
func_matches, bb_matches = find_func_matches(fh, self.ruleset_cache, func_features, bb_features)
|
||||
for (name, addrs) in itertools.chain(func_matches.items(), bb_matches.items()):
|
||||
rule = self.ruleset_cache[name]
|
||||
if rule.meta.get("capa/subscope-rule"):
|
||||
if rule.is_subscope_rule():
|
||||
continue
|
||||
for (ea, _) in res:
|
||||
func_features[capa.features.common.MatchedRule(name)].add(ea)
|
||||
for (addr, _) in addrs:
|
||||
func_features[capa.features.common.MatchedRule(name)].add(addr)
|
||||
except Exception as e:
|
||||
logger.error("Failed to match function/basic block rule scope (error: %s)" % e)
|
||||
logger.error("Failed to match function/basic block rule scope (error: %s)", e)
|
||||
return False
|
||||
else:
|
||||
func_features = {}
|
||||
@@ -902,7 +921,7 @@ class CapaExplorerForm(idaapi.PluginForm):
|
||||
logger.info("User cancelled analysis.")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error("Failed to extract function features (error: %s)" % e)
|
||||
logger.error("Failed to extract function features (error: %s)", e)
|
||||
return False
|
||||
|
||||
if ida_kernwin.user_cancelled():
|
||||
@@ -912,7 +931,7 @@ class CapaExplorerForm(idaapi.PluginForm):
|
||||
|
||||
try:
|
||||
file_features = find_file_features(extractor)
|
||||
self.rulegen_file_features_cache = collections.defaultdict(dict, copy.copy(file_features))
|
||||
self.rulegen_file_features_cache = copy.copy(file_features)
|
||||
|
||||
if ida_kernwin.user_cancelled():
|
||||
logger.info("User cancelled analysis.")
|
||||
@@ -921,17 +940,17 @@ class CapaExplorerForm(idaapi.PluginForm):
|
||||
|
||||
try:
|
||||
# add file matches to file features, for display purposes
|
||||
for (name, res) in find_file_matches(self.ruleset_cache, file_features).items():
|
||||
for (name, addrs) in find_file_matches(self.ruleset_cache, file_features).items():
|
||||
rule = self.ruleset_cache[name]
|
||||
if rule.meta.get("capa/subscope-rule"):
|
||||
if rule.is_subscope_rule():
|
||||
continue
|
||||
for (ea, _) in res:
|
||||
file_features[capa.features.common.MatchedRule(name)].add(ea)
|
||||
for (addr, _) in addrs:
|
||||
file_features[capa.features.common.MatchedRule(name)].add(addr)
|
||||
except Exception as e:
|
||||
logger.error("Failed to match file scope rules (error: %s)" % e)
|
||||
logger.error("Failed to match file scope rules (error: %s)", e)
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error("Failed to extract file features (error: %s)" % e)
|
||||
logger.error("Failed to extract file features (error: %s)", e)
|
||||
return False
|
||||
|
||||
if ida_kernwin.user_cancelled():
|
||||
@@ -942,7 +961,7 @@ class CapaExplorerForm(idaapi.PluginForm):
|
||||
try:
|
||||
# load preview and feature tree
|
||||
self.view_rulegen_preview.load_preview_meta(
|
||||
f.start_ea if f else None,
|
||||
fh.address if fh else None,
|
||||
settings.user.get(CAPA_SETTINGS_RULEGEN_AUTHOR, "<insert_author>"),
|
||||
settings.user.get(CAPA_SETTINGS_RULEGEN_SCOPE, "function"),
|
||||
)
|
||||
@@ -953,7 +972,7 @@ class CapaExplorerForm(idaapi.PluginForm):
|
||||
"capa rules directory: %s (%d rules)" % (settings.user[CAPA_SETTINGS_RULE_PATH], len(self.rules_cache))
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error("Failed to render views (error: %s)" % e)
|
||||
logger.error("Failed to render views (error: %s)", e, exc_info=True)
|
||||
return False
|
||||
|
||||
return True
|
||||
@@ -1151,7 +1170,7 @@ class CapaExplorerForm(idaapi.PluginForm):
|
||||
idaapi.info("No program analysis to save.")
|
||||
return
|
||||
|
||||
s = json.dumps(self.doc, sort_keys=True, cls=capa.render.json.CapaJsonObjectEncoder).encode("utf-8")
|
||||
s = self.doc.json().encode("utf-8")
|
||||
|
||||
path = self.ask_user_capa_json_file()
|
||||
if not path:
|
||||
|
||||
@@ -7,12 +7,14 @@
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import codecs
|
||||
from typing import List, Iterator, Optional
|
||||
|
||||
import idc
|
||||
import idaapi
|
||||
from PyQt5 import QtCore
|
||||
|
||||
import capa.ida.helpers
|
||||
from capa.features.address import Address, FileOffsetAddress, AbsoluteVirtualAddress
|
||||
|
||||
|
||||
def info_to_name(display):
|
||||
@@ -26,19 +28,19 @@ def info_to_name(display):
|
||||
return ""
|
||||
|
||||
|
||||
def location_to_hex(location):
|
||||
"""convert location to hex for display"""
|
||||
return "%08X" % location
|
||||
def ea_to_hex(ea):
|
||||
"""convert effective address (ea) to hex for display"""
|
||||
return "%08X" % ea
|
||||
|
||||
|
||||
class CapaExplorerDataItem:
|
||||
"""store data for CapaExplorerDataModel"""
|
||||
|
||||
def __init__(self, parent, data, can_check=True):
|
||||
def __init__(self, parent: "CapaExplorerDataItem", data: List[str], can_check=True):
|
||||
"""initialize item"""
|
||||
self.pred = parent
|
||||
self._data = data
|
||||
self.children = []
|
||||
self._children: List["CapaExplorerDataItem"] = []
|
||||
self._checked = False
|
||||
self._can_check = can_check
|
||||
|
||||
@@ -76,29 +78,29 @@ class CapaExplorerDataItem:
|
||||
"""get item is checked"""
|
||||
return self._checked
|
||||
|
||||
def appendChild(self, item):
|
||||
def appendChild(self, item: "CapaExplorerDataItem"):
|
||||
"""add a new child to specified item
|
||||
|
||||
@param item: CapaExplorerDataItem
|
||||
"""
|
||||
self.children.append(item)
|
||||
self._children.append(item)
|
||||
|
||||
def child(self, row):
|
||||
def child(self, row: int) -> "CapaExplorerDataItem":
|
||||
"""get child row
|
||||
|
||||
@param row: row number
|
||||
"""
|
||||
return self.children[row]
|
||||
return self._children[row]
|
||||
|
||||
def childCount(self):
|
||||
def childCount(self) -> int:
|
||||
"""get child count"""
|
||||
return len(self.children)
|
||||
return len(self._children)
|
||||
|
||||
def columnCount(self):
|
||||
def columnCount(self) -> int:
|
||||
"""get column count"""
|
||||
return len(self._data)
|
||||
|
||||
def data(self, column):
|
||||
def data(self, column: int) -> Optional[str]:
|
||||
"""get data at column
|
||||
|
||||
@param: column number
|
||||
@@ -108,17 +110,17 @@ class CapaExplorerDataItem:
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
def parent(self):
|
||||
def parent(self) -> "CapaExplorerDataItem":
|
||||
"""get parent"""
|
||||
return self.pred
|
||||
|
||||
def row(self):
|
||||
def row(self) -> int:
|
||||
"""get row location"""
|
||||
if self.pred:
|
||||
return self.pred.children.index(self)
|
||||
return self.pred._children.index(self)
|
||||
return 0
|
||||
|
||||
def setData(self, column, value):
|
||||
def setData(self, column: int, value: str):
|
||||
"""set data in column
|
||||
|
||||
@param column: column number
|
||||
@@ -126,14 +128,14 @@ class CapaExplorerDataItem:
|
||||
"""
|
||||
self._data[column] = value
|
||||
|
||||
def children(self):
|
||||
def children(self) -> Iterator["CapaExplorerDataItem"]:
|
||||
"""yield children"""
|
||||
for child in self.children:
|
||||
for child in self._children:
|
||||
yield child
|
||||
|
||||
def removeChildren(self):
|
||||
"""remove children"""
|
||||
del self.children[:]
|
||||
del self._children[:]
|
||||
|
||||
def __str__(self):
|
||||
"""get string representation of columns
|
||||
@@ -148,7 +150,7 @@ class CapaExplorerDataItem:
|
||||
return self._data[0]
|
||||
|
||||
@property
|
||||
def location(self):
|
||||
def location(self) -> Optional[int]:
|
||||
"""return data stored in location column"""
|
||||
try:
|
||||
# address stored as str, convert to int before return
|
||||
@@ -167,7 +169,9 @@ class CapaExplorerRuleItem(CapaExplorerDataItem):
|
||||
|
||||
fmt = "%s (%d matches)"
|
||||
|
||||
def __init__(self, parent, name, namespace, count, source, can_check=True):
|
||||
def __init__(
|
||||
self, parent: CapaExplorerDataItem, name: str, namespace: str, count: int, source: str, can_check=True
|
||||
):
|
||||
"""initialize item
|
||||
|
||||
@param parent: parent node
|
||||
@@ -189,7 +193,7 @@ class CapaExplorerRuleItem(CapaExplorerDataItem):
|
||||
class CapaExplorerRuleMatchItem(CapaExplorerDataItem):
|
||||
"""store data for rule match"""
|
||||
|
||||
def __init__(self, parent, display, source=""):
|
||||
def __init__(self, parent: CapaExplorerDataItem, display: str, source=""):
|
||||
"""initialize item
|
||||
|
||||
@param parent: parent node
|
||||
@@ -210,14 +214,16 @@ class CapaExplorerFunctionItem(CapaExplorerDataItem):
|
||||
|
||||
fmt = "function(%s)"
|
||||
|
||||
def __init__(self, parent, location, can_check=True):
|
||||
def __init__(self, parent: CapaExplorerDataItem, location: Address, can_check=True):
|
||||
"""initialize item
|
||||
|
||||
@param parent: parent node
|
||||
@param location: virtual address of function as seen by IDA
|
||||
"""
|
||||
assert isinstance(location, AbsoluteVirtualAddress)
|
||||
ea = int(location)
|
||||
super(CapaExplorerFunctionItem, self).__init__(
|
||||
parent, [self.fmt % idaapi.get_name(location), location_to_hex(location), ""], can_check
|
||||
parent, [self.fmt % idaapi.get_name(ea), ea_to_hex(ea), ""], can_check
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -243,7 +249,7 @@ class CapaExplorerSubscopeItem(CapaExplorerDataItem):
|
||||
|
||||
fmt = "subscope(%s)"
|
||||
|
||||
def __init__(self, parent, scope):
|
||||
def __init__(self, parent: CapaExplorerDataItem, scope):
|
||||
"""initialize item
|
||||
|
||||
@param parent: parent node
|
||||
@@ -257,19 +263,23 @@ class CapaExplorerBlockItem(CapaExplorerDataItem):
|
||||
|
||||
fmt = "basic block(loc_%08X)"
|
||||
|
||||
def __init__(self, parent, location):
|
||||
def __init__(self, parent: CapaExplorerDataItem, location: Address):
|
||||
"""initialize item
|
||||
|
||||
@param parent: parent node
|
||||
@param location: virtual address of basic block as seen by IDA
|
||||
"""
|
||||
super(CapaExplorerBlockItem, self).__init__(parent, [self.fmt % location, location_to_hex(location), ""])
|
||||
assert isinstance(location, AbsoluteVirtualAddress)
|
||||
ea = int(location)
|
||||
super(CapaExplorerBlockItem, self).__init__(parent, [self.fmt % ea, ea_to_hex(ea), ""])
|
||||
|
||||
|
||||
class CapaExplorerDefaultItem(CapaExplorerDataItem):
|
||||
"""store data for default match e.g. statement (and, or)"""
|
||||
|
||||
def __init__(self, parent, display, details="", location=None):
|
||||
def __init__(
|
||||
self, parent: CapaExplorerDataItem, display: str, details: str = "", location: Optional[Address] = None
|
||||
):
|
||||
"""initialize item
|
||||
|
||||
@param parent: parent node
|
||||
@@ -277,14 +287,22 @@ class CapaExplorerDefaultItem(CapaExplorerDataItem):
|
||||
@param details: text to display in details section of UI
|
||||
@param location: virtual address as seen by IDA
|
||||
"""
|
||||
location = location_to_hex(location) if location else ""
|
||||
super(CapaExplorerDefaultItem, self).__init__(parent, [display, location, details])
|
||||
ea = None
|
||||
if location:
|
||||
assert isinstance(location, AbsoluteVirtualAddress)
|
||||
ea = int(location)
|
||||
|
||||
super(CapaExplorerDefaultItem, self).__init__(
|
||||
parent, [display, ea_to_hex(ea) if ea is not None else "", details]
|
||||
)
|
||||
|
||||
|
||||
class CapaExplorerFeatureItem(CapaExplorerDataItem):
|
||||
"""store data for feature match"""
|
||||
|
||||
def __init__(self, parent, display, location="", details=""):
|
||||
def __init__(
|
||||
self, parent: CapaExplorerDataItem, display: str, location: Optional[Address] = None, details: str = ""
|
||||
):
|
||||
"""initialize item
|
||||
|
||||
@param parent: parent node
|
||||
@@ -292,14 +310,18 @@ class CapaExplorerFeatureItem(CapaExplorerDataItem):
|
||||
@param details: text to display in details section of UI
|
||||
@param location: virtual address as seen by IDA
|
||||
"""
|
||||
location = location_to_hex(location) if location else ""
|
||||
super(CapaExplorerFeatureItem, self).__init__(parent, [display, location, details])
|
||||
if location:
|
||||
assert isinstance(location, (AbsoluteVirtualAddress, FileOffsetAddress))
|
||||
ea = int(location)
|
||||
super(CapaExplorerFeatureItem, self).__init__(parent, [display, ea_to_hex(ea), details])
|
||||
else:
|
||||
super(CapaExplorerFeatureItem, self).__init__(parent, [display, "", details])
|
||||
|
||||
|
||||
class CapaExplorerInstructionViewItem(CapaExplorerFeatureItem):
|
||||
"""store data for instruction match"""
|
||||
|
||||
def __init__(self, parent, display, location):
|
||||
def __init__(self, parent: CapaExplorerDataItem, display: str, location: Address):
|
||||
"""initialize item
|
||||
|
||||
details section shows disassembly view for match
|
||||
@@ -308,15 +330,17 @@ class CapaExplorerInstructionViewItem(CapaExplorerFeatureItem):
|
||||
@param display: text to display in UI
|
||||
@param location: virtual address as seen by IDA
|
||||
"""
|
||||
details = capa.ida.helpers.get_disasm_line(location)
|
||||
assert isinstance(location, AbsoluteVirtualAddress)
|
||||
ea = int(location)
|
||||
details = capa.ida.helpers.get_disasm_line(ea)
|
||||
super(CapaExplorerInstructionViewItem, self).__init__(parent, display, location=location, details=details)
|
||||
self.ida_highlight = idc.get_color(location, idc.CIC_ITEM)
|
||||
self.ida_highlight = idc.get_color(ea, idc.CIC_ITEM)
|
||||
|
||||
|
||||
class CapaExplorerByteViewItem(CapaExplorerFeatureItem):
|
||||
"""store data for byte match"""
|
||||
|
||||
def __init__(self, parent, display, location):
|
||||
def __init__(self, parent: CapaExplorerDataItem, display: str, location: Address):
|
||||
"""initialize item
|
||||
|
||||
details section shows byte preview for match
|
||||
@@ -325,7 +349,10 @@ class CapaExplorerByteViewItem(CapaExplorerFeatureItem):
|
||||
@param display: text to display in UI
|
||||
@param location: virtual address as seen by IDA
|
||||
"""
|
||||
byte_snap = idaapi.get_bytes(location, 32)
|
||||
assert isinstance(location, (AbsoluteVirtualAddress, FileOffsetAddress))
|
||||
ea = int(location)
|
||||
|
||||
byte_snap = idaapi.get_bytes(ea, 32)
|
||||
|
||||
details = ""
|
||||
if byte_snap:
|
||||
@@ -333,18 +360,21 @@ class CapaExplorerByteViewItem(CapaExplorerFeatureItem):
|
||||
details = " ".join([byte_snap[i : i + 2].decode() for i in range(0, len(byte_snap), 2)])
|
||||
|
||||
super(CapaExplorerByteViewItem, self).__init__(parent, display, location=location, details=details)
|
||||
self.ida_highlight = idc.get_color(location, idc.CIC_ITEM)
|
||||
self.ida_highlight = idc.get_color(ea, idc.CIC_ITEM)
|
||||
|
||||
|
||||
class CapaExplorerStringViewItem(CapaExplorerFeatureItem):
|
||||
"""store data for string match"""
|
||||
|
||||
def __init__(self, parent, display, location, value):
|
||||
def __init__(self, parent: CapaExplorerDataItem, display: str, location: Address, value: str):
|
||||
"""initialize item
|
||||
|
||||
@param parent: parent node
|
||||
@param display: text to display in UI
|
||||
@param location: virtual address as seen by IDA
|
||||
"""
|
||||
assert isinstance(location, (AbsoluteVirtualAddress, FileOffsetAddress))
|
||||
ea = int(location)
|
||||
|
||||
super(CapaExplorerStringViewItem, self).__init__(parent, display, location=location, details=value)
|
||||
self.ida_highlight = idc.get_color(location, idc.CIC_ITEM)
|
||||
self.ida_highlight = idc.get_color(ea, idc.CIC_ITEM)
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
from collections import deque, defaultdict
|
||||
from typing import Set, Dict, List, Tuple
|
||||
from collections import deque
|
||||
|
||||
import idc
|
||||
import idaapi
|
||||
@@ -16,6 +17,9 @@ import capa.rules
|
||||
import capa.ida.helpers
|
||||
import capa.render.utils as rutils
|
||||
import capa.features.common
|
||||
import capa.features.freeze as frz
|
||||
import capa.render.result_document as rd
|
||||
import capa.features.freeze.features as frzf
|
||||
from capa.ida.plugin.item import (
|
||||
CapaExplorerDataItem,
|
||||
CapaExplorerRuleItem,
|
||||
@@ -29,6 +33,7 @@ from capa.ida.plugin.item import (
|
||||
CapaExplorerStringViewItem,
|
||||
CapaExplorerInstructionViewItem,
|
||||
)
|
||||
from capa.features.address import Address, AbsoluteVirtualAddress
|
||||
|
||||
# default highlight color used in IDA window
|
||||
DEFAULT_HIGHLIGHT = 0xE6C700
|
||||
@@ -342,7 +347,14 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
|
||||
|
||||
return item.childCount()
|
||||
|
||||
def render_capa_doc_statement_node(self, parent, statement, locations, doc):
|
||||
def render_capa_doc_statement_node(
|
||||
self,
|
||||
parent: CapaExplorerDataItem,
|
||||
match: rd.Match,
|
||||
statement: rd.Statement,
|
||||
locations: List[Address],
|
||||
doc: rd.ResultDocument,
|
||||
):
|
||||
"""render capa statement read from doc
|
||||
|
||||
@param parent: parent to which new child is assigned
|
||||
@@ -350,132 +362,143 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
|
||||
@param locations: locations of children (applies to range only?)
|
||||
@param doc: result doc
|
||||
"""
|
||||
if statement["type"] in ("and", "or", "optional"):
|
||||
display = statement["type"]
|
||||
if statement.get("description"):
|
||||
display += " (%s)" % statement["description"]
|
||||
|
||||
if isinstance(statement, (rd.AndStatement, rd.OrStatement, rd.OptionalStatement)):
|
||||
display = statement.type
|
||||
if statement.description:
|
||||
display += " (%s)" % statement.description
|
||||
return CapaExplorerDefaultItem(parent, display)
|
||||
elif statement["type"] == "not":
|
||||
elif isinstance(statement, rd.NotStatement):
|
||||
# TODO: do we display 'not'
|
||||
pass
|
||||
elif statement["type"] == "some":
|
||||
display = "%d or more" % statement["count"]
|
||||
if statement.get("description"):
|
||||
display += " (%s)" % statement["description"]
|
||||
elif isinstance(statement, rd.SomeStatement):
|
||||
display = "%d or more" % statement.count
|
||||
if statement.description:
|
||||
display += " (%s)" % statement.description
|
||||
return CapaExplorerDefaultItem(parent, display)
|
||||
elif statement["type"] == "range":
|
||||
elif isinstance(statement, rd.RangeStatement):
|
||||
# `range` is a weird node, its almost a hybrid of statement + feature.
|
||||
# it is a specific feature repeated multiple times.
|
||||
# there's no additional logic in the feature part, just the existence of a feature.
|
||||
# so, we have to inline some of the feature rendering here.
|
||||
display = "count(%s): " % self.capa_doc_feature_to_display(statement["child"])
|
||||
display = "count(%s): " % self.capa_doc_feature_to_display(statement.child)
|
||||
|
||||
if statement["max"] == statement["min"]:
|
||||
display += "%d" % (statement["min"])
|
||||
elif statement["min"] == 0:
|
||||
display += "%d or fewer" % (statement["max"])
|
||||
elif statement["max"] == (1 << 64 - 1):
|
||||
display += "%d or more" % (statement["min"])
|
||||
if statement.max == statement.min:
|
||||
display += "%d" % (statement.min)
|
||||
elif statement.min == 0:
|
||||
display += "%d or fewer" % (statement.max)
|
||||
elif statement.max == (1 << 64 - 1):
|
||||
display += "%d or more" % (statement.min)
|
||||
else:
|
||||
display += "between %d and %d" % (statement["min"], statement["max"])
|
||||
display += "between %d and %d" % (statement.min, statement.max)
|
||||
|
||||
if statement.get("description"):
|
||||
display += " (%s)" % statement["description"]
|
||||
if statement.description:
|
||||
display += " (%s)" % statement.description
|
||||
|
||||
parent2 = CapaExplorerFeatureItem(parent, display=display)
|
||||
|
||||
for location in locations:
|
||||
# for each location render child node for range statement
|
||||
self.render_capa_doc_feature(parent2, statement["child"], location, doc)
|
||||
self.render_capa_doc_feature(parent2, match, statement.child, location, doc)
|
||||
|
||||
return parent2
|
||||
elif statement["type"] == "subscope":
|
||||
display = statement[statement["type"]]
|
||||
if statement.get("description"):
|
||||
display += " (%s)" % statement["description"]
|
||||
elif isinstance(statement, rd.SubscopeStatement):
|
||||
display = str(statement.scope)
|
||||
if statement.description:
|
||||
display += " (%s)" % statement.description
|
||||
return CapaExplorerSubscopeItem(parent, display)
|
||||
else:
|
||||
raise RuntimeError("unexpected match statement type: " + str(statement))
|
||||
|
||||
def render_capa_doc_match(self, parent, match, doc):
|
||||
def render_capa_doc_match(self, parent: CapaExplorerDataItem, match: rd.Match, doc: rd.ResultDocument):
|
||||
"""render capa match read from doc
|
||||
|
||||
@param parent: parent node to which new child is assigned
|
||||
@param match: match read from doc
|
||||
@param doc: result doc
|
||||
"""
|
||||
if not match["success"]:
|
||||
if not match.success:
|
||||
# TODO: display failed branches at some point? Help with debugging rules?
|
||||
return
|
||||
|
||||
# optional statement with no successful children is empty
|
||||
if match["node"].get("statement", {}).get("type") == "optional" and not any(
|
||||
map(lambda m: m["success"], match["children"])
|
||||
):
|
||||
if isinstance(match.node, rd.StatementNode) and isinstance(match.node.statement, rd.OptionalStatement):
|
||||
if not any(map(lambda m: m.success, match.children)):
|
||||
return
|
||||
|
||||
if match["node"]["type"] == "statement":
|
||||
if isinstance(match.node, rd.StatementNode):
|
||||
parent2 = self.render_capa_doc_statement_node(
|
||||
parent, match["node"]["statement"], match.get("locations", []), doc
|
||||
parent, match, match.node.statement, [addr.to_capa() for addr in match.locations], doc
|
||||
)
|
||||
elif match["node"]["type"] == "feature":
|
||||
elif isinstance(match.node, rd.FeatureNode):
|
||||
parent2 = self.render_capa_doc_feature_node(
|
||||
parent, match["node"]["feature"], match.get("locations", []), doc
|
||||
parent, match, match.node.feature, [addr.to_capa() for addr in match.locations], doc
|
||||
)
|
||||
else:
|
||||
raise RuntimeError("unexpected node type: " + str(match["node"]["type"]))
|
||||
raise RuntimeError("unexpected node type: " + str(match.node.type))
|
||||
|
||||
for child in match.get("children", []):
|
||||
for child in match.children:
|
||||
self.render_capa_doc_match(parent2, child, doc)
|
||||
|
||||
def render_capa_doc_by_function(self, doc):
|
||||
def render_capa_doc_by_function(self, doc: rd.ResultDocument):
|
||||
""" """
|
||||
matches_by_function = {}
|
||||
matches_by_function: Dict[int, Tuple[CapaExplorerFunctionItem, Set[str]]] = {}
|
||||
for rule in rutils.capability_rules(doc):
|
||||
for ea in rule["matches"].keys():
|
||||
for location_, _ in rule.matches:
|
||||
location = location_.to_capa()
|
||||
|
||||
if not isinstance(location, AbsoluteVirtualAddress):
|
||||
# only handle matches with a VA
|
||||
continue
|
||||
ea = int(location)
|
||||
|
||||
ea = capa.ida.helpers.get_func_start_ea(ea)
|
||||
if ea is None:
|
||||
# file scope, skip rendering in this mode
|
||||
continue
|
||||
if not matches_by_function.get(ea, ()):
|
||||
# new function root
|
||||
matches_by_function[ea] = (CapaExplorerFunctionItem(self.root_node, ea, can_check=False), [])
|
||||
matches_by_function[ea] = (
|
||||
CapaExplorerFunctionItem(self.root_node, location, can_check=False),
|
||||
set(),
|
||||
)
|
||||
function_root, match_cache = matches_by_function[ea]
|
||||
if rule["meta"]["name"] in match_cache:
|
||||
if rule.meta.name in match_cache:
|
||||
# rule match already rendered for this function root, skip it
|
||||
continue
|
||||
match_cache.append(rule["meta"]["name"])
|
||||
match_cache.add(rule.meta.name)
|
||||
CapaExplorerRuleItem(
|
||||
function_root,
|
||||
rule["meta"]["name"],
|
||||
rule["meta"].get("namespace"),
|
||||
len(rule["matches"]),
|
||||
rule["source"],
|
||||
rule.meta.name,
|
||||
rule.meta.namespace or "",
|
||||
len(rule.matches),
|
||||
rule.source,
|
||||
can_check=False,
|
||||
)
|
||||
|
||||
def render_capa_doc_by_program(self, doc):
|
||||
def render_capa_doc_by_program(self, doc: rd.ResultDocument):
|
||||
""" """
|
||||
for rule in rutils.capability_rules(doc):
|
||||
rule_name = rule["meta"]["name"]
|
||||
rule_namespace = rule["meta"].get("namespace")
|
||||
parent = CapaExplorerRuleItem(
|
||||
self.root_node, rule_name, rule_namespace, len(rule["matches"]), rule["source"]
|
||||
)
|
||||
rule_name = rule.meta.name
|
||||
rule_namespace = rule.meta.namespace or ""
|
||||
parent = CapaExplorerRuleItem(self.root_node, rule_name, rule_namespace, len(rule.matches), rule.source)
|
||||
|
||||
for (location, match) in doc["rules"][rule["meta"]["name"]]["matches"].items():
|
||||
if rule["meta"]["scope"] == capa.rules.FILE_SCOPE:
|
||||
for (location_, match) in rule.matches:
|
||||
location = location_.to_capa()
|
||||
|
||||
parent2: CapaExplorerDataItem
|
||||
if rule.meta.scope == capa.rules.FILE_SCOPE:
|
||||
parent2 = parent
|
||||
elif rule["meta"]["scope"] == capa.rules.FUNCTION_SCOPE:
|
||||
elif rule.meta.scope == capa.rules.FUNCTION_SCOPE:
|
||||
parent2 = CapaExplorerFunctionItem(parent, location)
|
||||
elif rule["meta"]["scope"] == capa.rules.BASIC_BLOCK_SCOPE:
|
||||
elif rule.meta.scope == capa.rules.BASIC_BLOCK_SCOPE:
|
||||
parent2 = CapaExplorerBlockItem(parent, location)
|
||||
else:
|
||||
raise RuntimeError("unexpected rule scope: " + str(rule["meta"]["scope"]))
|
||||
raise RuntimeError("unexpected rule scope: " + str(rule.meta.scope))
|
||||
|
||||
self.render_capa_doc_match(parent2, match, doc)
|
||||
|
||||
def render_capa_doc(self, doc, by_function):
|
||||
def render_capa_doc(self, doc: rd.ResultDocument, by_function: bool):
|
||||
"""render capa features specified in doc
|
||||
|
||||
@param doc: capa result doc
|
||||
@@ -491,27 +514,36 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
|
||||
# inform model changes have ended
|
||||
self.endResetModel()
|
||||
|
||||
def capa_doc_feature_to_display(self, feature):
|
||||
def capa_doc_feature_to_display(self, feature: frzf.Feature):
|
||||
"""convert capa doc feature type string to display string for ui
|
||||
|
||||
@param feature: capa feature read from doc
|
||||
"""
|
||||
key = feature["type"]
|
||||
value = feature[feature["type"]]
|
||||
key = feature.type
|
||||
value = getattr(feature, feature.type)
|
||||
|
||||
if value:
|
||||
if key == "string":
|
||||
if isinstance(feature, frzf.StringFeature):
|
||||
value = '"%s"' % capa.features.common.escape_string(value)
|
||||
if feature.get("description", ""):
|
||||
return "%s(%s = %s)" % (key, value, feature["description"])
|
||||
if feature.description:
|
||||
return "%s(%s = %s)" % (key, value, feature.description)
|
||||
else:
|
||||
return "%s(%s)" % (key, value)
|
||||
else:
|
||||
return "%s" % key
|
||||
|
||||
def render_capa_doc_feature_node(self, parent, feature, locations, doc):
|
||||
def render_capa_doc_feature_node(
|
||||
self,
|
||||
parent: CapaExplorerDataItem,
|
||||
match: rd.Match,
|
||||
feature: frzf.Feature,
|
||||
locations: List[Address],
|
||||
doc: rd.ResultDocument,
|
||||
):
|
||||
"""process capa doc feature node
|
||||
|
||||
@param parent: parent node to which child is assigned
|
||||
@param match: match information
|
||||
@param feature: capa doc feature node
|
||||
@param locations: locations identified for feature
|
||||
@param doc: capa doc
|
||||
@@ -522,6 +554,7 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
|
||||
# only one location for feature so no need to nest children
|
||||
parent2 = self.render_capa_doc_feature(
|
||||
parent,
|
||||
match,
|
||||
feature,
|
||||
next(iter(locations)),
|
||||
doc,
|
||||
@@ -532,81 +565,112 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
|
||||
parent2 = CapaExplorerFeatureItem(parent, display)
|
||||
|
||||
for location in sorted(locations):
|
||||
self.render_capa_doc_feature(parent2, feature, location, doc)
|
||||
self.render_capa_doc_feature(parent2, match, feature, location, doc)
|
||||
|
||||
return parent2
|
||||
|
||||
def render_capa_doc_feature(self, parent, feature, location, doc, display="-"):
|
||||
def render_capa_doc_feature(
|
||||
self,
|
||||
parent: CapaExplorerDataItem,
|
||||
match: rd.Match,
|
||||
feature: frzf.Feature,
|
||||
location: Address,
|
||||
doc: rd.ResultDocument,
|
||||
display="-",
|
||||
):
|
||||
"""render capa feature read from doc
|
||||
|
||||
@param parent: parent node to which new child is assigned
|
||||
@param match: match information
|
||||
@param feature: feature read from doc
|
||||
@param doc: capa feature doc
|
||||
@param location: address of feature
|
||||
@param display: text to display in plugin UI
|
||||
"""
|
||||
|
||||
# special handling for characteristic pending type
|
||||
if feature["type"] == "characteristic":
|
||||
if feature[feature["type"]] in ("embedded pe",):
|
||||
if isinstance(feature, frzf.CharacteristicFeature):
|
||||
characteristic = feature.characteristic
|
||||
if characteristic in ("embedded pe",):
|
||||
return CapaExplorerByteViewItem(parent, display, location)
|
||||
|
||||
if feature[feature["type"]] in ("loop", "recursive call", "tight loop"):
|
||||
if characteristic in ("loop", "recursive call", "tight loop"):
|
||||
return CapaExplorerFeatureItem(parent, display=display)
|
||||
|
||||
# default to instruction view for all other characteristics
|
||||
return CapaExplorerInstructionViewItem(parent, display, location)
|
||||
|
||||
if feature["type"] == "match":
|
||||
elif isinstance(feature, frzf.MatchFeature):
|
||||
# display content of rule for all rule matches
|
||||
return CapaExplorerRuleMatchItem(
|
||||
parent, display, source=doc["rules"].get(feature[feature["type"]], {}).get("source", "")
|
||||
)
|
||||
matched_rule_source = ""
|
||||
|
||||
if feature["type"] in ("regex", "substring"):
|
||||
for s, locations in feature["matches"].items():
|
||||
if location in locations:
|
||||
# check if match is a matched rule
|
||||
matched_rule = doc.rules.get(feature.match, None)
|
||||
if matched_rule is not None:
|
||||
matched_rule_source = matched_rule.source
|
||||
|
||||
return CapaExplorerRuleMatchItem(parent, display, source=matched_rule_source)
|
||||
|
||||
elif isinstance(feature, (frzf.RegexFeature, frzf.SubstringFeature)):
|
||||
for capture, addrs in sorted(match.captures.items()):
|
||||
for addr in addrs:
|
||||
assert isinstance(addr, frz.Address)
|
||||
if location == addr.value:
|
||||
return CapaExplorerStringViewItem(
|
||||
parent, display, location, '"' + capa.features.common.escape_string(s) + '"'
|
||||
parent, display, location, '"' + capa.features.common.escape_string(capture) + '"'
|
||||
)
|
||||
|
||||
# programming error: the given location should always be found in the regex matches
|
||||
raise ValueError("regex match at location not found")
|
||||
|
||||
if feature["type"] == "basicblock":
|
||||
elif isinstance(feature, frzf.BasicBlockFeature):
|
||||
return CapaExplorerBlockItem(parent, location)
|
||||
|
||||
if feature["type"] in (
|
||||
"bytes",
|
||||
"api",
|
||||
"mnemonic",
|
||||
"number",
|
||||
"offset",
|
||||
"number/x32",
|
||||
"number/x64",
|
||||
"offset/x32",
|
||||
"offset/x64",
|
||||
elif isinstance(
|
||||
feature,
|
||||
(
|
||||
frzf.BytesFeature,
|
||||
frzf.APIFeature,
|
||||
frzf.MnemonicFeature,
|
||||
frzf.NumberFeature,
|
||||
frzf.OffsetFeature,
|
||||
),
|
||||
):
|
||||
# display instruction preview
|
||||
return CapaExplorerInstructionViewItem(parent, display, location)
|
||||
|
||||
if feature["type"] in ("section",):
|
||||
elif isinstance(feature, frzf.SectionFeature):
|
||||
# display byte preview
|
||||
return CapaExplorerByteViewItem(parent, display, location)
|
||||
|
||||
if feature["type"] in ("string",):
|
||||
elif isinstance(feature, frzf.StringFeature):
|
||||
# display string preview
|
||||
return CapaExplorerStringViewItem(
|
||||
parent, display, location, '"%s"' % capa.features.common.escape_string(feature[feature["type"]])
|
||||
parent, display, location, '"%s"' % capa.features.common.escape_string(feature.string)
|
||||
)
|
||||
|
||||
if feature["type"] in ("import", "export", "function-name"):
|
||||
elif isinstance(
|
||||
feature,
|
||||
(
|
||||
frzf.ImportFeature,
|
||||
frzf.ExportFeature,
|
||||
frzf.FunctionNameFeature,
|
||||
),
|
||||
):
|
||||
# display no preview
|
||||
return CapaExplorerFeatureItem(parent, location=location, display=display)
|
||||
|
||||
if feature["type"] in ("arch", "os", "format"):
|
||||
elif isinstance(
|
||||
feature,
|
||||
(
|
||||
frzf.ArchFeature,
|
||||
frzf.OSFeature,
|
||||
frzf.FormatFeature,
|
||||
),
|
||||
):
|
||||
return CapaExplorerFeatureItem(parent, display=display)
|
||||
|
||||
raise RuntimeError("unexpected feature type: " + str(feature["type"]))
|
||||
raise RuntimeError("unexpected feature type: " + str(feature.type))
|
||||
|
||||
def update_function_name(self, old_name, new_name):
|
||||
"""update all instances of old function name with new function name
|
||||
|
||||
@@ -18,6 +18,7 @@ import capa.ida.helpers
|
||||
import capa.features.common
|
||||
import capa.features.basicblock
|
||||
from capa.ida.plugin.item import CapaExplorerFunctionItem
|
||||
from capa.features.address import Address, _NoAddress
|
||||
from capa.ida.plugin.model import CapaExplorerDataModel
|
||||
|
||||
MAX_SECTION_SIZE = 750
|
||||
@@ -172,13 +173,13 @@ def resize_columns_to_content(header):
|
||||
header.resizeSection(0, MAX_SECTION_SIZE)
|
||||
|
||||
|
||||
class CapaExplorerRulgenPreview(QtWidgets.QTextEdit):
|
||||
class CapaExplorerRulegenPreview(QtWidgets.QTextEdit):
|
||||
|
||||
INDENT = " " * 2
|
||||
|
||||
def __init__(self, parent=None):
|
||||
""" """
|
||||
super(CapaExplorerRulgenPreview, self).__init__(parent)
|
||||
super(CapaExplorerRulegenPreview, self).__init__(parent)
|
||||
|
||||
self.setFont(QtGui.QFont("Courier", weight=QtGui.QFont.Bold))
|
||||
self.setLineWrapMode(QtWidgets.QTextEdit.NoWrap)
|
||||
@@ -196,7 +197,8 @@ class CapaExplorerRulgenPreview(QtWidgets.QTextEdit):
|
||||
" meta:",
|
||||
" name: <insert_name>",
|
||||
" namespace: <insert_namespace>",
|
||||
" author: %s" % author,
|
||||
" authors:",
|
||||
" - %s" % author,
|
||||
" scope: %s" % scope,
|
||||
" references: <insert_references>",
|
||||
" examples:",
|
||||
@@ -282,7 +284,7 @@ class CapaExplorerRulgenPreview(QtWidgets.QTextEdit):
|
||||
self.set_selection(select_start_ppos, select_end_ppos, len(self.toPlainText()))
|
||||
self.verticalScrollBar().setSliderPosition(scroll_ppos)
|
||||
else:
|
||||
super(CapaExplorerRulgenPreview, self).keyPressEvent(e)
|
||||
super(CapaExplorerRulegenPreview, self).keyPressEvent(e)
|
||||
|
||||
def count_previous_lines_from_block(self, block):
|
||||
"""calculate number of lines preceding block"""
|
||||
@@ -302,13 +304,13 @@ class CapaExplorerRulgenPreview(QtWidgets.QTextEdit):
|
||||
self.setTextCursor(cursor)
|
||||
|
||||
|
||||
class CapaExplorerRulgenEditor(QtWidgets.QTreeWidget):
|
||||
class CapaExplorerRulegenEditor(QtWidgets.QTreeWidget):
|
||||
|
||||
updated = QtCore.pyqtSignal()
|
||||
|
||||
def __init__(self, preview, parent=None):
|
||||
""" """
|
||||
super(CapaExplorerRulgenEditor, self).__init__(parent)
|
||||
super(CapaExplorerRulegenEditor, self).__init__(parent)
|
||||
|
||||
self.preview = preview
|
||||
|
||||
@@ -372,18 +374,18 @@ class CapaExplorerRulgenEditor(QtWidgets.QTreeWidget):
|
||||
|
||||
def dragMoveEvent(self, e):
|
||||
""" """
|
||||
super(CapaExplorerRulgenEditor, self).dragMoveEvent(e)
|
||||
super(CapaExplorerRulegenEditor, self).dragMoveEvent(e)
|
||||
|
||||
def dragEventEnter(self, e):
|
||||
""" """
|
||||
super(CapaExplorerRulgenEditor, self).dragEventEnter(e)
|
||||
super(CapaExplorerRulegenEditor, self).dragEventEnter(e)
|
||||
|
||||
def dropEvent(self, e):
|
||||
""" """
|
||||
if not self.indexAt(e.pos()).isValid():
|
||||
return
|
||||
|
||||
super(CapaExplorerRulgenEditor, self).dropEvent(e)
|
||||
super(CapaExplorerRulegenEditor, self).dropEvent(e)
|
||||
|
||||
self.update_preview()
|
||||
expand_tree(self.invisibleRootItem())
|
||||
@@ -437,7 +439,7 @@ class CapaExplorerRulgenEditor(QtWidgets.QTreeWidget):
|
||||
""" """
|
||||
expression, o = action.data()
|
||||
if "basic block" in expression and "basic block" not in o.text(
|
||||
CapaExplorerRulgenEditor.get_column_feature_index()
|
||||
CapaExplorerRulegenEditor.get_column_feature_index()
|
||||
):
|
||||
# current expression is "basic block", and not changing to "basic block" expression
|
||||
children = o.takeChildren()
|
||||
@@ -445,7 +447,7 @@ class CapaExplorerRulgenEditor(QtWidgets.QTreeWidget):
|
||||
for child in children:
|
||||
new_parent.addChild(child)
|
||||
new_parent.setExpanded(True)
|
||||
o.setText(CapaExplorerRulgenEditor.get_column_feature_index(), expression)
|
||||
o.setText(CapaExplorerRulegenEditor.get_column_feature_index(), expression)
|
||||
|
||||
def slot_clear_all(self, action):
|
||||
""" """
|
||||
@@ -456,7 +458,7 @@ class CapaExplorerRulgenEditor(QtWidgets.QTreeWidget):
|
||||
if not self.indexAt(pos).isValid():
|
||||
# user selected invalid index
|
||||
self.load_custom_context_menu_invalid_index(pos)
|
||||
elif self.itemAt(pos).capa_type == CapaExplorerRulgenEditor.get_node_type_expression():
|
||||
elif self.itemAt(pos).capa_type == CapaExplorerRulegenEditor.get_node_type_expression():
|
||||
# user selected expression node
|
||||
self.load_custom_context_menu_expression(pos)
|
||||
else:
|
||||
@@ -468,8 +470,8 @@ class CapaExplorerRulgenEditor(QtWidgets.QTreeWidget):
|
||||
def slot_item_double_clicked(self, o, column):
|
||||
""" """
|
||||
if column in (
|
||||
CapaExplorerRulgenEditor.get_column_comment_index(),
|
||||
CapaExplorerRulgenEditor.get_column_description_index(),
|
||||
CapaExplorerRulegenEditor.get_column_comment_index(),
|
||||
CapaExplorerRulegenEditor.get_column_description_index(),
|
||||
):
|
||||
o.setFlags(o.flags() | QtCore.Qt.ItemIsEditable)
|
||||
self.editItem(o, column)
|
||||
@@ -555,7 +557,7 @@ class CapaExplorerRulgenEditor(QtWidgets.QTreeWidget):
|
||||
font = QtGui.QFont()
|
||||
font.setBold(True)
|
||||
|
||||
o.setFont(CapaExplorerRulgenEditor.get_column_feature_index(), font)
|
||||
o.setFont(CapaExplorerRulegenEditor.get_column_feature_index(), font)
|
||||
|
||||
def style_feature_node(self, o):
|
||||
""" """
|
||||
@@ -566,8 +568,8 @@ class CapaExplorerRulgenEditor(QtWidgets.QTreeWidget):
|
||||
font.setWeight(QtGui.QFont.Medium)
|
||||
brush.setColor(QtGui.QColor(*COLOR_GREEN_RGB))
|
||||
|
||||
o.setFont(CapaExplorerRulgenEditor.get_column_feature_index(), font)
|
||||
o.setForeground(CapaExplorerRulgenEditor.get_column_feature_index(), brush)
|
||||
o.setFont(CapaExplorerRulegenEditor.get_column_feature_index(), font)
|
||||
o.setForeground(CapaExplorerRulegenEditor.get_column_feature_index(), brush)
|
||||
|
||||
def style_comment_node(self, o):
|
||||
""" """
|
||||
@@ -575,22 +577,22 @@ class CapaExplorerRulgenEditor(QtWidgets.QTreeWidget):
|
||||
font.setBold(True)
|
||||
font.setFamily("Courier")
|
||||
|
||||
o.setFont(CapaExplorerRulgenEditor.get_column_feature_index(), font)
|
||||
o.setFont(CapaExplorerRulegenEditor.get_column_feature_index(), font)
|
||||
|
||||
def set_expression_node(self, o):
|
||||
""" """
|
||||
setattr(o, "capa_type", CapaExplorerRulgenEditor.get_node_type_expression())
|
||||
setattr(o, "capa_type", CapaExplorerRulegenEditor.get_node_type_expression())
|
||||
self.style_expression_node(o)
|
||||
|
||||
def set_feature_node(self, o):
|
||||
""" """
|
||||
setattr(o, "capa_type", CapaExplorerRulgenEditor.get_node_type_feature())
|
||||
setattr(o, "capa_type", CapaExplorerRulegenEditor.get_node_type_feature())
|
||||
o.setFlags(o.flags() & ~QtCore.Qt.ItemIsDropEnabled)
|
||||
self.style_feature_node(o)
|
||||
|
||||
def set_comment_node(self, o):
|
||||
""" """
|
||||
setattr(o, "capa_type", CapaExplorerRulgenEditor.get_node_type_comment())
|
||||
setattr(o, "capa_type", CapaExplorerRulegenEditor.get_node_type_comment())
|
||||
o.setFlags(o.flags() & ~QtCore.Qt.ItemIsDropEnabled)
|
||||
|
||||
self.style_comment_node(o)
|
||||
@@ -692,11 +694,11 @@ class CapaExplorerRulgenEditor(QtWidgets.QTreeWidget):
|
||||
|
||||
# we need to set our own type so we can control the GUI accordingly
|
||||
if feature.startswith(("- and:", "- or:", "- not:", "- basic block:", "- optional:")):
|
||||
setattr(node, "capa_type", CapaExplorerRulgenEditor.get_node_type_expression())
|
||||
setattr(node, "capa_type", CapaExplorerRulegenEditor.get_node_type_expression())
|
||||
elif feature.startswith("#"):
|
||||
setattr(node, "capa_type", CapaExplorerRulgenEditor.get_node_type_comment())
|
||||
setattr(node, "capa_type", CapaExplorerRulegenEditor.get_node_type_comment())
|
||||
else:
|
||||
setattr(node, "capa_type", CapaExplorerRulgenEditor.get_node_type_feature())
|
||||
setattr(node, "capa_type", CapaExplorerRulegenEditor.get_node_type_feature())
|
||||
|
||||
# format the node based on its type
|
||||
(self.set_expression_node, self.set_feature_node, self.set_comment_node)[node.capa_type](node)
|
||||
@@ -758,7 +760,7 @@ class CapaExplorerRulgenEditor(QtWidgets.QTreeWidget):
|
||||
""" """
|
||||
for feature in filter(
|
||||
lambda o: o.capa_type
|
||||
in (CapaExplorerRulgenEditor.get_node_type_feature(), CapaExplorerRulgenEditor.get_node_type_comment()),
|
||||
in (CapaExplorerRulegenEditor.get_node_type_feature(), CapaExplorerRulegenEditor.get_node_type_comment()),
|
||||
tuple(iterate_tree(self)),
|
||||
):
|
||||
if feature in ignore:
|
||||
@@ -770,7 +772,7 @@ class CapaExplorerRulgenEditor(QtWidgets.QTreeWidget):
|
||||
def get_expressions(self, selected=False, ignore=()):
|
||||
""" """
|
||||
for expression in filter(
|
||||
lambda o: o.capa_type == CapaExplorerRulgenEditor.get_node_type_expression(), tuple(iterate_tree(self))
|
||||
lambda o: o.capa_type == CapaExplorerRulegenEditor.get_node_type_expression(), tuple(iterate_tree(self))
|
||||
):
|
||||
if expression in ignore:
|
||||
continue
|
||||
@@ -787,7 +789,7 @@ class CapaExplorerRulegenFeatures(QtWidgets.QTreeWidget):
|
||||
self.parent_items = {}
|
||||
self.editor = editor
|
||||
|
||||
self.setHeaderLabels(["Feature", "Virtual Address"])
|
||||
self.setHeaderLabels(["Feature", "Address"])
|
||||
self.setStyleSheet("QTreeView::item {padding-right: 15 px;padding-bottom: 2 px;}")
|
||||
|
||||
# configure view columns to auto-resize
|
||||
@@ -1010,7 +1012,8 @@ class CapaExplorerRulegenFeatures(QtWidgets.QTreeWidget):
|
||||
self.parent_items = {}
|
||||
|
||||
def format_address(e):
|
||||
return "%X" % e if e else ""
|
||||
assert isinstance(e, Address)
|
||||
return "%X" % e if not isinstance(e, _NoAddress) else ""
|
||||
|
||||
def format_feature(feature):
|
||||
""" """
|
||||
@@ -1020,7 +1023,7 @@ class CapaExplorerRulegenFeatures(QtWidgets.QTreeWidget):
|
||||
value = '"%s"' % capa.features.common.escape_string(value)
|
||||
return "%s(%s)" % (name, value)
|
||||
|
||||
for (feature, eas) in sorted(features.items(), key=lambda k: sorted(k[1])):
|
||||
for (feature, addrs) in sorted(features.items(), key=lambda k: sorted(k[1])):
|
||||
if isinstance(feature, capa.features.basicblock.BasicBlock):
|
||||
# filter basic blocks for now, we may want to add these back in some time
|
||||
# in the future
|
||||
@@ -1032,7 +1035,7 @@ class CapaExplorerRulegenFeatures(QtWidgets.QTreeWidget):
|
||||
|
||||
# level 1
|
||||
if feature not in self.parent_items:
|
||||
if len(eas) > 1:
|
||||
if len(addrs) > 1:
|
||||
self.parent_items[feature] = self.new_parent_node(
|
||||
self.parent_items[type(feature)], (format_feature(feature),), feature=feature
|
||||
)
|
||||
@@ -1042,18 +1045,18 @@ class CapaExplorerRulegenFeatures(QtWidgets.QTreeWidget):
|
||||
)
|
||||
|
||||
# level n > 1
|
||||
if len(eas) > 1:
|
||||
for ea in sorted(eas):
|
||||
if len(addrs) > 1:
|
||||
for addr in sorted(addrs):
|
||||
self.new_leaf_node(
|
||||
self.parent_items[feature], (format_feature(feature), format_address(ea)), feature=feature
|
||||
self.parent_items[feature], (format_feature(feature), format_address(addr)), feature=feature
|
||||
)
|
||||
else:
|
||||
if eas:
|
||||
ea = eas.pop()
|
||||
if addrs:
|
||||
addr = addrs.pop()
|
||||
else:
|
||||
# some features may not have an address e.g. "format"
|
||||
ea = ""
|
||||
for (i, v) in enumerate((format_feature(feature), format_address(ea))):
|
||||
addr = _NoAddress()
|
||||
for (i, v) in enumerate((format_feature(feature), format_address(addr))):
|
||||
self.parent_items[feature].setText(i, v)
|
||||
self.parent_items[feature].setData(0, 0x100, feature)
|
||||
|
||||
|
||||
434
capa/main.py
434
capa/main.py
@@ -17,6 +17,7 @@ import os.path
|
||||
import argparse
|
||||
import datetime
|
||||
import textwrap
|
||||
import warnings
|
||||
import itertools
|
||||
import contextlib
|
||||
import collections
|
||||
@@ -41,18 +42,38 @@ import capa.render.vverbose
|
||||
import capa.features.extractors
|
||||
import capa.features.extractors.common
|
||||
import capa.features.extractors.pefile
|
||||
import capa.features.extractors.dnfile_
|
||||
import capa.features.extractors.elffile
|
||||
import capa.features.extractors.dotnetfile
|
||||
import capa.features.extractors.base_extractor
|
||||
from capa.rules import Rule, Scope, RuleSet
|
||||
from capa.engine import FeatureSet, MatchResults
|
||||
from capa.helpers import get_file_taste
|
||||
from capa.features.extractors.base_extractor import FunctionHandle, FeatureExtractor
|
||||
from capa.helpers import (
|
||||
get_format,
|
||||
get_file_taste,
|
||||
get_auto_format,
|
||||
log_unsupported_os_error,
|
||||
log_unsupported_arch_error,
|
||||
log_unsupported_format_error,
|
||||
)
|
||||
from capa.exceptions import UnsupportedOSError, UnsupportedArchError, UnsupportedFormatError, UnsupportedRuntimeError
|
||||
from capa.features.common import (
|
||||
FORMAT_PE,
|
||||
FORMAT_ELF,
|
||||
FORMAT_AUTO,
|
||||
FORMAT_SC32,
|
||||
FORMAT_SC64,
|
||||
FORMAT_DOTNET,
|
||||
FORMAT_FREEZE,
|
||||
)
|
||||
from capa.features.address import NO_ADDRESS
|
||||
from capa.features.extractors.base_extractor import BBHandle, InsnHandle, FunctionHandle, FeatureExtractor
|
||||
|
||||
RULES_PATH_DEFAULT_STRING = "(embedded rules)"
|
||||
SIGNATURES_PATH_DEFAULT_STRING = "(embedded signatures)"
|
||||
BACKEND_VIV = "vivisect"
|
||||
BACKEND_SMDA = "smda"
|
||||
EXTENSIONS_SHELLCODE_32 = ("sc32", "raw32")
|
||||
EXTENSIONS_SHELLCODE_64 = ("sc64", "raw64")
|
||||
BACKEND_DOTNET = "dotnet"
|
||||
|
||||
E_MISSING_RULES = -10
|
||||
E_MISSING_FILE = -11
|
||||
@@ -83,47 +104,112 @@ def set_vivisect_log_level(level):
|
||||
logging.getLogger("vtrace").setLevel(level)
|
||||
logging.getLogger("envi").setLevel(level)
|
||||
logging.getLogger("envi.codeflow").setLevel(level)
|
||||
logging.getLogger("Elf").setLevel(level)
|
||||
|
||||
|
||||
def find_function_capabilities(ruleset: RuleSet, extractor: FeatureExtractor, f: FunctionHandle):
|
||||
# contains features from:
|
||||
# - insns
|
||||
# - function
|
||||
function_features = collections.defaultdict(set) # type: FeatureSet
|
||||
bb_matches = collections.defaultdict(list) # type: MatchResults
|
||||
def find_instruction_capabilities(
|
||||
ruleset: RuleSet, extractor: FeatureExtractor, f: FunctionHandle, bb: BBHandle, insn: InsnHandle
|
||||
) -> Tuple[FeatureSet, MatchResults]:
|
||||
"""
|
||||
find matches for the given rules for the given instruction.
|
||||
|
||||
for feature, va in itertools.chain(extractor.extract_function_features(f), extractor.extract_global_features()):
|
||||
function_features[feature].add(va)
|
||||
returns: tuple containing (features for instruction, match results for instruction)
|
||||
"""
|
||||
# all features found for the instruction.
|
||||
features = collections.defaultdict(set) # type: FeatureSet
|
||||
|
||||
for bb in extractor.get_basic_blocks(f):
|
||||
# contains features from:
|
||||
# - insns
|
||||
# - basic blocks
|
||||
bb_features = 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: FeatureExtractor, 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 = collections.defaultdict(set) # type: FeatureSet
|
||||
|
||||
# matches found at the instruction scope.
|
||||
# might be found at different instructions, thats ok.
|
||||
insn_matches = collections.defaultdict(list) # type: MatchResults
|
||||
|
||||
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()
|
||||
):
|
||||
bb_features[feature].add(va)
|
||||
function_features[feature].add(va)
|
||||
features[feature].add(va)
|
||||
|
||||
for insn in extractor.get_instructions(f, bb):
|
||||
for feature, va in itertools.chain(
|
||||
extractor.extract_insn_features(f, bb, insn), extractor.extract_global_features()
|
||||
):
|
||||
bb_features[feature].add(va)
|
||||
function_features[feature].add(va)
|
||||
|
||||
_, matches = ruleset.match(Scope.BASIC_BLOCK, bb_features, int(bb))
|
||||
# matches found within this basic block.
|
||||
_, matches = ruleset.match(Scope.BASIC_BLOCK, features, bb.address)
|
||||
|
||||
for rule_name, res in matches.items():
|
||||
bb_matches[rule_name].extend(res)
|
||||
rule = ruleset[rule_name]
|
||||
for va, _ in res:
|
||||
capa.engine.index_rule_matches(function_features, rule, [va])
|
||||
capa.engine.index_rule_matches(features, rule, [va])
|
||||
|
||||
_, function_matches = ruleset.match(Scope.FUNCTION, function_features, int(f))
|
||||
return function_matches, bb_matches, len(function_features)
|
||||
return features, matches, insn_matches
|
||||
|
||||
|
||||
def find_code_capabilities(
|
||||
ruleset: RuleSet, extractor: FeatureExtractor, 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 = collections.defaultdict(set) # type: FeatureSet
|
||||
|
||||
# matches found at the basic block scope.
|
||||
# might be found at different basic blocks, thats ok.
|
||||
bb_matches = collections.defaultdict(list) # type: MatchResults
|
||||
|
||||
# matches found at the instruction scope.
|
||||
# might be found at different instructions, thats ok.
|
||||
insn_matches = collections.defaultdict(list) # type: MatchResults
|
||||
|
||||
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_file_capabilities(ruleset: RuleSet, extractor: FeatureExtractor, function_features: FeatureSet):
|
||||
@@ -143,13 +229,14 @@ def find_file_capabilities(ruleset: RuleSet, extractor: FeatureExtractor, functi
|
||||
|
||||
file_features.update(function_features)
|
||||
|
||||
_, matches = ruleset.match(Scope.FILE, file_features, 0x0)
|
||||
_, matches = ruleset.match(Scope.FILE, file_features, NO_ADDRESS)
|
||||
return matches, len(file_features)
|
||||
|
||||
|
||||
def find_capabilities(ruleset: RuleSet, extractor: FeatureExtractor, disable_progress=None) -> Tuple[MatchResults, Any]:
|
||||
all_function_matches = collections.defaultdict(list) # type: MatchResults
|
||||
all_bb_matches = collections.defaultdict(list) # type: MatchResults
|
||||
all_insn_matches = collections.defaultdict(list) # type: MatchResults
|
||||
|
||||
meta = {
|
||||
"feature_counts": {
|
||||
@@ -170,31 +257,33 @@ def find_capabilities(ruleset: RuleSet, extractor: FeatureExtractor, disable_pro
|
||||
|
||||
pb = pbar(functions, desc="matching", unit=" functions", postfix="skipped 0 library functions")
|
||||
for f in pb:
|
||||
function_address = int(f)
|
||||
|
||||
if extractor.is_library_function(function_address):
|
||||
function_name = extractor.get_function_name(function_address)
|
||||
logger.debug("skipping library function 0x%x (%s)", function_address, function_name)
|
||||
meta["library_functions"][function_address] = function_name
|
||||
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)
|
||||
meta["library_functions"][f.address] = function_name
|
||||
n_libs = len(meta["library_functions"])
|
||||
percentage = 100 * (n_libs / n_funcs)
|
||||
if isinstance(pb, tqdm.tqdm):
|
||||
pb.set_postfix_str("skipped %d library functions (%d%%)" % (n_libs, percentage))
|
||||
continue
|
||||
|
||||
function_matches, bb_matches, feature_count = find_function_capabilities(ruleset, extractor, f)
|
||||
meta["feature_counts"]["functions"][function_address] = feature_count
|
||||
logger.debug("analyzed function 0x%x and extracted %d features", function_address, feature_count)
|
||||
function_matches, bb_matches, insn_matches, feature_count = find_code_capabilities(ruleset, extractor, f)
|
||||
meta["feature_counts"]["functions"][f.address] = feature_count
|
||||
logger.debug("analyzed function 0x%x and extracted %d features", f.address, feature_count)
|
||||
|
||||
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 and BB scopes.
|
||||
# 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()):
|
||||
for rule_name, results in itertools.chain(
|
||||
all_function_matches.items(), all_bb_matches.items(), all_insn_matches.items()
|
||||
):
|
||||
locations = set(map(lambda p: p[0], results))
|
||||
rule = ruleset[rule_name]
|
||||
capa.engine.index_rule_matches(function_and_lower_features, rule, locations)
|
||||
@@ -208,6 +297,7 @@ def find_capabilities(ruleset: RuleSet, extractor: FeatureExtractor, disable_pro
|
||||
# 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(),
|
||||
@@ -217,6 +307,7 @@ def find_capabilities(ruleset: RuleSet, extractor: FeatureExtractor, disable_pro
|
||||
return matches, meta
|
||||
|
||||
|
||||
# TODO move all to helpers?
|
||||
def has_rule_with_namespace(rules, capabilities, rule_cat):
|
||||
for rule_name in capabilities.keys():
|
||||
if rules.rules[rule_name].meta.get("namespace", "").startswith(rule_cat):
|
||||
@@ -264,17 +355,6 @@ def is_supported_format(sample: str) -> bool:
|
||||
return len(list(capa.features.extractors.common.extract_format(taste))) == 1
|
||||
|
||||
|
||||
def get_format(sample: str) -> str:
|
||||
with open(sample, "rb") as f:
|
||||
buf = f.read()
|
||||
|
||||
for feature, _ in capa.features.extractors.common.extract_format(buf):
|
||||
assert isinstance(feature.value, str)
|
||||
return feature.value
|
||||
|
||||
return "unknown"
|
||||
|
||||
|
||||
def is_supported_arch(sample: str) -> bool:
|
||||
with open(sample, "rb") as f:
|
||||
buf = f.read()
|
||||
@@ -363,19 +443,7 @@ def get_default_signatures() -> List[str]:
|
||||
return ret
|
||||
|
||||
|
||||
class UnsupportedFormatError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class UnsupportedArchError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class UnsupportedOSError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def get_workspace(path, format, sigpaths):
|
||||
def get_workspace(path, format_, sigpaths):
|
||||
"""
|
||||
load the program at the given path into a vivisect workspace using the given format.
|
||||
also apply the given FLIRT signatures.
|
||||
@@ -395,21 +463,22 @@ def get_workspace(path, format, sigpaths):
|
||||
import viv_utils
|
||||
|
||||
logger.debug("generating vivisect workspace for: %s", path)
|
||||
if format == "auto":
|
||||
# TODO should not be auto at this point, anymore
|
||||
if format_ == FORMAT_AUTO:
|
||||
if not is_supported_format(path):
|
||||
raise UnsupportedFormatError()
|
||||
|
||||
# don't analyze, so that we can add our Flirt function analyzer first.
|
||||
vw = viv_utils.getWorkspace(path, analyze=False, should_save=False)
|
||||
elif format in {"pe", "elf"}:
|
||||
elif format_ in {FORMAT_PE, FORMAT_ELF}:
|
||||
vw = viv_utils.getWorkspace(path, analyze=False, should_save=False)
|
||||
elif format == "sc32":
|
||||
elif format_ == FORMAT_SC32:
|
||||
# these are not analyzed nor saved.
|
||||
vw = viv_utils.getShellcodeWorkspaceFromFile(path, arch="i386", analyze=False)
|
||||
elif format == "sc64":
|
||||
elif format_ == FORMAT_SC64:
|
||||
vw = viv_utils.getShellcodeWorkspaceFromFile(path, arch="amd64", analyze=False)
|
||||
else:
|
||||
raise ValueError("unexpected format: " + format)
|
||||
raise ValueError("unexpected format: " + format_)
|
||||
|
||||
viv_utils.flirt.register_flirt_signature_analyzers(vw, sigpaths)
|
||||
|
||||
@@ -419,12 +488,9 @@ def get_workspace(path, format, sigpaths):
|
||||
return vw
|
||||
|
||||
|
||||
class UnsupportedRuntimeError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
# TODO get_extractors -> List[FeatureExtractor]?
|
||||
def get_extractor(
|
||||
path: str, format: str, backend: str, sigpaths: List[str], should_save_workspace=False, disable_progress=False
|
||||
path: str, format_: str, backend: str, sigpaths: List[str], should_save_workspace=False, disable_progress=False
|
||||
) -> FeatureExtractor:
|
||||
"""
|
||||
raises:
|
||||
@@ -432,7 +498,7 @@ def get_extractor(
|
||||
UnsupportedArchError
|
||||
UnsupportedOSError
|
||||
"""
|
||||
if format not in ("sc32", "sc64"):
|
||||
if format_ not in (FORMAT_SC32, FORMAT_SC64):
|
||||
if not is_supported_format(path):
|
||||
raise UnsupportedFormatError()
|
||||
|
||||
@@ -442,12 +508,19 @@ def get_extractor(
|
||||
if not is_supported_os(path):
|
||||
raise UnsupportedOSError()
|
||||
|
||||
if format_ == FORMAT_DOTNET:
|
||||
import capa.features.extractors.dnfile.extractor
|
||||
|
||||
return capa.features.extractors.dnfile.extractor.DnfileFeatureExtractor(path)
|
||||
|
||||
if backend == "smda":
|
||||
from smda.SmdaConfig import SmdaConfig
|
||||
from smda.Disassembler import Disassembler
|
||||
|
||||
import capa.features.extractors.smda.extractor
|
||||
|
||||
logger.warning("Deprecation warning: v4.0 will be the last capa version to support the SMDA backend.")
|
||||
warnings.warn("v4.0 will be the last capa version to support the SMDA backend.", DeprecationWarning)
|
||||
smda_report = None
|
||||
with halo.Halo(text="analyzing program", spinner="simpleDots", stream=sys.stderr, enabled=not disable_progress):
|
||||
config = SmdaConfig()
|
||||
@@ -460,7 +533,7 @@ def get_extractor(
|
||||
import capa.features.extractors.viv.extractor
|
||||
|
||||
with halo.Halo(text="analyzing program", spinner="simpleDots", stream=sys.stderr, enabled=not disable_progress):
|
||||
vw = get_workspace(path, format, sigpaths)
|
||||
vw = get_workspace(path, format_, sigpaths)
|
||||
|
||||
if should_save_workspace:
|
||||
logger.debug("saving workspace")
|
||||
@@ -475,6 +548,22 @@ def get_extractor(
|
||||
return capa.features.extractors.viv.extractor.VivisectFeatureExtractor(vw, path)
|
||||
|
||||
|
||||
def get_file_extractors(sample: str, format_: str) -> List[FeatureExtractor]:
|
||||
file_extractors: List[FeatureExtractor] = list()
|
||||
|
||||
if format_ == capa.features.extractors.common.FORMAT_PE:
|
||||
file_extractors.append(capa.features.extractors.pefile.PefileFeatureExtractor(sample))
|
||||
|
||||
dnfile_extractor = capa.features.extractors.dnfile_.DnfileFeatureExtractor(sample)
|
||||
if dnfile_extractor.is_dotnet_file():
|
||||
file_extractors.append(dnfile_extractor)
|
||||
|
||||
elif format_ == capa.features.extractors.common.FORMAT_ELF:
|
||||
file_extractors.append(capa.features.extractors.elffile.ElfFeatureExtractor(sample))
|
||||
|
||||
return file_extractors
|
||||
|
||||
|
||||
def is_nursery_rule_path(path: str) -> bool:
|
||||
"""
|
||||
The nursery is a spot for rules that have not yet been fully polished.
|
||||
@@ -488,22 +577,24 @@ def is_nursery_rule_path(path: str) -> bool:
|
||||
return "nursery" in path
|
||||
|
||||
|
||||
def get_rules(rule_path: str, disable_progress=False) -> List[Rule]:
|
||||
def get_rules(rule_paths: List[str], disable_progress=False) -> List[Rule]:
|
||||
rule_file_paths = []
|
||||
for rule_path in rule_paths:
|
||||
if not os.path.exists(rule_path):
|
||||
raise IOError("rule path %s does not exist or cannot be accessed" % rule_path)
|
||||
|
||||
rule_paths = []
|
||||
if os.path.isfile(rule_path):
|
||||
rule_paths.append(rule_path)
|
||||
rule_file_paths.append(rule_path)
|
||||
elif os.path.isdir(rule_path):
|
||||
logger.debug("reading rules from directory %s", rule_path)
|
||||
for root, dirs, files in os.walk(rule_path):
|
||||
if ".github" in root:
|
||||
if ".git" in root:
|
||||
# the .github directory contains CI config in capa-rules
|
||||
# this includes some .yml files
|
||||
# these are not rules
|
||||
# additionally, .git has files that are not .yml and generate the warning
|
||||
# skip those too
|
||||
continue
|
||||
|
||||
for file in files:
|
||||
if not file.endswith(".yml"):
|
||||
if not (file.startswith(".git") or file.endswith((".git", ".md", ".txt"))):
|
||||
@@ -511,9 +602,8 @@ def get_rules(rule_path: str, disable_progress=False) -> List[Rule]:
|
||||
# other things maybe are rules, but are mis-named.
|
||||
logger.warning("skipping non-.yml file: %s", file)
|
||||
continue
|
||||
|
||||
rule_path = os.path.join(root, file)
|
||||
rule_paths.append(rule_path)
|
||||
rule_file_paths.append(rule_path)
|
||||
|
||||
rules = [] # type: List[Rule]
|
||||
|
||||
@@ -523,14 +613,14 @@ def get_rules(rule_path: str, disable_progress=False) -> List[Rule]:
|
||||
# to disable progress completely
|
||||
pbar = lambda s, *args, **kwargs: s
|
||||
|
||||
for rule_path in pbar(list(rule_paths), desc="loading ", unit=" rules"):
|
||||
for rule_file_path in pbar(list(rule_file_paths), desc="loading ", unit=" rules"):
|
||||
try:
|
||||
rule = capa.rules.Rule.from_yaml_file(rule_path)
|
||||
rule = capa.rules.Rule.from_yaml_file(rule_file_path)
|
||||
except capa.rules.InvalidRule:
|
||||
raise
|
||||
else:
|
||||
rule.meta["capa/path"] = rule_path
|
||||
if is_nursery_rule_path(rule_path):
|
||||
rule.meta["capa/path"] = rule_file_path
|
||||
if is_nursery_rule_path(rule_file_path):
|
||||
rule.meta["capa/nursery"] = True
|
||||
|
||||
rules.append(rule)
|
||||
@@ -567,7 +657,12 @@ def get_signatures(sigs_path):
|
||||
return paths
|
||||
|
||||
|
||||
def collect_metadata(argv, sample_path, rules_path, extractor):
|
||||
def collect_metadata(
|
||||
argv: List[str],
|
||||
sample_path: str,
|
||||
rules_path: List[str],
|
||||
extractor: capa.features.extractors.base_extractor.FeatureExtractor,
|
||||
):
|
||||
md5 = hashlib.md5()
|
||||
sha1 = hashlib.sha1()
|
||||
sha256 = hashlib.sha256()
|
||||
@@ -579,10 +674,10 @@ def collect_metadata(argv, sample_path, rules_path, extractor):
|
||||
sha1.update(buf)
|
||||
sha256.update(buf)
|
||||
|
||||
if rules_path != RULES_PATH_DEFAULT_STRING:
|
||||
rules_path = os.path.abspath(os.path.normpath(rules_path))
|
||||
if rules_path != [RULES_PATH_DEFAULT_STRING]:
|
||||
rules_path = [os.path.abspath(os.path.normpath(r)) for r in rules_path]
|
||||
|
||||
format = get_format(sample_path)
|
||||
format_ = get_format(sample_path)
|
||||
arch = get_arch(sample_path)
|
||||
os_ = get_os(sample_path)
|
||||
|
||||
@@ -597,7 +692,7 @@ def collect_metadata(argv, sample_path, rules_path, extractor):
|
||||
"path": os.path.normpath(sample_path),
|
||||
},
|
||||
"analysis": {
|
||||
"format": format,
|
||||
"format": format_,
|
||||
"arch": arch,
|
||||
"os": os_,
|
||||
"extractor": extractor.__class__.__name__,
|
||||
@@ -625,10 +720,10 @@ def compute_layout(rules, extractor, capabilities):
|
||||
functions_by_bb = {}
|
||||
bbs_by_function = {}
|
||||
for f in extractor.get_functions():
|
||||
bbs_by_function[int(f)] = []
|
||||
bbs_by_function[f.address] = []
|
||||
for bb in extractor.get_basic_blocks(f):
|
||||
functions_by_bb[int(bb)] = int(f)
|
||||
bbs_by_function[int(f)].append(int(bb))
|
||||
functions_by_bb[bb.address] = f.address
|
||||
bbs_by_function[f.address].append(bb.address)
|
||||
|
||||
matched_bbs = set()
|
||||
for rule_name, matches in capabilities.items():
|
||||
@@ -712,19 +807,20 @@ def install_common_args(parser, wanted=None):
|
||||
|
||||
if "format" in wanted:
|
||||
formats = [
|
||||
("auto", "(default) detect file type automatically"),
|
||||
("pe", "Windows PE file"),
|
||||
("elf", "Executable and Linkable Format"),
|
||||
("sc32", "32-bit shellcode"),
|
||||
("sc64", "64-bit shellcode"),
|
||||
("freeze", "features previously frozen by capa"),
|
||||
(FORMAT_AUTO, "(default) detect file type automatically"),
|
||||
(FORMAT_PE, "Windows PE file"),
|
||||
(FORMAT_DOTNET, ".NET PE file"),
|
||||
(FORMAT_ELF, "Executable and Linkable Format"),
|
||||
(FORMAT_SC32, "32-bit shellcode"),
|
||||
(FORMAT_SC64, "64-bit shellcode"),
|
||||
(FORMAT_FREEZE, "features previously frozen by capa"),
|
||||
]
|
||||
format_help = ", ".join(["%s: %s" % (f[0], f[1]) for f in formats])
|
||||
parser.add_argument(
|
||||
"-f",
|
||||
"--format",
|
||||
choices=[f[0] for f in formats],
|
||||
default="auto",
|
||||
default=FORMAT_AUTO,
|
||||
help="select sample format, %s" % format_help,
|
||||
)
|
||||
|
||||
@@ -743,7 +839,8 @@ def install_common_args(parser, wanted=None):
|
||||
"-r",
|
||||
"--rules",
|
||||
type=str,
|
||||
default=RULES_PATH_DEFAULT_STRING,
|
||||
default=[RULES_PATH_DEFAULT_STRING],
|
||||
action="append",
|
||||
help="path to rule file or directory, use embedded rules by default",
|
||||
)
|
||||
|
||||
@@ -784,7 +881,7 @@ def handle_common_args(args):
|
||||
# disable vivisect-related logging, it's verbose and not relevant for capa users
|
||||
set_vivisect_log_level(logging.CRITICAL)
|
||||
|
||||
# Since Python 3.8 cp65001 is an alias to utf_8, but not for Pyhton < 3.8
|
||||
# Since Python 3.8 cp65001 is an alias to utf_8, but not for Python < 3.8
|
||||
# TODO: remove this code when only supporting Python 3.8+
|
||||
# https://stackoverflow.com/a/3259271/87207
|
||||
import codecs
|
||||
@@ -805,7 +902,9 @@ def handle_common_args(args):
|
||||
raise RuntimeError("unexpected --color value: " + args.color)
|
||||
|
||||
if hasattr(args, "rules"):
|
||||
if args.rules == RULES_PATH_DEFAULT_STRING:
|
||||
rules_paths: List[str] = []
|
||||
|
||||
if args.rules == [RULES_PATH_DEFAULT_STRING]:
|
||||
logger.debug("-" * 80)
|
||||
logger.debug(" Using default embedded rules.")
|
||||
logger.debug(" To provide your own rules, use the form `capa.exe -r ./path/to/rules/ /path/to/mal.exe`.")
|
||||
@@ -813,9 +912,9 @@ def handle_common_args(args):
|
||||
logger.debug(" https://github.com/mandiant/capa-rules")
|
||||
logger.debug("-" * 80)
|
||||
|
||||
rules_path = os.path.join(get_default_root(), "rules")
|
||||
default_rule_path = os.path.join(get_default_root(), "rules")
|
||||
|
||||
if not os.path.exists(rules_path):
|
||||
if not os.path.exists(default_rule_path):
|
||||
# when a users installs capa via pip,
|
||||
# this pulls down just the source code - not the default rules.
|
||||
# i'm not sure the default rules should even be written to the library directory,
|
||||
@@ -823,11 +922,18 @@ def handle_common_args(args):
|
||||
logger.error("default embedded rules not found! (maybe you installed capa as a library?)")
|
||||
logger.error("provide your own rule set via the `-r` option.")
|
||||
return E_MISSING_RULES
|
||||
else:
|
||||
rules_path = args.rules
|
||||
logger.debug("using rules path: %s", rules_path)
|
||||
|
||||
args.rules = rules_path
|
||||
rules_paths.append(default_rule_path)
|
||||
else:
|
||||
rules_paths = args.rules
|
||||
|
||||
if RULES_PATH_DEFAULT_STRING in rules_paths:
|
||||
rules_paths.remove(RULES_PATH_DEFAULT_STRING)
|
||||
|
||||
for rule_path in rules_paths:
|
||||
logger.debug("using rules path: %s", rule_path)
|
||||
|
||||
args.rules = rules_paths
|
||||
|
||||
if hasattr(args, "signatures"):
|
||||
if args.signatures == SIGNATURES_PATH_DEFAULT_STRING:
|
||||
@@ -847,8 +953,8 @@ def handle_common_args(args):
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
if sys.version_info < (3, 6):
|
||||
raise UnsupportedRuntimeError("This version of capa can only be used with Python 3.6+")
|
||||
if sys.version_info < (3, 7):
|
||||
raise UnsupportedRuntimeError("This version of capa can only be used with Python 3.7+")
|
||||
|
||||
if argv is None:
|
||||
argv = sys.argv[1:]
|
||||
@@ -893,13 +999,21 @@ def main(argv=None):
|
||||
return ret
|
||||
|
||||
try:
|
||||
taste = get_file_taste(args.sample)
|
||||
_ = get_file_taste(args.sample)
|
||||
except IOError as e:
|
||||
# per our research there's not a programmatic way to render the IOError with non-ASCII filename unless we
|
||||
# handle the IOError separately and reach into the args
|
||||
logger.error("%s", e.args[0])
|
||||
return E_MISSING_FILE
|
||||
|
||||
format_ = args.format
|
||||
if format_ == FORMAT_AUTO:
|
||||
try:
|
||||
format_ = get_auto_format(args.sample)
|
||||
except UnsupportedFormatError:
|
||||
log_unsupported_format_error()
|
||||
return E_INVALID_FILE_TYPE
|
||||
|
||||
try:
|
||||
rules = get_rules(args.rules, disable_progress=args.quiet)
|
||||
rules = capa.rules.RuleSet(rules)
|
||||
@@ -909,7 +1023,7 @@ def main(argv=None):
|
||||
# during the load of the RuleSet, we extract subscope statements into their own rules
|
||||
# that are subsequently `match`ed upon. this inflates the total rule count.
|
||||
# so, filter out the subscope rules when reporting total number of loaded rules.
|
||||
len([i for i in filter(lambda r: "capa/subscope-rule" not in r.meta, rules.rules.values())]),
|
||||
len([i for i in filter(lambda r: not r.is_subscope_rule(), rules.rules.values())]),
|
||||
)
|
||||
if args.tag:
|
||||
rules = rules.filter_rules_by_meta(args.tag)
|
||||
@@ -919,28 +1033,37 @@ def main(argv=None):
|
||||
logger.debug(" %d. %s", i, r)
|
||||
except (IOError, capa.rules.InvalidRule, capa.rules.InvalidRuleSet) as e:
|
||||
logger.error("%s", str(e))
|
||||
logger.error(
|
||||
"Please ensure you're using the rules that correspond to your major version of capa (%s)",
|
||||
capa.version.get_major_version(),
|
||||
)
|
||||
logger.error(
|
||||
"You can check out these rules with the following command:\n %s",
|
||||
capa.version.get_rules_checkout_command(),
|
||||
)
|
||||
logger.error(
|
||||
"Or, for more details, see the rule set documentation here: %s",
|
||||
"https://github.com/mandiant/capa/blob/master/doc/rules.md",
|
||||
)
|
||||
return E_INVALID_RULE
|
||||
|
||||
file_extractor = None
|
||||
if args.format == "pe" or (args.format == "auto" and taste.startswith(b"MZ")):
|
||||
# these pefile and elffile file feature extractors are pretty light weight: they don't do any code analysis.
|
||||
# file feature extractors are pretty lightweight: they don't do any code analysis.
|
||||
# so we can fairly quickly determine if the given file has "pure" file-scope rules
|
||||
# that indicate a limitation (like "file is packed based on section names")
|
||||
# and avoid doing a full code analysis on difficult/impossible binaries.
|
||||
#
|
||||
# this pass can inspect multiple file extractors, e.g., dotnet and pe to identify
|
||||
# various limitations
|
||||
try:
|
||||
file_extractor = capa.features.extractors.pefile.PefileFeatureExtractor(args.sample)
|
||||
file_extractors = get_file_extractors(args.sample, format_)
|
||||
except PEFormatError as e:
|
||||
logger.error("Input file '%s' is not a valid PE file: %s", args.sample, str(e))
|
||||
return E_CORRUPT_FILE
|
||||
|
||||
elif args.format == "elf" or (args.format == "auto" and taste.startswith(b"\x7fELF")):
|
||||
try:
|
||||
file_extractor = capa.features.extractors.elffile.ElfFeatureExtractor(args.sample)
|
||||
except (ELFError, OverflowError) as e:
|
||||
logger.error("Input file '%s' is not a valid ELF file: %s", args.sample, str(e))
|
||||
return E_CORRUPT_FILE
|
||||
|
||||
if file_extractor:
|
||||
for file_extractor in file_extractors:
|
||||
try:
|
||||
pure_file_capabilities, _ = find_file_capabilities(rules, file_extractor, {})
|
||||
except PEFormatError as e:
|
||||
@@ -950,6 +1073,9 @@ def main(argv=None):
|
||||
logger.error("Input file '%s' is not a valid ELF file: %s", args.sample, str(e))
|
||||
return E_CORRUPT_FILE
|
||||
|
||||
if isinstance(file_extractor, capa.features.extractors.dnfile_.DnfileFeatureExtractor):
|
||||
format_ = FORMAT_DOTNET
|
||||
|
||||
# file limitations that rely on non-file scope won't be detected here.
|
||||
# nor on FunctionName features, because pefile doesn't support this.
|
||||
if has_file_limitation(rules, pure_file_capabilities):
|
||||
@@ -959,58 +1085,34 @@ def main(argv=None):
|
||||
logger.debug("file limitation short circuit, won't analyze fully.")
|
||||
return E_FILE_LIMITATION
|
||||
|
||||
try:
|
||||
if args.format == "pe" or (args.format == "auto" and taste.startswith(b"MZ")):
|
||||
sig_paths = get_signatures(args.signatures)
|
||||
else:
|
||||
sig_paths = []
|
||||
logger.debug("skipping library code matching: only have PE signatures")
|
||||
except (IOError) as e:
|
||||
logger.error("%s", str(e))
|
||||
return E_INVALID_SIG
|
||||
|
||||
if (args.format == "freeze") or (args.format == "auto" and capa.features.freeze.is_freeze(taste)):
|
||||
format = "freeze"
|
||||
if format_ == FORMAT_FREEZE:
|
||||
with open(args.sample, "rb") as f:
|
||||
extractor = capa.features.freeze.load(f.read())
|
||||
else:
|
||||
format = args.format
|
||||
if format == "auto" and args.sample.endswith(EXTENSIONS_SHELLCODE_32):
|
||||
format = "sc32"
|
||||
elif format == "auto" and args.sample.endswith(EXTENSIONS_SHELLCODE_64):
|
||||
format = "sc64"
|
||||
try:
|
||||
if format_ == FORMAT_PE:
|
||||
sig_paths = get_signatures(args.signatures)
|
||||
else:
|
||||
sig_paths = []
|
||||
logger.debug("skipping library code matching: only have native PE signatures")
|
||||
except IOError as e:
|
||||
logger.error("%s", str(e))
|
||||
return E_INVALID_SIG
|
||||
|
||||
should_save_workspace = os.environ.get("CAPA_SAVE_WORKSPACE") not in ("0", "no", "NO", "n", None)
|
||||
|
||||
try:
|
||||
extractor = get_extractor(
|
||||
args.sample, format, args.backend, sig_paths, should_save_workspace, disable_progress=args.quiet
|
||||
args.sample, format_, args.backend, sig_paths, should_save_workspace, disable_progress=args.quiet
|
||||
)
|
||||
except UnsupportedFormatError:
|
||||
logger.error("-" * 80)
|
||||
logger.error(" Input file does not appear to be a PE or ELF file.")
|
||||
logger.error(" ")
|
||||
logger.error(
|
||||
" capa currently only supports analyzing PE and ELF files (or shellcode, when using --format sc32|sc64)."
|
||||
)
|
||||
logger.error(" If you don't know the input file type, you can try using the `file` utility to guess it.")
|
||||
logger.error("-" * 80)
|
||||
log_unsupported_format_error()
|
||||
return E_INVALID_FILE_TYPE
|
||||
except UnsupportedArchError:
|
||||
logger.error("-" * 80)
|
||||
logger.error(" Input file does not appear to target a supported architecture.")
|
||||
logger.error(" ")
|
||||
logger.error(" capa currently only supports analyzing x86 (32- and 64-bit).")
|
||||
logger.error("-" * 80)
|
||||
log_unsupported_arch_error()
|
||||
return E_INVALID_FILE_ARCH
|
||||
except UnsupportedOSError:
|
||||
logger.error("-" * 80)
|
||||
logger.error(" Input file does not appear to target a supported OS.")
|
||||
logger.error(" ")
|
||||
logger.error(
|
||||
" capa currently only supports analyzing executables for some operating systems (including Windows and Linux)."
|
||||
)
|
||||
logger.error("-" * 80)
|
||||
log_unsupported_os_error()
|
||||
return E_INVALID_FILE_OS
|
||||
|
||||
meta = collect_metadata(argv, args.sample, args.rules, extractor)
|
||||
@@ -1067,7 +1169,7 @@ def ida_main():
|
||||
rules = get_rules(rules_path)
|
||||
rules = capa.rules.RuleSet(rules)
|
||||
|
||||
meta = capa.ida.helpers.collect_metadata()
|
||||
meta = capa.ida.helpers.collect_metadata([rules_path])
|
||||
|
||||
capabilities, counts = find_capabilities(rules, capa.features.extractors.ida.extractor.IdaFeatureExtractor())
|
||||
meta["analysis"].update(counts)
|
||||
|
||||
@@ -11,7 +11,9 @@ import collections
|
||||
import tabulate
|
||||
|
||||
import capa.render.utils as rutils
|
||||
import capa.render.result_document
|
||||
import capa.features.freeze as frz
|
||||
import capa.render.result_document as rd
|
||||
import capa.features.freeze.features as frzf
|
||||
from capa.rules import RuleSet
|
||||
from capa.engine import MatchResults
|
||||
from capa.render.utils import StringIO
|
||||
@@ -27,50 +29,49 @@ def width(s: str, character_count: int) -> str:
|
||||
return s
|
||||
|
||||
|
||||
def render_meta(doc, ostream: StringIO):
|
||||
def render_meta(doc: rd.ResultDocument, ostream: StringIO):
|
||||
rows = [
|
||||
(width("md5", 22), width(doc["meta"]["sample"]["md5"], 82)),
|
||||
("sha1", doc["meta"]["sample"]["sha1"]),
|
||||
("sha256", doc["meta"]["sample"]["sha256"]),
|
||||
("os", doc["meta"]["analysis"]["os"]),
|
||||
("format", doc["meta"]["analysis"]["format"]),
|
||||
("arch", doc["meta"]["analysis"]["arch"]),
|
||||
("path", doc["meta"]["sample"]["path"]),
|
||||
(width("md5", 22), width(doc.meta.sample.md5, 82)),
|
||||
("sha1", doc.meta.sample.sha1),
|
||||
("sha256", doc.meta.sample.sha256),
|
||||
("os", doc.meta.analysis.os),
|
||||
("format", doc.meta.analysis.format),
|
||||
("arch", doc.meta.analysis.arch),
|
||||
("path", doc.meta.sample.path),
|
||||
]
|
||||
|
||||
ostream.write(tabulate.tabulate(rows, tablefmt="psql"))
|
||||
ostream.write("\n")
|
||||
|
||||
|
||||
def find_subrule_matches(doc):
|
||||
def find_subrule_matches(doc: rd.ResultDocument):
|
||||
"""
|
||||
collect the rule names that have been matched as a subrule match.
|
||||
this way we can avoid displaying entries for things that are too specific.
|
||||
"""
|
||||
matches = set([])
|
||||
|
||||
def rec(node):
|
||||
if not node["success"]:
|
||||
def rec(match: rd.Match):
|
||||
if not match.success:
|
||||
# there's probably a bug here for rules that do `not: match: ...`
|
||||
# but we don't have any examples of this yet
|
||||
return
|
||||
|
||||
elif node["node"]["type"] == "statement":
|
||||
for child in node["children"]:
|
||||
elif isinstance(match.node, rd.StatementNode):
|
||||
for child in match.children:
|
||||
rec(child)
|
||||
|
||||
elif node["node"]["type"] == "feature":
|
||||
if node["node"]["feature"]["type"] == "match":
|
||||
matches.add(node["node"]["feature"]["match"])
|
||||
elif isinstance(match.node, rd.FeatureNode) and isinstance(match.node.feature, frzf.MatchFeature):
|
||||
matches.add(match.node.feature.match)
|
||||
|
||||
for rule in rutils.capability_rules(doc):
|
||||
for node in rule["matches"].values():
|
||||
rec(node)
|
||||
for address, match in rule.matches:
|
||||
rec(match)
|
||||
|
||||
return matches
|
||||
|
||||
|
||||
def render_capabilities(doc, ostream: StringIO):
|
||||
def render_capabilities(doc: rd.ResultDocument, ostream: StringIO):
|
||||
"""
|
||||
example::
|
||||
|
||||
@@ -86,18 +87,18 @@ def render_capabilities(doc, ostream: StringIO):
|
||||
|
||||
rows = []
|
||||
for rule in rutils.capability_rules(doc):
|
||||
if rule["meta"]["name"] in subrule_matches:
|
||||
if rule.meta.name in subrule_matches:
|
||||
# rules that are also matched by other rules should not get rendered by default.
|
||||
# this cuts down on the amount of output while giving approx the same detail.
|
||||
# see #224
|
||||
continue
|
||||
|
||||
count = len(rule["matches"])
|
||||
count = len(rule.matches)
|
||||
if count == 1:
|
||||
capability = rutils.bold(rule["meta"]["name"])
|
||||
capability = rutils.bold(rule.meta.name)
|
||||
else:
|
||||
capability = "%s (%d matches)" % (rutils.bold(rule["meta"]["name"]), count)
|
||||
rows.append((capability, rule["meta"]["namespace"]))
|
||||
capability = "%s (%d matches)" % (rutils.bold(rule.meta.name), count)
|
||||
rows.append((capability, rule.meta.namespace))
|
||||
|
||||
if rows:
|
||||
ostream.write(
|
||||
@@ -108,7 +109,7 @@ def render_capabilities(doc, ostream: StringIO):
|
||||
ostream.writeln(rutils.bold("no capabilities found"))
|
||||
|
||||
|
||||
def render_attack(doc, ostream: StringIO):
|
||||
def render_attack(doc: rd.ResultDocument, ostream: StringIO):
|
||||
"""
|
||||
example::
|
||||
|
||||
@@ -126,17 +127,14 @@ def render_attack(doc, ostream: StringIO):
|
||||
"""
|
||||
tactics = collections.defaultdict(set)
|
||||
for rule in rutils.capability_rules(doc):
|
||||
if not rule["meta"].get("att&ck"):
|
||||
continue
|
||||
|
||||
for attack in rule["meta"]["att&ck"]:
|
||||
tactics[attack["tactic"]].add((attack["technique"], attack.get("subtechnique"), attack["id"]))
|
||||
for attack in rule.meta.attack:
|
||||
tactics[attack.tactic].add((attack.technique, attack.subtechnique, attack.id))
|
||||
|
||||
rows = []
|
||||
for tactic, techniques in sorted(tactics.items()):
|
||||
inner_rows = []
|
||||
for (technique, subtechnique, id) in sorted(techniques):
|
||||
if subtechnique is None:
|
||||
if not subtechnique:
|
||||
inner_rows.append("%s %s" % (rutils.bold(technique), id))
|
||||
else:
|
||||
inner_rows.append("%s::%s %s" % (rutils.bold(technique), subtechnique, id))
|
||||
@@ -156,7 +154,7 @@ def render_attack(doc, ostream: StringIO):
|
||||
ostream.write("\n")
|
||||
|
||||
|
||||
def render_mbc(doc, ostream: StringIO):
|
||||
def render_mbc(doc: rd.ResultDocument, ostream: StringIO):
|
||||
"""
|
||||
example::
|
||||
|
||||
@@ -172,17 +170,14 @@ def render_mbc(doc, ostream: StringIO):
|
||||
"""
|
||||
objectives = collections.defaultdict(set)
|
||||
for rule in rutils.capability_rules(doc):
|
||||
if not rule["meta"].get("mbc"):
|
||||
continue
|
||||
|
||||
for mbc in rule["meta"]["mbc"]:
|
||||
objectives[mbc["objective"]].add((mbc["behavior"], mbc.get("method"), mbc["id"]))
|
||||
for mbc in rule.meta.mbc:
|
||||
objectives[mbc.objective].add((mbc.behavior, mbc.method, mbc.id))
|
||||
|
||||
rows = []
|
||||
for objective, behaviors in sorted(objectives.items()):
|
||||
inner_rows = []
|
||||
for (behavior, method, id) in sorted(behaviors):
|
||||
if method is None:
|
||||
if not method:
|
||||
inner_rows.append("%s [%s]" % (rutils.bold(behavior), id))
|
||||
else:
|
||||
inner_rows.append("%s::%s [%s]" % (rutils.bold(behavior), method, id))
|
||||
@@ -200,7 +195,7 @@ def render_mbc(doc, ostream: StringIO):
|
||||
ostream.write("\n")
|
||||
|
||||
|
||||
def render_default(doc):
|
||||
def render_default(doc: rd.ResultDocument):
|
||||
ostream = rutils.StringIO()
|
||||
|
||||
render_meta(doc, ostream)
|
||||
@@ -215,5 +210,5 @@ def render_default(doc):
|
||||
|
||||
|
||||
def render(meta, rules: RuleSet, capabilities: MatchResults) -> str:
|
||||
doc = capa.render.result_document.convert_capabilities_to_result_document(meta, rules, capabilities)
|
||||
doc = rd.ResultDocument.from_capa(meta, rules, capabilities)
|
||||
return render_default(doc)
|
||||
|
||||
@@ -5,29 +5,10 @@
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
import json
|
||||
|
||||
import capa.render.result_document
|
||||
import capa.render.result_document as rd
|
||||
from capa.rules import RuleSet
|
||||
from capa.engine import MatchResults
|
||||
|
||||
|
||||
class CapaJsonObjectEncoder(json.JSONEncoder):
|
||||
"""JSON encoder that emits Python sets as sorted lists"""
|
||||
|
||||
def default(self, obj):
|
||||
if isinstance(obj, (list, dict, int, float, bool, type(None))) or isinstance(obj, str):
|
||||
return json.JSONEncoder.default(self, obj)
|
||||
elif isinstance(obj, set):
|
||||
return list(sorted(obj))
|
||||
else:
|
||||
# probably will TypeError
|
||||
return json.JSONEncoder.default(self, obj)
|
||||
|
||||
|
||||
def render(meta, rules: RuleSet, capabilities: MatchResults) -> str:
|
||||
return json.dumps(
|
||||
capa.render.result_document.convert_capabilities_to_result_document(meta, rules, capabilities),
|
||||
cls=CapaJsonObjectEncoder,
|
||||
sort_keys=True,
|
||||
)
|
||||
return rd.ResultDocument.from_capa(meta, rules, capabilities).json(exclude_none=True)
|
||||
|
||||
@@ -5,126 +5,283 @@
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
import copy
|
||||
import datetime
|
||||
from typing import Any, Dict, Tuple, Union, Optional
|
||||
|
||||
from pydantic import Field, BaseModel
|
||||
|
||||
import capa.rules
|
||||
import capa.engine
|
||||
import capa.render.utils
|
||||
import capa.features.common
|
||||
import capa.features.freeze as frz
|
||||
import capa.features.address
|
||||
from capa.rules import RuleSet
|
||||
from capa.engine import MatchResults
|
||||
from capa.helpers import assert_never
|
||||
|
||||
|
||||
def convert_statement_to_result_document(statement):
|
||||
"""
|
||||
"statement": {
|
||||
"type": "or"
|
||||
},
|
||||
|
||||
"statement": {
|
||||
"max": 9223372036854775808,
|
||||
"min": 2,
|
||||
"type": "range"
|
||||
},
|
||||
"""
|
||||
statement_type = statement.name.lower()
|
||||
result = {"type": statement_type}
|
||||
if statement.description:
|
||||
result["description"] = statement.description
|
||||
|
||||
if statement_type == "some" and statement.count == 0:
|
||||
result["type"] = "optional"
|
||||
elif statement_type == "some":
|
||||
result["count"] = statement.count
|
||||
elif statement_type == "range":
|
||||
result["min"] = statement.min
|
||||
result["max"] = statement.max
|
||||
result["child"] = convert_feature_to_result_document(statement.child)
|
||||
elif statement_type == "subscope":
|
||||
result["subscope"] = statement.scope
|
||||
|
||||
return result
|
||||
class FrozenModel(BaseModel):
|
||||
class Config:
|
||||
frozen = True
|
||||
|
||||
|
||||
def convert_feature_to_result_document(feature):
|
||||
"""
|
||||
"feature": {
|
||||
"number": 6,
|
||||
"type": "number"
|
||||
},
|
||||
class Sample(FrozenModel):
|
||||
md5: str
|
||||
sha1: str
|
||||
sha256: str
|
||||
path: str
|
||||
|
||||
"feature": {
|
||||
"api": "ws2_32.WSASocket",
|
||||
"type": "api"
|
||||
},
|
||||
|
||||
"feature": {
|
||||
"match": "create TCP socket",
|
||||
"type": "match"
|
||||
},
|
||||
class BasicBlockLayout(FrozenModel):
|
||||
address: frz.Address
|
||||
|
||||
"feature": {
|
||||
"characteristic": [
|
||||
"loop",
|
||||
true
|
||||
|
||||
class FunctionLayout(FrozenModel):
|
||||
address: frz.Address
|
||||
matched_basic_blocks: Tuple[BasicBlockLayout, ...]
|
||||
|
||||
|
||||
class Layout(FrozenModel):
|
||||
functions: Tuple[FunctionLayout, ...]
|
||||
|
||||
|
||||
class LibraryFunction(FrozenModel):
|
||||
address: frz.Address
|
||||
name: str
|
||||
|
||||
|
||||
class FunctionFeatureCount(FrozenModel):
|
||||
address: frz.Address
|
||||
count: int
|
||||
|
||||
|
||||
class FeatureCounts(FrozenModel):
|
||||
file: int
|
||||
functions: Tuple[FunctionFeatureCount, ...]
|
||||
|
||||
|
||||
class Analysis(FrozenModel):
|
||||
format: str
|
||||
arch: str
|
||||
os: str
|
||||
extractor: str
|
||||
rules: Tuple[str, ...]
|
||||
base_address: frz.Address
|
||||
layout: Layout
|
||||
feature_counts: FeatureCounts
|
||||
library_functions: Tuple[LibraryFunction, ...]
|
||||
|
||||
|
||||
class Metadata(FrozenModel):
|
||||
timestamp: datetime.datetime
|
||||
version: str
|
||||
argv: Tuple[str, ...]
|
||||
sample: Sample
|
||||
analysis: Analysis
|
||||
|
||||
@classmethod
|
||||
def from_capa(cls, meta: Any) -> "Metadata":
|
||||
return cls(
|
||||
timestamp=meta["timestamp"],
|
||||
version=meta["version"],
|
||||
argv=meta["argv"] if "argv" in meta else None,
|
||||
sample=Sample(
|
||||
md5=meta["sample"]["md5"],
|
||||
sha1=meta["sample"]["sha1"],
|
||||
sha256=meta["sample"]["sha256"],
|
||||
path=meta["sample"]["path"],
|
||||
),
|
||||
analysis=Analysis(
|
||||
format=meta["analysis"]["format"],
|
||||
arch=meta["analysis"]["arch"],
|
||||
os=meta["analysis"]["os"],
|
||||
extractor=meta["analysis"]["extractor"],
|
||||
rules=meta["analysis"]["rules"],
|
||||
base_address=frz.Address.from_capa(meta["analysis"]["base_address"]),
|
||||
layout=Layout(
|
||||
functions=[
|
||||
FunctionLayout(
|
||||
address=frz.Address.from_capa(address),
|
||||
matched_basic_blocks=[
|
||||
BasicBlockLayout(address=frz.Address.from_capa(bb)) for bb in f["matched_basic_blocks"]
|
||||
],
|
||||
"type": "characteristic"
|
||||
},
|
||||
"""
|
||||
result = {"type": feature.name, feature.name: feature.get_value_str()}
|
||||
if feature.description:
|
||||
result["description"] = feature.description
|
||||
if feature.name in ("regex", "substring"):
|
||||
result["matches"] = feature.matches
|
||||
return result
|
||||
)
|
||||
for address, f in meta["analysis"]["layout"]["functions"].items()
|
||||
]
|
||||
),
|
||||
feature_counts=FeatureCounts(
|
||||
file=meta["analysis"]["feature_counts"]["file"],
|
||||
functions=[
|
||||
FunctionFeatureCount(address=frz.Address.from_capa(address), count=count)
|
||||
for address, count in meta["analysis"]["feature_counts"]["functions"].items()
|
||||
],
|
||||
),
|
||||
library_functions=[
|
||||
LibraryFunction(address=frz.Address.from_capa(address), name=name)
|
||||
for address, name in meta["analysis"]["library_functions"].items()
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def convert_node_to_result_document(node):
|
||||
"""
|
||||
"node": {
|
||||
"type": "statement",
|
||||
"statement": { ... }
|
||||
},
|
||||
class StatementModel(FrozenModel):
|
||||
...
|
||||
|
||||
"node": {
|
||||
"type": "feature",
|
||||
"feature": { ... }
|
||||
},
|
||||
"""
|
||||
|
||||
if isinstance(node, capa.engine.Statement):
|
||||
return {
|
||||
"type": "statement",
|
||||
"statement": convert_statement_to_result_document(node),
|
||||
}
|
||||
elif isinstance(node, capa.features.common.Feature):
|
||||
return {
|
||||
"type": "feature",
|
||||
"feature": convert_feature_to_result_document(node),
|
||||
}
|
||||
class AndStatement(StatementModel):
|
||||
type = "and"
|
||||
description: Optional[str]
|
||||
|
||||
|
||||
class OrStatement(StatementModel):
|
||||
type = "or"
|
||||
description: Optional[str]
|
||||
|
||||
|
||||
class NotStatement(StatementModel):
|
||||
type = "not"
|
||||
description: Optional[str]
|
||||
|
||||
|
||||
class SomeStatement(StatementModel):
|
||||
type = "some"
|
||||
description: Optional[str]
|
||||
count: int
|
||||
|
||||
|
||||
class OptionalStatement(StatementModel):
|
||||
type = "optional"
|
||||
description: Optional[str]
|
||||
|
||||
|
||||
class RangeStatement(StatementModel):
|
||||
type = "range"
|
||||
description: Optional[str]
|
||||
min: int
|
||||
max: int
|
||||
child: frz.Feature
|
||||
|
||||
|
||||
class SubscopeStatement(StatementModel):
|
||||
type = "subscope"
|
||||
description: Optional[str]
|
||||
scope = capa.rules.Scope
|
||||
|
||||
|
||||
Statement = Union[
|
||||
OptionalStatement,
|
||||
AndStatement,
|
||||
OrStatement,
|
||||
NotStatement,
|
||||
SomeStatement,
|
||||
RangeStatement,
|
||||
SubscopeStatement,
|
||||
]
|
||||
|
||||
|
||||
class StatementNode(FrozenModel):
|
||||
type = "statement"
|
||||
statement: Statement
|
||||
|
||||
|
||||
def statement_from_capa(node: capa.engine.Statement) -> Statement:
|
||||
if isinstance(node, capa.engine.And):
|
||||
return AndStatement(description=node.description)
|
||||
|
||||
elif isinstance(node, capa.engine.Or):
|
||||
return OrStatement(description=node.description)
|
||||
|
||||
elif isinstance(node, capa.engine.Not):
|
||||
return NotStatement(description=node.description)
|
||||
|
||||
elif isinstance(node, capa.engine.Some):
|
||||
if node.count == 0:
|
||||
return OptionalStatement(description=node.description)
|
||||
|
||||
else:
|
||||
raise RuntimeError("unexpected match node type")
|
||||
return SomeStatement(
|
||||
description=node.description,
|
||||
count=node.count,
|
||||
)
|
||||
|
||||
elif isinstance(node, capa.engine.Range):
|
||||
return RangeStatement(
|
||||
description=node.description,
|
||||
min=node.min,
|
||||
max=node.max,
|
||||
child=frz.feature_from_capa(node.child),
|
||||
)
|
||||
|
||||
elif isinstance(node, capa.engine.Subscope):
|
||||
return SubscopeStatement(
|
||||
description=node.description,
|
||||
scope=capa.rules.Scope(node.scope),
|
||||
)
|
||||
|
||||
else:
|
||||
raise NotImplementedError(f"statement_from_capa({type(node)}) not implemented")
|
||||
|
||||
|
||||
def convert_match_to_result_document(rules, capabilities, result):
|
||||
class FeatureNode(FrozenModel):
|
||||
type = "feature"
|
||||
feature: frz.Feature
|
||||
|
||||
|
||||
Node = Union[StatementNode, FeatureNode]
|
||||
|
||||
|
||||
def node_from_capa(node: Union[capa.engine.Statement, capa.engine.Feature]) -> Node:
|
||||
if isinstance(node, capa.engine.Statement):
|
||||
return StatementNode(statement=statement_from_capa(node))
|
||||
|
||||
elif isinstance(node, capa.engine.Feature):
|
||||
return FeatureNode(feature=frz.feature_from_capa(node))
|
||||
|
||||
else:
|
||||
assert_never(node)
|
||||
|
||||
|
||||
class Match(BaseModel):
|
||||
"""
|
||||
convert the given Result instance into a common, Python-native data structure.
|
||||
this will become part of the "result document" format that can be emitted to JSON.
|
||||
args:
|
||||
success: did the node match?
|
||||
node: the logic node or feature node.
|
||||
children: any children of the logic node. not relevent for features, can be empty.
|
||||
locations: where the feature matched. not relevant for logic nodes (except range), can be empty.
|
||||
captures: captured values from the string/regex feature, and the locations of those values.
|
||||
"""
|
||||
doc = {
|
||||
"success": bool(result.success),
|
||||
"node": convert_node_to_result_document(result.statement),
|
||||
"children": [convert_match_to_result_document(rules, capabilities, child) for child in result.children],
|
||||
}
|
||||
|
||||
success: bool
|
||||
node: Node
|
||||
children: Tuple["Match", ...]
|
||||
locations: Tuple[frz.Address, ...]
|
||||
captures: Dict[str, Tuple[frz.Address, ...]]
|
||||
|
||||
@classmethod
|
||||
def from_capa(
|
||||
cls,
|
||||
rules: RuleSet,
|
||||
capabilities: MatchResults,
|
||||
result: capa.engine.Result,
|
||||
) -> "Match":
|
||||
success = bool(result)
|
||||
|
||||
node = node_from_capa(result.statement)
|
||||
children = [Match.from_capa(rules, capabilities, child) for child in result.children]
|
||||
|
||||
# logic expression, like `and`, don't have locations - their children do.
|
||||
# so only add `locations` to feature nodes.
|
||||
if isinstance(result.statement, capa.features.common.Feature):
|
||||
if bool(result.success):
|
||||
doc["locations"] = result.locations
|
||||
elif isinstance(result.statement, capa.engine.Range):
|
||||
if bool(result.success):
|
||||
doc["locations"] = result.locations
|
||||
locations = []
|
||||
if isinstance(node, FeatureNode) and success:
|
||||
locations = list(map(frz.Address.from_capa, result.locations))
|
||||
elif isinstance(node, StatementNode) and isinstance(node.statement, RangeStatement) and success:
|
||||
locations = list(map(frz.Address.from_capa, result.locations))
|
||||
|
||||
captures = {}
|
||||
if isinstance(result.statement, (capa.features.common._MatchedSubstring, capa.features.common._MatchedRegex)):
|
||||
captures = {
|
||||
capture: list(map(frz.Address.from_capa, locs)) for capture, locs in result.statement.matches.items()
|
||||
}
|
||||
|
||||
# if we have a `match` statement, then we're referencing another rule or namespace.
|
||||
# this could an external rule (written by a human), or
|
||||
@@ -135,45 +292,43 @@ def convert_match_to_result_document(rules, capabilities, result):
|
||||
# and then filter those down to the address used here.
|
||||
# finally, splice that logic into this tree.
|
||||
if (
|
||||
doc["node"]["type"] == "feature"
|
||||
and doc["node"]["feature"]["type"] == "match"
|
||||
isinstance(node, FeatureNode)
|
||||
and isinstance(node.feature, frz.features.MatchFeature)
|
||||
# only add subtree on success,
|
||||
# because there won't be results for the other rule on failure.
|
||||
and doc["success"]
|
||||
and success
|
||||
):
|
||||
|
||||
name = doc["node"]["feature"]["match"]
|
||||
name = node.feature.match
|
||||
|
||||
if name in rules:
|
||||
# this is a rule that we're matching
|
||||
#
|
||||
# pull matches from the referenced rule into our tree here.
|
||||
rule_name = doc["node"]["feature"]["match"]
|
||||
rule_name = name
|
||||
rule = rules[rule_name]
|
||||
rule_matches = {address: result for (address, result) in capabilities[rule_name]}
|
||||
|
||||
if rule.meta.get("capa/subscope-rule"):
|
||||
if rule.is_subscope_rule():
|
||||
# for a subscope rule, fixup the node to be a scope node, rather than a match feature node.
|
||||
#
|
||||
# e.g. `contain loop/30c4c78e29bf4d54894fc74f664c62e8` -> `basic block`
|
||||
scope = rule.meta["scope"]
|
||||
doc["node"] = {
|
||||
"type": "statement",
|
||||
"statement": {
|
||||
"type": "subscope",
|
||||
"subscope": scope,
|
||||
},
|
||||
}
|
||||
#
|
||||
# note! replace `node`
|
||||
node = StatementNode(
|
||||
statement=SubscopeStatement(
|
||||
scope=rule.meta["scope"],
|
||||
)
|
||||
)
|
||||
|
||||
for location in doc["locations"]:
|
||||
doc["children"].append(convert_match_to_result_document(rules, capabilities, rule_matches[location]))
|
||||
for location in result.locations:
|
||||
children.append(Match.from_capa(rules, capabilities, rule_matches[location]))
|
||||
else:
|
||||
# this is a namespace that we're matching
|
||||
#
|
||||
# check for all rules in the namespace,
|
||||
# seeing if they matched.
|
||||
# if so, pull their matches into our match tree here.
|
||||
ns_name = doc["node"]["feature"]["match"]
|
||||
ns_name = name
|
||||
ns_rules = rules.rules_by_namespace[ns_name]
|
||||
|
||||
for rule in ns_rules:
|
||||
@@ -197,7 +352,7 @@ def convert_match_to_result_document(rules, capabilities, result):
|
||||
# this would be a breaking change and require updates to the renderers.
|
||||
# in the meantime, the above might be sufficient.
|
||||
rule_matches = {address: result for (address, result) in capabilities[rule.name]}
|
||||
for location in doc["locations"]:
|
||||
for location in result.locations:
|
||||
# doc[locations] contains all matches for the given namespace.
|
||||
# for example, the feature might be `match: anti-analysis/packer`
|
||||
# which matches against "generic unpacker" and "UPX".
|
||||
@@ -208,32 +363,52 @@ def convert_match_to_result_document(rules, capabilities, result):
|
||||
#
|
||||
# so, grab only the locations for current rule.
|
||||
if location in rule_matches:
|
||||
doc["children"].append(
|
||||
convert_match_to_result_document(rules, capabilities, rule_matches[location])
|
||||
children.append(Match.from_capa(rules, capabilities, rule_matches[location]))
|
||||
|
||||
return cls(
|
||||
success=success,
|
||||
node=node,
|
||||
children=children,
|
||||
locations=locations,
|
||||
captures=captures,
|
||||
)
|
||||
|
||||
return doc
|
||||
|
||||
def parse_parts_id(s: str):
|
||||
id = ""
|
||||
parts = s.split("::")
|
||||
if len(parts) > 0:
|
||||
last = parts.pop()
|
||||
last, _, id = last.rpartition(" ")
|
||||
id = id.lstrip("[").rstrip("]")
|
||||
parts.append(last)
|
||||
return parts, id
|
||||
|
||||
|
||||
def convert_meta_to_result_document(meta):
|
||||
# make a copy so that we don't modify the given parameter
|
||||
meta = copy.deepcopy(meta)
|
||||
|
||||
attacks = meta.get("att&ck", [])
|
||||
meta["att&ck"] = [parse_canonical_attack(attack) for attack in attacks]
|
||||
mbcs = meta.get("mbc", [])
|
||||
meta["mbc"] = [parse_canonical_mbc(mbc) for mbc in mbcs]
|
||||
return meta
|
||||
|
||||
|
||||
def parse_canonical_attack(attack: str):
|
||||
class AttackSpec(FrozenModel):
|
||||
"""
|
||||
parse capa's canonical ATT&CK representation: `Tactic::Technique::Subtechnique [Identifier]`
|
||||
given an ATT&CK spec like: `Tactic::Technique::Subtechnique [Identifier]`
|
||||
e.g., `Execution::Command and Scripting Interpreter::Python [T1059.006]`
|
||||
|
||||
args:
|
||||
tactic: like `Tactic` above, perhaps "Execution"
|
||||
technique: like `Technique` above, perhaps "Command and Scripting Interpreter"
|
||||
subtechnique: like `Subtechnique` above, perhaps "Python"
|
||||
id: like `Identifier` above, perhaps "T1059.006"
|
||||
"""
|
||||
|
||||
parts: Tuple[str, ...]
|
||||
tactic: str
|
||||
technique: str
|
||||
subtechnique: str
|
||||
id: str
|
||||
|
||||
@classmethod
|
||||
def from_str(cls, s) -> "AttackSpec":
|
||||
tactic = ""
|
||||
technique = ""
|
||||
subtechnique = ""
|
||||
parts, id = capa.render.utils.parse_parts_id(attack)
|
||||
parts, id = parse_parts_id(s)
|
||||
if len(parts) > 0:
|
||||
tactic = parts[0]
|
||||
if len(parts) > 1:
|
||||
@@ -241,23 +416,39 @@ def parse_canonical_attack(attack: str):
|
||||
if len(parts) > 2:
|
||||
subtechnique = parts[2]
|
||||
|
||||
return {
|
||||
"parts": parts,
|
||||
"id": id,
|
||||
"tactic": tactic,
|
||||
"technique": technique,
|
||||
"subtechnique": subtechnique,
|
||||
}
|
||||
return cls(
|
||||
parts=parts,
|
||||
tactic=tactic,
|
||||
technique=technique,
|
||||
subtechnique=subtechnique,
|
||||
id=id,
|
||||
)
|
||||
|
||||
|
||||
def parse_canonical_mbc(mbc: str):
|
||||
class MBCSpec(FrozenModel):
|
||||
"""
|
||||
parse capa's canonical MBC representation: `Objective::Behavior::Method [Identifier]`
|
||||
given an MBC spec like: `Objective::Behavior::Method [Identifier]`
|
||||
e.g., `Collection::Input Capture::Mouse Events [E1056.m01]`
|
||||
|
||||
args:
|
||||
objective: like `Objective` above, perhaps "Collection"
|
||||
behavior: like `Behavior` above, perhaps "Input Capture"
|
||||
method: like `Method` above, perhaps "Mouse Events"
|
||||
id: like `Identifier` above, perhaps "E1056.m01"
|
||||
"""
|
||||
|
||||
parts: Tuple[str, ...]
|
||||
objective: str
|
||||
behavior: str
|
||||
method: str
|
||||
id: str
|
||||
|
||||
@classmethod
|
||||
def from_str(cls, s) -> "MBCSpec":
|
||||
objective = ""
|
||||
behavior = ""
|
||||
method = ""
|
||||
parts, id = capa.render.utils.parse_parts_id(mbc)
|
||||
parts, id = parse_parts_id(s)
|
||||
if len(parts) > 0:
|
||||
objective = parts[0]
|
||||
if len(parts) > 1:
|
||||
@@ -265,65 +456,102 @@ def parse_canonical_mbc(mbc: str):
|
||||
if len(parts) > 2:
|
||||
method = parts[2]
|
||||
|
||||
return {
|
||||
"parts": parts,
|
||||
"id": id,
|
||||
"objective": objective,
|
||||
"behavior": behavior,
|
||||
"method": method,
|
||||
}
|
||||
return cls(
|
||||
parts=parts,
|
||||
objective=objective,
|
||||
behavior=behavior,
|
||||
method=method,
|
||||
id=id,
|
||||
)
|
||||
|
||||
|
||||
def convert_capabilities_to_result_document(meta, rules: RuleSet, capabilities: MatchResults):
|
||||
class MaecMetadata(FrozenModel):
|
||||
analysis_conclusion: Optional[str] = Field(None, alias="analysis-conclusion")
|
||||
analysis_conclusion_ov: Optional[str] = Field(None, alias="analysis-conclusion-ov")
|
||||
malware_family: Optional[str] = Field(None, alias="malware-family")
|
||||
malware_category: Optional[str] = Field(None, alias="malware-category")
|
||||
malware_category_ov: Optional[str] = Field(None, alias="malware-category-ov")
|
||||
|
||||
class Config:
|
||||
frozen = True
|
||||
allow_population_by_field_name = True
|
||||
|
||||
|
||||
class RuleMetadata(FrozenModel):
|
||||
name: str
|
||||
namespace: Optional[str]
|
||||
authors: Tuple[str, ...]
|
||||
scope: capa.rules.Scope
|
||||
attack: Tuple[AttackSpec, ...] = Field(alias="att&ck")
|
||||
mbc: Tuple[MBCSpec, ...]
|
||||
references: Tuple[str, ...]
|
||||
examples: Tuple[str, ...]
|
||||
description: str
|
||||
|
||||
lib: bool = Field(False, alias="lib")
|
||||
is_subscope_rule: bool = Field(False, alias="capa/subscope")
|
||||
maec: MaecMetadata
|
||||
|
||||
@classmethod
|
||||
def from_capa(cls, rule: capa.rules.Rule) -> "RuleMetadata":
|
||||
return cls(
|
||||
name=rule.meta.get("name"),
|
||||
namespace=rule.meta.get("namespace"),
|
||||
authors=rule.meta.get("authors"),
|
||||
scope=capa.rules.Scope(rule.meta.get("scope")),
|
||||
attack=list(map(AttackSpec.from_str, rule.meta.get("att&ck", []))),
|
||||
mbc=list(map(MBCSpec.from_str, rule.meta.get("mbc", []))),
|
||||
references=rule.meta.get("references", []),
|
||||
examples=rule.meta.get("examples", []),
|
||||
description=rule.meta.get("description", ""),
|
||||
lib=rule.meta.get("lib", False),
|
||||
capa_subscope=rule.meta.get("capa/subscope", False),
|
||||
maec=MaecMetadata(
|
||||
analysis_conclusion=rule.meta.get("maec/analysis-conclusion"),
|
||||
analysis_conclusion_ov=rule.meta.get("maec/analysis-conclusion-ov"),
|
||||
malware_family=rule.meta.get("maec/malware-family"),
|
||||
malware_category=rule.meta.get("maec/malware-category"),
|
||||
malware_category_ov=rule.meta.get("maec/malware-category-ov"),
|
||||
),
|
||||
)
|
||||
|
||||
class Config:
|
||||
frozen = True
|
||||
allow_population_by_field_name = True
|
||||
|
||||
|
||||
class RuleMatches(BaseModel):
|
||||
"""
|
||||
convert the given rule set and capabilities result to a common, Python-native data structure.
|
||||
this format can be directly emitted to JSON, or passed to the other `capa.render.*.render()` routines
|
||||
to render as text.
|
||||
|
||||
see examples of substructures in above routines.
|
||||
|
||||
schema:
|
||||
|
||||
```json
|
||||
{
|
||||
"meta": {...},
|
||||
"rules: {
|
||||
$rule-name: {
|
||||
"meta": {...copied from rule.meta...},
|
||||
"matches: {
|
||||
$address: {...match details...},
|
||||
...
|
||||
}
|
||||
},
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Args:
|
||||
meta (Dict[str, Any]):
|
||||
rules (RuleSet):
|
||||
capabilities (Dict[str, List[Tuple[int, Result]]]):
|
||||
args:
|
||||
meta: the metadata from the rule
|
||||
source: the raw rule text
|
||||
"""
|
||||
doc = {
|
||||
"meta": meta,
|
||||
"rules": {},
|
||||
}
|
||||
|
||||
meta: RuleMetadata
|
||||
source: str
|
||||
matches: Tuple[Tuple[frz.Address, Match], ...]
|
||||
|
||||
|
||||
class ResultDocument(BaseModel):
|
||||
meta: Metadata
|
||||
rules: Dict[str, RuleMatches]
|
||||
|
||||
@classmethod
|
||||
def from_capa(cls, meta, rules: RuleSet, capabilities: MatchResults) -> "ResultDocument":
|
||||
rule_matches: Dict[str, RuleMatches] = {}
|
||||
for rule_name, matches in capabilities.items():
|
||||
rule = rules[rule_name]
|
||||
|
||||
if rule.meta.get("capa/subscope-rule"):
|
||||
continue
|
||||
|
||||
rule_meta = convert_meta_to_result_document(rule.meta)
|
||||
rule_matches[rule_name] = RuleMatches(
|
||||
meta=RuleMetadata.from_capa(rule),
|
||||
source=rule.definition,
|
||||
matches=[
|
||||
(frz.Address.from_capa(addr), Match.from_capa(rules, capabilities, match))
|
||||
for addr, match in matches
|
||||
],
|
||||
)
|
||||
|
||||
doc["rules"][rule_name] = {
|
||||
"meta": rule_meta,
|
||||
"source": rule.definition,
|
||||
"matches": {
|
||||
addr: convert_match_to_result_document(rules, capabilities, match) for (addr, match) in matches
|
||||
},
|
||||
}
|
||||
|
||||
return doc
|
||||
return ResultDocument(meta=Metadata.from_capa(meta), rules=rule_matches)
|
||||
|
||||
@@ -7,9 +7,12 @@
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import io
|
||||
from typing import Union, Iterator
|
||||
|
||||
import termcolor
|
||||
|
||||
import capa.render.result_document as rd
|
||||
|
||||
|
||||
def bold(s: str) -> str:
|
||||
"""draw attention to the given string"""
|
||||
@@ -29,42 +32,29 @@ def hex(n: int) -> str:
|
||||
return "0x%X" % n
|
||||
|
||||
|
||||
def parse_parts_id(s: str):
|
||||
id = ""
|
||||
parts = s.split("::")
|
||||
if len(parts) > 0:
|
||||
last = parts.pop()
|
||||
last, _, id = last.rpartition(" ")
|
||||
id = id.lstrip("[").rstrip("]")
|
||||
parts.append(last)
|
||||
return parts, id
|
||||
|
||||
|
||||
def format_parts_id(data):
|
||||
def format_parts_id(data: Union[rd.AttackSpec, rd.MBCSpec]):
|
||||
"""
|
||||
format canonical representation of ATT&CK/MBC parts and ID
|
||||
"""
|
||||
return "%s [%s]" % ("::".join(data["parts"]), data["id"])
|
||||
return "%s [%s]" % ("::".join(data.parts), data.id)
|
||||
|
||||
|
||||
def capability_rules(doc):
|
||||
def capability_rules(doc: rd.ResultDocument) -> Iterator[rd.RuleMatches]:
|
||||
"""enumerate the rules in (namespace, name) order that are 'capability' rules (not lib/subscope/disposition/etc)."""
|
||||
for (_, _, rule) in sorted(
|
||||
map(lambda rule: (rule["meta"].get("namespace", ""), rule["meta"]["name"], rule), doc["rules"].values())
|
||||
):
|
||||
if rule["meta"].get("lib"):
|
||||
for (_, _, rule) in sorted(map(lambda rule: (rule.meta.namespace or "", rule.meta.name, rule), doc.rules.values())):
|
||||
if rule.meta.lib:
|
||||
continue
|
||||
if rule["meta"].get("capa/subscope"):
|
||||
if rule.meta.is_subscope_rule:
|
||||
continue
|
||||
if rule["meta"].get("maec/analysis-conclusion"):
|
||||
if rule.meta.maec.analysis_conclusion:
|
||||
continue
|
||||
if rule["meta"].get("maec/analysis-conclusion-ov"):
|
||||
if rule.meta.maec.analysis_conclusion_ov:
|
||||
continue
|
||||
if rule["meta"].get("maec/malware-family"):
|
||||
if rule.meta.maec.malware_family:
|
||||
continue
|
||||
if rule["meta"].get("maec/malware-category"):
|
||||
if rule.meta.maec.malware_category:
|
||||
continue
|
||||
if rule["meta"].get("maec/malware-category-ov"):
|
||||
if rule.meta.maec.malware_category_ov:
|
||||
continue
|
||||
|
||||
yield rule
|
||||
|
||||
@@ -23,15 +23,39 @@ Unless required by applicable law or agreed to in writing, software distributed
|
||||
See the License for the specific language governing permissions and limitations under the License.
|
||||
"""
|
||||
import tabulate
|
||||
import dnfile.mdtable
|
||||
import dncil.clr.token
|
||||
|
||||
import capa.rules
|
||||
import capa.render.utils as rutils
|
||||
import capa.features.freeze as frz
|
||||
import capa.render.result_document
|
||||
import capa.render.result_document as rd
|
||||
from capa.rules import RuleSet
|
||||
from capa.engine import MatchResults
|
||||
|
||||
|
||||
def render_meta(ostream, doc):
|
||||
def format_address(address: frz.Address) -> str:
|
||||
if address.type == frz.AddressType.ABSOLUTE:
|
||||
return rutils.hex(address.value)
|
||||
elif address.type == frz.AddressType.RELATIVE:
|
||||
return f"base address+{rutils.hex(address.value)}"
|
||||
elif address.type == frz.AddressType.FILE:
|
||||
return f"file+{rutils.hex(address.value)}"
|
||||
elif address.type == frz.AddressType.DN_TOKEN:
|
||||
token = dncil.clr.token.Token(address.value)
|
||||
return f"token({rutils.hex(token.value)})"
|
||||
elif address.type == frz.AddressType.DN_TOKEN_OFFSET:
|
||||
token, offset = address.value
|
||||
token = dncil.clr.token.Token(token)
|
||||
return f"token({rutils.hex(token.value)})+{rutils.hex(offset)}"
|
||||
elif address.type == frz.AddressType.NO_ADDRESS:
|
||||
return "global"
|
||||
else:
|
||||
raise ValueError("unexpected address type")
|
||||
|
||||
|
||||
def render_meta(ostream, doc: rd.ResultDocument):
|
||||
"""
|
||||
like:
|
||||
|
||||
@@ -51,30 +75,31 @@ def render_meta(ostream, doc):
|
||||
total feature count 1918
|
||||
"""
|
||||
rows = [
|
||||
("md5", doc["meta"]["sample"]["md5"]),
|
||||
("sha1", doc["meta"]["sample"]["sha1"]),
|
||||
("sha256", doc["meta"]["sample"]["sha256"]),
|
||||
("path", doc["meta"]["sample"]["path"]),
|
||||
("timestamp", doc["meta"]["timestamp"]),
|
||||
("capa version", doc["meta"]["version"]),
|
||||
("os", doc["meta"]["analysis"]["os"]),
|
||||
("format", doc["meta"]["analysis"]["format"]),
|
||||
("arch", doc["meta"]["analysis"]["arch"]),
|
||||
("extractor", doc["meta"]["analysis"]["extractor"]),
|
||||
("base address", hex(doc["meta"]["analysis"]["base_address"])),
|
||||
("rules", doc["meta"]["analysis"]["rules"]),
|
||||
("function count", len(doc["meta"]["analysis"]["feature_counts"]["functions"])),
|
||||
("library function count", len(doc["meta"]["analysis"]["library_functions"])),
|
||||
("md5", doc.meta.sample.md5),
|
||||
("sha1", doc.meta.sample.sha1),
|
||||
("sha256", doc.meta.sample.sha256),
|
||||
("path", doc.meta.sample.path),
|
||||
("timestamp", doc.meta.timestamp),
|
||||
("capa version", doc.meta.version),
|
||||
("os", doc.meta.analysis.os),
|
||||
("format", doc.meta.analysis.format),
|
||||
("arch", doc.meta.analysis.arch),
|
||||
("extractor", doc.meta.analysis.extractor),
|
||||
("base address", format_address(doc.meta.analysis.base_address)),
|
||||
("rules", "\n".join(doc.meta.analysis.rules)),
|
||||
("function count", len(doc.meta.analysis.feature_counts.functions)),
|
||||
("library function count", len(doc.meta.analysis.library_functions)),
|
||||
(
|
||||
"total feature count",
|
||||
doc["meta"]["analysis"]["feature_counts"]["file"]
|
||||
+ sum(doc["meta"]["analysis"]["feature_counts"]["functions"].values()),
|
||||
doc.meta.analysis.feature_counts.file
|
||||
+ sum(map(lambda f: f.count, doc.meta.analysis.feature_counts.functions)),
|
||||
),
|
||||
]
|
||||
|
||||
ostream.writeln(tabulate.tabulate(rows, tablefmt="plain"))
|
||||
|
||||
|
||||
def render_rules(ostream, doc):
|
||||
def render_rules(ostream, doc: rd.ResultDocument):
|
||||
"""
|
||||
like:
|
||||
|
||||
@@ -87,28 +112,29 @@ def render_rules(ostream, doc):
|
||||
"""
|
||||
had_match = False
|
||||
for rule in rutils.capability_rules(doc):
|
||||
count = len(rule["matches"])
|
||||
count = len(rule.matches)
|
||||
if count == 1:
|
||||
capability = rutils.bold(rule["meta"]["name"])
|
||||
capability = rutils.bold(rule.meta.name)
|
||||
else:
|
||||
capability = "%s (%d matches)" % (rutils.bold(rule["meta"]["name"]), count)
|
||||
capability = "%s (%d matches)" % (rutils.bold(rule.meta.name), count)
|
||||
|
||||
ostream.writeln(capability)
|
||||
had_match = True
|
||||
|
||||
rows = []
|
||||
for key in ("namespace", "description", "scope"):
|
||||
if key == "name" or key not in rule["meta"]:
|
||||
v = getattr(rule.meta, key)
|
||||
if not v:
|
||||
continue
|
||||
|
||||
v = rule["meta"][key]
|
||||
if isinstance(v, list) and len(v) == 1:
|
||||
v = v[0]
|
||||
|
||||
rows.append((key, v))
|
||||
|
||||
if rule["meta"]["scope"] != capa.rules.FILE_SCOPE:
|
||||
locations = doc["rules"][rule["meta"]["name"]]["matches"].keys()
|
||||
rows.append(("matches", "\n".join(map(rutils.hex, locations))))
|
||||
if rule.meta.scope != capa.rules.FILE_SCOPE:
|
||||
locations = list(map(lambda m: m[0], doc.rules[rule.meta.name].matches))
|
||||
rows.append(("matches", "\n".join(map(format_address, locations))))
|
||||
|
||||
ostream.writeln(tabulate.tabulate(rows, tablefmt="plain"))
|
||||
ostream.write("\n")
|
||||
@@ -117,7 +143,7 @@ def render_rules(ostream, doc):
|
||||
ostream.writeln(rutils.bold("no capabilities found"))
|
||||
|
||||
|
||||
def render_verbose(doc):
|
||||
def render_verbose(doc: rd.ResultDocument):
|
||||
ostream = rutils.StringIO()
|
||||
|
||||
render_meta(ostream, doc)
|
||||
@@ -130,5 +156,4 @@ def render_verbose(doc):
|
||||
|
||||
|
||||
def render(meta, rules: RuleSet, capabilities: MatchResults) -> str:
|
||||
doc = capa.render.result_document.convert_capabilities_to_result_document(meta, rules, capabilities)
|
||||
return render_verbose(doc)
|
||||
return render_verbose(rd.ResultDocument.from_capa(meta, rules, capabilities))
|
||||
|
||||
@@ -6,96 +6,135 @@
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
from typing import Dict, List, Iterable
|
||||
|
||||
import tabulate
|
||||
|
||||
import capa.rules
|
||||
import capa.render.utils as rutils
|
||||
import capa.render.verbose
|
||||
import capa.features.common
|
||||
import capa.render.result_document
|
||||
import capa.features.freeze as frz
|
||||
import capa.features.address
|
||||
import capa.render.result_document as rd
|
||||
import capa.features.freeze.features as frzf
|
||||
from capa.rules import RuleSet
|
||||
from capa.engine import MatchResults
|
||||
|
||||
|
||||
def render_locations(ostream, match):
|
||||
def render_locations(ostream, locations: Iterable[frz.Address]):
|
||||
import capa.render.verbose as v
|
||||
|
||||
# its possible to have an empty locations array here,
|
||||
# such as when we're in MODE_FAILURE and showing the logic
|
||||
# under a `not` statement (which will have no matched locations).
|
||||
locations = list(sorted(match.get("locations", [])))
|
||||
locations = list(sorted(locations))
|
||||
|
||||
if len(locations) == 0:
|
||||
return
|
||||
|
||||
ostream.write(" @ ")
|
||||
|
||||
if len(locations) == 1:
|
||||
ostream.write(" @ ")
|
||||
ostream.write(rutils.hex(locations[0]))
|
||||
elif len(locations) > 1:
|
||||
ostream.write(" @ ")
|
||||
if len(locations) > 4:
|
||||
ostream.write(v.format_address(locations[0]))
|
||||
|
||||
elif len(locations) > 4:
|
||||
# don't display too many locations, because it becomes very noisy.
|
||||
# probably only the first handful of locations will be useful for inspection.
|
||||
ostream.write(", ".join(map(rutils.hex, locations[0:4])))
|
||||
ostream.write(", ".join(map(v.format_address, locations[0:4])))
|
||||
ostream.write(", and %d more..." % (len(locations) - 4))
|
||||
|
||||
elif len(locations) > 1:
|
||||
ostream.write(", ".join(map(v.format_address, locations)))
|
||||
|
||||
else:
|
||||
ostream.write(", ".join(map(rutils.hex, locations)))
|
||||
raise RuntimeError("unreachable")
|
||||
|
||||
|
||||
def render_statement(ostream, match, statement, indent=0):
|
||||
def render_statement(ostream, match: rd.Match, statement: rd.Statement, indent=0):
|
||||
ostream.write(" " * indent)
|
||||
if statement["type"] in ("and", "or", "optional", "not", "subscope"):
|
||||
ostream.write(statement["type"])
|
||||
|
||||
if isinstance(statement, rd.SubscopeStatement):
|
||||
# emit `basic block:`
|
||||
# rather than `subscope:`
|
||||
ostream.write(statement.scope)
|
||||
|
||||
ostream.write(":")
|
||||
if statement.get("description"):
|
||||
ostream.write(" = %s" % statement["description"])
|
||||
if statement.description:
|
||||
ostream.write(" = %s" % statement.description)
|
||||
ostream.writeln("")
|
||||
elif statement["type"] == "some":
|
||||
ostream.write("%d or more:" % (statement["count"]))
|
||||
if statement.get("description"):
|
||||
ostream.write(" = %s" % statement["description"])
|
||||
|
||||
elif isinstance(statement, (rd.AndStatement, rd.OrStatement, rd.OptionalStatement, rd.NotStatement)):
|
||||
# emit `and:` `or:` `optional:` `not:`
|
||||
ostream.write(statement.type)
|
||||
|
||||
ostream.write(":")
|
||||
if statement.description:
|
||||
ostream.write(" = %s" % statement.description)
|
||||
ostream.writeln("")
|
||||
elif statement["type"] == "range":
|
||||
|
||||
elif isinstance(statement, rd.SomeStatement):
|
||||
ostream.write("%d or more:" % (statement.count))
|
||||
|
||||
if statement.description:
|
||||
ostream.write(" = %s" % statement.description)
|
||||
ostream.writeln("")
|
||||
|
||||
elif isinstance(statement, rd.RangeStatement):
|
||||
# `range` is a weird node, its almost a hybrid of statement+feature.
|
||||
# it is a specific feature repeated multiple times.
|
||||
# there's no additional logic in the feature part, just the existence of a feature.
|
||||
# so, we have to inline some of the feature rendering here.
|
||||
|
||||
child = statement["child"]
|
||||
child = statement.child
|
||||
value = getattr(child, child.type)
|
||||
|
||||
if value:
|
||||
if isinstance(child, frzf.StringFeature):
|
||||
value = '"%s"' % capa.features.common.escape_string(value)
|
||||
|
||||
if child[child["type"]]:
|
||||
if child["type"] == "string":
|
||||
value = '"%s"' % capa.features.common.escape_string(child[child["type"]])
|
||||
else:
|
||||
value = child[child["type"]]
|
||||
value = rutils.bold2(value)
|
||||
if child.get("description"):
|
||||
ostream.write("count(%s(%s = %s)): " % (child["type"], value, child["description"]))
|
||||
else:
|
||||
ostream.write("count(%s(%s)): " % (child["type"], value))
|
||||
else:
|
||||
ostream.write("count(%s): " % child["type"])
|
||||
|
||||
if statement["max"] == statement["min"]:
|
||||
ostream.write("%d" % (statement["min"]))
|
||||
elif statement["min"] == 0:
|
||||
ostream.write("%d or fewer" % (statement["max"]))
|
||||
elif statement["max"] == (1 << 64 - 1):
|
||||
ostream.write("%d or more" % (statement["min"]))
|
||||
if child.description:
|
||||
ostream.write("count(%s(%s = %s)): " % (child.type, value, child.description))
|
||||
else:
|
||||
ostream.write("between %d and %d" % (statement["min"], statement["max"]))
|
||||
ostream.write("count(%s(%s)): " % (child.type, value))
|
||||
else:
|
||||
ostream.write("count(%s): " % child.type)
|
||||
|
||||
if statement.get("description"):
|
||||
ostream.write(" = %s" % statement["description"])
|
||||
render_locations(ostream, match)
|
||||
if statement.max == statement.min:
|
||||
ostream.write("%d" % (statement.min))
|
||||
elif statement.min == 0:
|
||||
ostream.write("%d or fewer" % (statement.max))
|
||||
elif statement.max == (1 << 64 - 1):
|
||||
ostream.write("%d or more" % (statement.min))
|
||||
else:
|
||||
ostream.write("between %d and %d" % (statement.min, statement.max))
|
||||
|
||||
if statement.description:
|
||||
ostream.write(" = %s" % statement.description)
|
||||
render_locations(ostream, match.locations)
|
||||
ostream.writeln("")
|
||||
|
||||
else:
|
||||
raise RuntimeError("unexpected match statement type: " + str(statement))
|
||||
|
||||
|
||||
def render_string_value(s):
|
||||
def render_string_value(s: str) -> str:
|
||||
return '"%s"' % capa.features.common.escape_string(s)
|
||||
|
||||
|
||||
def render_feature(ostream, match, feature, indent=0):
|
||||
def render_feature(ostream, match: rd.Match, feature: frzf.Feature, indent=0):
|
||||
ostream.write(" " * indent)
|
||||
|
||||
key = feature["type"]
|
||||
value = feature[feature["type"]]
|
||||
key = feature.type
|
||||
if isinstance(feature, frzf.ImportFeature):
|
||||
# fixup access to Python reserved name
|
||||
value = feature.import_
|
||||
if isinstance(feature, frzf.ClassFeature):
|
||||
value = feature.class_
|
||||
else:
|
||||
value = getattr(feature, key)
|
||||
|
||||
if key not in ("regex", "substring"):
|
||||
# like:
|
||||
@@ -103,18 +142,22 @@ def render_feature(ostream, match, feature, indent=0):
|
||||
if key == "string":
|
||||
value = render_string_value(value)
|
||||
|
||||
if key == "number":
|
||||
assert isinstance(value, int)
|
||||
value = hex(value)
|
||||
|
||||
ostream.write(key)
|
||||
ostream.write(": ")
|
||||
|
||||
if value:
|
||||
ostream.write(rutils.bold2(value))
|
||||
|
||||
if "description" in feature:
|
||||
if feature.description:
|
||||
ostream.write(capa.rules.DESCRIPTION_SEPARATOR)
|
||||
ostream.write(feature["description"])
|
||||
ostream.write(feature.description)
|
||||
|
||||
if key not in ("os", "arch"):
|
||||
render_locations(ostream, match)
|
||||
render_locations(ostream, match.locations)
|
||||
ostream.write("\n")
|
||||
else:
|
||||
# like:
|
||||
@@ -126,19 +169,19 @@ def render_feature(ostream, match, feature, indent=0):
|
||||
ostream.write(value)
|
||||
ostream.write("\n")
|
||||
|
||||
for match, locations in sorted(feature["matches"].items(), key=lambda p: p[0]):
|
||||
for capture, locations in sorted(match.captures.items()):
|
||||
ostream.write(" " * (indent + 1))
|
||||
ostream.write("- ")
|
||||
ostream.write(rutils.bold2(render_string_value(match)))
|
||||
render_locations(ostream, {"locations": locations})
|
||||
ostream.write(rutils.bold2(render_string_value(capture)))
|
||||
render_locations(ostream, locations)
|
||||
ostream.write("\n")
|
||||
|
||||
|
||||
def render_node(ostream, match, node, indent=0):
|
||||
if node["type"] == "statement":
|
||||
render_statement(ostream, match, node["statement"], indent=indent)
|
||||
elif node["type"] == "feature":
|
||||
render_feature(ostream, match, node["feature"], indent=indent)
|
||||
def render_node(ostream, match: rd.Match, node: rd.Node, indent=0):
|
||||
if isinstance(node, rd.StatementNode):
|
||||
render_statement(ostream, match, node.statement, indent=indent)
|
||||
elif isinstance(node, rd.FeatureNode):
|
||||
render_feature(ostream, match, node.feature, indent=indent)
|
||||
else:
|
||||
raise RuntimeError("unexpected node type: " + str(node))
|
||||
|
||||
@@ -151,42 +194,45 @@ MODE_SUCCESS = "success"
|
||||
MODE_FAILURE = "failure"
|
||||
|
||||
|
||||
def render_match(ostream, match, indent=0, mode=MODE_SUCCESS):
|
||||
def render_match(ostream, match: rd.Match, indent=0, mode=MODE_SUCCESS):
|
||||
child_mode = mode
|
||||
if mode == MODE_SUCCESS:
|
||||
# display only nodes that evaluated successfully.
|
||||
if not match["success"]:
|
||||
if not match.success:
|
||||
return
|
||||
|
||||
# optional statement with no successful children is empty
|
||||
if match["node"].get("statement", {}).get("type") == "optional" and not any(
|
||||
map(lambda m: m["success"], match["children"])
|
||||
):
|
||||
if isinstance(match.node, rd.StatementNode) and isinstance(match.node.statement, rd.OptionalStatement):
|
||||
if not any(map(lambda m: m.success, match.children)):
|
||||
return
|
||||
|
||||
# not statement, so invert the child mode to show failed evaluations
|
||||
if match["node"].get("statement", {}).get("type") == "not":
|
||||
if isinstance(match.node, rd.StatementNode) and isinstance(match.node.statement, rd.NotStatement):
|
||||
child_mode = MODE_FAILURE
|
||||
|
||||
elif mode == MODE_FAILURE:
|
||||
# display only nodes that did not evaluate to True
|
||||
if match["success"]:
|
||||
if match.success:
|
||||
return
|
||||
|
||||
# optional statement with successful children is not relevant
|
||||
if match["node"].get("statement", {}).get("type") == "optional" and any(
|
||||
map(lambda m: m["success"], match["children"])
|
||||
):
|
||||
if isinstance(match.node, rd.StatementNode) and isinstance(match.node.statement, rd.OptionalStatement):
|
||||
if any(map(lambda m: m.success, match.children)):
|
||||
return
|
||||
|
||||
# not statement, so invert the child mode to show successful evaluations
|
||||
if match["node"].get("statement", {}).get("type") == "not":
|
||||
if isinstance(match.node, rd.StatementNode) and isinstance(match.node.statement, rd.NotStatement):
|
||||
child_mode = MODE_SUCCESS
|
||||
else:
|
||||
raise RuntimeError("unexpected mode: " + mode)
|
||||
|
||||
render_node(ostream, match, match["node"], indent=indent)
|
||||
render_node(ostream, match, match.node, indent=indent)
|
||||
|
||||
for child in match["children"]:
|
||||
for child in match.children:
|
||||
render_match(ostream, child, indent=indent + 1, mode=child_mode)
|
||||
|
||||
|
||||
def render_rules(ostream, doc):
|
||||
def render_rules(ostream, doc: rd.ResultDocument):
|
||||
"""
|
||||
like:
|
||||
|
||||
@@ -196,66 +242,97 @@ def render_rules(ostream, doc):
|
||||
author michael.hunhoff@mandiant.com
|
||||
scope function
|
||||
mbc Anti-Behavioral Analysis::Detect Debugger::OutputDebugString
|
||||
examples Practical Malware Analysis Lab 16-02.exe_:0x401020
|
||||
function @ 0x10004706
|
||||
and:
|
||||
api: kernel32.SetLastError @ 0x100047C2
|
||||
api: kernel32.GetLastError @ 0x10004A87
|
||||
api: kernel32.OutputDebugString @ 0x10004767, 0x10004787, 0x10004816, 0x10004895
|
||||
"""
|
||||
functions_by_bb = {}
|
||||
for function, info in doc["meta"]["analysis"]["layout"]["functions"].items():
|
||||
for bb in info["matched_basic_blocks"]:
|
||||
functions_by_bb[bb] = function
|
||||
functions_by_bb: Dict[capa.features.address.Address, capa.features.address.Address] = {}
|
||||
for finfo in doc.meta.analysis.layout.functions:
|
||||
faddress = finfo.address.to_capa()
|
||||
|
||||
for bb in finfo.matched_basic_blocks:
|
||||
bbaddress = bb.address.to_capa()
|
||||
functions_by_bb[bbaddress] = faddress
|
||||
|
||||
had_match = False
|
||||
for rule in rutils.capability_rules(doc):
|
||||
count = len(rule["matches"])
|
||||
|
||||
for (_, _, rule) in sorted(map(lambda rule: (rule.meta.namespace or "", rule.meta.name, rule), doc.rules.values())):
|
||||
# default scope hides things like lib rules, malware-category rules, etc.
|
||||
# but in vverbose mode, we really want to show everything.
|
||||
#
|
||||
# still ignore subscope rules because they're stitched into the final document.
|
||||
if rule.meta.is_subscope_rule:
|
||||
continue
|
||||
|
||||
count = len(rule.matches)
|
||||
if count == 1:
|
||||
capability = rutils.bold(rule["meta"]["name"])
|
||||
capability = rutils.bold(rule.meta.name)
|
||||
else:
|
||||
capability = "%s (%d matches)" % (rutils.bold(rule["meta"]["name"]), count)
|
||||
capability = "%s (%d matches)" % (rutils.bold(rule.meta.name), count)
|
||||
|
||||
ostream.writeln(capability)
|
||||
had_match = True
|
||||
|
||||
rows = []
|
||||
for key in capa.rules.META_KEYS:
|
||||
if key == "name" or key not in rule["meta"]:
|
||||
continue
|
||||
rows.append(("namespace", rule.meta.namespace))
|
||||
|
||||
v = rule["meta"][key]
|
||||
if not v:
|
||||
continue
|
||||
if rule.meta.maec.analysis_conclusion or rule.meta.maec.analysis_conclusion_ov:
|
||||
rows.append(
|
||||
(
|
||||
"maec/analysis-conclusion",
|
||||
rule.meta.maec.analysis_conclusion or rule.meta.maec.analysis_conclusion_ov,
|
||||
)
|
||||
)
|
||||
|
||||
if key in ("att&ck", "mbc"):
|
||||
v = [rutils.format_parts_id(vv) for vv in v]
|
||||
if rule.meta.maec.malware_family:
|
||||
rows.append(("maec/malware-family", rule.meta.maec.malware_family))
|
||||
|
||||
if isinstance(v, list) and len(v) == 1:
|
||||
v = v[0]
|
||||
elif isinstance(v, list) and len(v) > 1:
|
||||
v = ", ".join(v)
|
||||
rows.append((key, v))
|
||||
if rule.meta.maec.malware_category or rule.meta.maec.malware_category:
|
||||
rows.append(
|
||||
("maec/malware-category", rule.meta.maec.malware_category or rule.meta.maec.malware_category_ov)
|
||||
)
|
||||
|
||||
rows.append(("author", ", ".join(rule.meta.authors)))
|
||||
|
||||
rows.append(("scope", rule.meta.scope.value))
|
||||
|
||||
if rule.meta.attack:
|
||||
rows.append(("att&ck", ", ".join([rutils.format_parts_id(v) for v in rule.meta.attack])))
|
||||
|
||||
if rule.meta.mbc:
|
||||
rows.append(("mbc", ", ".join([rutils.format_parts_id(v) for v in rule.meta.mbc])))
|
||||
|
||||
if rule.meta.references:
|
||||
rows.append(("references", ", ".join(rule.meta.references)))
|
||||
|
||||
if rule.meta.description:
|
||||
rows.append(("description", rule.meta.description))
|
||||
|
||||
ostream.writeln(tabulate.tabulate(rows, tablefmt="plain"))
|
||||
|
||||
if rule["meta"]["scope"] == capa.rules.FILE_SCOPE:
|
||||
matches = list(doc["rules"][rule["meta"]["name"]]["matches"].values())
|
||||
if rule.meta.scope == capa.rules.FILE_SCOPE:
|
||||
matches = doc.rules[rule.meta.name].matches
|
||||
if len(matches) != 1:
|
||||
# i think there should only ever be one match per file-scope rule,
|
||||
# because we do the file-scope evaluation a single time.
|
||||
# but i'm not 100% sure if this is/will always be true.
|
||||
# so, lets be explicit about our assumptions and raise an exception if they fail.
|
||||
raise RuntimeError("unexpected file scope match count: %d" % (len(matches)))
|
||||
render_match(ostream, matches[0], indent=0)
|
||||
first_address, first_match = matches[0]
|
||||
render_match(ostream, first_match, indent=0)
|
||||
else:
|
||||
for location, match in sorted(doc["rules"][rule["meta"]["name"]]["matches"].items()):
|
||||
ostream.write(rule["meta"]["scope"])
|
||||
for location, match in sorted(doc.rules[rule.meta.name].matches):
|
||||
ostream.write(rule.meta.scope)
|
||||
ostream.write(" @ ")
|
||||
ostream.write(rutils.hex(location))
|
||||
ostream.write(capa.render.verbose.format_address(location))
|
||||
|
||||
if rule["meta"]["scope"] == capa.rules.BASIC_BLOCK_SCOPE:
|
||||
ostream.write(" in function " + rutils.hex(functions_by_bb[location]))
|
||||
if rule.meta.scope == capa.rules.BASIC_BLOCK_SCOPE:
|
||||
ostream.write(
|
||||
" in function "
|
||||
+ capa.render.verbose.format_address(frz.Address.from_capa(functions_by_bb[location.to_capa()]))
|
||||
)
|
||||
|
||||
ostream.write("\n")
|
||||
render_match(ostream, match, indent=1)
|
||||
@@ -265,7 +342,7 @@ def render_rules(ostream, doc):
|
||||
ostream.writeln(rutils.bold("no capabilities found"))
|
||||
|
||||
|
||||
def render_vverbose(doc):
|
||||
def render_vverbose(doc: rd.ResultDocument):
|
||||
ostream = rutils.StringIO()
|
||||
|
||||
capa.render.verbose.render_meta(ostream, doc)
|
||||
@@ -278,5 +355,4 @@ def render_vverbose(doc):
|
||||
|
||||
|
||||
def render(meta, rules: RuleSet, capabilities: MatchResults) -> str:
|
||||
doc = capa.render.result_document.convert_capabilities_to_result_document(meta, rules, capabilities)
|
||||
return render_vverbose(doc)
|
||||
return render_vverbose(rd.ResultDocument.from_capa(meta, rules, capabilities))
|
||||
|
||||
178
capa/rules.py
178
capa/rules.py
@@ -12,7 +12,6 @@ import uuid
|
||||
import codecs
|
||||
import logging
|
||||
import binascii
|
||||
import functools
|
||||
import collections
|
||||
from enum import Enum
|
||||
|
||||
@@ -40,6 +39,7 @@ import capa.features.common
|
||||
import capa.features.basicblock
|
||||
from capa.engine import Statement, FeatureSet
|
||||
from capa.features.common import MAX_BYTES_FEATURE_SIZE, Feature
|
||||
from capa.features.address import Address
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -48,13 +48,12 @@ logger = logging.getLogger(__name__)
|
||||
META_KEYS = (
|
||||
"name",
|
||||
"namespace",
|
||||
"rule-category",
|
||||
"maec/analysis-conclusion",
|
||||
"maec/analysis-conclusion-ov",
|
||||
"maec/malware-family",
|
||||
"maec/malware-category",
|
||||
"maec/malware-category-ov",
|
||||
"author",
|
||||
"authors",
|
||||
"description",
|
||||
"lib",
|
||||
"scope",
|
||||
@@ -74,14 +73,25 @@ class Scope(str, Enum):
|
||||
FILE = "file"
|
||||
FUNCTION = "function"
|
||||
BASIC_BLOCK = "basic block"
|
||||
INSTRUCTION = "instruction"
|
||||
|
||||
|
||||
FILE_SCOPE = Scope.FILE.value
|
||||
FUNCTION_SCOPE = Scope.FUNCTION.value
|
||||
BASIC_BLOCK_SCOPE = Scope.BASIC_BLOCK.value
|
||||
INSTRUCTION_SCOPE = Scope.INSTRUCTION.value
|
||||
# used only to specify supported features per scope.
|
||||
# not used to validate rules.
|
||||
GLOBAL_SCOPE = "global"
|
||||
|
||||
|
||||
SUPPORTED_FEATURES = {
|
||||
SUPPORTED_FEATURES: Dict[str, Set] = {
|
||||
GLOBAL_SCOPE: {
|
||||
# these will be added to other scopes, see below.
|
||||
capa.features.common.OS,
|
||||
capa.features.common.Arch,
|
||||
capa.features.common.Format,
|
||||
},
|
||||
FILE_SCOPE: {
|
||||
capa.features.common.MatchedRule,
|
||||
capa.features.file.Export,
|
||||
@@ -90,21 +100,26 @@ SUPPORTED_FEATURES = {
|
||||
capa.features.file.FunctionName,
|
||||
capa.features.common.Characteristic("embedded pe"),
|
||||
capa.features.common.String,
|
||||
capa.features.common.Format,
|
||||
capa.features.common.OS,
|
||||
capa.features.common.Arch,
|
||||
capa.features.common.Class,
|
||||
capa.features.common.Namespace,
|
||||
capa.features.common.Characteristic("mixed mode"),
|
||||
},
|
||||
FUNCTION_SCOPE: {
|
||||
# plus basic block scope features, see below
|
||||
capa.features.common.MatchedRule,
|
||||
capa.features.basicblock.BasicBlock,
|
||||
capa.features.common.Characteristic("calls from"),
|
||||
capa.features.common.Characteristic("calls to"),
|
||||
capa.features.common.Characteristic("loop"),
|
||||
capa.features.common.Characteristic("recursive call"),
|
||||
capa.features.common.OS,
|
||||
capa.features.common.Arch,
|
||||
# plus basic block scope features, see below
|
||||
},
|
||||
BASIC_BLOCK_SCOPE: {
|
||||
capa.features.common.MatchedRule,
|
||||
capa.features.common.Characteristic("tight loop"),
|
||||
capa.features.common.Characteristic("stack string"),
|
||||
# plus instruction scope features, see below
|
||||
},
|
||||
INSTRUCTION_SCOPE: {
|
||||
capa.features.common.MatchedRule,
|
||||
capa.features.insn.API,
|
||||
capa.features.insn.Number,
|
||||
@@ -112,19 +127,29 @@ SUPPORTED_FEATURES = {
|
||||
capa.features.common.Bytes,
|
||||
capa.features.insn.Offset,
|
||||
capa.features.insn.Mnemonic,
|
||||
capa.features.insn.OperandNumber,
|
||||
capa.features.insn.OperandOffset,
|
||||
capa.features.common.Characteristic("nzxor"),
|
||||
capa.features.common.Characteristic("peb access"),
|
||||
capa.features.common.Characteristic("fs access"),
|
||||
capa.features.common.Characteristic("gs access"),
|
||||
capa.features.common.Characteristic("cross section flow"),
|
||||
capa.features.common.Characteristic("tight loop"),
|
||||
capa.features.common.Characteristic("stack string"),
|
||||
capa.features.common.Characteristic("indirect call"),
|
||||
capa.features.common.OS,
|
||||
capa.features.common.Arch,
|
||||
capa.features.common.Characteristic("call $+5"),
|
||||
capa.features.common.Characteristic("cross section flow"),
|
||||
capa.features.common.Characteristic("unmanaged call"),
|
||||
capa.features.common.Class,
|
||||
capa.features.common.Namespace,
|
||||
},
|
||||
}
|
||||
|
||||
# global scope features are available in all other scopes
|
||||
SUPPORTED_FEATURES[INSTRUCTION_SCOPE].update(SUPPORTED_FEATURES[GLOBAL_SCOPE])
|
||||
SUPPORTED_FEATURES[BASIC_BLOCK_SCOPE].update(SUPPORTED_FEATURES[GLOBAL_SCOPE])
|
||||
SUPPORTED_FEATURES[FUNCTION_SCOPE].update(SUPPORTED_FEATURES[GLOBAL_SCOPE])
|
||||
SUPPORTED_FEATURES[FILE_SCOPE].update(SUPPORTED_FEATURES[GLOBAL_SCOPE])
|
||||
|
||||
# all instruction scope features are also basic block features
|
||||
SUPPORTED_FEATURES[BASIC_BLOCK_SCOPE].update(SUPPORTED_FEATURES[INSTRUCTION_SCOPE])
|
||||
# all basic block scope features are also function scope features
|
||||
SUPPORTED_FEATURES[FUNCTION_SCOPE].update(SUPPORTED_FEATURES[BASIC_BLOCK_SCOPE])
|
||||
|
||||
@@ -237,20 +262,8 @@ def parse_feature(key: str):
|
||||
return capa.features.common.Bytes
|
||||
elif key == "number":
|
||||
return capa.features.insn.Number
|
||||
elif key.startswith("number/"):
|
||||
bitness = key.partition("/")[2]
|
||||
# the other handlers here return constructors for features,
|
||||
# and we want to as well,
|
||||
# however, we need to preconfigure one of the arguments (`bitness`).
|
||||
# so, instead we return a partially-applied function that
|
||||
# provides `bitness` to the feature constructor.
|
||||
# it forwards any other arguments provided to the closure along to the constructor.
|
||||
return functools.partial(capa.features.insn.Number, bitness=bitness)
|
||||
elif key == "offset":
|
||||
return capa.features.insn.Offset
|
||||
elif key.startswith("offset/"):
|
||||
bitness = key.partition("/")[2]
|
||||
return functools.partial(capa.features.insn.Offset, bitness=bitness)
|
||||
elif key == "mnemonic":
|
||||
return capa.features.insn.Mnemonic
|
||||
elif key == "basic blocks":
|
||||
@@ -272,8 +285,11 @@ def parse_feature(key: str):
|
||||
elif key == "format":
|
||||
return capa.features.common.Format
|
||||
elif key == "arch":
|
||||
|
||||
return capa.features.common.Arch
|
||||
elif key == "class":
|
||||
return capa.features.common.Class
|
||||
elif key == "namespace":
|
||||
return capa.features.common.Namespace
|
||||
else:
|
||||
raise InvalidRule("unexpected statement: %s" % key)
|
||||
|
||||
@@ -340,7 +356,14 @@ def parse_description(s: Union[str, int, bytes], value_type: str, description=No
|
||||
# the string "10" that needs to become the number 10.
|
||||
if value_type == "bytes":
|
||||
value = parse_bytes(value)
|
||||
elif value_type in ("number", "offset") or value_type.startswith(("number/", "offset/")):
|
||||
elif (
|
||||
value_type in ("number", "offset")
|
||||
or value_type.startswith(("number/", "offset/"))
|
||||
or (
|
||||
value_type.startswith("operand[")
|
||||
and (value_type.endswith("].number") or value_type.endswith("].offset"))
|
||||
)
|
||||
):
|
||||
try:
|
||||
value = parse_int(value)
|
||||
except ValueError:
|
||||
@@ -418,7 +441,7 @@ def build_statements(d, scope: str):
|
||||
if len(d[key]) != 1:
|
||||
raise InvalidRule("subscope must have exactly one child statement")
|
||||
|
||||
return ceng.Subscope(FUNCTION_SCOPE, build_statements(d[key][0], FUNCTION_SCOPE))
|
||||
return ceng.Subscope(FUNCTION_SCOPE, build_statements(d[key][0], FUNCTION_SCOPE), description=description)
|
||||
|
||||
elif key == "basic block":
|
||||
if scope != FUNCTION_SCOPE:
|
||||
@@ -427,7 +450,30 @@ def build_statements(d, scope: str):
|
||||
if len(d[key]) != 1:
|
||||
raise InvalidRule("subscope must have exactly one child statement")
|
||||
|
||||
return ceng.Subscope(BASIC_BLOCK_SCOPE, build_statements(d[key][0], BASIC_BLOCK_SCOPE))
|
||||
return ceng.Subscope(BASIC_BLOCK_SCOPE, build_statements(d[key][0], BASIC_BLOCK_SCOPE), description=description)
|
||||
|
||||
elif key == "instruction":
|
||||
if scope not in (FUNCTION_SCOPE, BASIC_BLOCK_SCOPE):
|
||||
raise InvalidRule("instruction subscope supported only for function and basic block scope")
|
||||
|
||||
if len(d[key]) == 1:
|
||||
statements = build_statements(d[key][0], INSTRUCTION_SCOPE)
|
||||
else:
|
||||
# for instruction subscopes, we support a shorthand in which the top level AND is implied.
|
||||
# the following are equivalent:
|
||||
#
|
||||
# - instruction:
|
||||
# - and:
|
||||
# - arch: i386
|
||||
# - mnemonic: cmp
|
||||
#
|
||||
# - instruction:
|
||||
# - arch: i386
|
||||
# - mnemonic: cmp
|
||||
#
|
||||
statements = ceng.And([build_statements(dd, INSTRUCTION_SCOPE) for dd in d[key]])
|
||||
|
||||
return ceng.Subscope(INSTRUCTION_SCOPE, statements, description=description)
|
||||
|
||||
elif key.startswith("count(") and key.endswith(")"):
|
||||
# e.g.:
|
||||
@@ -484,6 +530,37 @@ def build_statements(d, scope: str):
|
||||
raise InvalidRule("unexpected range: %s" % (count))
|
||||
elif key == "string" and not isinstance(d[key], str):
|
||||
raise InvalidRule("ambiguous string value %s, must be defined as explicit string" % d[key])
|
||||
|
||||
elif key.startswith("operand[") and key.endswith("].number"):
|
||||
index = key[len("operand[") : -len("].number")]
|
||||
try:
|
||||
index = int(index)
|
||||
except ValueError:
|
||||
raise InvalidRule("operand index must be an integer")
|
||||
|
||||
value, description = parse_description(d[key], key, d.get("description"))
|
||||
try:
|
||||
feature = capa.features.insn.OperandNumber(index, value, description=description)
|
||||
except ValueError as e:
|
||||
raise InvalidRule(str(e))
|
||||
ensure_feature_valid_for_scope(scope, feature)
|
||||
return feature
|
||||
|
||||
elif key.startswith("operand[") and key.endswith("].offset"):
|
||||
index = key[len("operand[") : -len("].offset")]
|
||||
try:
|
||||
index = int(index)
|
||||
except ValueError:
|
||||
raise InvalidRule("operand index must be an integer")
|
||||
|
||||
value, description = parse_description(d[key], key, d.get("description"))
|
||||
try:
|
||||
feature = capa.features.insn.OperandOffset(index, value, description=description)
|
||||
except ValueError as e:
|
||||
raise InvalidRule(str(e))
|
||||
ensure_feature_valid_for_scope(scope, feature)
|
||||
return feature
|
||||
|
||||
elif (
|
||||
(key == "os" and d[key] not in capa.features.common.VALID_OS)
|
||||
or (key == "format" and d[key] not in capa.features.common.VALID_FORMAT)
|
||||
@@ -607,6 +684,9 @@ class Rule:
|
||||
for new_rule in self._extract_subscope_rules_rec(child):
|
||||
yield new_rule
|
||||
|
||||
def is_subscope_rule(self):
|
||||
return bool(self.meta.get("capa/subscope-rule", False))
|
||||
|
||||
def extract_subscope_rules(self):
|
||||
"""
|
||||
scan through the statements of this rule,
|
||||
@@ -977,6 +1057,7 @@ class RuleSet:
|
||||
self.file_rules = self._get_rules_for_scope(rules, FILE_SCOPE)
|
||||
self.function_rules = self._get_rules_for_scope(rules, FUNCTION_SCOPE)
|
||||
self.basic_block_rules = self._get_rules_for_scope(rules, BASIC_BLOCK_SCOPE)
|
||||
self.instruction_rules = self._get_rules_for_scope(rules, INSTRUCTION_SCOPE)
|
||||
self.rules = {rule.name: rule for rule in rules}
|
||||
self.rules_by_namespace = index_rules_by_namespace(rules)
|
||||
|
||||
@@ -988,6 +1069,9 @@ class RuleSet:
|
||||
(self._easy_basic_block_rules_by_feature, self._hard_basic_block_rules) = self._index_rules_by_feature(
|
||||
self.basic_block_rules
|
||||
)
|
||||
(self._easy_instruction_rules_by_feature, self._hard_instruction_rules) = self._index_rules_by_feature(
|
||||
self.instruction_rules
|
||||
)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.rules)
|
||||
@@ -1013,6 +1097,9 @@ class RuleSet:
|
||||
at this time, a rule evaluator can't do anything special with
|
||||
the "hard rules". it must still do a full top-down match of each
|
||||
rule, in topological order.
|
||||
|
||||
this does not index global features, because these are not selective, and
|
||||
won't be used as the sole feature used to match.
|
||||
"""
|
||||
|
||||
# we'll do a couple phases:
|
||||
@@ -1051,6 +1138,18 @@ class RuleSet:
|
||||
# hard feature: requires scan or match lookup
|
||||
rules_with_hard_features.add(rule_name)
|
||||
elif isinstance(node, capa.features.common.Feature):
|
||||
if capa.features.common.is_global_feature(node):
|
||||
# we don't want to index global features
|
||||
# because they're not very selective.
|
||||
#
|
||||
# they're global, so if they match at one location in a file,
|
||||
# they'll match at every location in a file.
|
||||
# so thats not helpful to decide how to downselect.
|
||||
#
|
||||
# and, a global rule will never be the sole selector in a rule.
|
||||
# TODO: probably want a lint for this.
|
||||
pass
|
||||
else:
|
||||
# easy feature: hash lookup
|
||||
rules_with_easy_features.add(rule_name)
|
||||
rules_by_feature[node].add(rule_name)
|
||||
@@ -1149,7 +1248,7 @@ class RuleSet:
|
||||
# at lower scope, e.g. function scope.
|
||||
# so, we find all dependencies of all rules, and later will filter them down.
|
||||
for rule in rules:
|
||||
if rule.meta.get("capa/subscope-rule", False):
|
||||
if rule.is_subscope_rule():
|
||||
continue
|
||||
|
||||
scope_rules.update(get_rules_and_dependencies(rules, rule.name))
|
||||
@@ -1194,9 +1293,15 @@ class RuleSet:
|
||||
logger.debug('using rule "%s" and dependencies, found tag in meta.%s: %s', rule.name, k, v)
|
||||
rules_filtered.update(set(capa.rules.get_rules_and_dependencies(rules, rule.name)))
|
||||
break
|
||||
if isinstance(v, list):
|
||||
for vv in v:
|
||||
if tag in vv:
|
||||
logger.debug('using rule "%s" and dependencies, found tag in meta.%s: %s', rule.name, k, vv)
|
||||
rules_filtered.update(set(capa.rules.get_rules_and_dependencies(rules, rule.name)))
|
||||
break
|
||||
return RuleSet(list(rules_filtered))
|
||||
|
||||
def match(self, scope: Scope, features: FeatureSet, va: int) -> Tuple[FeatureSet, ceng.MatchResults]:
|
||||
def match(self, scope: Scope, features: FeatureSet, addr: Address) -> Tuple[FeatureSet, ceng.MatchResults]:
|
||||
"""
|
||||
match rules from this ruleset at the given scope against the given features.
|
||||
|
||||
@@ -1213,6 +1318,9 @@ class RuleSet:
|
||||
elif scope is Scope.BASIC_BLOCK:
|
||||
easy_rules_by_feature = self._easy_basic_block_rules_by_feature
|
||||
hard_rule_names = self._hard_basic_block_rules
|
||||
elif scope is Scope.INSTRUCTION:
|
||||
easy_rules_by_feature = self._easy_instruction_rules_by_feature
|
||||
hard_rule_names = self._hard_instruction_rules
|
||||
else:
|
||||
assert_never(scope)
|
||||
|
||||
@@ -1225,7 +1333,7 @@ class RuleSet:
|
||||
# first, match against the set of rules that have at least one
|
||||
# feature shared with our feature set.
|
||||
candidate_rules = [self.rules[name] for name in candidate_rule_names]
|
||||
features2, easy_matches = ceng.match(candidate_rules, features, va)
|
||||
features2, easy_matches = ceng.match(candidate_rules, features, addr)
|
||||
|
||||
# note that we've stored the updated feature set in `features2`.
|
||||
# this contains a superset of the features in `features`;
|
||||
@@ -1244,7 +1352,7 @@ class RuleSet:
|
||||
# that we can't really make any guesses about.
|
||||
# these are rules with hard features, like substring/regex/bytes and match statements.
|
||||
hard_rules = [self.rules[name] for name in hard_rule_names]
|
||||
features3, hard_matches = ceng.match(hard_rules, features2, va)
|
||||
features3, hard_matches = ceng.match(hard_rules, features2, addr)
|
||||
|
||||
# note that above, we probably are skipping matching a bunch of
|
||||
# rules that definitely would never hit.
|
||||
|
||||
@@ -1 +1,13 @@
|
||||
__version__ = "3.1.0"
|
||||
__version__ = "4.0.1"
|
||||
|
||||
|
||||
def get_major_version():
|
||||
return int(__version__.partition(".")[0])
|
||||
|
||||
|
||||
def get_rules_branch():
|
||||
return f"v{get_major_version()}"
|
||||
|
||||
|
||||
def get_rules_checkout_command():
|
||||
return f"$ git clone https://github.com/mandiant/capa-rules.git -b {get_rules_branch()} /local/path/to/rules"
|
||||
|
||||
@@ -26,7 +26,13 @@ To install capa as a Python library use `pip` to fetch the `flare-capa` module.
|
||||
|
||||
#### *Note*:
|
||||
This method is appropriate for integrating capa in an existing project.
|
||||
This technique doesn't pull the default rule set, so you should check it out separately from [capa-rules](https://github.com/mandiant/capa-rules/) and pass the directory to the entrypoint using `-r` or set the rules path in the IDA Pro plugin.
|
||||
This technique doesn't pull the default rule set, so you should check it out separately from [capa-rules](https://github.com/mandiant/capa-rules/) and pass the directory to the entrypoint using `-r` or set the rules path in the IDA Pro plugin:
|
||||
|
||||
```console
|
||||
$ git clone https://github.com/mandiant/capa-rules.git -b v3 /local/path/to/rules
|
||||
$ capa -r /local/path/to/rules suspicious.exe
|
||||
```
|
||||
|
||||
This technique also doesn't set up the default library identification [signatures](https://github.com/mandiant/capa/tree/master/sigs). You can pass the signature directory using the `-s` argument.
|
||||
For example, to run capa with both a rule path and a signature path:
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ If capa detects that a program may be packed using its rules it warns the user.
|
||||
|
||||
|
||||
# Installers, run-time programs, etc.
|
||||
capa cannot handle installers, run-time programs like .NET applications, or other packaged applications like AutoIt well. This means that the results may be misleading or incomplete.
|
||||
capa cannot handle installers, run-time programs, or other packaged applications like AutoIt well. This means that the results may be misleading or incomplete.
|
||||
|
||||
If capa detects an installer, run-time program, etc. it warns the user.
|
||||
|
||||
|
||||
@@ -38,6 +38,12 @@
|
||||
- [ ] Create a PR with the updated [CHANGELOG.md](https://github.com/mandiant/capa/blob/master/CHANGELOG.md) and [capa/version.py](https://github.com/mandiant/capa/blob/master/capa/version.py). Copy this checklist in the PR description.
|
||||
- [ ] After PR review, merge the PR and [create the release in GH](https://github.com/mandiant/capa/releases/new) using text from the [CHANGELOG.md](https://github.com/mandiant/capa/blob/master/CHANGELOG.md).
|
||||
- [ ] Verify GH actions [upload artifacts](https://github.com/mandiant/capa/releases), [publish to PyPI](https://pypi.org/project/flare-capa) and [create a tag in capa rules](https://github.com/mandiant/capa-rules/tags) upon completion.
|
||||
- [ ] Manually update capa rules major version rule branch
|
||||
```commandline
|
||||
[capa/rules] $ git pull master
|
||||
[capa/rules] $ git checkout v3 # create if new major version: git checkout -b vX
|
||||
[capa/rules] $ git merge master
|
||||
[capa/rules] $ git push origin v3
|
||||
```
|
||||
- [ ] [Spread the word](https://twitter.com)
|
||||
- [ ] Update internal service
|
||||
|
||||
|
||||
72
doc/rules.md
Normal file
72
doc/rules.md
Normal file
@@ -0,0 +1,72 @@
|
||||
### rules
|
||||
|
||||
|
||||
capa uses a collection of rules to identify capabilities within a program.
|
||||
The [github.com/mandiant/capa-rules](https://github.com/mandiant/capa-rules) repository contains hundreds of standard library rules that are distributed with capa.
|
||||
|
||||
When you download a standalone version of capa, this standard library is embedded within the executable and capa will use these rules by default:
|
||||
|
||||
```console
|
||||
$ capa suspicious.exe
|
||||
```
|
||||
|
||||
However, you may want to modify the rules for a variety of reasons:
|
||||
|
||||
- develop new rules to find behaviors, and/or
|
||||
- tweak existing rules to reduce false positives, and/or
|
||||
- collect a private selection of rules not shared publicly.
|
||||
|
||||
Or, you may want to use capa as a Python library within another application.
|
||||
|
||||
In these scenarios, you must provide the rule set to capa as a directory on your file system. Do this using the `-r`/`--rules` parameter:
|
||||
|
||||
```console
|
||||
$ capa --rules /local/path/to/rules suspicious.exe
|
||||
```
|
||||
|
||||
You can collect the standard set of rules in two ways:
|
||||
|
||||
- [download from the Github releases page](#download-release-archive), or
|
||||
- [clone from Github](#clone-with-git).
|
||||
|
||||
Note that you must use match the rules major version with the capa major version,
|
||||
i.e., use `v1` rules with `v1` of capa.
|
||||
This is so that new versions of capa can update rule syntax, such as by adding new fields and logic.
|
||||
|
||||
Otherwise, using rules with a mismatched version of capa may lead to errors like:
|
||||
|
||||
```
|
||||
$ capa --rules /path/to/mismatched/rules suspicious.exe
|
||||
ERROR:lint:invalid rule: injection.yml: invalid rule: unexpected statement: instruction
|
||||
```
|
||||
|
||||
You can check the version of capa you're currently using like this:
|
||||
|
||||
```console
|
||||
$ capa --version
|
||||
capa 3.0.3
|
||||
```
|
||||
|
||||
#### download release archive
|
||||
|
||||
The releases page is [here](https://github.com/mandiant/capa-rules/tags/).
|
||||
Find the most recent release corresponding to your major version of capa and download the ZIP archive.
|
||||
Here are some quick links:
|
||||
- v1: [v1](https://github.com/mandiant/capa-rules/releases/tag/v1)
|
||||
- v2: [v2](https://github.com/mandiant/capa-rules/releases/tag/v2)
|
||||
- v3: [v3](https://github.com/mandiant/capa-rules/releases/tag/v3)
|
||||
|
||||
#### clone with git
|
||||
|
||||
To fetch with git, clone the appropriate branch like this:
|
||||
|
||||
```console
|
||||
$ git clone https://github.com/mandiant/capa-rules.git -b v3 /local/path/to/rules
|
||||
```
|
||||
|
||||
Note that the branch name (`v3` in the example above) must match the major version of capa you're using.
|
||||
|
||||
- [v1](https://github.com/mandiant/capa-rules/tree/v1): `v1`
|
||||
- [v2](https://github.com/mandiant/capa-rules/tree/v2): `v2`
|
||||
- [v3](https://github.com/mandiant/capa-rules/tree/v3): `v3`
|
||||
|
||||
2
rules
2
rules
Submodule rules updated: 954f22acd8...506e3132bc
@@ -68,6 +68,7 @@ import capa
|
||||
import capa.main
|
||||
import capa.rules
|
||||
import capa.render.json
|
||||
import capa.render.result_document as rd
|
||||
|
||||
logger = logging.getLogger("capa")
|
||||
|
||||
@@ -126,19 +127,14 @@ def get_capa_results(args):
|
||||
"error": "unexpected error: %s" % (e),
|
||||
}
|
||||
|
||||
meta = capa.main.collect_metadata("", path, "", extractor)
|
||||
meta = capa.main.collect_metadata([], path, [], extractor)
|
||||
capabilities, counts = capa.main.find_capabilities(rules, extractor, disable_progress=True)
|
||||
meta["analysis"].update(counts)
|
||||
meta["analysis"]["layout"] = capa.main.compute_layout(rules, extractor, capabilities)
|
||||
|
||||
return {
|
||||
"path": path,
|
||||
"status": "ok",
|
||||
"ok": {
|
||||
"meta": meta,
|
||||
"capabilities": capabilities,
|
||||
},
|
||||
}
|
||||
doc = rd.ResultDocument.from_capa(meta, rules, capabilities)
|
||||
|
||||
return {"path": path, "status": "ok", "ok": doc.dict(exclude_none=True)}
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
@@ -205,11 +201,7 @@ def main(argv=None):
|
||||
if result["status"] == "error":
|
||||
logger.warning(result["error"])
|
||||
elif result["status"] == "ok":
|
||||
meta = result["ok"]["meta"]
|
||||
capabilities = result["ok"]["capabilities"]
|
||||
# our renderer expects to emit a json document for a single sample
|
||||
# so we deserialize the json document, store it in a larger dict, and we'll subsequently re-encode.
|
||||
results[result["path"]] = json.loads(capa.render.json.render(meta, rules, capabilities))
|
||||
results[result["path"]] = rd.ResultDocument.parse_obj(result["ok"]).json(exclude_none=True)
|
||||
else:
|
||||
raise ValueError("unexpected status: %s" % (result["status"]))
|
||||
|
||||
|
||||
@@ -43,7 +43,6 @@ import capa.rules
|
||||
import capa.engine
|
||||
import capa.features
|
||||
import capa.features.insn
|
||||
from capa.features.common import BITNESS_X32, BITNESS_X64, String
|
||||
|
||||
logger = logging.getLogger("capa2yara")
|
||||
|
||||
@@ -181,7 +180,15 @@ def convert_rule(rule, rulename, cround, depth):
|
||||
logger.info("doing api: " + repr(api))
|
||||
|
||||
# e.g. kernel32.CreateNamedPipe => look for kernel32.dll and CreateNamedPipe
|
||||
if "." in api:
|
||||
# TODO: improve .NET API call handling
|
||||
if "::" in api:
|
||||
mod, api = api.split("::")
|
||||
|
||||
var_name = "api_" + var_names.pop(0)
|
||||
yara_strings += "\t$" + var_name + " = /\\b" + api + "(A|W)?\\b/ ascii wide\n"
|
||||
yara_condition += "\t$" + var_name + " "
|
||||
|
||||
elif api.count(".") == 1:
|
||||
dll, api = api.split(".")
|
||||
|
||||
# usage of regex is needed and /i because string search for "CreateMutex" in imports() doesn't look for e.g. CreateMutexA
|
||||
@@ -311,7 +318,7 @@ def convert_rule(rule, rulename, cround, depth):
|
||||
|
||||
return yara_strings, yara_condition
|
||||
|
||||
############################## end def do_statement
|
||||
# end: def do_statement
|
||||
|
||||
yara_strings_list = []
|
||||
yara_condition_list = []
|
||||
@@ -390,7 +397,9 @@ def convert_rule(rule, rulename, cround, depth):
|
||||
logger.info("kid coming: " + repr(kid.name))
|
||||
# logger.info("grandchildren: " + repr(kid.children))
|
||||
|
||||
##### here we go into RECURSION ##################################################################################
|
||||
#
|
||||
# here we go into RECURSION
|
||||
#
|
||||
yara_strings_sub, yara_condition_sub, rule_comment_sub, incomplete_sub = convert_rule(
|
||||
kid, rulename, cround, depth
|
||||
)
|
||||
@@ -496,9 +505,7 @@ def convert_rule(rule, rulename, cround, depth):
|
||||
|
||||
yara_condition = "\n\t" + yara_condition_list[0]
|
||||
|
||||
logger.info(
|
||||
f"################# end of convert_rule() #strings: {len(yara_strings_list)} #conditions: {len(yara_condition_list)}"
|
||||
)
|
||||
logger.info(f"# end of convert_rule() #strings: {len(yara_strings_list)} #conditions: {len(yara_condition_list)}")
|
||||
logger.info(f"strings: {yara_strings} conditions: {yara_condition}")
|
||||
|
||||
return yara_strings, yara_condition, rule_comment, incomplete
|
||||
@@ -535,7 +542,7 @@ def convert_rules(rules, namespaces, cround):
|
||||
|
||||
rule_name = convert_rule_name(rule.name)
|
||||
|
||||
if rule.meta.get("capa/subscope-rule", False):
|
||||
if rule.is_subscope_rule():
|
||||
logger.info("skipping sub scope rule capa: " + rule.name)
|
||||
continue
|
||||
|
||||
@@ -617,7 +624,7 @@ def convert_rules(rules, namespaces, cround):
|
||||
|
||||
# examples in capa can contain the same hash several times with different offset, so check if it's already there:
|
||||
# (keeping the offset might be interessting for some but breaks yara-ci for checking of the final rules
|
||||
if not value in seen_hashes:
|
||||
if value not in seen_hashes:
|
||||
yara_meta += "\t" + meta_name + ' = "' + value + '"\n'
|
||||
seen_hashes.append(value)
|
||||
|
||||
@@ -703,7 +710,7 @@ def main(argv=None):
|
||||
logging.getLogger("capa2yara").setLevel(level)
|
||||
|
||||
try:
|
||||
rules = capa.main.get_rules(args.rules, disable_progress=True)
|
||||
rules = capa.main.get_rules([args.rules], disable_progress=True)
|
||||
namespaces = capa.rules.index_rules_by_namespace(list(rules))
|
||||
rules = capa.rules.RuleSet(rules)
|
||||
logger.info("successfully loaded %s rules (including subscope rules which will be ignored)", len(rules))
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import json
|
||||
import collections
|
||||
from typing import Any, Dict
|
||||
|
||||
import capa.main
|
||||
import capa.rules
|
||||
@@ -10,52 +11,48 @@ import capa.features
|
||||
import capa.render.json
|
||||
import capa.render.utils as rutils
|
||||
import capa.render.default
|
||||
import capa.render.result_document
|
||||
import capa.render.result_document as rd
|
||||
import capa.features.freeze.features as frzf
|
||||
from capa.engine import *
|
||||
|
||||
# edit this to set the path for file to analyze and rule directory
|
||||
RULES_PATH = "/tmp/capa/rules/"
|
||||
|
||||
# load rules from disk
|
||||
rules = capa.rules.RuleSet(capa.main.get_rules(RULES_PATH, disable_progress=True))
|
||||
|
||||
# == Render ddictionary helpers
|
||||
def render_meta(doc, ostream):
|
||||
ostream["md5"] = doc["meta"]["sample"]["md5"]
|
||||
ostream["sha1"] = doc["meta"]["sample"]["sha1"]
|
||||
ostream["sha256"] = doc["meta"]["sample"]["sha256"]
|
||||
ostream["path"] = doc["meta"]["sample"]["path"]
|
||||
def render_meta(doc: rd.ResultDocument, result):
|
||||
result["md5"] = doc.meta.sample.md5
|
||||
result["sha1"] = doc.meta.sample.sha1
|
||||
result["sha256"] = doc.meta.sample.sha256
|
||||
result["path"] = doc.meta.sample.path
|
||||
|
||||
|
||||
def find_subrule_matches(doc):
|
||||
def find_subrule_matches(doc: rd.ResultDocument) -> Set[str]:
|
||||
"""
|
||||
collect the rule names that have been matched as a subrule match.
|
||||
this way we can avoid displaying entries for things that are too specific.
|
||||
"""
|
||||
matches = set([])
|
||||
|
||||
def rec(node):
|
||||
if not node["success"]:
|
||||
def rec(node: rd.Match):
|
||||
if not node.success:
|
||||
# there's probably a bug here for rules that do `not: match: ...`
|
||||
# but we don't have any examples of this yet
|
||||
return
|
||||
|
||||
elif node["node"]["type"] == "statement":
|
||||
for child in node["children"]:
|
||||
elif isinstance(node.node, rd.StatementNode):
|
||||
for child in node.children:
|
||||
rec(child)
|
||||
|
||||
elif node["node"]["type"] == "feature":
|
||||
if node["node"]["feature"]["type"] == "match":
|
||||
matches.add(node["node"]["feature"]["match"])
|
||||
elif isinstance(node.node, rd.FeatureNode):
|
||||
if isinstance(node.node.feature, frzf.MatchFeature):
|
||||
matches.add(node.node.feature.match)
|
||||
|
||||
for rule in rutils.capability_rules(doc):
|
||||
for node in rule["matches"].values():
|
||||
for _, node in rule.matches:
|
||||
rec(node)
|
||||
|
||||
return matches
|
||||
|
||||
|
||||
def render_capabilities(doc, ostream):
|
||||
def render_capabilities(doc: rd.ResultDocument, result):
|
||||
"""
|
||||
example::
|
||||
{'CAPABILITY': {'accept command line arguments': 'host-interaction/cli',
|
||||
@@ -68,25 +65,25 @@ def render_capabilities(doc, ostream):
|
||||
"""
|
||||
subrule_matches = find_subrule_matches(doc)
|
||||
|
||||
ostream["CAPABILITY"] = dict()
|
||||
result["CAPABILITY"] = dict()
|
||||
for rule in rutils.capability_rules(doc):
|
||||
if rule["meta"]["name"] in subrule_matches:
|
||||
if rule.meta.name in subrule_matches:
|
||||
# rules that are also matched by other rules should not get rendered by default.
|
||||
# this cuts down on the amount of output while giving approx the same detail.
|
||||
# see #224
|
||||
continue
|
||||
|
||||
count = len(rule["matches"])
|
||||
count = len(rule.matches)
|
||||
if count == 1:
|
||||
capability = rule["meta"]["name"]
|
||||
capability = rule.meta.name
|
||||
else:
|
||||
capability = "%s (%d matches)" % (rule["meta"]["name"], count)
|
||||
capability = "%s (%d matches)" % (rule.meta.name, count)
|
||||
|
||||
ostream["CAPABILITY"].setdefault(rule["meta"]["namespace"], list())
|
||||
ostream["CAPABILITY"][rule["meta"]["namespace"]].append(capability)
|
||||
result["CAPABILITY"].setdefault(rule.meta.namespace, list())
|
||||
result["CAPABILITY"][rule.meta.namespace].append(capability)
|
||||
|
||||
|
||||
def render_attack(doc, ostream):
|
||||
def render_attack(doc, result):
|
||||
"""
|
||||
example::
|
||||
{'ATT&CK': {'COLLECTION': ['Input Capture::Keylogging [T1056.001]'],
|
||||
@@ -99,13 +96,13 @@ def render_attack(doc, ostream):
|
||||
'EXECUTION': ['Shared Modules [T1129]']}
|
||||
}
|
||||
"""
|
||||
ostream["ATTCK"] = dict()
|
||||
result["ATTCK"] = dict()
|
||||
tactics = collections.defaultdict(set)
|
||||
for rule in rutils.capability_rules(doc):
|
||||
if not rule["meta"].get("att&ck"):
|
||||
if not rule.meta.attack:
|
||||
continue
|
||||
for attack in rule["meta"]["att&ck"]:
|
||||
tactics[attack["tactic"]].add((attack["technique"], attack.get("subtechnique"), attack["id"]))
|
||||
for attack in rule.meta.attack:
|
||||
tactics[attack.tactic].add((attack.technique, attack.subtechnique, attack.id))
|
||||
|
||||
for tactic, techniques in sorted(tactics.items()):
|
||||
inner_rows = []
|
||||
@@ -114,10 +111,10 @@ def render_attack(doc, ostream):
|
||||
inner_rows.append("%s %s" % (technique, id))
|
||||
else:
|
||||
inner_rows.append("%s::%s %s" % (technique, subtechnique, id))
|
||||
ostream["ATTCK"].setdefault(tactic.upper(), inner_rows)
|
||||
result["ATTCK"].setdefault(tactic.upper(), inner_rows)
|
||||
|
||||
|
||||
def render_mbc(doc, ostream):
|
||||
def render_mbc(doc, result):
|
||||
"""
|
||||
example::
|
||||
{'MBC': {'ANTI-BEHAVIORAL ANALYSIS': ['Debugger Detection::Timing/Delay Check '
|
||||
@@ -132,14 +129,14 @@ def render_mbc(doc, ostream):
|
||||
'[C0021.004]']}
|
||||
}
|
||||
"""
|
||||
ostream["MBC"] = dict()
|
||||
result["MBC"] = dict()
|
||||
objectives = collections.defaultdict(set)
|
||||
for rule in rutils.capability_rules(doc):
|
||||
if not rule["meta"].get("mbc"):
|
||||
if not rule.meta.mbc:
|
||||
continue
|
||||
|
||||
for mbc in rule["meta"]["mbc"]:
|
||||
objectives[mbc["objective"]].add((mbc["behavior"], mbc.get("method"), mbc["id"]))
|
||||
for mbc in rule.meta.mbc:
|
||||
objectives[mbc.objective].add((mbc.behavior, mbc.method, mbc.id))
|
||||
|
||||
for objective, behaviors in sorted(objectives.items()):
|
||||
inner_rows = []
|
||||
@@ -148,35 +145,37 @@ def render_mbc(doc, ostream):
|
||||
inner_rows.append("%s [%s]" % (behavior, id))
|
||||
else:
|
||||
inner_rows.append("%s::%s [%s]" % (behavior, method, id))
|
||||
ostream["MBC"].setdefault(objective.upper(), inner_rows)
|
||||
result["MBC"].setdefault(objective.upper(), inner_rows)
|
||||
|
||||
|
||||
def render_dictionary(doc):
|
||||
ostream = dict()
|
||||
render_meta(doc, ostream)
|
||||
render_attack(doc, ostream)
|
||||
render_mbc(doc, ostream)
|
||||
render_capabilities(doc, ostream)
|
||||
def render_dictionary(doc: rd.ResultDocument) -> Dict[str, Any]:
|
||||
result: Dict[str, Any] = dict()
|
||||
render_meta(doc, result)
|
||||
render_attack(doc, result)
|
||||
render_mbc(doc, result)
|
||||
render_capabilities(doc, result)
|
||||
|
||||
return ostream
|
||||
return result
|
||||
|
||||
|
||||
# ==== render dictionary helpers
|
||||
def capa_details(file_path, output_format="dictionary"):
|
||||
# collect metadata (used only to make rendering more complete)
|
||||
meta = capa.main.collect_metadata("", file_path, RULES_PATH, extractor)
|
||||
def capa_details(rules_path, file_path, output_format="dictionary"):
|
||||
# load rules from disk
|
||||
rules = capa.rules.RuleSet(capa.main.get_rules([rules_path], disable_progress=True))
|
||||
|
||||
# extract features and find capabilities
|
||||
extractor = capa.main.get_extractor(file_path, "auto", capa.main.BACKEND_VIV, [], False, disable_progress=True)
|
||||
capabilities, counts = capa.main.find_capabilities(rules, extractor, disable_progress=True)
|
||||
|
||||
# collect metadata (used only to make rendering more complete)
|
||||
meta = capa.main.collect_metadata([], file_path, rules_path, extractor)
|
||||
meta["analysis"].update(counts)
|
||||
meta["analysis"]["layout"] = capa.main.compute_layout(rules, extractor, capabilities)
|
||||
|
||||
capa_output = False
|
||||
if output_format == "dictionary":
|
||||
# ...as python dictionary, simplified as textable but in dictionary
|
||||
doc = capa.render.result_document.convert_capabilities_to_result_document(meta, rules, capabilities)
|
||||
doc = rd.ResultDocument.from_capa(meta, rules, capabilities)
|
||||
capa_output = render_dictionary(doc)
|
||||
elif output_format == "json":
|
||||
# render results
|
||||
@@ -187,3 +186,22 @@ def capa_details(file_path, output_format="dictionary"):
|
||||
capa_output = capa.render.default.render(meta, rules, capabilities)
|
||||
|
||||
return capa_output
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
import os.path
|
||||
import argparse
|
||||
|
||||
RULES_PATH = os.path.join(os.path.dirname(__file__), "..", "rules")
|
||||
|
||||
parser = argparse.ArgumentParser(description="Extract capabilities from a file")
|
||||
parser.add_argument("file", help="file to extract capabilities from")
|
||||
parser.add_argument("--rules", help="path to rules directory", default=os.path.abspath(RULES_PATH))
|
||||
parser.add_argument(
|
||||
"--output", help="output format", choices=["dictionary", "json", "texttable"], default="dictionary"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
print(capa_details(args.rules, args.file, args.output))
|
||||
sys.exit(0)
|
||||
|
||||
126
scripts/lint.py
126
scripts/lint.py
@@ -15,14 +15,15 @@ See the License for the specific language governing permissions and limitations
|
||||
"""
|
||||
import gc
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
import string
|
||||
import difflib
|
||||
import hashlib
|
||||
import inspect
|
||||
import logging
|
||||
import os.path
|
||||
import pathlib
|
||||
import argparse
|
||||
import itertools
|
||||
@@ -33,6 +34,7 @@ from pathlib import Path
|
||||
from dataclasses import field, dataclass
|
||||
|
||||
import tqdm
|
||||
import pydantic
|
||||
import termcolor
|
||||
import ruamel.yaml
|
||||
import tqdm.contrib.logging
|
||||
@@ -40,10 +42,11 @@ import tqdm.contrib.logging
|
||||
import capa.main
|
||||
import capa.rules
|
||||
import capa.engine
|
||||
import capa.helpers
|
||||
import capa.features.insn
|
||||
import capa.features.common
|
||||
from capa.rules import Rule, RuleSet
|
||||
from capa.features.common import Feature
|
||||
from capa.features.common import FORMAT_PE, FORMAT_DOTNET, String, Feature, Substring
|
||||
from capa.render.result_document import RuleMetadata
|
||||
|
||||
logger = logging.getLogger("lint")
|
||||
|
||||
@@ -104,6 +107,7 @@ class FilenameDoesntMatchRuleName(Lint):
|
||||
def check_rule(self, ctx: Context, rule: Rule):
|
||||
expected = rule.name
|
||||
expected = expected.lower()
|
||||
expected = expected.replace(".net", "dotnet")
|
||||
expected = expected.replace(" ", "-")
|
||||
expected = expected.replace("(", "")
|
||||
expected = expected.replace(")", "")
|
||||
@@ -160,18 +164,18 @@ class MissingScope(Lint):
|
||||
|
||||
class InvalidScope(Lint):
|
||||
name = "invalid scope"
|
||||
recommendation = "Use only file, function, or basic block rule scopes"
|
||||
recommendation = "Use only file, function, basic block, or instruction rule scopes"
|
||||
|
||||
def check_rule(self, ctx: Context, rule: Rule):
|
||||
return rule.meta.get("scope") not in ("file", "function", "basic block")
|
||||
return rule.meta.get("scope") not in ("file", "function", "basic block", "instruction")
|
||||
|
||||
|
||||
class MissingAuthor(Lint):
|
||||
name = "missing author"
|
||||
recommendation = "Add meta.author so that users know who to contact with questions"
|
||||
class MissingAuthors(Lint):
|
||||
name = "missing authors"
|
||||
recommendation = "Add meta.authors so that users know who to contact with questions"
|
||||
|
||||
def check_rule(self, ctx: Context, rule: Rule):
|
||||
return "author" not in rule.meta
|
||||
return "authors" not in rule.meta
|
||||
|
||||
|
||||
class MissingExamples(Lint):
|
||||
@@ -221,6 +225,74 @@ class ExampleFileDNE(Lint):
|
||||
return not found
|
||||
|
||||
|
||||
class IncorrectValueType(Lint):
|
||||
name = "incorrect value type"
|
||||
recommendation = "Change value type"
|
||||
|
||||
def check_rule(self, ctx: Context, rule: Rule):
|
||||
try:
|
||||
_ = RuleMetadata.from_capa(rule)
|
||||
except pydantic.ValidationError as e:
|
||||
self.recommendation = str(e).strip()
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class InvalidAttckOrMbcTechnique(Lint):
|
||||
name = "att&ck/mbc entry is malformed or does not exist"
|
||||
recommendation = """
|
||||
The att&ck and mbc fields must respect the following format:
|
||||
<Tactic/Objective>::<Technique/Behavior> [<ID>]
|
||||
OR
|
||||
<Tactic/Objective>::<Technique/Behavior>::<Subtechnique/Method> [<ID.SubID>]
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super(InvalidAttckOrMbcTechnique, self).__init__()
|
||||
|
||||
try:
|
||||
with open(f"{os.path.dirname(__file__)}/linter-data.json", "rb") as fd:
|
||||
self.data = json.load(fd)
|
||||
self.enabled_frameworks = self.data.keys()
|
||||
except BaseException:
|
||||
# If linter-data.json is not present, or if an error happen
|
||||
# we log an error and lint nothing.
|
||||
logger.warning(
|
||||
"Could not load 'scripts/linter-data.json'. The att&ck and mbc information will not be linted."
|
||||
)
|
||||
self.enabled_frameworks = []
|
||||
|
||||
# This regex matches the format defined in the recommendation attribute
|
||||
self.reg = re.compile(r"^([\w\s-]+)::(.+) \[([A-Za-z0-9.]+)\]$")
|
||||
|
||||
def _entry_check(self, framework, category, entry, eid):
|
||||
if category not in self.data[framework].keys():
|
||||
self.recommendation = f'Unknown category: "{category}"'
|
||||
return True
|
||||
if eid not in self.data[framework][category].keys():
|
||||
self.recommendation = f"Unknown entry ID: {eid}"
|
||||
return True
|
||||
if self.data[framework][category][eid] != entry:
|
||||
self.recommendation = (
|
||||
f'{eid} should be associated to entry "{self.data[framework][category][eid]}" instead of "{entry}"'
|
||||
)
|
||||
return True
|
||||
return False
|
||||
|
||||
def check_rule(self, ctx: Context, rule: Rule):
|
||||
for framework in self.enabled_frameworks:
|
||||
if framework in rule.meta.keys():
|
||||
for r in rule.meta[framework]:
|
||||
m = self.reg.match(r)
|
||||
if m is None:
|
||||
return True
|
||||
|
||||
args = m.group(1, 2, 3)
|
||||
if self._entry_check(framework, *args):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
DEFAULT_SIGNATURES = capa.main.get_default_signatures()
|
||||
|
||||
|
||||
@@ -230,17 +302,19 @@ def get_sample_capabilities(ctx: Context, path: Path) -> Set[str]:
|
||||
logger.debug("found cached results: %s: %d capabilities", nice_path, len(ctx.capabilities_by_sample[path]))
|
||||
return ctx.capabilities_by_sample[path]
|
||||
|
||||
if nice_path.endswith(capa.main.EXTENSIONS_SHELLCODE_32):
|
||||
format = "sc32"
|
||||
elif nice_path.endswith(capa.main.EXTENSIONS_SHELLCODE_64):
|
||||
format = "sc64"
|
||||
if nice_path.endswith(capa.helpers.EXTENSIONS_SHELLCODE_32):
|
||||
format_ = "sc32"
|
||||
elif nice_path.endswith(capa.helpers.EXTENSIONS_SHELLCODE_64):
|
||||
format_ = "sc64"
|
||||
else:
|
||||
format = "auto"
|
||||
format_ = "auto"
|
||||
if not nice_path.endswith(capa.helpers.EXTENSIONS_ELF):
|
||||
dnfile_extractor = capa.features.extractors.dnfile_.DnfileFeatureExtractor(nice_path)
|
||||
if dnfile_extractor.is_dotnet_file():
|
||||
format_ = FORMAT_DOTNET
|
||||
|
||||
logger.debug("analyzing sample: %s", nice_path)
|
||||
extractor = capa.main.get_extractor(
|
||||
nice_path, format, capa.main.BACKEND_VIV, DEFAULT_SIGNATURES, False, disable_progress=True
|
||||
)
|
||||
extractor = capa.main.get_extractor(nice_path, format_, "", DEFAULT_SIGNATURES, False, disable_progress=True)
|
||||
|
||||
capabilities, _ = capa.main.find_capabilities(ctx.rules, extractor, disable_progress=True)
|
||||
# mypy doesn't seem to be happy with the MatchResults type alias & set(...keys())?
|
||||
@@ -433,7 +507,7 @@ class FeatureStringTooShort(Lint):
|
||||
|
||||
def check_features(self, ctx: Context, features: List[Feature]):
|
||||
for feature in features:
|
||||
if isinstance(feature, (capa.features.common.String, capa.features.common.Substring)):
|
||||
if isinstance(feature, (String, Substring)):
|
||||
assert isinstance(feature.value, str)
|
||||
if len(feature.value) < 4:
|
||||
self.recommendation = self.recommendation.format(feature.value)
|
||||
@@ -640,13 +714,15 @@ def lint_scope(ctx: Context, rule: Rule):
|
||||
META_LINTS = (
|
||||
MissingNamespace(),
|
||||
NamespaceDoesntMatchRulePath(),
|
||||
MissingAuthor(),
|
||||
MissingAuthors(),
|
||||
MissingExamples(),
|
||||
MissingExampleOffset(),
|
||||
ExampleFileDNE(),
|
||||
UnusualMetaField(),
|
||||
LibRuleNotInLibDirectory(),
|
||||
LibRuleHasNamespace(),
|
||||
InvalidAttckOrMbcTechnique(),
|
||||
IncorrectValueType(),
|
||||
)
|
||||
|
||||
|
||||
@@ -743,15 +819,12 @@ def lint_rule(ctx: Context, rule: Rule):
|
||||
# this is by far the most common reason to be in the nursery,
|
||||
# and ends up just producing a lot of noise.
|
||||
if not (is_nursery_rule(rule) and len(violations) == 1 and violations[0].name == "missing examples"):
|
||||
category = rule.meta.get("rule-category")
|
||||
|
||||
print("")
|
||||
print(
|
||||
"%s%s %s"
|
||||
"%s%s"
|
||||
% (
|
||||
" (nursery) " if is_nursery_rule(rule) else "",
|
||||
rule.name,
|
||||
("(%s)" % category) if category else "",
|
||||
)
|
||||
)
|
||||
|
||||
@@ -847,7 +920,7 @@ def lint(ctx: Context):
|
||||
with tqdm.contrib.logging.tqdm_logging_redirect(ctx.rules.rules.items(), unit="rule") as pbar:
|
||||
with redirecting_print_to_tqdm():
|
||||
for name, rule in pbar:
|
||||
if rule.meta.get("capa/subscope-rule", False):
|
||||
if rule.is_subscope_rule():
|
||||
continue
|
||||
|
||||
pbar.set_description(width("linting rule: %s" % (name), 48))
|
||||
@@ -905,7 +978,7 @@ def main(argv=None):
|
||||
|
||||
parser = argparse.ArgumentParser(description="Lint capa rules.")
|
||||
capa.main.install_common_args(parser, wanted={"tag"})
|
||||
parser.add_argument("rules", type=str, help="Path to rules")
|
||||
parser.add_argument("rules", type=str, action="append", help="Path to rules")
|
||||
parser.add_argument("--samples", type=str, default=samples_path, help="Path to samples")
|
||||
parser.add_argument(
|
||||
"--thorough",
|
||||
@@ -926,8 +999,9 @@ def main(argv=None):
|
||||
|
||||
try:
|
||||
rules = capa.main.get_rules(args.rules, disable_progress=True)
|
||||
rule_count = len(rules)
|
||||
rules = capa.rules.RuleSet(rules)
|
||||
logger.info("successfully loaded %s rules", len(rules))
|
||||
logger.info("successfully loaded %s rules", rule_count)
|
||||
if args.tag:
|
||||
rules = rules.filter_rules_by_meta(args.tag)
|
||||
logger.debug("selected %s rules", len(rules))
|
||||
|
||||
1580
scripts/linter-data.json
Normal file
1580
scripts/linter-data.json
Normal file
File diff suppressed because it is too large
Load Diff
190
scripts/setup-linter-dependencies.py
Normal file
190
scripts/setup-linter-dependencies.py
Normal file
@@ -0,0 +1,190 @@
|
||||
"""
|
||||
Generate capa linter-data.json, used to validate Att&ck/MBC IDs and names.
|
||||
|
||||
Use the --extractor option to extract data from Att&ck or MBC (or both) frameworks.
|
||||
Use the --output to choose the output json file.
|
||||
By default, the script will create a linter-data.json in the scripts/ directory for both frameworks.
|
||||
|
||||
Note: The capa rules linter will try to load from its default location (scripts/linter-data.json).
|
||||
|
||||
Usage:
|
||||
|
||||
usage: setup-linter-dependencies.py [-h] [--extractor {both,mbc,att&ck}] [--output OUTPUT]
|
||||
|
||||
Setup linter dependencies.
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
--extractor {both,mbc,att&ck}
|
||||
Extractor that will be run
|
||||
--output OUTPUT, -o OUTPUT
|
||||
Path to output file (lint.py will be looking for linter-data.json)
|
||||
|
||||
|
||||
Example:
|
||||
|
||||
$ python3 setup-linter-dependencies.py
|
||||
2022-01-24 22:35:06,901 [INFO] Extracting Mitre Att&ck techniques...
|
||||
2022-01-24 22:35:06,901 [INFO] Downloading STIX data at: https://raw.githubusercontent.com/mitre-attack/attack-stix-data/master/enterprise-attack/enterprise-attack.json
|
||||
2022-01-24 22:35:13,001 [INFO] Starting extraction...
|
||||
2022-01-24 22:35:39,395 [INFO] Extracting MBC behaviors...
|
||||
2022-01-24 22:35:39,395 [INFO] Downloading STIX data at: https://raw.githubusercontent.com/MBCProject/mbc-stix2/master/mbc/mbc.json
|
||||
2022-01-24 22:35:39,839 [INFO] Starting extraction...
|
||||
2022-01-24 22:35:42,632 [INFO] Writing results to linter-data.json
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
import argparse
|
||||
from sys import argv
|
||||
from typing import Dict, List
|
||||
from os.path import dirname
|
||||
|
||||
import requests
|
||||
from stix2 import Filter, MemoryStore, AttackPattern # type: ignore
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
||||
|
||||
|
||||
class MitreExtractor:
|
||||
"""
|
||||
This class extract Mitre techniques and sub techniques that are represented as "attack-pattern" in STIX format.
|
||||
The STIX data is collected in JSON format by requesting the specified URL.
|
||||
|
||||
url: must point to json stix location
|
||||
kill_chain_name: mitre-attack, mitre-mbc...
|
||||
"""
|
||||
|
||||
url = ""
|
||||
kill_chain_name = ""
|
||||
|
||||
def __init__(self):
|
||||
"""Download and store in memory the STIX data on instantiation."""
|
||||
if self.kill_chain_name == "":
|
||||
raise ValueError(f"Kill chain name not specified in class {self.__class__.__name__}")
|
||||
|
||||
if self.url == "":
|
||||
raise ValueError(f"URL not specified in class {self.__class__.__name__}")
|
||||
|
||||
logging.info(f"Downloading STIX data at: {self.url}")
|
||||
stix_json = requests.get(self.url).json()
|
||||
self._memory_store = MemoryStore(stix_data=stix_json["objects"])
|
||||
|
||||
@staticmethod
|
||||
def _remove_deprecated_objetcs(stix_objects) -> List[AttackPattern]:
|
||||
"""Remove any revoked or deprecated objects from queries made to the data source."""
|
||||
return list(
|
||||
filter(
|
||||
lambda x: x.get("x_mitre_deprecated", False) is False and x.get("revoked", False) is False,
|
||||
stix_objects,
|
||||
)
|
||||
)
|
||||
|
||||
def _get_tactics(self) -> List[Dict]:
|
||||
"""Get tactics IDs from Mitre matrix."""
|
||||
# Only one matrix for enterprise att&ck framework
|
||||
matrix = self._remove_deprecated_objetcs(
|
||||
self._memory_store.query(
|
||||
[
|
||||
Filter("type", "=", "x-mitre-matrix"),
|
||||
]
|
||||
)
|
||||
)[0]
|
||||
return list(map(self._memory_store.get, matrix["tactic_refs"]))
|
||||
|
||||
def _get_techniques_from_tactic(self, tactic: str) -> List[AttackPattern]:
|
||||
"""Get techniques and sub techniques from a Mitre tactic (kill_chain_phases->phase_name)"""
|
||||
techniques = self._remove_deprecated_objetcs(
|
||||
self._memory_store.query(
|
||||
[
|
||||
Filter("type", "=", "attack-pattern"),
|
||||
Filter("kill_chain_phases.phase_name", "=", tactic),
|
||||
Filter("kill_chain_phases.kill_chain_name", "=", self.kill_chain_name),
|
||||
]
|
||||
)
|
||||
)
|
||||
return techniques
|
||||
|
||||
def _get_parent_technique_from_subtechnique(self, technique: AttackPattern) -> AttackPattern:
|
||||
"""Get parent technique of a sub technique using the technique ID TXXXX.YYY"""
|
||||
sub_id = technique["external_references"][0]["external_id"].split(".")[0]
|
||||
parent_technique = self._remove_deprecated_objetcs(
|
||||
self._memory_store.query(
|
||||
[
|
||||
Filter("type", "=", "attack-pattern"),
|
||||
Filter("external_references.external_id", "=", sub_id),
|
||||
]
|
||||
)
|
||||
)[0]
|
||||
return parent_technique
|
||||
|
||||
def run(self) -> Dict[str, Dict[str, str]]:
|
||||
"""Iterate over every technique over every tactic. If the technique is a sub technique, then
|
||||
we also search for the parent technique name.
|
||||
"""
|
||||
logging.info("Starting extraction...")
|
||||
data: Dict[str, Dict[str, str]] = {}
|
||||
for tactic in self._get_tactics():
|
||||
data[tactic["name"]] = {}
|
||||
for technique in self._get_techniques_from_tactic(tactic["x_mitre_shortname"]):
|
||||
tid = technique["external_references"][0]["external_id"]
|
||||
technique_name = technique["name"].split("::")[0]
|
||||
if technique["x_mitre_is_subtechnique"]:
|
||||
parent_technique = self._get_parent_technique_from_subtechnique(technique)
|
||||
data[tactic["name"]][tid] = f"{parent_technique['name']}::{technique_name}"
|
||||
else:
|
||||
data[tactic["name"]][tid] = technique_name
|
||||
return data
|
||||
|
||||
|
||||
class AttckExtractor(MitreExtractor):
|
||||
"""Extractor for the Mitre Enterprise Att&ck Framework."""
|
||||
|
||||
url = "https://raw.githubusercontent.com/mitre-attack/attack-stix-data/master/enterprise-attack/enterprise-attack.json"
|
||||
kill_chain_name = "mitre-attack"
|
||||
|
||||
|
||||
class MbcExtractor(MitreExtractor):
|
||||
"""Extractor for the Mitre Malware Behavior Catalog."""
|
||||
|
||||
url = "https://raw.githubusercontent.com/MBCProject/mbc-stix2/master/mbc/mbc.json"
|
||||
kill_chain_name = "mitre-mbc"
|
||||
|
||||
def _get_tactics(self) -> List[Dict]:
|
||||
"""Override _get_tactics to edit the tactic name for Micro-objective"""
|
||||
tactics = super(MbcExtractor, self)._get_tactics()
|
||||
# We don't want the Micro-objective string inside objective names
|
||||
for tactic in tactics:
|
||||
tactic["name"] = tactic["name"].replace(" Micro-objective", "")
|
||||
return tactics
|
||||
|
||||
|
||||
def main(args: argparse.Namespace) -> None:
|
||||
data = {}
|
||||
if args.extractor == "att&ck" or args.extractor == "both":
|
||||
logging.info("Extracting Mitre Att&ck techniques...")
|
||||
data["att&ck"] = AttckExtractor().run()
|
||||
if args.extractor == "mbc" or args.extractor == "both":
|
||||
logging.info("Extracting MBC behaviors...")
|
||||
data["mbc"] = MbcExtractor().run()
|
||||
|
||||
logging.info(f"Writing results to {args.output}")
|
||||
try:
|
||||
with open(args.output, "w") as jf:
|
||||
json.dump(data, jf, indent=2)
|
||||
except BaseException as e:
|
||||
logging.error(f"Exception encountered when writing results: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Setup linter dependencies.")
|
||||
parser.add_argument(
|
||||
"--extractor", type=str, choices=["both", "mbc", "att&ck"], default="both", help="Extractor that will be run"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output",
|
||||
"-o",
|
||||
type=str,
|
||||
default=f"{dirname(__file__)}/linter-data.json",
|
||||
help="Path to output file (lint.py will be looking for linter-data.json)",
|
||||
)
|
||||
main(parser.parse_args(args=argv[1:]))
|
||||
@@ -53,22 +53,27 @@ import sys
|
||||
import logging
|
||||
import argparse
|
||||
import collections
|
||||
from typing import Dict
|
||||
|
||||
import colorama
|
||||
|
||||
import capa.main
|
||||
import capa.rules
|
||||
import capa.engine
|
||||
import capa.helpers
|
||||
import capa.features
|
||||
import capa.exceptions
|
||||
import capa.render.utils as rutils
|
||||
import capa.render.verbose
|
||||
import capa.features.freeze
|
||||
import capa.render.result_document
|
||||
import capa.render.result_document as rd
|
||||
from capa.helpers import get_file_taste
|
||||
from capa.features.freeze import Address
|
||||
|
||||
logger = logging.getLogger("capa.show-capabilities-by-function")
|
||||
|
||||
|
||||
def render_matches_by_function(doc):
|
||||
def render_matches_by_function(doc: rd.ResultDocument):
|
||||
"""
|
||||
like:
|
||||
|
||||
@@ -87,32 +92,34 @@ def render_matches_by_function(doc):
|
||||
- send HTTP request
|
||||
- connect to HTTP server
|
||||
"""
|
||||
functions_by_bb = {}
|
||||
for function, info in doc["meta"]["analysis"]["layout"]["functions"].items():
|
||||
for bb in info["matched_basic_blocks"]:
|
||||
functions_by_bb[bb] = function
|
||||
functions_by_bb: Dict[Address, Address] = {}
|
||||
for finfo in doc.meta.analysis.layout.functions:
|
||||
faddress = finfo.address
|
||||
|
||||
for bb in finfo.matched_basic_blocks:
|
||||
bbaddress = bb.address
|
||||
functions_by_bb[bbaddress] = faddress
|
||||
|
||||
ostream = rutils.StringIO()
|
||||
|
||||
matches_by_function = collections.defaultdict(set)
|
||||
for rule in rutils.capability_rules(doc):
|
||||
if rule["meta"]["scope"] == capa.rules.FUNCTION_SCOPE:
|
||||
for va in rule["matches"].keys():
|
||||
matches_by_function[va].add(rule["meta"]["name"])
|
||||
elif rule["meta"]["scope"] == capa.rules.BASIC_BLOCK_SCOPE:
|
||||
for va in rule["matches"].keys():
|
||||
function = functions_by_bb[va]
|
||||
matches_by_function[function].add(rule["meta"]["name"])
|
||||
if rule.meta.scope == capa.rules.FUNCTION_SCOPE:
|
||||
for addr, _ in rule.matches:
|
||||
matches_by_function[addr].add(rule.meta.name)
|
||||
elif rule.meta.scope == capa.rules.BASIC_BLOCK_SCOPE:
|
||||
for addr, _ in rule.matches:
|
||||
function = functions_by_bb[addr]
|
||||
matches_by_function[function].add(rule.meta.name)
|
||||
else:
|
||||
# file scope
|
||||
pass
|
||||
|
||||
for va, feature_count in sorted(doc["meta"]["analysis"]["feature_counts"]["functions"].items()):
|
||||
va = int(va)
|
||||
if not matches_by_function.get(va, {}):
|
||||
for f in doc.meta.analysis.feature_counts.functions:
|
||||
if not matches_by_function.get(f.address, {}):
|
||||
continue
|
||||
ostream.writeln("function at 0x%X with %d features: " % (va, feature_count))
|
||||
for rule_name in sorted(matches_by_function[va]):
|
||||
ostream.writeln("function at %s with %d features: " % (capa.render.verbose.format_address(addr), f.count))
|
||||
for rule_name in sorted(matches_by_function[f.address]):
|
||||
ostream.writeln(" - " + rule_name)
|
||||
|
||||
return ostream.getvalue()
|
||||
@@ -162,25 +169,11 @@ def main(argv=None):
|
||||
extractor = capa.main.get_extractor(
|
||||
args.sample, args.format, args.backend, sig_paths, should_save_workspace
|
||||
)
|
||||
except capa.main.UnsupportedFormatError:
|
||||
logger.error("-" * 80)
|
||||
logger.error(" Input file does not appear to be a PE file.")
|
||||
logger.error(" ")
|
||||
logger.error(
|
||||
" capa currently only supports analyzing PE files (or shellcode, when using --format sc32|sc64)."
|
||||
)
|
||||
logger.error(" If you don't know the input file type, you can try using the `file` utility to guess it.")
|
||||
logger.error("-" * 80)
|
||||
except capa.exceptions.UnsupportedFormatError:
|
||||
capa.helpers.log_unsupported_format_error()
|
||||
return -1
|
||||
except capa.main.UnsupportedRuntimeError:
|
||||
logger.error("-" * 80)
|
||||
logger.error(" Unsupported runtime or Python interpreter.")
|
||||
logger.error(" ")
|
||||
logger.error(" capa supports running under Python 2.7 using Vivisect for binary analysis.")
|
||||
logger.error(" It can also run within IDA Pro, using either Python 2.7 or 3.5+.")
|
||||
logger.error(" ")
|
||||
logger.error(" If you're seeing this message on the command line, please ensure you're running Python 2.7.")
|
||||
logger.error("-" * 80)
|
||||
except capa.exceptions.UnsupportedRuntimeError:
|
||||
capa.helpers.log_unsupported_runtime_error()
|
||||
return -1
|
||||
|
||||
meta = capa.main.collect_metadata(argv, args.sample, args.rules, extractor)
|
||||
@@ -199,7 +192,7 @@ def main(argv=None):
|
||||
# - when not an interactive session, and disable coloring
|
||||
# renderers should use coloring and assume it will be stripped out if necessary.
|
||||
colorama.init()
|
||||
doc = capa.render.result_document.convert_capabilities_to_result_document(meta, rules, capabilities)
|
||||
doc = rd.ResultDocument.from_capa(meta, rules, capabilities)
|
||||
print(render_matches_by_function(doc))
|
||||
colorama.deinit()
|
||||
|
||||
|
||||
@@ -75,12 +75,20 @@ import capa.rules
|
||||
import capa.engine
|
||||
import capa.helpers
|
||||
import capa.features
|
||||
import capa.exceptions
|
||||
import capa.render.verbose as v
|
||||
import capa.features.common
|
||||
import capa.features.freeze
|
||||
import capa.features.extractors.base_extractor
|
||||
from capa.helpers import log_unsupported_runtime_error
|
||||
|
||||
logger = logging.getLogger("capa.show-features")
|
||||
|
||||
|
||||
def format_address(addr: capa.features.address.Address) -> str:
|
||||
return v.format_address(capa.features.freeze.Address.from_capa((addr)))
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
if argv is None:
|
||||
argv = sys.argv[1:]
|
||||
@@ -88,7 +96,7 @@ def main(argv=None):
|
||||
parser = argparse.ArgumentParser(description="Show the features that capa extracts from the given sample")
|
||||
capa.main.install_common_args(parser, wanted={"format", "sample", "signatures", "backend"})
|
||||
|
||||
parser.add_argument("-F", "--function", type=lambda x: int(x, 0x10), help="Show features for specific function")
|
||||
parser.add_argument("-F", "--function", type=str, help="Show features for specific function")
|
||||
args = parser.parse_args(args=argv)
|
||||
capa.main.handle_common_args(args)
|
||||
|
||||
@@ -113,51 +121,38 @@ def main(argv=None):
|
||||
extractor = capa.main.get_extractor(
|
||||
args.sample, args.format, args.backend, sig_paths, should_save_workspace
|
||||
)
|
||||
except capa.main.UnsupportedFormatError:
|
||||
logger.error("-" * 80)
|
||||
logger.error(" Input file does not appear to be a PE file.")
|
||||
logger.error(" ")
|
||||
logger.error(
|
||||
" capa currently only supports analyzing PE files (or shellcode, when using --format sc32|sc64)."
|
||||
)
|
||||
logger.error(" If you don't know the input file type, you can try using the `file` utility to guess it.")
|
||||
logger.error("-" * 80)
|
||||
except capa.exceptions.UnsupportedFormatError:
|
||||
capa.helpers.log_unsupported_format_error()
|
||||
return -1
|
||||
except capa.main.UnsupportedRuntimeError:
|
||||
logger.error("-" * 80)
|
||||
logger.error(" Unsupported runtime or Python interpreter.")
|
||||
logger.error(" ")
|
||||
logger.error(" capa supports running under Python 2.7 using Vivisect for binary analysis.")
|
||||
logger.error(" It can also run within IDA Pro, using either Python 2.7 or 3.5+.")
|
||||
logger.error(" ")
|
||||
logger.error(" If you're seeing this message on the command line, please ensure you're running Python 2.7.")
|
||||
logger.error("-" * 80)
|
||||
except capa.exceptions.UnsupportedRuntimeError:
|
||||
log_unsupported_runtime_error()
|
||||
return -1
|
||||
|
||||
for feature, addr in extractor.extract_global_features():
|
||||
print("global: %s: %s" % (format_address(addr), feature))
|
||||
|
||||
if not args.function:
|
||||
for feature, va in extractor.extract_file_features():
|
||||
if va:
|
||||
print("file: 0x%08x: %s" % (va, feature))
|
||||
else:
|
||||
print("file: 0x00000000: %s" % (feature))
|
||||
for feature, addr in extractor.extract_file_features():
|
||||
print("file: %s: %s" % (format_address(addr), feature))
|
||||
|
||||
functions = extractor.get_functions()
|
||||
function_handles = extractor.get_functions()
|
||||
|
||||
if args.function:
|
||||
if args.format == "freeze":
|
||||
functions = tuple(filter(lambda f: f == args.function, functions))
|
||||
# TODO fix
|
||||
function_handles = tuple(filter(lambda fh: fh.address == args.function, function_handles))
|
||||
else:
|
||||
functions = tuple(filter(lambda f: int(f) == args.function, functions))
|
||||
function_handles = tuple(filter(lambda fh: format_address(fh.address) == args.function, function_handles))
|
||||
|
||||
if args.function not in [int(f) for f in functions]:
|
||||
print("0x%X not a function" % args.function)
|
||||
if args.function not in [format_address(fh.address) for fh in function_handles]:
|
||||
print("%s not a function" % args.function)
|
||||
return -1
|
||||
|
||||
if len(functions) == 0:
|
||||
print("0x%X not a function")
|
||||
if len(function_handles) == 0:
|
||||
print("%s not a function", args.function)
|
||||
return -1
|
||||
|
||||
print_features(functions, extractor)
|
||||
print_features(function_handles, extractor)
|
||||
|
||||
return 0
|
||||
|
||||
@@ -173,58 +168,71 @@ def ida_main():
|
||||
extractor = capa.features.extractors.ida.extractor.IdaFeatureExtractor()
|
||||
|
||||
if not function:
|
||||
for feature, va in extractor.extract_file_features():
|
||||
if va:
|
||||
print("file: 0x%08x: %s" % (va, feature))
|
||||
else:
|
||||
print("file: 0x00000000: %s" % (feature))
|
||||
for feature, addr in extractor.extract_file_features():
|
||||
print("file: %s: %s" % (format_address(addr), feature))
|
||||
return
|
||||
|
||||
functions = extractor.get_functions()
|
||||
function_handles = extractor.get_functions()
|
||||
|
||||
if function:
|
||||
functions = tuple(filter(lambda f: f.start_ea == function, functions))
|
||||
function_handles = tuple(filter(lambda fh: fh.inner.start_ea == function, function_handles))
|
||||
|
||||
if len(functions) == 0:
|
||||
if len(function_handles) == 0:
|
||||
print("0x%X not a function" % function)
|
||||
return -1
|
||||
|
||||
print_features(functions, extractor)
|
||||
print_features(function_handles, extractor)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def print_features(functions, extractor):
|
||||
def print_features(functions, extractor: capa.features.extractors.base_extractor.FeatureExtractor):
|
||||
for f in functions:
|
||||
function_address = int(f)
|
||||
|
||||
if extractor.is_library_function(function_address):
|
||||
function_name = extractor.get_function_name(function_address)
|
||||
logger.debug("skipping library function 0x%x (%s)", function_address, function_name)
|
||||
if extractor.is_library_function(f.address):
|
||||
function_name = extractor.get_function_name(f.address)
|
||||
logger.debug("skipping library function %s (%s)", format_address(f.address), function_name)
|
||||
continue
|
||||
|
||||
print("func: 0x%08x" % (function_address))
|
||||
print("func: %s" % (format_address(f.address)))
|
||||
|
||||
for feature, va in extractor.extract_function_features(f):
|
||||
for feature, addr in extractor.extract_function_features(f):
|
||||
if capa.features.common.is_global_feature(feature):
|
||||
continue
|
||||
|
||||
print("func: 0x%08x: %s" % (va, feature))
|
||||
if f.address != addr:
|
||||
print(" func: %s: %s -> %s" % (format_address(f.address), feature, format_address(addr)))
|
||||
else:
|
||||
print(" func: %s: %s" % (format_address(f.address), feature))
|
||||
|
||||
for bb in extractor.get_basic_blocks(f):
|
||||
for feature, va in extractor.extract_basic_block_features(f, bb):
|
||||
for feature, addr in extractor.extract_basic_block_features(f, bb):
|
||||
if capa.features.common.is_global_feature(feature):
|
||||
continue
|
||||
|
||||
print("bb : 0x%08x: %s" % (va, feature))
|
||||
if bb.address != addr:
|
||||
print(" bb: %s: %s -> %s" % (format_address(bb.address), feature, format_address(addr)))
|
||||
else:
|
||||
print(" bb: %s: %s" % (format_address(bb.address), feature))
|
||||
|
||||
for insn in extractor.get_instructions(f, bb):
|
||||
for feature, va in extractor.extract_insn_features(f, bb, insn):
|
||||
for feature, addr in extractor.extract_insn_features(f, bb, insn):
|
||||
if capa.features.common.is_global_feature(feature):
|
||||
continue
|
||||
|
||||
try:
|
||||
print("insn: 0x%08x: %s" % (va, feature))
|
||||
if insn.address != addr:
|
||||
print(
|
||||
" insn: %s: %s: %s -> %s"
|
||||
% (
|
||||
format_address(f.address),
|
||||
format_address(insn.address),
|
||||
feature,
|
||||
format_address(addr),
|
||||
)
|
||||
)
|
||||
else:
|
||||
print(" insn: %s: %s" % (format_address(insn.address), feature))
|
||||
|
||||
except UnicodeEncodeError:
|
||||
# may be an issue while piping to less and encountering non-ascii characters
|
||||
continue
|
||||
|
||||
21
setup.cfg
Normal file
21
setup.cfg
Normal file
@@ -0,0 +1,21 @@
|
||||
[bdist_wheel]
|
||||
universal = 1
|
||||
|
||||
[aliases]
|
||||
test = pytest
|
||||
|
||||
[pycodestyle]
|
||||
# the following suppress lints that conflict with the project's style:
|
||||
#
|
||||
# E203 Whitespace before :
|
||||
# E302 expected 2 blank lines, found 1
|
||||
# E402 module level import not at top of file
|
||||
# E501 line too long (209 > 180 characters)
|
||||
# E712 comparison to False should be 'if cond is False:' or 'if not cond:'
|
||||
# E722 do not use bare 'except'
|
||||
# E731 do not assign a lambda expression, use a def
|
||||
# W291 trailing whitespace
|
||||
# W503 line break before binary operator
|
||||
ignore = E203, E302, E402, E501, E712, E722, E731, W291, W503
|
||||
max-line-length = 180
|
||||
statistics = True
|
||||
48
setup.py
48
setup.py
@@ -11,22 +11,24 @@ import os
|
||||
import setuptools
|
||||
|
||||
requirements = [
|
||||
"tqdm==4.62.3",
|
||||
"tqdm==4.64.0",
|
||||
"pyyaml==6.0",
|
||||
"tabulate==0.8.9",
|
||||
"colorama==0.4.4",
|
||||
"colorama==0.4.5",
|
||||
"termcolor==1.1.0",
|
||||
"wcwidth==0.2.5",
|
||||
"ida-settings==2.1.0",
|
||||
"viv-utils[flirt]==0.6.9",
|
||||
"viv-utils[flirt]==0.7.5",
|
||||
"halo==0.0.31",
|
||||
"networkx==2.5.1",
|
||||
"ruamel.yaml==0.17.20",
|
||||
"vivisect==1.0.5",
|
||||
"smda==1.6.2",
|
||||
"pefile==2021.9.3",
|
||||
"typing==3.7.4.3",
|
||||
"pyelftools==0.27",
|
||||
"ruamel.yaml==0.17.21",
|
||||
"vivisect==1.0.8",
|
||||
"smda==1.8.4",
|
||||
"pefile==2022.5.30",
|
||||
"pyelftools==0.28",
|
||||
"dnfile==0.11.0",
|
||||
"dncil==1.0.1",
|
||||
"pydantic==1.9.1",
|
||||
]
|
||||
|
||||
# this sets __version__
|
||||
@@ -67,22 +69,28 @@ setuptools.setup(
|
||||
install_requires=requirements,
|
||||
extras_require={
|
||||
"dev": [
|
||||
"pytest==6.2.5",
|
||||
"pytest==7.1.2",
|
||||
"pytest-sugar==0.9.4",
|
||||
"pytest-instafail==0.4.2",
|
||||
"pytest-cov==3.0.0",
|
||||
"pycodestyle==2.8.0",
|
||||
"black==21.12b0",
|
||||
"pycodestyle==2.9.1",
|
||||
"black==22.6.0",
|
||||
"isort==5.10.1",
|
||||
"mypy==0.931",
|
||||
"psutil==5.9.0",
|
||||
"mypy==0.971",
|
||||
"psutil==5.9.1",
|
||||
"stix2==3.0.1",
|
||||
"requests==2.28.0",
|
||||
# type stubs for mypy
|
||||
"types-backports==0.1.3",
|
||||
"types-colorama==0.4.6",
|
||||
"types-PyYAML==6.0.3",
|
||||
"types-tabulate==0.8.5",
|
||||
"types-termcolor==1.1.3",
|
||||
"types-psutil==5.8.19",
|
||||
"types-colorama==0.4.15",
|
||||
"types-PyYAML==6.0.8",
|
||||
"types-tabulate==0.8.9",
|
||||
"types-termcolor==1.1.4",
|
||||
"types-psutil==5.8.23",
|
||||
"types_requests==2.28.1",
|
||||
],
|
||||
"build": [
|
||||
"pyinstaller==5.3",
|
||||
],
|
||||
},
|
||||
zip_safe=False,
|
||||
@@ -96,5 +104,5 @@ setuptools.setup(
|
||||
"Programming Language :: Python :: 3",
|
||||
"Topic :: Security",
|
||||
],
|
||||
python_requires=">=3.6",
|
||||
python_requires=">=3.7",
|
||||
)
|
||||
|
||||
Submodule tests/data updated: d9ee638835...a594988464
@@ -13,6 +13,7 @@ import binascii
|
||||
import itertools
|
||||
import contextlib
|
||||
import collections
|
||||
from typing import Set, Dict
|
||||
from functools import lru_cache
|
||||
|
||||
import pytest
|
||||
@@ -24,19 +25,25 @@ import capa.features.common
|
||||
import capa.features.basicblock
|
||||
from capa.features.common import (
|
||||
OS,
|
||||
OS_ANY,
|
||||
OS_LINUX,
|
||||
ARCH_I386,
|
||||
FORMAT_PE,
|
||||
ARCH_AMD64,
|
||||
FORMAT_ELF,
|
||||
OS_WINDOWS,
|
||||
BITNESS_X32,
|
||||
BITNESS_X64,
|
||||
FORMAT_DOTNET,
|
||||
Arch,
|
||||
Format,
|
||||
Feature,
|
||||
)
|
||||
from capa.features.address import Address
|
||||
from capa.features.extractors.base_extractor import BBHandle, InsnHandle, FunctionHandle
|
||||
from capa.features.extractors.dnfile.extractor import DnfileFeatureExtractor
|
||||
|
||||
CD = os.path.dirname(__file__)
|
||||
DOTNET_DIR = os.path.join(CD, "data", "dotnet")
|
||||
DNFILE_TESTFILES = os.path.join(DOTNET_DIR, "dnfile-testfiles")
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
@@ -131,7 +138,35 @@ def get_smda_extractor(path):
|
||||
def get_pefile_extractor(path):
|
||||
import capa.features.extractors.pefile
|
||||
|
||||
return capa.features.extractors.pefile.PefileFeatureExtractor(path)
|
||||
extractor = capa.features.extractors.pefile.PefileFeatureExtractor(path)
|
||||
|
||||
# overload the extractor so that the fixture exposes `extractor.path`
|
||||
setattr(extractor, "path", path)
|
||||
|
||||
return extractor
|
||||
|
||||
|
||||
def get_dotnetfile_extractor(path):
|
||||
import capa.features.extractors.dotnetfile
|
||||
|
||||
extractor = capa.features.extractors.dotnetfile.DotnetFileFeatureExtractor(path)
|
||||
|
||||
# overload the extractor so that the fixture exposes `extractor.path`
|
||||
setattr(extractor, "path", path)
|
||||
|
||||
return extractor
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def get_dnfile_extractor(path):
|
||||
import capa.features.extractors.dnfile.extractor
|
||||
|
||||
extractor = capa.features.extractors.dnfile.extractor.DnfileFeatureExtractor(path)
|
||||
|
||||
# overload the extractor so that the fixture exposes `extractor.path`
|
||||
setattr(extractor, "path", path)
|
||||
|
||||
return extractor
|
||||
|
||||
|
||||
def extract_global_features(extractor):
|
||||
@@ -150,30 +185,38 @@ def extract_file_features(extractor):
|
||||
|
||||
|
||||
# f may not be hashable (e.g. ida func_t) so cannot @lru_cache this
|
||||
def extract_function_features(extractor, f):
|
||||
def extract_function_features(extractor, fh):
|
||||
features = collections.defaultdict(set)
|
||||
for bb in extractor.get_basic_blocks(f):
|
||||
for insn in extractor.get_instructions(f, bb):
|
||||
for feature, va in extractor.extract_insn_features(f, bb, insn):
|
||||
for bb in extractor.get_basic_blocks(fh):
|
||||
for insn in extractor.get_instructions(fh, bb):
|
||||
for feature, va in extractor.extract_insn_features(fh, bb, insn):
|
||||
features[feature].add(va)
|
||||
for feature, va in extractor.extract_basic_block_features(f, bb):
|
||||
for feature, va in extractor.extract_basic_block_features(fh, bb):
|
||||
features[feature].add(va)
|
||||
for feature, va in extractor.extract_function_features(f):
|
||||
for feature, va in extractor.extract_function_features(fh):
|
||||
features[feature].add(va)
|
||||
return features
|
||||
|
||||
|
||||
# f may not be hashable (e.g. ida func_t) so cannot @lru_cache this
|
||||
def extract_basic_block_features(extractor, f, bb):
|
||||
def extract_basic_block_features(extractor, fh, bbh):
|
||||
features = collections.defaultdict(set)
|
||||
for insn in extractor.get_instructions(f, bb):
|
||||
for feature, va in extractor.extract_insn_features(f, bb, insn):
|
||||
for insn in extractor.get_instructions(fh, bbh):
|
||||
for feature, va in extractor.extract_insn_features(fh, bbh, insn):
|
||||
features[feature].add(va)
|
||||
for feature, va in extractor.extract_basic_block_features(f, bb):
|
||||
for feature, va in extractor.extract_basic_block_features(fh, bbh):
|
||||
features[feature].add(va)
|
||||
return features
|
||||
|
||||
|
||||
# f may not be hashable (e.g. ida func_t) so cannot @lru_cache this
|
||||
def extract_instruction_features(extractor, fh, bbh, ih) -> Dict[Feature, Set[Address]]:
|
||||
features = collections.defaultdict(set)
|
||||
for feature, addr in extractor.extract_insn_features(fh, bbh, ih):
|
||||
features[feature].add(addr)
|
||||
return features
|
||||
|
||||
|
||||
# note: too reduce the testing time it's recommended to reuse already existing test samples, if possible
|
||||
def get_data_path_by_name(name):
|
||||
if name == "mimikatz":
|
||||
@@ -220,6 +263,22 @@ def get_data_path_by_name(name):
|
||||
return os.path.join(CD, "data", "3b13b6f1d7cd14dc4a097a12e2e505c0a4cff495262261e2bfc991df238b9b04.dll_")
|
||||
elif name == "7351f.elf":
|
||||
return os.path.join(CD, "data", "7351f8a40c5450557b24622417fc478d.elf_")
|
||||
elif name.startswith("79abd"):
|
||||
return os.path.join(CD, "data", "79abd17391adc6251ecdc58d13d76baf.dll_")
|
||||
elif name.startswith("946a9"):
|
||||
return os.path.join(CD, "data", "946a99f36a46d335dec080d9a4371940.dll_")
|
||||
elif name.startswith("2f7f5f"):
|
||||
return os.path.join(CD, "data", "2f7f5fb5de175e770d7eae87666f9831.elf_")
|
||||
elif name.startswith("b9f5b"):
|
||||
return os.path.join(CD, "data", "b9f5bd514485fb06da39beff051b9fdc.exe_")
|
||||
elif name.startswith("mixed-mode-64"):
|
||||
return os.path.join(DNFILE_TESTFILES, "mixed-mode", "ModuleCode", "bin", "ModuleCode_amd64.exe")
|
||||
elif name.startswith("hello-world"):
|
||||
return os.path.join(DNFILE_TESTFILES, "hello-world", "hello-world.exe")
|
||||
elif name.startswith("_1c444"):
|
||||
return os.path.join(CD, "data", "dotnet", "1c444ebeba24dcba8628b7dfe5fec7c6.exe_")
|
||||
elif name.startswith("_692f"):
|
||||
return os.path.join(CD, "data", "dotnet", "692f7fd6d198e804d6af98eb9e390d61.exe_")
|
||||
else:
|
||||
raise ValueError("unexpected sample fixture: %s" % name)
|
||||
|
||||
@@ -269,6 +328,12 @@ def get_sample_md5_by_name(name):
|
||||
return "56a6ffe6a02941028cc8235204eef31d"
|
||||
elif name == "7351f.elf":
|
||||
return "7351f8a40c5450557b24622417fc478d"
|
||||
elif name.startswith("79abd"):
|
||||
return "79abd17391adc6251ecdc58d13d76baf"
|
||||
elif name.startswith("946a9"):
|
||||
return "946a99f36a46d335dec080d9a4371940"
|
||||
elif name.startswith("b9f5b"):
|
||||
return "b9f5bd514485fb06da39beff051b9fdc"
|
||||
else:
|
||||
raise ValueError("unexpected sample fixture: %s" % name)
|
||||
|
||||
@@ -282,20 +347,46 @@ def sample(request):
|
||||
return resolve_sample(request.param)
|
||||
|
||||
|
||||
def get_function(extractor, fva):
|
||||
for f in extractor.get_functions():
|
||||
if int(f) == fva:
|
||||
return f
|
||||
def get_function(extractor, fva: int) -> FunctionHandle:
|
||||
for fh in extractor.get_functions():
|
||||
if isinstance(extractor, DnfileFeatureExtractor):
|
||||
addr = fh.inner.offset
|
||||
else:
|
||||
addr = fh.address
|
||||
if addr == fva:
|
||||
return fh
|
||||
raise ValueError("function not found")
|
||||
|
||||
|
||||
def get_basic_block(extractor, f, va):
|
||||
for bb in extractor.get_basic_blocks(f):
|
||||
if int(bb) == va:
|
||||
return bb
|
||||
def get_function_by_token(extractor, token: int) -> FunctionHandle:
|
||||
for fh in extractor.get_functions():
|
||||
if fh.address.token.value == token:
|
||||
return fh
|
||||
raise ValueError("function not found by token")
|
||||
|
||||
|
||||
def get_basic_block(extractor, fh: FunctionHandle, va: int) -> BBHandle:
|
||||
for bbh in extractor.get_basic_blocks(fh):
|
||||
if isinstance(extractor, DnfileFeatureExtractor):
|
||||
addr = bbh.inner.offset
|
||||
else:
|
||||
addr = bbh.address
|
||||
if addr == va:
|
||||
return bbh
|
||||
raise ValueError("basic block not found")
|
||||
|
||||
|
||||
def get_instruction(extractor, fh: FunctionHandle, bbh: BBHandle, va: int) -> InsnHandle:
|
||||
for ih in extractor.get_instructions(fh, bbh):
|
||||
if isinstance(extractor, DnfileFeatureExtractor):
|
||||
addr = ih.inner.offset
|
||||
else:
|
||||
addr = ih.address
|
||||
if addr == va:
|
||||
return ih
|
||||
raise ValueError("instruction not found")
|
||||
|
||||
|
||||
def resolve_scope(scope):
|
||||
if scope == "file":
|
||||
|
||||
@@ -307,29 +398,56 @@ def resolve_scope(scope):
|
||||
|
||||
inner_file.__name__ = scope
|
||||
return inner_file
|
||||
elif "insn=" in scope:
|
||||
# like `function=0x401000,bb=0x40100A,insn=0x40100A`
|
||||
assert "function=" in scope
|
||||
assert "bb=" in scope
|
||||
assert "insn=" in scope
|
||||
fspec, _, spec = scope.partition(",")
|
||||
bbspec, _, ispec = spec.partition(",")
|
||||
fva = int(fspec.partition("=")[2], 0x10)
|
||||
bbva = int(bbspec.partition("=")[2], 0x10)
|
||||
iva = int(ispec.partition("=")[2], 0x10)
|
||||
|
||||
def inner_insn(extractor):
|
||||
fh = get_function(extractor, fva)
|
||||
bbh = get_basic_block(extractor, fh, bbva)
|
||||
ih = get_instruction(extractor, fh, bbh, iva)
|
||||
features = extract_instruction_features(extractor, fh, bbh, ih)
|
||||
for k, vs in extract_global_features(extractor).items():
|
||||
features[k].update(vs)
|
||||
return features
|
||||
|
||||
inner_insn.__name__ = scope
|
||||
return inner_insn
|
||||
elif "bb=" in scope:
|
||||
# like `function=0x401000,bb=0x40100A`
|
||||
assert "function=" in scope
|
||||
assert "bb=" in scope
|
||||
fspec, _, bbspec = scope.partition(",")
|
||||
fva = int(fspec.partition("=")[2], 0x10)
|
||||
bbva = int(bbspec.partition("=")[2], 0x10)
|
||||
|
||||
def inner_bb(extractor):
|
||||
f = get_function(extractor, fva)
|
||||
bb = get_basic_block(extractor, f, bbva)
|
||||
features = extract_basic_block_features(extractor, f, bb)
|
||||
fh = get_function(extractor, fva)
|
||||
bbh = get_basic_block(extractor, fh, bbva)
|
||||
features = extract_basic_block_features(extractor, fh, bbh)
|
||||
for k, vs in extract_global_features(extractor).items():
|
||||
features[k].update(vs)
|
||||
return features
|
||||
|
||||
inner_bb.__name__ = scope
|
||||
return inner_bb
|
||||
elif scope.startswith("function"):
|
||||
# like `function=0x401000`
|
||||
elif scope.startswith(("function", "token")):
|
||||
# like `function=0x401000` or `token=0x6000001`
|
||||
va = int(scope.partition("=")[2], 0x10)
|
||||
|
||||
def inner_function(extractor):
|
||||
f = get_function(extractor, va)
|
||||
features = extract_function_features(extractor, f)
|
||||
if scope.startswith("token"):
|
||||
fh = get_function_by_token(extractor, va)
|
||||
else:
|
||||
fh = get_function(extractor, va)
|
||||
features = extract_function_features(extractor, fh)
|
||||
for k, vs in extract_global_features(extractor).items():
|
||||
features[k].update(vs)
|
||||
return features
|
||||
@@ -410,6 +528,12 @@ FEATURE_PRESENCE_TESTS = sorted(
|
||||
("mimikatz", "function=0x40105D", capa.features.insn.Mnemonic("xor"), True),
|
||||
("mimikatz", "function=0x40105D", capa.features.insn.Mnemonic("in"), False),
|
||||
("mimikatz", "function=0x40105D", capa.features.insn.Mnemonic("out"), False),
|
||||
# insn/operand.number
|
||||
("mimikatz", "function=0x40105D,bb=0x401073", capa.features.insn.OperandNumber(1, 0xFF), True),
|
||||
("mimikatz", "function=0x40105D,bb=0x401073", capa.features.insn.OperandNumber(0, 0xFF), False),
|
||||
# insn/operand.offset
|
||||
("mimikatz", "function=0x40105D,bb=0x4010B0", capa.features.insn.OperandOffset(0, 4), True),
|
||||
("mimikatz", "function=0x40105D,bb=0x4010B0", capa.features.insn.OperandOffset(1, 4), False),
|
||||
# insn/number
|
||||
("mimikatz", "function=0x40105D", capa.features.insn.Number(0xFF), True),
|
||||
("mimikatz", "function=0x40105D", capa.features.insn.Number(0x3136B0), True),
|
||||
@@ -417,10 +541,6 @@ FEATURE_PRESENCE_TESTS = sorted(
|
||||
# insn/number: stack adjustments
|
||||
("mimikatz", "function=0x40105D", capa.features.insn.Number(0xC), False),
|
||||
("mimikatz", "function=0x40105D", capa.features.insn.Number(0x10), False),
|
||||
# insn/number: bitness flavors
|
||||
("mimikatz", "function=0x40105D", capa.features.insn.Number(0xFF), True),
|
||||
("mimikatz", "function=0x40105D", capa.features.insn.Number(0xFF, bitness=BITNESS_X32), True),
|
||||
("mimikatz", "function=0x40105D", capa.features.insn.Number(0xFF, bitness=BITNESS_X64), False),
|
||||
# insn/number: negative
|
||||
("mimikatz", "function=0x401553", capa.features.insn.Number(0xFFFFFFFF), True),
|
||||
("mimikatz", "function=0x43e543", capa.features.insn.Number(0xFFFFFFF0), True),
|
||||
@@ -436,10 +556,30 @@ FEATURE_PRESENCE_TESTS = sorted(
|
||||
# insn/offset: negative
|
||||
("mimikatz", "function=0x4011FB", capa.features.insn.Offset(-0x1), True),
|
||||
("mimikatz", "function=0x4011FB", capa.features.insn.Offset(-0x2), True),
|
||||
# insn/offset: bitness flavors
|
||||
("mimikatz", "function=0x40105D", capa.features.insn.Offset(0x0), True),
|
||||
("mimikatz", "function=0x40105D", capa.features.insn.Offset(0x0, bitness=BITNESS_X32), True),
|
||||
("mimikatz", "function=0x40105D", capa.features.insn.Offset(0x0, bitness=BITNESS_X64), False),
|
||||
#
|
||||
# insn/offset from mnemonic: add
|
||||
#
|
||||
# should not be considered, too big for an offset:
|
||||
# .text:00401D85 81 C1 00 00 00 80 add ecx, 80000000h
|
||||
("mimikatz", "function=0x401D64,bb=0x401D73,insn=0x401D85", capa.features.insn.Offset(0x80000000), False),
|
||||
# should not be considered, relative to stack:
|
||||
# .text:00401CF6 83 C4 10 add esp, 10h
|
||||
("mimikatz", "function=0x401CC7,bb=0x401CDE,insn=0x401CF6", capa.features.insn.Offset(0x10), False),
|
||||
# yes, this is also a offset (imagine eax is a pointer):
|
||||
# .text:0040223C 83 C0 04 add eax, 4
|
||||
("mimikatz", "function=0x402203,bb=0x402221,insn=0x40223C", capa.features.insn.Offset(0x4), True),
|
||||
#
|
||||
# insn/number from mnemonic: lea
|
||||
#
|
||||
# should not be considered, lea operand invalid encoding
|
||||
# .text:00471EE6 8D 1C 81 lea ebx, [ecx+eax*4]
|
||||
("mimikatz", "function=0x471EAB,bb=0x471ED8,insn=0x471EE6", capa.features.insn.Number(0x4), False),
|
||||
# should not be considered, lea operand invalid encoding
|
||||
# .text:004717B1 8D 4C 31 D0 lea ecx, [ecx+esi-30h]
|
||||
("mimikatz", "function=0x47153B,bb=0x4717AB,insn=0x4717B1", capa.features.insn.Number(-0x30), False),
|
||||
# yes, this is also a number (imagine edx is zero):
|
||||
# .text:004018C0 8D 4B 02 lea ecx, [ebx+2]
|
||||
("mimikatz", "function=0x401873,bb=0x4018B2,insn=0x4018C0", capa.features.insn.Number(0x2), True),
|
||||
# insn/api
|
||||
("mimikatz", "function=0x403BAC", capa.features.insn.API("advapi32.CryptAcquireContextW"), True),
|
||||
("mimikatz", "function=0x403BAC", capa.features.insn.API("advapi32.CryptAcquireContext"), True),
|
||||
@@ -561,6 +701,63 @@ FEATURE_PRESENCE_TESTS = sorted(
|
||||
("7351f.elf", "file", Arch(ARCH_AMD64), True),
|
||||
("7351f.elf", "function=0x408753", capa.features.common.String("/dev/null"), True),
|
||||
("7351f.elf", "function=0x408753,bb=0x408781", capa.features.insn.API("open"), True),
|
||||
("79abd...", "function=0x10002385,bb=0x10002385", capa.features.common.Characteristic("call $+5"), True),
|
||||
("946a9...", "function=0x10001510,bb=0x100015c0", capa.features.common.Characteristic("call $+5"), True),
|
||||
],
|
||||
# order tests by (file, item)
|
||||
# so that our LRU cache is most effective.
|
||||
key=lambda t: (t[0], t[1]),
|
||||
)
|
||||
|
||||
FEATURE_PRESENCE_TESTS_DOTNET = sorted(
|
||||
[
|
||||
("b9f5b", "file", Arch(ARCH_I386), True),
|
||||
("b9f5b", "file", Arch(ARCH_AMD64), False),
|
||||
("mixed-mode-64", "file", Arch(ARCH_AMD64), True),
|
||||
("mixed-mode-64", "file", Arch(ARCH_I386), False),
|
||||
("mixed-mode-64", "file", capa.features.common.Characteristic("mixed mode"), True),
|
||||
("hello-world", "file", capa.features.common.Characteristic("mixed mode"), False),
|
||||
("b9f5b", "file", OS(OS_ANY), True),
|
||||
("b9f5b", "file", Format(FORMAT_DOTNET), True),
|
||||
("hello-world", "file", capa.features.file.FunctionName("HelloWorld::Main"), True),
|
||||
("hello-world", "file", capa.features.file.FunctionName("HelloWorld::.ctor"), True),
|
||||
("hello-world", "file", capa.features.file.FunctionName("HelloWorld::.cctor"), False),
|
||||
("hello-world", "file", capa.features.common.String("Hello World!"), True),
|
||||
("hello-world", "file", capa.features.common.Class("HelloWorld"), True),
|
||||
("hello-world", "file", capa.features.common.Class("System.Console"), True),
|
||||
("hello-world", "file", capa.features.common.Namespace("System.Diagnostics"), True),
|
||||
("hello-world", "function=0x250", capa.features.common.String("Hello World!"), True),
|
||||
("hello-world", "function=0x250, bb=0x250, insn=0x252", capa.features.common.String("Hello World!"), True),
|
||||
("hello-world", "function=0x250, bb=0x250, insn=0x257", capa.features.common.Class("System.Console"), True),
|
||||
("hello-world", "function=0x250, bb=0x250, insn=0x257", capa.features.common.Namespace("System"), True),
|
||||
("hello-world", "function=0x250", capa.features.insn.API("System.Console::WriteLine"), True),
|
||||
("hello-world", "file", capa.features.file.Import("System.Console::WriteLine"), True),
|
||||
("_1c444", "file", capa.features.common.String(r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"), True),
|
||||
("_1c444", "file", capa.features.common.String("get_IsAlive"), True),
|
||||
("_1c444", "file", capa.features.file.Import("gdi32.CreateCompatibleBitmap"), True),
|
||||
("_1c444", "file", capa.features.file.Import("CreateCompatibleBitmap"), True),
|
||||
("_1c444", "file", capa.features.file.Import("gdi32::CreateCompatibleBitmap"), False),
|
||||
("_1c444", "function=0x1F68", capa.features.insn.API("GetWindowDC"), True),
|
||||
("_1c444", "function=0x1F68", capa.features.insn.API("user32.GetWindowDC"), True),
|
||||
("_1c444", "function=0x1F68", capa.features.insn.Number(0xCC0020), True),
|
||||
("_1c444", "function=0x1F68", capa.features.insn.Number(0x0), True),
|
||||
("_1c444", "function=0x1F68", capa.features.insn.Number(0x1), False),
|
||||
(
|
||||
"_1c444",
|
||||
"function=0x1F59, bb=0x1F59, insn=0x1F5B",
|
||||
capa.features.common.Characteristic("unmanaged call"),
|
||||
True,
|
||||
),
|
||||
("_1c444", "function=0x2544", capa.features.common.Characteristic("unmanaged call"), False),
|
||||
# same as above but using token instead of function
|
||||
("_1c444", "token=0x6000088", capa.features.common.Characteristic("unmanaged call"), False),
|
||||
(
|
||||
"_1c444",
|
||||
"function=0x1F68, bb=0x1F68, insn=0x1FF9",
|
||||
capa.features.insn.API("System.Drawing.Image::FromHbitmap"),
|
||||
True,
|
||||
),
|
||||
("_1c444", "function=0x1F68, bb=0x1F68, insn=0x1FF9", capa.features.insn.API("FromHbitmap"), False),
|
||||
],
|
||||
# order tests by (file, item)
|
||||
# so that our LRU cache is most effective.
|
||||
@@ -582,6 +779,9 @@ FEATURE_COUNT_TESTS = [
|
||||
]
|
||||
|
||||
|
||||
FEATURE_COUNT_TESTS_DOTNET = [] # type: ignore
|
||||
|
||||
|
||||
def do_test_feature_presence(get_extractor, sample, scope, feature, expected):
|
||||
extractor = get_extractor(sample)
|
||||
features = scope(extractor)
|
||||
@@ -679,3 +879,28 @@ def al_khaser_x86_extractor():
|
||||
@pytest.fixture
|
||||
def pingtaest_extractor():
|
||||
return get_extractor(get_data_path_by_name("pingtaest"))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def b9f5b_dotnetfile_extractor():
|
||||
return get_dotnetfile_extractor(get_data_path_by_name("b9f5b"))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mixed_mode_64_dotnetfile_extractor():
|
||||
return get_dotnetfile_extractor(get_data_path_by_name("mixed-mode-64"))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def hello_world_dotnetfile_extractor():
|
||||
return get_dnfile_extractor(get_data_path_by_name("hello-world"))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def _1c444_dotnetfile_extractor():
|
||||
return get_dnfile_extractor(get_data_path_by_name("_1c444"))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def _692f_dotnetfile_extractor():
|
||||
return get_dnfile_extractor(get_data_path_by_name("_692f"))
|
||||
|
||||
30
tests/test_dnfile_features.py
Normal file
30
tests/test_dnfile_features.py
Normal file
@@ -0,0 +1,30 @@
|
||||
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import pytest
|
||||
import fixtures
|
||||
from fixtures import *
|
||||
from fixtures import parametrize
|
||||
|
||||
|
||||
@parametrize(
|
||||
"sample,scope,feature,expected",
|
||||
fixtures.FEATURE_PRESENCE_TESTS_DOTNET,
|
||||
indirect=["sample", "scope"],
|
||||
)
|
||||
def test_dnfile_features(sample, scope, feature, expected):
|
||||
fixtures.do_test_feature_presence(fixtures.get_dnfile_extractor, sample, scope, feature, expected)
|
||||
|
||||
|
||||
@parametrize(
|
||||
"sample,scope,feature,expected",
|
||||
fixtures.FEATURE_COUNT_TESTS_DOTNET,
|
||||
indirect=["sample", "scope"],
|
||||
)
|
||||
def test_dnfile_feature_counts(sample, scope, feature, expected):
|
||||
fixtures.do_test_feature_count(fixtures.get_dnfile_extractor, sample, scope, feature, expected)
|
||||
37
tests/test_dotnet_features.py
Normal file
37
tests/test_dotnet_features.py
Normal file
@@ -0,0 +1,37 @@
|
||||
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import pytest
|
||||
import fixtures
|
||||
from fixtures import *
|
||||
from fixtures import parametrize
|
||||
|
||||
|
||||
@parametrize(
|
||||
"sample,scope,feature,expected",
|
||||
fixtures.FEATURE_PRESENCE_TESTS_DOTNET,
|
||||
indirect=["sample", "scope"],
|
||||
)
|
||||
def test_dnfile_features(sample, scope, feature, expected):
|
||||
fixtures.do_test_feature_presence(fixtures.get_dnfile_extractor, sample, scope, feature, expected)
|
||||
|
||||
|
||||
@parametrize(
|
||||
"extractor,function,expected",
|
||||
[
|
||||
("b9f5b_dotnetfile_extractor", "is_dotnet_file", True),
|
||||
("b9f5b_dotnetfile_extractor", "is_mixed_mode", False),
|
||||
("mixed_mode_64_dotnetfile_extractor", "is_mixed_mode", True),
|
||||
("b9f5b_dotnetfile_extractor", "get_entry_point", 0x6000007),
|
||||
("b9f5b_dotnetfile_extractor", "get_runtime_version", (2, 5)),
|
||||
("b9f5b_dotnetfile_extractor", "get_meta_version_string", "v2.0.50727"),
|
||||
],
|
||||
)
|
||||
def test_dnfile_extractor(request, extractor, function, expected):
|
||||
extractor_function = getattr(request.getfixturevalue(extractor), function)
|
||||
assert extractor_function() == expected
|
||||
43
tests/test_dotnetfile_features.py
Normal file
43
tests/test_dotnetfile_features.py
Normal file
@@ -0,0 +1,43 @@
|
||||
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import pytest
|
||||
import fixtures
|
||||
from fixtures import *
|
||||
from fixtures import parametrize
|
||||
|
||||
|
||||
@parametrize(
|
||||
"sample,scope,feature,expected",
|
||||
fixtures.FEATURE_PRESENCE_TESTS_DOTNET,
|
||||
indirect=["sample", "scope"],
|
||||
)
|
||||
def test_dotnetfile_features(sample, scope, feature, expected):
|
||||
if scope.__name__ != "file":
|
||||
pytest.xfail("dotnetfile only extracts file scope features")
|
||||
|
||||
if isinstance(feature, capa.features.file.FunctionName):
|
||||
pytest.xfail("dotnetfile doesn't extract function names")
|
||||
|
||||
fixtures.do_test_feature_presence(fixtures.get_dotnetfile_extractor, sample, scope, feature, expected)
|
||||
|
||||
|
||||
@parametrize(
|
||||
"extractor,function,expected",
|
||||
[
|
||||
("b9f5b_dotnetfile_extractor", "is_dotnet_file", True),
|
||||
("b9f5b_dotnetfile_extractor", "is_mixed_mode", False),
|
||||
("mixed_mode_64_dotnetfile_extractor", "is_mixed_mode", True),
|
||||
("b9f5b_dotnetfile_extractor", "get_entry_point", 0x6000007),
|
||||
("b9f5b_dotnetfile_extractor", "get_runtime_version", (2, 5)),
|
||||
("b9f5b_dotnetfile_extractor", "get_meta_version_string", "v2.0.50727"),
|
||||
],
|
||||
)
|
||||
def test_dotnetfile_extractor(request, extractor, function, expected):
|
||||
extractor_function = getattr(request.getfixturevalue(extractor), function)
|
||||
assert extractor_function() == expected
|
||||
@@ -15,7 +15,8 @@ EXPECTED = textwrap.dedent(
|
||||
rule:
|
||||
meta:
|
||||
name: test rule
|
||||
author: user@domain.com
|
||||
authors:
|
||||
- user@domain.com
|
||||
scope: function
|
||||
examples:
|
||||
- foo1234
|
||||
@@ -38,7 +39,8 @@ def test_rule_reformat_top_level_elements():
|
||||
- number: 2
|
||||
meta:
|
||||
name: test rule
|
||||
author: user@domain.com
|
||||
authors:
|
||||
- user@domain.com
|
||||
scope: function
|
||||
examples:
|
||||
- foo1234
|
||||
@@ -55,7 +57,8 @@ def test_rule_reformat_indentation():
|
||||
rule:
|
||||
meta:
|
||||
name: test rule
|
||||
author: user@domain.com
|
||||
authors:
|
||||
- user@domain.com
|
||||
scope: function
|
||||
examples:
|
||||
- foo1234
|
||||
@@ -75,7 +78,8 @@ def test_rule_reformat_order():
|
||||
"""
|
||||
rule:
|
||||
meta:
|
||||
author: user@domain.com
|
||||
authors:
|
||||
- user@domain.com
|
||||
examples:
|
||||
- foo1234
|
||||
- bar5678
|
||||
@@ -98,7 +102,8 @@ def test_rule_reformat_meta_update():
|
||||
"""
|
||||
rule:
|
||||
meta:
|
||||
author: user@domain.com
|
||||
authors:
|
||||
- user@domain.com
|
||||
examples:
|
||||
- foo1234
|
||||
- bar5678
|
||||
@@ -124,7 +129,8 @@ def test_rule_reformat_string_description():
|
||||
rule:
|
||||
meta:
|
||||
name: test rule
|
||||
author: user@domain.com
|
||||
authors:
|
||||
- user@domain.com
|
||||
scope: function
|
||||
features:
|
||||
- and:
|
||||
|
||||
@@ -6,6 +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.
|
||||
import textwrap
|
||||
from typing import List
|
||||
|
||||
from fixtures import *
|
||||
|
||||
@@ -17,49 +18,60 @@ import capa.features.insn
|
||||
import capa.features.common
|
||||
import capa.features.freeze
|
||||
import capa.features.basicblock
|
||||
import capa.features.extractors.null
|
||||
import capa.features.extractors.base_extractor
|
||||
from capa.features.address import AbsoluteVirtualAddress
|
||||
|
||||
EXTRACTOR = capa.features.extractors.base_extractor.NullFeatureExtractor(
|
||||
{
|
||||
"base address": 0x401000,
|
||||
"file features": [
|
||||
(0x402345, capa.features.common.Characteristic("embedded pe")),
|
||||
EXTRACTOR = capa.features.extractors.null.NullFeatureExtractor(
|
||||
base_address=AbsoluteVirtualAddress(0x401000),
|
||||
global_features=[],
|
||||
file_features=[
|
||||
(AbsoluteVirtualAddress(0x402345), capa.features.common.Characteristic("embedded pe")),
|
||||
],
|
||||
"functions": {
|
||||
0x401000: {
|
||||
"features": [
|
||||
(0x401000, capa.features.common.Characteristic("indirect call")),
|
||||
functions={
|
||||
AbsoluteVirtualAddress(0x401000): capa.features.extractors.null.FunctionFeatures(
|
||||
features=[
|
||||
(AbsoluteVirtualAddress(0x401000), capa.features.common.Characteristic("indirect call")),
|
||||
],
|
||||
"basic blocks": {
|
||||
0x401000: {
|
||||
"features": [
|
||||
(0x401000, capa.features.common.Characteristic("tight loop")),
|
||||
basic_blocks={
|
||||
AbsoluteVirtualAddress(0x401000): capa.features.extractors.null.BasicBlockFeatures(
|
||||
features=[
|
||||
(AbsoluteVirtualAddress(0x401000), capa.features.common.Characteristic("tight loop")),
|
||||
],
|
||||
"instructions": {
|
||||
0x401000: {
|
||||
"features": [
|
||||
(0x401000, capa.features.insn.Mnemonic("xor")),
|
||||
(0x401000, capa.features.common.Characteristic("nzxor")),
|
||||
instructions={
|
||||
AbsoluteVirtualAddress(0x401000): capa.features.extractors.null.InstructionFeatures(
|
||||
features=[
|
||||
(AbsoluteVirtualAddress(0x401000), capa.features.insn.Mnemonic("xor")),
|
||||
(AbsoluteVirtualAddress(0x401000), capa.features.common.Characteristic("nzxor")),
|
||||
],
|
||||
},
|
||||
0x401002: {
|
||||
"features": [
|
||||
(0x401002, capa.features.insn.Mnemonic("mov")),
|
||||
),
|
||||
AbsoluteVirtualAddress(0x401002): capa.features.extractors.null.InstructionFeatures(
|
||||
features=[
|
||||
(AbsoluteVirtualAddress(0x401002), capa.features.insn.Mnemonic("mov")),
|
||||
],
|
||||
),
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def addresses(s) -> List[Address]:
|
||||
return list(sorted(map(lambda i: i.address, s)))
|
||||
|
||||
|
||||
def test_null_feature_extractor():
|
||||
assert list(EXTRACTOR.get_functions()) == [0x401000]
|
||||
assert list(EXTRACTOR.get_basic_blocks(0x401000)) == [0x401000]
|
||||
assert list(EXTRACTOR.get_instructions(0x401000, 0x0401000)) == [0x401000, 0x401002]
|
||||
fh = FunctionHandle(AbsoluteVirtualAddress(0x401000), None)
|
||||
bbh = BBHandle(AbsoluteVirtualAddress(0x401000), None)
|
||||
|
||||
assert addresses(EXTRACTOR.get_functions()) == [AbsoluteVirtualAddress(0x401000)]
|
||||
assert addresses(EXTRACTOR.get_basic_blocks(fh)) == [AbsoluteVirtualAddress(0x401000)]
|
||||
assert addresses(EXTRACTOR.get_instructions(fh, bbh)) == [
|
||||
AbsoluteVirtualAddress(0x401000),
|
||||
AbsoluteVirtualAddress(0x401002),
|
||||
]
|
||||
|
||||
rules = capa.rules.RuleSet(
|
||||
[
|
||||
@@ -85,63 +97,33 @@ def test_null_feature_extractor():
|
||||
|
||||
|
||||
def compare_extractors(a, b):
|
||||
"""
|
||||
args:
|
||||
a (capa.features.extractors.NullFeatureExtractor)
|
||||
b (capa.features.extractors.NullFeatureExtractor)
|
||||
"""
|
||||
|
||||
# TODO: ordering of these things probably doesn't work yet
|
||||
|
||||
assert list(a.extract_file_features()) == list(b.extract_file_features())
|
||||
assert list(a.get_functions()) == list(b.get_functions())
|
||||
|
||||
assert addresses(a.get_functions()) == addresses(b.get_functions())
|
||||
for f in a.get_functions():
|
||||
assert list(a.get_basic_blocks(f)) == list(b.get_basic_blocks(f))
|
||||
assert list(a.extract_function_features(f)) == list(b.extract_function_features(f))
|
||||
assert addresses(a.get_basic_blocks(f)) == addresses(b.get_basic_blocks(f))
|
||||
assert list(sorted(set(a.extract_function_features(f)))) == list(sorted(set(b.extract_function_features(f))))
|
||||
|
||||
for bb in a.get_basic_blocks(f):
|
||||
assert list(a.get_instructions(f, bb)) == list(b.get_instructions(f, bb))
|
||||
assert list(a.extract_basic_block_features(f, bb)) == list(b.extract_basic_block_features(f, bb))
|
||||
assert addresses(a.get_instructions(f, bb)) == addresses(b.get_instructions(f, bb))
|
||||
assert list(sorted(set(a.extract_basic_block_features(f, bb)))) == list(
|
||||
sorted(set(b.extract_basic_block_features(f, bb)))
|
||||
)
|
||||
|
||||
for insn in a.get_instructions(f, bb):
|
||||
assert list(a.extract_insn_features(f, bb, insn)) == list(b.extract_insn_features(f, bb, insn))
|
||||
|
||||
|
||||
def compare_extractors_viv_null(viv_ext, null_ext):
|
||||
"""
|
||||
almost identical to compare_extractors but adds casts to ints since the VivisectFeatureExtractor returns objects
|
||||
and NullFeatureExtractor returns ints
|
||||
|
||||
args:
|
||||
viv_ext (capa.features.extractors.viv.extractor.VivisectFeatureExtractor)
|
||||
null_ext (capa.features.extractors.NullFeatureExtractor)
|
||||
"""
|
||||
assert list(viv_ext.extract_file_features()) == list(null_ext.extract_file_features())
|
||||
assert list(map(int, viv_ext.get_functions())) == list(null_ext.get_functions())
|
||||
for f in viv_ext.get_functions():
|
||||
assert list(map(int, viv_ext.get_basic_blocks(f))) == list(null_ext.get_basic_blocks(int(f)))
|
||||
assert list(viv_ext.extract_function_features(f)) == list(null_ext.extract_function_features(int(f)))
|
||||
|
||||
for bb in viv_ext.get_basic_blocks(f):
|
||||
assert list(map(int, viv_ext.get_instructions(f, bb))) == list(null_ext.get_instructions(int(f), int(bb)))
|
||||
assert list(viv_ext.extract_basic_block_features(f, bb)) == list(
|
||||
null_ext.extract_basic_block_features(int(f), int(bb))
|
||||
)
|
||||
|
||||
for insn in viv_ext.get_instructions(f, bb):
|
||||
assert list(viv_ext.extract_insn_features(f, bb, insn)) == list(
|
||||
null_ext.extract_insn_features(int(f), int(bb), int(insn))
|
||||
assert list(sorted(set(a.extract_insn_features(f, bb, insn)))) == list(
|
||||
sorted(set(b.extract_insn_features(f, bb, insn)))
|
||||
)
|
||||
|
||||
|
||||
def test_freeze_s_roundtrip():
|
||||
def test_freeze_str_roundtrip():
|
||||
load = capa.features.freeze.loads
|
||||
dump = capa.features.freeze.dumps
|
||||
reanimated = load(dump(EXTRACTOR))
|
||||
compare_extractors(EXTRACTOR, reanimated)
|
||||
|
||||
|
||||
def test_freeze_b_roundtrip():
|
||||
def test_freeze_bytes_roundtrip():
|
||||
load = capa.features.freeze.load
|
||||
dump = capa.features.freeze.dump
|
||||
reanimated = load(dump(EXTRACTOR))
|
||||
@@ -149,9 +131,7 @@ def test_freeze_b_roundtrip():
|
||||
|
||||
|
||||
def roundtrip_feature(feature):
|
||||
serialize = capa.features.freeze.serialize_feature
|
||||
deserialize = capa.features.freeze.deserialize_feature
|
||||
assert feature == deserialize(serialize(feature))
|
||||
assert feature == capa.features.freeze.feature_from_capa(feature).to_capa()
|
||||
|
||||
|
||||
def test_serialize_features():
|
||||
@@ -166,6 +146,7 @@ def test_serialize_features():
|
||||
roundtrip_feature(capa.features.file.Export("BaseThreadInitThunk"))
|
||||
roundtrip_feature(capa.features.file.Import("kernel32.IsWow64Process"))
|
||||
roundtrip_feature(capa.features.file.Import("#11"))
|
||||
roundtrip_feature(capa.features.insn.OperandOffset(0, 0x8))
|
||||
|
||||
|
||||
def test_freeze_sample(tmpdir, z9324d_extractor):
|
||||
@@ -175,13 +156,21 @@ def test_freeze_sample(tmpdir, z9324d_extractor):
|
||||
assert capa.features.freeze.main([path, o, "-v"]) == 0
|
||||
|
||||
|
||||
def test_freeze_load_sample(tmpdir, z9324d_extractor):
|
||||
@pytest.mark.parametrize(
|
||||
"extractor",
|
||||
[
|
||||
pytest.param("z9324d_extractor"),
|
||||
],
|
||||
)
|
||||
def test_freeze_load_sample(tmpdir, request, extractor):
|
||||
o = tmpdir.mkdir("capa").join("test.frz")
|
||||
|
||||
extractor = request.getfixturevalue(extractor)
|
||||
|
||||
with open(o.strpath, "wb") as f:
|
||||
f.write(capa.features.freeze.dump(z9324d_extractor))
|
||||
f.write(capa.features.freeze.dump(extractor))
|
||||
|
||||
with open(o.strpath, "rb") as f:
|
||||
null_extractor = capa.features.freeze.load(f.read())
|
||||
|
||||
compare_extractors_viv_null(z9324d_extractor, null_extractor)
|
||||
compare_extractors(extractor, null_extractor)
|
||||
|
||||
@@ -11,6 +11,7 @@ import textwrap
|
||||
|
||||
import fixtures
|
||||
from fixtures import *
|
||||
from fixtures import _692f_dotnetfile_extractor, _1c444_dotnetfile_extractor
|
||||
|
||||
import capa.main
|
||||
import capa.rules
|
||||
@@ -326,6 +327,62 @@ def test_count_bb(z9324d_extractor):
|
||||
assert "count bb" in capabilities
|
||||
|
||||
|
||||
def test_instruction_scope(z9324d_extractor):
|
||||
# .text:004071A4 68 E8 03 00 00 push 3E8h
|
||||
rules = capa.rules.RuleSet(
|
||||
[
|
||||
capa.rules.Rule.from_yaml(
|
||||
textwrap.dedent(
|
||||
"""
|
||||
rule:
|
||||
meta:
|
||||
name: push 1000
|
||||
namespace: test
|
||||
scope: instruction
|
||||
features:
|
||||
- and:
|
||||
- mnemonic: push
|
||||
- number: 1000
|
||||
"""
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
||||
capabilities, meta = capa.main.find_capabilities(rules, z9324d_extractor)
|
||||
assert "push 1000" in capabilities
|
||||
assert 0x4071A4 in set(map(lambda result: result[0], capabilities["push 1000"]))
|
||||
|
||||
|
||||
def test_instruction_subscope(z9324d_extractor):
|
||||
# .text:00406F60 sub_406F60 proc near
|
||||
# [...]
|
||||
# .text:004071A4 68 E8 03 00 00 push 3E8h
|
||||
rules = capa.rules.RuleSet(
|
||||
[
|
||||
capa.rules.Rule.from_yaml(
|
||||
textwrap.dedent(
|
||||
"""
|
||||
rule:
|
||||
meta:
|
||||
name: push 1000 on i386
|
||||
namespace: test
|
||||
scope: function
|
||||
features:
|
||||
- and:
|
||||
- arch: i386
|
||||
- instruction:
|
||||
- mnemonic: push
|
||||
- number: 1000
|
||||
"""
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
||||
capabilities, meta = capa.main.find_capabilities(rules, z9324d_extractor)
|
||||
assert "push 1000 on i386" in capabilities
|
||||
assert 0x406F60 in set(map(lambda result: result[0], capabilities["push 1000 on i386"]))
|
||||
|
||||
|
||||
def test_fix262(pma16_01_extractor, capsys):
|
||||
# tests rules can be loaded successfully and all output modes
|
||||
path = pma16_01_extractor.path
|
||||
@@ -382,6 +439,31 @@ def test_json_meta(capsys):
|
||||
assert capa.main.main([path, "-j"]) == 0
|
||||
std = capsys.readouterr()
|
||||
std_json = json.loads(std.out)
|
||||
# remember: json can't have integer keys :-(
|
||||
assert str(0x10001010) in std_json["meta"]["analysis"]["layout"]["functions"]
|
||||
assert 0x10001179 in std_json["meta"]["analysis"]["layout"]["functions"][str(0x10001010)]["matched_basic_blocks"]
|
||||
|
||||
assert {"type": "absolute", "value": 0x10001010} in list(
|
||||
map(lambda f: f["address"], std_json["meta"]["analysis"]["layout"]["functions"])
|
||||
)
|
||||
|
||||
for addr, info in std_json["meta"]["analysis"]["layout"]["functions"]:
|
||||
if addr == ["absolute", 0x10001010]:
|
||||
assert {"address": ["absolute", 0x10001179]} in info["matched_basic_blocks"]
|
||||
|
||||
|
||||
def test_main_dotnet(_1c444_dotnetfile_extractor):
|
||||
# tests rules can be loaded successfully and all output modes
|
||||
path = _1c444_dotnetfile_extractor.path
|
||||
assert capa.main.main([path, "-vv"]) == 0
|
||||
assert capa.main.main([path, "-v"]) == 0
|
||||
assert capa.main.main([path, "-j"]) == 0
|
||||
assert capa.main.main([path, "-q"]) == 0
|
||||
assert capa.main.main([path]) == 0
|
||||
|
||||
|
||||
def test_main_dotnet2(_692f_dotnetfile_extractor):
|
||||
# tests rules can be loaded successfully and all output modes
|
||||
path = _692f_dotnetfile_extractor.path
|
||||
assert capa.main.main([path, "-vv"]) == 0
|
||||
assert capa.main.main([path, "-v"]) == 0
|
||||
assert capa.main.main([path, "-j"]) == 0
|
||||
assert capa.main.main([path, "-q"]) == 0
|
||||
assert capa.main.main([path]) == 0
|
||||
|
||||
@@ -531,3 +531,57 @@ def test_match_not_not():
|
||||
|
||||
_, matches = match([r], {capa.features.insn.Number(100): {1, 2}}, 0x0)
|
||||
assert "test rule" in matches
|
||||
|
||||
|
||||
def test_match_operand_number():
|
||||
rule = textwrap.dedent(
|
||||
"""
|
||||
rule:
|
||||
meta:
|
||||
name: test rule
|
||||
features:
|
||||
- and:
|
||||
- operand[0].number: 0x10
|
||||
"""
|
||||
)
|
||||
r = capa.rules.Rule.from_yaml(rule)
|
||||
|
||||
assert capa.features.insn.OperandNumber(0, 0x10) in {capa.features.insn.OperandNumber(0, 0x10)}
|
||||
|
||||
_, matches = match([r], {capa.features.insn.OperandNumber(0, 0x10): {1, 2}}, 0x0)
|
||||
assert "test rule" in matches
|
||||
|
||||
# mismatching index
|
||||
_, matches = match([r], {capa.features.insn.OperandNumber(1, 0x10): {1, 2}}, 0x0)
|
||||
assert "test rule" not in matches
|
||||
|
||||
# mismatching value
|
||||
_, matches = match([r], {capa.features.insn.OperandNumber(0, 0x11): {1, 2}}, 0x0)
|
||||
assert "test rule" not in matches
|
||||
|
||||
|
||||
def test_match_operand_offset():
|
||||
rule = textwrap.dedent(
|
||||
"""
|
||||
rule:
|
||||
meta:
|
||||
name: test rule
|
||||
features:
|
||||
- and:
|
||||
- operand[0].offset: 0x10
|
||||
"""
|
||||
)
|
||||
r = capa.rules.Rule.from_yaml(rule)
|
||||
|
||||
assert capa.features.insn.OperandOffset(0, 0x10) in {capa.features.insn.OperandOffset(0, 0x10)}
|
||||
|
||||
_, matches = match([r], {capa.features.insn.OperandOffset(0, 0x10): {1, 2}}, 0x0)
|
||||
assert "test rule" in matches
|
||||
|
||||
# mismatching index
|
||||
_, matches = match([r], {capa.features.insn.OperandOffset(1, 0x10): {1, 2}}, 0x0)
|
||||
assert "test rule" not in matches
|
||||
|
||||
# mismatching value
|
||||
_, matches = match([r], {capa.features.insn.OperandOffset(0, 0x11): {1, 2}}, 0x0)
|
||||
assert "test rule" not in matches
|
||||
|
||||
26
tests/test_os_detection.py
Normal file
26
tests/test_os_detection.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (C) 2022 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 pytest
|
||||
from fixtures import *
|
||||
|
||||
import capa.features.extractors.elf
|
||||
|
||||
|
||||
def test_elf_section_gnu_abi_tag():
|
||||
path = get_data_path_by_name("2f7f5f")
|
||||
with open(path, "rb") as f:
|
||||
assert capa.features.extractors.elf.detect_elf_os(f) == "linux"
|
||||
|
||||
|
||||
def test_elf_program_header_gnu_abi_tag():
|
||||
path = get_data_path_by_name("7351f.elf")
|
||||
with open(path, "rb") as f:
|
||||
assert capa.features.extractors.elf.detect_elf_os(f) == "linux"
|
||||
@@ -9,14 +9,10 @@ import capa.render.result_document
|
||||
|
||||
def test_render_number():
|
||||
assert str(capa.features.insn.Number(1)) == "number(0x1)"
|
||||
assert str(capa.features.insn.Number(1, bitness=capa.features.common.BITNESS_X32)) == "number/x32(0x1)"
|
||||
assert str(capa.features.insn.Number(1, bitness=capa.features.common.BITNESS_X64)) == "number/x64(0x1)"
|
||||
|
||||
|
||||
def test_render_offset():
|
||||
assert str(capa.features.insn.Offset(1)) == "offset(0x1)"
|
||||
assert str(capa.features.insn.Offset(1, bitness=capa.features.common.BITNESS_X32)) == "offset/x32(0x1)"
|
||||
assert str(capa.features.insn.Offset(1, bitness=capa.features.common.BITNESS_X64)) == "offset/x64(0x1)"
|
||||
|
||||
|
||||
def test_render_meta_attack():
|
||||
@@ -32,6 +28,9 @@ def test_render_meta_attack():
|
||||
rule:
|
||||
meta:
|
||||
name: test rule
|
||||
scope: function
|
||||
authors:
|
||||
- foo
|
||||
att&ck:
|
||||
- {:s}
|
||||
features:
|
||||
@@ -41,13 +40,13 @@ def test_render_meta_attack():
|
||||
)
|
||||
)
|
||||
r = capa.rules.Rule.from_yaml(rule)
|
||||
rule_meta = capa.render.result_document.convert_meta_to_result_document(r.meta)
|
||||
attack = rule_meta["att&ck"][0]
|
||||
rule_meta = capa.render.result_document.RuleMetadata.from_capa(r)
|
||||
attack = rule_meta.attack[0]
|
||||
|
||||
assert attack["id"] == id
|
||||
assert attack["tactic"] == tactic
|
||||
assert attack["technique"] == technique
|
||||
assert attack["subtechnique"] == subtechnique
|
||||
assert attack.id == id
|
||||
assert attack.tactic == tactic
|
||||
assert attack.technique == technique
|
||||
assert attack.subtechnique == subtechnique
|
||||
|
||||
assert capa.render.utils.format_parts_id(attack) == canonical
|
||||
|
||||
@@ -65,6 +64,9 @@ def test_render_meta_mbc():
|
||||
rule:
|
||||
meta:
|
||||
name: test rule
|
||||
scope: function
|
||||
authors:
|
||||
- foo
|
||||
mbc:
|
||||
- {:s}
|
||||
features:
|
||||
@@ -74,12 +76,12 @@ def test_render_meta_mbc():
|
||||
)
|
||||
)
|
||||
r = capa.rules.Rule.from_yaml(rule)
|
||||
rule_meta = capa.render.result_document.convert_meta_to_result_document(r.meta)
|
||||
attack = rule_meta["mbc"][0]
|
||||
rule_meta = capa.render.result_document.RuleMetadata.from_capa(r)
|
||||
mbc = rule_meta.mbc[0]
|
||||
|
||||
assert attack["id"] == id
|
||||
assert attack["objective"] == objective
|
||||
assert attack["behavior"] == behavior
|
||||
assert attack["method"] == method
|
||||
assert mbc.id == id
|
||||
assert mbc.objective == objective
|
||||
assert mbc.behavior == behavior
|
||||
assert mbc.method == method
|
||||
|
||||
assert capa.render.utils.format_parts_id(attack) == canonical
|
||||
assert capa.render.utils.format_parts_id(mbc) == canonical
|
||||
|
||||
@@ -23,8 +23,6 @@ from capa.features.common import (
|
||||
ARCH_AMD64,
|
||||
FORMAT_ELF,
|
||||
OS_WINDOWS,
|
||||
BITNESS_X32,
|
||||
BITNESS_X64,
|
||||
Arch,
|
||||
Format,
|
||||
String,
|
||||
@@ -44,7 +42,8 @@ def test_rule_yaml():
|
||||
rule:
|
||||
meta:
|
||||
name: test rule
|
||||
author: user@domain.com
|
||||
authors:
|
||||
- user@domain.com
|
||||
scope: function
|
||||
examples:
|
||||
- foo1234
|
||||
@@ -110,8 +109,6 @@ def test_rule_descriptions():
|
||||
- description: and description
|
||||
- and:
|
||||
- description: and description
|
||||
- offset/x64: 0x50 = offset/x64 description
|
||||
- offset/x64: 0x30 = offset/x64 description
|
||||
"""
|
||||
)
|
||||
r = capa.rules.Rule.from_yaml(rule)
|
||||
@@ -531,39 +528,6 @@ def test_invalid_number():
|
||||
)
|
||||
|
||||
|
||||
def test_number_bitness():
|
||||
r = capa.rules.Rule.from_yaml(
|
||||
textwrap.dedent(
|
||||
"""
|
||||
rule:
|
||||
meta:
|
||||
name: test rule
|
||||
features:
|
||||
- number/x32: 2
|
||||
"""
|
||||
)
|
||||
)
|
||||
assert r.evaluate({Number(2, bitness=BITNESS_X32): {1}}) == True
|
||||
|
||||
assert r.evaluate({Number(2): {1}}) == False
|
||||
assert r.evaluate({Number(2, bitness=BITNESS_X64): {1}}) == False
|
||||
|
||||
|
||||
def test_number_bitness_symbol():
|
||||
r = capa.rules.Rule.from_yaml(
|
||||
textwrap.dedent(
|
||||
"""
|
||||
rule:
|
||||
meta:
|
||||
name: test rule
|
||||
features:
|
||||
- number/x32: 2 = some constant
|
||||
"""
|
||||
)
|
||||
)
|
||||
assert r.evaluate({Number(2, bitness=BITNESS_X32, description="some constant"): {1}}) == True
|
||||
|
||||
|
||||
def test_offset_symbol():
|
||||
rule = textwrap.dedent(
|
||||
"""
|
||||
@@ -609,39 +573,6 @@ def test_count_offset_symbol():
|
||||
assert r.evaluate({Offset(0x100, description="symbol name"): {1, 2, 3}}) == True
|
||||
|
||||
|
||||
def test_offset_bitness():
|
||||
r = capa.rules.Rule.from_yaml(
|
||||
textwrap.dedent(
|
||||
"""
|
||||
rule:
|
||||
meta:
|
||||
name: test rule
|
||||
features:
|
||||
- offset/x32: 2
|
||||
"""
|
||||
)
|
||||
)
|
||||
assert r.evaluate({Offset(2, bitness=BITNESS_X32): {1}}) == True
|
||||
|
||||
assert r.evaluate({Offset(2): {1}}) == False
|
||||
assert r.evaluate({Offset(2, bitness=BITNESS_X64): {1}}) == False
|
||||
|
||||
|
||||
def test_offset_bitness_symbol():
|
||||
r = capa.rules.Rule.from_yaml(
|
||||
textwrap.dedent(
|
||||
"""
|
||||
rule:
|
||||
meta:
|
||||
name: test rule
|
||||
features:
|
||||
- offset/x32: 2 = some constant
|
||||
"""
|
||||
)
|
||||
)
|
||||
assert r.evaluate({Offset(2, bitness=BITNESS_X32, description="some constant"): {1}}) == True
|
||||
|
||||
|
||||
def test_invalid_offset():
|
||||
with pytest.raises(capa.rules.InvalidRule):
|
||||
r = capa.rules.Rule.from_yaml(
|
||||
@@ -794,7 +725,8 @@ def test_filter_rules():
|
||||
rule:
|
||||
meta:
|
||||
name: rule 1
|
||||
author: joe
|
||||
authors:
|
||||
- joe
|
||||
features:
|
||||
- api: CreateFile
|
||||
"""
|
||||
@@ -873,7 +805,8 @@ def test_filter_rules_missing_dependency():
|
||||
rule:
|
||||
meta:
|
||||
name: rule 1
|
||||
author: joe
|
||||
authors:
|
||||
- joe
|
||||
features:
|
||||
- match: rule 2
|
||||
"""
|
||||
|
||||
133
tests/test_rules_insn_scope.py
Normal file
133
tests/test_rules_insn_scope.py
Normal file
@@ -0,0 +1,133 @@
|
||||
# Copyright (C) 2022 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 textwrap
|
||||
|
||||
import pytest
|
||||
|
||||
import capa.rules
|
||||
|
||||
|
||||
def test_rule_scope_instruction():
|
||||
capa.rules.Rule.from_yaml(
|
||||
textwrap.dedent(
|
||||
"""
|
||||
rule:
|
||||
meta:
|
||||
name: test rule
|
||||
scope: instruction
|
||||
features:
|
||||
- and:
|
||||
- mnemonic: mov
|
||||
- arch: i386
|
||||
- os: windows
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
with pytest.raises(capa.rules.InvalidRule):
|
||||
capa.rules.Rule.from_yaml(
|
||||
textwrap.dedent(
|
||||
"""
|
||||
rule:
|
||||
meta:
|
||||
name: test rule
|
||||
scope: instruction
|
||||
features:
|
||||
- characteristic: embedded pe
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def test_rule_subscope_instruction():
|
||||
rules = capa.rules.RuleSet(
|
||||
[
|
||||
capa.rules.Rule.from_yaml(
|
||||
textwrap.dedent(
|
||||
"""
|
||||
rule:
|
||||
meta:
|
||||
name: test rule
|
||||
scope: function
|
||||
features:
|
||||
- and:
|
||||
- instruction:
|
||||
- and:
|
||||
- mnemonic: mov
|
||||
- arch: i386
|
||||
- os: windows
|
||||
"""
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
||||
# the function rule scope will have one rules:
|
||||
# - `test rule`
|
||||
assert len(rules.function_rules) == 1
|
||||
|
||||
# the insn rule scope have one rule:
|
||||
# - the rule on which `test rule` depends
|
||||
assert len(rules.instruction_rules) == 1
|
||||
|
||||
|
||||
def test_scope_instruction_implied_and():
|
||||
capa.rules.Rule.from_yaml(
|
||||
textwrap.dedent(
|
||||
"""
|
||||
rule:
|
||||
meta:
|
||||
name: test rule
|
||||
scope: function
|
||||
features:
|
||||
- and:
|
||||
- instruction:
|
||||
- mnemonic: mov
|
||||
- arch: i386
|
||||
- os: windows
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def test_scope_instruction_description():
|
||||
capa.rules.Rule.from_yaml(
|
||||
textwrap.dedent(
|
||||
"""
|
||||
rule:
|
||||
meta:
|
||||
name: test rule
|
||||
scope: function
|
||||
features:
|
||||
- and:
|
||||
- instruction:
|
||||
- description: foo
|
||||
- mnemonic: mov
|
||||
- arch: i386
|
||||
- os: windows
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
capa.rules.Rule.from_yaml(
|
||||
textwrap.dedent(
|
||||
"""
|
||||
rule:
|
||||
meta:
|
||||
name: test rule
|
||||
scope: function
|
||||
features:
|
||||
- and:
|
||||
- instruction:
|
||||
- description: foo
|
||||
- mnemonic: mov
|
||||
- arch: i386
|
||||
- os: windows
|
||||
"""
|
||||
)
|
||||
)
|
||||
@@ -41,6 +41,7 @@ def get_rule_path():
|
||||
pytest.param("show-capabilities-by-function.py", [get_file_path()]),
|
||||
pytest.param("show-features.py", [get_file_path()]),
|
||||
pytest.param("show-features.py", ["-F", "0x407970", get_file_path()]),
|
||||
pytest.param("capa_as_library.py", [get_file_path()]),
|
||||
],
|
||||
)
|
||||
def test_scripts(script, args):
|
||||
|
||||
Reference in New Issue
Block a user