├── .github └── workflows │ ├── pr-secret-scan.yml │ └── pr-semgrep-code-check.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── NOTICE ├── README.md ├── SECURITY.md ├── activate-tracer-service.sh ├── deactivate-tracer-service.sh ├── install-deps.sh ├── run-tests.sh ├── src ├── bpf.c └── imds_snoop.py └── tests ├── event_proto.py ├── test_check_v2.py ├── test_gen_log_msg.py └── test_proc_info.py /.github/workflows/pr-secret-scan.yml: -------------------------------------------------------------------------------- 1 | name: Leaked Secrets Scan 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | 8 | jobs: 9 | TruffleHog: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v3 14 | with: 15 | fetch-depth: 0 16 | - name: TruffleHog OSS 17 | uses: trufflesecurity/trufflehog@main 18 | with: 19 | path: ./ 20 | base: ${{ github.event.repository.default_branch }} 21 | head: HEAD 22 | extra_args: --debug --only-verified 23 | -------------------------------------------------------------------------------- /.github/workflows/pr-semgrep-code-check.yml: -------------------------------------------------------------------------------- 1 | # Name of this GitHub Actions workflow. 2 | name: Semgrep 3 | 4 | on: [pull_request, push] 5 | 6 | jobs: 7 | Semgrep: 8 | runs-on: ubuntu-latest 9 | container: 10 | image: returntocorp/semgrep 11 | 12 | # Skip any PR created by dependabot to avoid permission issues: 13 | if: (github.actor != 'dependabot[bot]') 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | # Run the "semgrep ci" command on the command line of the docker image. 18 | - run: semgrep ci 19 | env: 20 | # Connect to Semgrep Cloud Platform through your SEMGREP_APP_TOKEN. 21 | # Generate a token from Semgrep Cloud Platform > Settings 22 | # and add it to your GitHub secrets. 23 | SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | /*.iml -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | We do not currently release this software using formal release cycles, the version numbers are best-effort only 3 | 4 | ## 1.4.0 (2024-09-10) 5 | - Change to redact tokens from the log messages as tokens were logged as a result of adding the headers. Log file (and folder) has always been accessible only by root user. 6 | 7 | ## 1.3.0 (2023-10-09) 8 | - Remove the After dependency on the `multi-user.target` as it is not needed and causes a possible cyclic dependency. 9 | 10 | ## 1.2.0 (2023-07-17) 11 | - Remove logging of zero process_id - resulting in misleading message in log. 12 | - Fix logging of wrong process id for forth process in process tree. 13 | 14 | ## 1.1.0 (2023-06-19) 15 | - Remove need for log config file 16 | - Add check to ensure software is run as root, also ensure the log files are restricted to the running user. 17 | - Add additional logging of HTTP request details (these include request method, url requested, IP and more) 18 | 19 | ## 1.0.0 (2023-04-01) 20 | Initial release. 21 | 22 | **Closed issues:** 23 | 24 | **Merged pull requests:** 25 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | ImdsPacketAnalyser 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | The AWS ImdsPacketAnalyzer is a tool that traces TCP interactions with the EC2 Instance Metadata Service (IMDS). This can assist in identifying the processes making IMDSv1 calls on a host. Traces contain the `pid`, the `argv` used to launch the process, and the parent `pids` up to four levels deep. This information allows you to identify a Process making IMDSv1 calls for further investigation. 4 | 5 | The ImdsPacketAnalyzer leverages the [BCC (BPF Compiler Collection)](https://github.com/iovisor/bcc/blob/master/INSTALL.md#Amazon-Linux-2---Binary). In order to successfully run the analyzer the BCC pre-requisites need to be installed. 6 | 7 | 8 | # AWS ImdsPacketAnalyzer 9 | 10 | - [Packages - Installing BCC](#packages---installing-bcc) 11 | - [Amazon Linux 2023](#amazon-linux-2023) 12 | - [Amazon Linux 2](#amazon-linux-2) 13 | - [Amazon Linux 1, 2018.03](#amazon-linux-1-201803) 14 | - [Debian 11](#debian-11) 15 | - [Debian 10](#debian-10) 16 | - [Ubuntu 20 / 22](#ubuntu-20--22) 17 | - [RHEL 8 / 9 / Fedora](#rhel-8--9--fedora) 18 | - [SLES 15](#sles-15) 19 | - [CentOS 9](#centos9) 20 | - [Windows](#windows) 21 | - [Usage](#usage-) 22 | - [Amazon Linux 2023](#amazon-linux-2023-1) 23 | - [Amazon Linux 2](#amazon-linux-2-1) 24 | - [Amazon Linux 1](#amazon-linux-1) 25 | - [Debian 11](#debian-11-1) 26 | - [Debian 10](#debian-10-1) 27 | - [Ubuntu 20 / 22](#ubuntu-20--22-1) 28 | - [RHEL 8 / 9 / Fedora](#rhel-8--9--fedora-1) 29 | - [SLES 15](#sles-15-1) 30 | - [CentOS 9](#centos9-1) 31 | - [Windows](#windows-1) 32 | - [Logging](#logging) 33 | - [Running the tool as a service](#running-the-tool-as-a-service) 34 | - [Activating the tool as a service](#activating-the-tool-as-a-service) 35 | - [Deactivating the tool as a service](#deactivating-the-tool-as-a-service) 36 | - [Limitations](#limitations) 37 | 38 | 39 | # Packages - Installing BCC 40 | For hosts with internet access, the install script can be used. It is advised that this script is run only on non-production instances. Installation will update dependancies and may affect other functionality. 41 | For instances without internet access you will need to share the files on an S3 folder. 42 | ``` 43 | sudo bash install-deps.sh 44 | ``` 45 | --- 46 | 47 | **OR** run the following commands per OS 48 | 49 | 50 | ## Amazon Linux 2023 51 | 52 | Install BCC 53 | 54 | ``` 55 | sudo dnf install bcc-tools 56 | ``` 57 | --- 58 | 59 | ## Amazon Linux 2 60 | 61 | Install [BCC (BPF Compiler Collection)](https://github.com/iovisor/bcc/blob/master/INSTALL.md#Amazon-Linux-2---Binary): 62 | 63 | ``` 64 | sudo amazon-linux-extras enable BCC 65 | sudo yum install kernel-devel-$(uname -r) 66 | sudo yum install bcc 67 | ``` 68 | --- 69 | 70 | ## Amazon Linux 1, 2018.03 71 | 72 | Install [BCC (BPF Compiler Collection)](https://github.com/iovisor/bcc/blob/master/INSTALL.md#Amazon-Linux-1---Binary): 73 | 74 | ``` 75 | sudo yum install kernel-headers-$(uname -r | cut -d'.' -f1-5) 76 | sudo yum install kernel-devel-$(uname -r | cut -d'.' -f1-5) 77 | sudo yum install bcc 78 | ``` 79 | --- 80 | 81 | ## Debian 11 82 | 83 | ``` 84 | echo deb http://cloudfront.debian.net/debian sid main | sudo tee -a /etc/apt/sources.list 85 | sudo apt-get update 86 | sudo apt-get install -y bpfcc-tools libbpfcc libbpfcc-dev linux-headers-$(uname -r) 87 | sudo apt-get install linux-headers-$(uname -r) bcc 88 | ``` 89 | --- 90 | 91 | ## Debian 10 92 | 93 | Note : During the Dependency installation, the ["libcrypt1"](https://www.mail-archive.com/debian-bugs-dist@lists.debian.org/msg1818037.html) related error occurs so the execution has step to fix and continue with the installation process further, also the OS libraries can cause the restart of the system releated services like sshd and crond. 94 | 95 | ``` 96 | echo deb http://cloudfront.debian.net/debian sid main | sudo tee -a /etc/apt/sources.list 97 | sudo -i # Need to switch to root user in the CLI before running below command 98 | apt-get update 99 | #Set the environment variable DEBIAN_FRONTEND to 'noninteractive' to avoid the prompts and accept the default answers 100 | export DEBIAN_FRONTEND=noninteractive 101 | apt-get install -y bpfcc-tools libbpfcc libbpfcc-dev linux-headers-$(uname -r) bcc --no-install-recommends 102 | #Steps to fix the libcrypt1 error 103 | cd /tmp/ 104 | apt -y download libcrypt1 105 | dpkg-deb -x libcrypt1* . 106 | cp -av lib/x86_64-linux-gnu/* /lib/x86_64-linux-gnu/ 107 | #Re-run the install command 108 | apt-get install -y bpfcc-tools libbpfcc libbpfcc-dev linux-headers-$(uname -r) bcc --no-install-recommends 109 | apt install -y --fix-broken 110 | # Run the install command 111 | apt-get install -y bpfcc-tools libbpfcc libbpfcc-dev linux-headers-$(uname -r) bcc --no-install-recommends 112 | ``` 113 | --- 114 | 115 | ## Ubuntu 20 / 22 116 | For Ubuntu 20 - 117 | ``` 118 | sudo apt install -y bison build-essential cmake flex git libedit-dev libllvm12 llvm-12-dev libclang-12-dev python zlib1g-dev libelf-dev libfl-dev python3-distutils zip 119 | ``` 120 | For Ubuntu 22 - 121 | ``` 122 | sudo apt install -y bison build-essential cmake flex git libedit-dev libllvm14 llvm-14-dev libclang-14-dev python3 zlib1g-dev libelf-dev libfl-dev python3-distutils zip 123 | ``` 124 | 125 | ``` 126 | git clone https://github.com/iovisor/bcc.git 127 | mkdir bcc/build; cd bcc/build 128 | cmake .. 129 | make 130 | sudo make install 131 | cmake -DPYTHON_CMD=python3 .. # build python3 binding 132 | pushd src/python/ 133 | make 134 | sudo make install 135 | popd 136 | sudo apt-get install linux-headers-$(uname -r) 137 | ``` 138 | --- 139 | 140 | ## RHEL 8 / 9 / Fedora 141 | 142 | ``` 143 | sudo yum -y install bcc-tools libbpf 144 | ``` 145 | --- 146 | 147 | ## SLES 15 148 | ``` 149 | sudo zypper ref 150 | sudo zypper in bcc-tools bcc-examples 151 | sudo zypper in --oldpackage kernel-default-devel-$(zypper se -s kernel-default-devel | awk '{split($0,a,"|"); print a[4]}' | grep $(uname -r | awk '{gsub("-default", "");print}') | sed -e 's/^[ \t]*//' | tail -n 1) 152 | ``` 153 | 154 | --- 155 | 156 | ## CentOS 9 157 | ``` 158 | sudo yum -y install bcc 159 | ``` 160 | 161 | --- 162 | 163 | ## WINDOWS 164 | 165 | INSTALL PYTHON 166 | - Check if Python is installed with ```python -V``` 167 | - Download python3 msi https://www.python.org/downloads/ 168 | - Select to add python.exe to PATH 169 | 170 | 171 | INSTALL PIP 172 | - Check if PIP is installed with ```pip help``` 173 | - Download PIP ```curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py``` 174 | - Install with ```python get-pip.py``` 175 | 176 | 177 | INSTALL GIT 178 | - Check if GIT is installed with ```git version``` 179 | - Download ad install the MSI for Windows via ```https://git-scm.com/download/win``` 180 | 181 | 182 | INSTALL AWS CLI 183 | - Run ```msiexec.exe /i https://awscli.amazonaws.com/AWSCLIV2.msi``` 184 | - Run and configure the CLI with ```aws configure``` 185 | - Ensure EC2 Instance IAM Profile is assigned with access to Cloudwatch. 186 | 187 | 188 | INSTALL METABADGER (https://github.com/salesforce/metabadger) 189 | - Run ```pip3 install --user metabadger``` 190 | - Go to working directory ```cd C:\\\AppData\Roaming\Python\Python311\scripts``` 191 | 192 | --- 193 | 194 | **Note:** Troubleshooting + Installation on other distros please see: [BCC (BPF Compiler Collection)](https://github.com/iovisor/bcc/blob/master/INSTALL.md) 195 | 196 | --- 197 | 198 | ## Usage 199 | BCC requires that the analyzer is run with root permissions. Typically, you can execute the following script and IMDS calls will be logged to the console and to a log file by default (see [logging.conf](#log-configuration)). 200 | ``` 201 | sudo python3 src/imds_snoop.py 202 | ``` 203 | 204 | 205 | #### Example v1 call: 206 | The following IMDSv1 curl command 207 | ``` 208 | curl http://169.254.169.254/latest/meta-data/ 209 | ``` 210 | will result the following IMDS packet analyzer output 211 | ``` 212 | IMDSv1(!) (pid:6028:curl argv:curl http://169.254.169.254/latest/meta-data/) called by -> (pid:6027:makeCalls.sh argv:/bin/bash ./makeCalls.sh) -> (pid:4081:zsh argv:-zsh) -> (pid:4081:sshd argv:sshd: kianred@pts/0) 213 | ``` 214 | --- 215 | 216 | ## Amazon Linux 2023 217 | ``` 218 | sudo python3 src/imds_snoop.py 219 | ``` 220 | --- 221 | 222 | ## Amazon Linux 2 223 | ``` 224 | sudo python3 src/imds_snoop.py 225 | ``` 226 | --- 227 | 228 | ## Amazon Linux 1 229 | ``` 230 | sudo python3 src/imds_snoop.py 231 | ``` 232 | --- 233 | 234 | ## Debian 11 235 | ``` 236 | sudo python3 src/imds_snoop.py 237 | ``` 238 | --- 239 | 240 | ## Debian 10 241 | ``` 242 | sudo python3 src/imds_snoop.py 243 | ``` 244 | --- 245 | 246 | ## Ubuntu 20 / 22 247 | ``` 248 | sudo LD_PRELOAD=/home/ubuntu/bcc/build/src/cc/libbcc.so.0 PYTHONPATH=/home/ubuntu/bcc/build/src/python src/imds_snoop.py 249 | ``` 250 | --- 251 | 252 | ## RHEL 8 / 9 / Fedora 253 | ``` 254 | sudo python3 src/imds_snoop.py 255 | ``` 256 | --- 257 | 258 | ## SLES 15 259 | ``` 260 | sudo python3 src/imds_snoop.py 261 | ``` 262 | --- 263 | 264 | ## CentOS 9 265 | ``` 266 | sudo python src/imds_snoop.py 267 | ``` 268 | --- 269 | 270 | ## WINDOWS 271 | - From the Working directory E.g ```cd C:\\\AppData\Roaming\Python\Python311\scripts``` 272 | - Run to view IMDSv1 calls: ```metabadger cloudwatch-metrics --region us-east-1``` 273 | 274 | The output table will highlight if the instance has made IMDSv1 calls 275 | 276 | To find the specific app making the IMDSv1 calls, use the inbuilt Windows Resource Monitor Network monitor to find the Image and PID of the application making calls. 277 | 278 | To do this, open Resource Monitor (Start->Search ->Resource Monitor) and click on the Network tab. 279 | Then look for calls in the Network Activity section made to either the IP or DNS entries listed: 280 | - IP: ```169.254.169.254``` 281 | - DNS: instance-data..compute.internal E.g ```instance-data.us-east-1.compute.internal``` 282 | 283 | Network Analyzer will show the calls and you should proceed to update the software/application. 284 | 285 | More details and thanks to https://github.com/salesforce/metabadger and https://www.greystone.co.uk/2022/03/24/how-greystone-upgraded-its-aws-ec2-instances-to-use-instance-meta-data-service-version-2-imdsv2/ 286 | 287 | 288 | --- 289 | 290 | # Logging 291 | The ImdsPacketAnalyzer will also capture IMDS calls to log files. Log entries follow the format: `[Time] [Level] [message]` where: 292 | - **Time:** the time at which the IMDS call was made in the format: `%Y-%m-%dT%H:%M:%S` eg.) [2022-12-20T12:57:51] 293 | - **Level:** the level of the log entry, where IMDSv2 calls are logged at `INFO` level and IMDSv1 calls are logged at `WARNING` level 294 | - If there are any instances where an ImdsPacketAnalyzer fail to interpret the packets, `ERROR` level messages will be traced. 295 | - **Note** The only reason a call cannot be identified is if the analyzer is unable to find a request payload for the IMDS call, this missing payload means the analyzer will not be able to discern V1 from V2 IMDS calls. (see what to do in case of missing payload below). 296 | - Errors (due to a *missing payload*) in the log indicate that the analyzer was not able to capture the payload that was sent to the IMDS ip. This is expected for AL2 kernel 4.14 on Graviton (ARM) instances (see "Limitations" heading below). If this is a new error case, please log a defect with detailed information and consider alternative ways to identify the source of the IMDS call. 297 | - **message:** the details of the IMDS call as it would be outputted to the terminal 298 | 299 | Example of a IMDSv1 log entry: 300 | ``` 301 | [2022-12-20T11:03:58] [WARNING] IMDSv1(!) (pid:1016:curl argv:curl http://169.254.169.254/latest/meta-data/) called by -> (pid:1015:makeCalls.sh argv:/bin/bash ./makeCalls.sh) -> (pid:32678:zsh argv:-zsh) -> (pid:32678:sshd argv:sshd: ) 302 | ``` 303 | Example of a IMDSv2 log entry: 304 | ``` 305 | [2022-12-20T11:03:58] [INFO] IMDSv2 (pid:1018:curl argv:curl -H X-aws-ec2-metadata-token: AQAEAFEOMInKb-S7me-hLqzu83lYdeDV7r-sPh2D4SJF6v5IwD4S8g== -v http://169.254.169.254/latest/meta-data/) called by -> (pid:1015:makeCalls.sh argv:/bin/bash ./makeCalls.sh) -> (pid:32678:zsh argv:-zsh) -> (pid:32678:sshd argv:sshd: ) 306 | ``` 307 | 308 | ### Log configuration 309 | 310 | The logging configuration can be adjusted by editing the **logging.conf** file. 311 | 312 | By default: 313 | - Logs will be saved to the `/var/log/imds/` folder in a file called `imds-trace.log` 314 | - Log files will be appended (if the analyzer is stopped and then run again on multiple occasions) 315 | - Each log file will reach a maximum size of 1 megabyte before rollover occurs 316 | - When a log file reaches 1mb in size it will rollover to a new log file **i.e) imds-trace.log.1 or imds-trace.log.2** 317 | - Rollover occurs a maximum of 5 times meaning that at most log files will at most take up 6 x 1mb => 6mb storage space (the prominent `imds-trace.log` file + 5 rollover log files `imds-trace.log.x` where x ranges from 1 to **backupCount**) 318 | 319 | ### Analyzing log files 320 | **Assuming default logging setup:** 321 | - Running the command `cat /var/log/imds/imds-trace.* | grep WARNING` will output all IMDSv1 calls to the terminal. 322 | - Note that this grep will only identify the call, sometimes the calls leading up to the V1 call can provide additional context. 323 | 324 | # Running the tool as a service 325 | 326 | ## Activating the tool as a service 327 | Configuring the analyzer to run as a service will ensure that the tool will run as soon as possible upon the boot up of an instance. This will increase the chances of identifying services making IMDSv1 calls as early as the instance is inited onto a network. 328 | 329 | A shell script has been provided in the package that will automate the process of setting up the analyzer tool as a service. **Note:** the script/service will only work if the structure of the package is left unchanged. 330 | 331 | Run the script from the command line as follows: 332 | 333 | ``` 334 | sudo ./activate-tracer-service.sh 335 | ``` 336 | 337 | or 338 | 339 | ``` 340 | sudo bash activate-tracer-service.sh 341 | ``` 342 | 343 | The permissions for the shell script may need to be changed using: 344 | ``` 345 | chmod +x activate-tracer-service.sh 346 | ``` 347 | 348 | You can check if the service is running after activating the service or a host reboot: 349 | ``` 350 | systemctl status -l imds_tracer_tool.service 351 | ``` 352 | 353 | ## Deactivating the tool as a service 354 | When the tool is configured as a service using the previous script, a service file is added into the OS. In order to restore the system, run the script from the command line: 355 | 356 | ``` 357 | sudo ./deactivate-tracer-service.sh 358 | ``` 359 | 360 | or 361 | 362 | ``` 363 | sudo bash deactivate-tracer-service.sh 364 | ``` 365 | 366 | Permissions for the script may need to be changed: 367 | ``` 368 | chmod +x deactivate-tracer-service.sh 369 | ``` 370 | 371 | --- 372 | 373 | ## Limitations 374 | We are aware of some limitations with the current version of the ImdsPacketAnalyzer. Contributions are welcomed. 375 | - The `install-deps.sh` script assumes AL2 and internet connectivity 376 | - Althought the ImdsPacketAnalyzer have been run on multiple distributions, it is only tested on AL2 before new commits are pushed. 377 | - ImdsPacketAnalyzer only supports IPv4 378 | - ImdsPacketAnalyzer is intended to be used to identify processes making IMDSv1 calls. There is no guarantee that IMDS calls will be detected. Also be aware that a network can be configured to route other (non-IMDS) traffic to the IMDS ip address. The analyzer is not reliable enough to be used to alarm or audit IMDSv1 calls. 379 | - ImdsPacketAnalyzer has not been tested with production traffic in mind, it is intended to be run as a analysis tool to be removed once the source of IMDSv1 calls have been identified. 380 | - AL2 kernel 4.14 on Graviton (ARM) lack the eBPF features required to determine if a call is IMDSv1 or V2. This is reported as `{MISSING PAYLOAD}` error. We do not have a workaround for this and do not have a planned fix. 381 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | ## Reporting Security Issues 2 | 3 | We take all security reports seriously. 4 | When we receive such reports, we will investigate and subsequently address any potential vulnerabilities as quickly as possible. 5 | If you discover a potential security issue in this project, please notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/) or directly via email to [AWS Security](mailto:aws-security@amazon.com). 6 | 7 | Please do *not* create a public GitHub issue in this project. -------------------------------------------------------------------------------- /activate-tracer-service.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | set -e # Exit immediately if a command exits with a non-zero status 6 | 7 | BPF_TRACE_SERVICE="imds_tracer_tool.service" 8 | BPF_TRACE_PATH=$(pwd) 9 | BPF_TRACE_SYSTEMD_PATH="/etc/systemd/system/$BPF_TRACE_SERVICE" 10 | 11 | # Check if script is running as root 12 | if [[ $EUID -ne 0 ]]; then 13 | echo "[ERROR] Please run as root (sudo)" >&2 14 | exit 1 15 | fi 16 | 17 | # Function to create service file 18 | create_service_file() { 19 | cat << EOF > "$BPF_TRACE_SYSTEMD_PATH" 20 | [Unit] 21 | Description=ImdsPacketAnalyzer IMDS detection tooling from AWS 22 | Before=network-online.target 23 | 24 | [Service] 25 | Type=simple 26 | Restart=always 27 | WorkingDirectory=$BPF_TRACE_PATH 28 | ExecStart=$(command -v python3) $BPF_TRACE_PATH/src/imds_snoop.py 29 | 30 | [Install] 31 | WantedBy=multi-user.target 32 | EOF 33 | } 34 | 35 | # Main script execution 36 | echo "--- Removing old service file" 37 | rm -f "$BPF_TRACE_SYSTEMD_PATH" 38 | 39 | echo "--- Creating new service file" 40 | create_service_file 41 | 42 | echo "--- Service details:" 43 | cat "$BPF_TRACE_SYSTEMD_PATH" 44 | 45 | echo 46 | echo "--- Reloading daemon and enabling the $BPF_TRACE_SERVICE service" 47 | systemctl daemon-reload 48 | systemctl enable "$BPF_TRACE_SERVICE" 49 | 50 | echo "--- Starting the $BPF_TRACE_SERVICE service" 51 | systemctl start "$BPF_TRACE_SERVICE" 52 | 53 | echo "--- Done" -------------------------------------------------------------------------------- /deactivate-tracer-service.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | set -e 6 | 7 | BPF_TRACE_SERVICE="imds_tracer_tool.service" 8 | BPF_TRACE_SYSTEMD_PATH="/etc/systemd/system/$BPF_TRACE_SERVICE" 9 | LOG_DIR="/var/log/imds" 10 | 11 | # Check if script is running as root 12 | if [[ $EUID -ne 0 ]]; then 13 | echo "[ERROR] Please run as root (sudo)" >&2 14 | exit 1 15 | fi 16 | 17 | # Function to stop and remove the service 18 | remove_service() { 19 | echo "--- Stopping tracer service" 20 | systemctl stop "$BPF_TRACE_SERVICE" || echo "[WARNING] Failed to stop service. It may not be running." 21 | 22 | echo "--- Removing service file (unit file)" 23 | rm -f "$BPF_TRACE_SYSTEMD_PATH" 24 | 25 | echo "--- Reloading daemons" 26 | systemctl daemon-reload 27 | } 28 | 29 | # Function to display log information 30 | display_log_info() { 31 | echo 32 | echo "[INFO] Associated log files can be found in the $LOG_DIR directory" 33 | echo "To delete all log files, run the following command:" 34 | echo "sudo rm $LOG_DIR/imds-trace.*" 35 | } 36 | 37 | # Main script execution 38 | remove_service 39 | display_log_info 40 | 41 | echo 42 | echo "--- Uninstallation complete" -------------------------------------------------------------------------------- /install-deps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | set -e # Exit on error 6 | 7 | # Check for root privileges 8 | if [ "$EUID" -ne 0 ]; then 9 | echo "[ERROR] Please run as root (sudo)" >&2 10 | exit 1 11 | fi 12 | 13 | # Function to install BCC on Ubuntu 14 | install_ubuntu_bcc() { 15 | local llvm_version=$1 16 | apt-get update 17 | apt install -y bison build-essential cmake flex git libedit-dev "libllvm${llvm_version}" \ 18 | "llvm-${llvm_version}-dev" "libclang-${llvm_version}-dev" python3 zlib1g-dev libelf-dev \ 19 | libfl-dev python3-distutils zip --no-install-recommends 20 | 21 | if [ ! -d "/home/ubuntu" ]; then 22 | echo "[ERROR] Directory '/home/ubuntu' does not exist" >&2 23 | exit 1 24 | fi 25 | 26 | cd /home/ubuntu 27 | rm -rf bcc 28 | git clone https://github.com/iovisor/bcc.git 29 | cd bcc 30 | mkdir -p build && cd build 31 | cmake .. 32 | make && make install 33 | cmake -DPYTHON_CMD=python3 .. 34 | cd src/python 35 | make && make install 36 | apt-get -y install "linux-headers-$(uname -r)" 37 | } 38 | 39 | # Function to check sudo user home directory 40 | check_sudo_user_home() { 41 | if [ -n "${SUDO_USER}" ]; then 42 | local homedir=$(getent passwd "${SUDO_USER}" | cut -d: -f6) 43 | if [ ! -d "${homedir}" ]; then 44 | echo "[ERROR] SUDO_USER: ${SUDO_USER} homedir '${homedir}' does not exist" >&2 45 | exit 1 46 | fi 47 | fi 48 | } 49 | 50 | # Function to add Debian repository 51 | add_debian_repo() { 52 | local repo_line="deb http://cloudfront.debian.net/debian sid main" 53 | if ! grep -qx "$repo_line" /etc/apt/sources.list; then 54 | echo "$repo_line" | tee -a /etc/apt/sources.list 55 | fi 56 | } 57 | 58 | # Install BCC based on OS 59 | case "$(. /etc/os-release; echo "$ID$VERSION_ID")" in 60 | "amzn2023") 61 | dnf install -y bcc-tools 62 | ;; 63 | "amzn2") 64 | check_sudo_user_home 65 | amazon-linux-extras enable BCC 66 | yum -y install "kernel-devel-$(uname -r)" bcc-devel 67 | ;; 68 | "debian11") 69 | add_debian_repo 70 | export DEBIAN_FRONTEND=noninteractive 71 | apt-get update 72 | apt-get install -y bpfcc-tools libbpfcc libbpfcc-dev "linux-headers-$(uname -r)" bcc --no-install-recommends 73 | ;; 74 | "debian10") 75 | add_debian_repo 76 | export DEBIAN_FRONTEND=noninteractive 77 | apt-get update 78 | apt-get install -y bpfcc-tools libbpfcc libbpfcc-dev "linux-headers-$(uname -r)" bcc --no-install-recommends || true 79 | cd /tmp 80 | apt -y download libcrypt1 81 | dpkg-deb -x libcrypt1* . 82 | cp -av lib/x86_64-linux-gnu/* /lib/x86_64-linux-gnu/ 83 | apt-get install -y bpfcc-tools libbpfcc libbpfcc-dev "linux-headers-$(uname -r)" bcc --no-install-recommends 84 | apt install -y --fix-broken 85 | ;; 86 | "ubuntu22.04") 87 | install_ubuntu_bcc 14 88 | ;; 89 | "ubuntu20.04") 90 | install_ubuntu_bcc 12 91 | ;; 92 | "rhel"*) 93 | yum -y install bcc-tools libbpf 94 | ;; 95 | "fedora"*) 96 | yum -y install bcc-tools libbpf 97 | ;; 98 | "sles15"*) 99 | zypper ref 100 | zypper in -y bcc-tools bcc-examples 101 | zypper in -y --oldpackage "kernel-default-devel-$(zypper se -s kernel-default-devel | awk '{split($0,a,"|"); print a[4]}' | grep "$(uname -r | sed 's/-default//')" | sed -e 's/^[ \t]*//' | tail -n 1)" 102 | ;; 103 | "centos"*) 104 | yum -y install bcc 105 | ;; 106 | *) 107 | echo "[ERROR] Unsupported operating system" >&2 108 | exit 1 109 | ;; 110 | esac 111 | 112 | echo "[INFO] BCC installation completed successfully" -------------------------------------------------------------------------------- /run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | python3 -m pytest tests/ -v 6 | rm -r .pytest_cache 7 | rm -r src/__pycache__ 8 | rm -r tests/__pycache__ -------------------------------------------------------------------------------- /src/bpf.c: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #define IP_169_254_169_254 0xFEA9FEA9 10 | 11 | #define MAX_PKT 31*1024 12 | struct imds_http_data_t { 13 | u32 pid[4]; 14 | // i could not get 2d type conversion right in python, so... 15 | char comm[TASK_COMM_LEN]; 16 | char parent_comm[TASK_COMM_LEN]; 17 | char gparent_comm[TASK_COMM_LEN]; 18 | char ggparent_comm[TASK_COMM_LEN]; 19 | u32 pkt_size; 20 | char pkt[MAX_PKT]; 21 | u32 contains_payload; 22 | }; 23 | BPF_PERF_OUTPUT(imds_events); 24 | 25 | // single element per-cpu array to hold the current event off the stack 26 | BPF_PERCPU_ARRAY(imds_http_data,struct imds_http_data_t,1); 27 | 28 | int trace_sock_sendmsg(struct pt_regs *ctx) 29 | { 30 | // stash the sock ptr for lookup on return 31 | // only if it is imds traffic 32 | struct socket *skt = (struct socket *)PT_REGS_PARM1(ctx); 33 | struct sock *sk = skt->sk; 34 | if (sk->__sk_common.skc_daddr == IP_169_254_169_254) { 35 | struct msghdr *msghdr = (struct msghdr *)PT_REGS_PARM2(ctx); 36 | u32 zero = 0; 37 | 38 | // pull in details 39 | u32 daddr = sk->__sk_common.skc_daddr; 40 | u16 dport = sk->__sk_common.skc_dport; 41 | 42 | struct imds_http_data_t *data = imds_http_data.lookup(&zero); 43 | 44 | if (!data) // this should never happen, just making the verifier happy 45 | return 0; 46 | 47 | #if defined(iter_iov) || defined (iter_iov_len) 48 | const struct iovec * iov = msghdr->msg_iter.__iov; 49 | #else 50 | const struct iovec * iov = msghdr->msg_iter.iov; 51 | #endif 52 | const void *iovbase; 53 | if (*(char *)iov->iov_base == '\0'){ 54 | iovbase = iov; 55 | } 56 | else{ 57 | iovbase = iov->iov_base; 58 | } 59 | const size_t iovlen = iov->iov_len > MAX_PKT ? MAX_PKT : iov->iov_len; 60 | 61 | if (!iovlen) { 62 | return 0; 63 | } 64 | 65 | //The size parameter in the line of code below seems to be incorrectly set 66 | //however if we were to intuitively use the size of the payload itself as the size parameter by using iovlen (size of the payload) 67 | //the interpreter will throw an invalid mem access error and the script will not run (most probably due to the method requiring a const value at compile time) 68 | bpf_probe_read_str(data->pkt, sizeof(data->pkt), iovbase); 69 | 70 | //check if payload is empty or not -> check char buffer -> if char buffer starts with a termination vales \0 => null buffer 71 | if(data->pkt[0] == '\0'){ 72 | data->contains_payload = 0; 73 | } 74 | else{ 75 | data->contains_payload = 1; 76 | } 77 | 78 | data->pkt_size = iovlen; 79 | 80 | struct task_struct *t = (struct task_struct *)bpf_get_current_task(); 81 | data->pid[0] = t->tgid; 82 | bpf_probe_read(data->comm, TASK_COMM_LEN, t->comm); 83 | // loops not supported in bpf 84 | if (t->real_parent) { 85 | struct task_struct *parent = t->real_parent; 86 | data->pid[1] = parent->tgid; 87 | bpf_probe_read(data->parent_comm, TASK_COMM_LEN, parent->comm); 88 | if (parent->real_parent) { 89 | struct task_struct *gparent = parent->real_parent; 90 | data->pid[2] = gparent->tgid; 91 | bpf_probe_read(data->gparent_comm, TASK_COMM_LEN, gparent->comm); 92 | if (gparent->real_parent) { 93 | struct task_struct *ggparent = gparent->real_parent; 94 | bpf_probe_read(data->ggparent_comm, TASK_COMM_LEN, ggparent->comm); 95 | data->pid[3] = ggparent->tgid; 96 | } 97 | } 98 | } 99 | 100 | 101 | imds_events.perf_submit(ctx, data, sizeof(struct imds_http_data_t)); 102 | } 103 | 104 | return 0; 105 | 106 | } -------------------------------------------------------------------------------- /src/imds_snoop.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | from bcc import BPF 6 | import os 7 | import logging 8 | from logging.handlers import RotatingFileHandler 9 | from logging.config import fileConfig 10 | 11 | EC_METADATA_TOKEN_ = "x-aws-ec2-metadata-token:" 12 | 13 | LOGGING_CONFIG_FILE = 'logging.conf' 14 | LOG_IMDS_FOLDER = "/var/log/imds" 15 | 16 | # GLOBAL => set logger object as global because initializing the logger in the bpf callback function could 17 | # cause unnecessary overhead 18 | logger = None 19 | 20 | """Check if a IMDS call is a imdsV1/2 call 21 | 22 | :param payload: payload of network call 23 | :type payload: str 24 | :returns: is_v2 25 | :rtype is_v2: bool 26 | """ 27 | def check_v2(payload: str, is_debug=False) -> bool: 28 | if (is_debug): 29 | print("========================================================================") 30 | print("[DEBUG] Payload being checked: ") 31 | print(payload, end="\n") 32 | print("========================================================================") 33 | 34 | IMDSV2_TOKEN_PREFIX = "x-aws-ec2-metadata-token" 35 | 36 | # determine if event was imdsv2 call or not 37 | is_v2 = False 38 | if IMDSV2_TOKEN_PREFIX in payload.lower(): 39 | is_v2 = True 40 | 41 | return (is_v2) 42 | 43 | 44 | """Remove the token from the message 45 | 46 | :param comms: message that need to be redacted. 47 | :type: str 48 | :returns: redacted message 49 | :rtype: str 50 | """ 51 | def hideToken(comms: str) -> str: 52 | startToken = comms.find(EC_METADATA_TOKEN_) 53 | endToken = comms.find("==", startToken) + len("==") 54 | 55 | if (startToken >= len(EC_METADATA_TOKEN_)) and (endToken > startToken): 56 | newTxt = comms[:startToken] + "**token redacted**" + comms[endToken:] 57 | else: 58 | newTxt = comms 59 | 60 | return newTxt 61 | 62 | def recurseHideToken(comms: str) -> str: 63 | newTxt = comms.lower() 64 | while newTxt.find(EC_METADATA_TOKEN_) >= 0: 65 | newTxt = hideToken(newTxt) 66 | 67 | return newTxt 68 | 69 | """ get argv info per calling process 70 | 71 | :param pid: process id of calling process 72 | :type pid: int 73 | :param proc_name: name of calling process 74 | :type proc_name: str 75 | :returns: proc_info 76 | :rtype proc_info: str 77 | """ 78 | def get_proc_info(pid: int, proc_name: str, is_debug=False) -> str: 79 | if (is_debug): 80 | print("========================================================================") 81 | print("[DEBUG] pid: " + str(pid)) 82 | print("proc_name: " + proc_info, end="\n") 83 | print("========================================================================") 84 | 85 | try: 86 | cmdline = open("/proc/" + str(pid) + "/cmdline").read() 87 | proc_info = ":" + proc_name 88 | proc_info += " argv:" + cmdline.replace('\x00', ' ').rstrip() 89 | return (proc_info) 90 | except Exception as e: 91 | print("Info: ", e) 92 | error_message = " Unable to get argv information" 93 | return (error_message) 94 | 95 | 96 | """ generate output message per imds network call 97 | 98 | :param is_v2: flag to represent whether or not the current event is an imdsv1 or imdsv2 event 99 | :type is_v2: bool 100 | :param event: event object returned by C code into per_buffers -> essentially the imds_http_data_t struct in the C code 101 | :type event: bcc.table 102 | :returns: log_msg 103 | :rtype log_msg: str 104 | """ 105 | def gen_log_msg(is_v2: bool, event) -> str: 106 | 107 | entry_init = "(pid:" 108 | log_msg = "IMDSv2 " if is_v2 else "IMDSv1(!) " 109 | 110 | log_msg += entry_init + \ 111 | str(event.pid[0]) + get_proc_info(event.pid[0], 112 | event.comm.decode()) + ")" 113 | 114 | if event.parent_comm and event.pid[1]: 115 | log_msg += " called by -> " + entry_init + \ 116 | str(event.pid[1]) + get_proc_info(event.pid[1], 117 | event.parent_comm.decode()) + ")" 118 | if event.gparent_comm and event.pid[2]: 119 | log_msg += " -> " + entry_init + \ 120 | str(event.pid[2]) + get_proc_info(event.pid[2], 121 | event.gparent_comm.decode()) + ")" 122 | if event.ggparent_comm and event.pid[3]: 123 | log_msg += " -> " + entry_init + \ 124 | str(event.pid[3]) + get_proc_info(event.pid[3], 125 | event.ggparent_comm.decode()) + ")" 126 | 127 | return log_msg 128 | 129 | 130 | def print_imds_event(cpu, data, size): 131 | # let bcc generate the data structure from C declaration automatically given the eBPF event reference (int) -> essentially 132 | # generates the imds_http_data_t struct in the C code as a bcc.table object 133 | event = b["imds_events"].event(data) 134 | """event object 135 | :attribute pid: stores pids of calling processes in the communication chain (4 pids) 136 | :type pid: int array[4] (u32 ints) 137 | :attribute comm: communication process name 138 | :type comm: bytes (specific encoding unknown) 139 | :attribute parent_comm: communication process name (parent) 140 | :type parent_comm: bytes (specific encoding unknown) 141 | :attribute gparent_comm: communication process name (grand-parent) 142 | :type gparent_comm: bytes (specific encoding unknown) 143 | :attribute ggparent_comm: communication process name (great-grand-parent) 144 | :type parent_comm: bytes (specific encoding unknown) 145 | :attribute pkt_size: size packet request 146 | :type pkt_size: int (u32) 147 | :attribute pkt: the data payload contained in a network request of request 148 | :type pkt: bytes (specific encoding unknown) 149 | :attribute contains_payload: flag to indicate if the event has a viable payload to analyze or not 150 | :type contains_payload: int (u32) 151 | """ 152 | # pass whatever data bcc has captured as the event payload to test IMDSv1/2? 153 | is_v2 = check_v2(event.pkt[:event.pkt_size].decode()) 154 | # generate information string to be logged 155 | log_msg = gen_log_msg(is_v2, event) 156 | pkt_size = event.pkt_size 157 | payload = event.pkt[:pkt_size].decode() 158 | log_msg = log_msg + " Req details: " + ", ".join(payload.splitlines()) 159 | log_msg = recurseHideToken(log_msg) 160 | 161 | if(event.contains_payload): 162 | # log identifiable trace info 163 | if(is_v2): 164 | logger.info(log_msg) 165 | print('[INFO] ' + log_msg, end="\n") 166 | else: 167 | logger.warning(log_msg) 168 | print('[WARNING] ' + log_msg, end="\n") 169 | else: 170 | # unidentifiable call -> needs further attention -> hence log at error level 171 | log_msg = "{MISSING PAYLOAD} " + log_msg 172 | logger.error(log_msg) 173 | print('[ERROR] ' + log_msg, end="\n") 174 | 175 | 176 | if(__name__ == "__main__"): 177 | if os.geteuid() != 0: 178 | exit("You need to have root privileges to run this script.") 179 | 180 | # create and lock down the logging folder, since root is running the trace, only root can view the log 181 | if not os.path.exists(LOG_IMDS_FOLDER): 182 | os.makedirs(LOG_IMDS_FOLDER) 183 | 184 | st = os.stat(LOG_IMDS_FOLDER) 185 | if bool(st.st_mode & 0o00077): 186 | print("Setting log folder to root RW access only, permission was: " + str(oct(st.st_mode & 0o00777))) 187 | os.chmod(LOG_IMDS_FOLDER, 0o600) # only user RW needed. 188 | 189 | # initialize logger 190 | if os.path.exists(LOGGING_CONFIG_FILE): 191 | print("Using config file as one was provided.") 192 | fileConfig(LOGGING_CONFIG_FILE) 193 | logger = logging.getLogger() 194 | else: # No config file is preferred as we want to ensure the locked down folder is used. 195 | print("Logging to /var/log/imds/imds-trace.log") 196 | logger = logging.getLogger() 197 | c_handler = RotatingFileHandler('/var/log/imds/imds-trace.log', 'a', 1048576, 5, 'UTF-8') 198 | c_handler.setLevel(logging.INFO) 199 | c_format = logging.Formatter('[%(asctime)s] [%(levelname)s] %(message)s') 200 | c_handler.setFormatter(c_format) 201 | logger.addHandler(c_handler) 202 | 203 | # initialize BPF 204 | b = BPF('bpf.c') 205 | # Instruments the kernel function event() using kernel dynamic tracing of the function entry, and attaches our C 206 | # defined function name() to be called when the kernel function is called. 207 | # 208 | # kernel update https://github.com/torvalds/linux/commit/81d03e2518945c4bc7b9a7b3f1935203954bf3ba cause the event to not fire, trying previous implementation now in `__sock_sendmsg` 209 | event_list = ['__sock_sendmsg', 'sock_sendmsg', 'security_socket_sendmsg', 'sock_sendmsg_nosec'] 210 | logger.info("Try to attach multiple kernel functions to make sure the event can be triggered in most cases.") 211 | for event in event_list: 212 | try: 213 | b.attach_kprobe(event=event, fn_name="trace_sock_sendmsg") 214 | except Exception as exec: 215 | logger.info("Cannot attach kprobe to {}, it depends on your kernels.".format(event)) 216 | 217 | # This operates on a table as defined in BPF via BPF_PERF_OUTPUT() [Defined in C code as imds_events, line 32], and 218 | # associates the callback Python function to be called when data is available in the perf ring buffer. 219 | b["imds_events"].open_perf_buffer(print_imds_event) 220 | 221 | # header 222 | print("Starting ImdsPacketAnalyzer...") 223 | print("Output format: Info Level:[INFO/ERROR...] IMDS version:[IMDSV1/2?] (pid:[pid]:[process name]:argv:[argv]) -> repeats 3 times for parent process") 224 | 225 | # filter and format output 226 | while 1: 227 | # Read messages from kernel pipe 228 | try: 229 | # This polls from all open perf ring buffers, calling the callback function that was provided when calling 230 | # open_perf_buffer for each entry. 231 | b.perf_buffer_poll() 232 | except ValueError: 233 | # Ignore messages from other tracers 234 | print("ValueError here") 235 | continue 236 | except KeyboardInterrupt: 237 | exit() 238 | -------------------------------------------------------------------------------- /tests/event_proto.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | import ctypes as ct 5 | 6 | class Event(object): 7 | #namespace variables 8 | arr = [1,2,3,4] 9 | pid = (ct.c_uint32 * 4)(*arr) 10 | comm = "curl".encode() 11 | parent_comm = "zsh".encode() 12 | gparent_comm = "sshd".encode() 13 | ggparent_comm = "sshd".encode() 14 | pkt_size = 96 15 | pkt = "GET /latest/meta-data/ HTTP/1.1\r\nHost: 169.254.169.254\r\nUser-Agent: curl/7.79.1\r\nAccept: */*\r\nX-aws-ec2-metadata-token: AQAEAN2Tw3oWq21W6D3AkC5KA6zMf43zRt6voF04K-2I-jLxiu1E-A==\r\n\r\n".encode() 16 | 17 | def __init__(self) -> None: 18 | pass 19 | 20 | if(__name__ == '__main__'): 21 | event = Event() 22 | print('\n =============================Debug Info=============================') 23 | print('event obj: ', end=" ") 24 | print(event, end=" -> type: ") 25 | print(type(event)) 26 | print('event pid obj: ', end=" ") 27 | print(event.pid, end=" -> type: ") 28 | print(type(event.pid)) 29 | for i in range(4): 30 | print('\t pid ' + str(i), end=": ") 31 | print(event.pid[i], end=" -> type: ") 32 | print(type(event.pid[i])) 33 | print('event comm: ', end=" ") 34 | print(event.comm, end=" -> type: ") 35 | print(type(event.comm)) 36 | print('event parent_comm: ', end=" ") 37 | print(event.parent_comm, end=" -> type: ") 38 | print(type(event.parent_comm)) 39 | print('event gparent_comm: ', end=" ") 40 | print(event.gparent_comm, end=" -> type: ") 41 | print(type(event.gparent_comm)) 42 | print('event ggparent_comm: ', end=" ") 43 | print(event.ggparent_comm, end=" -> type: ") 44 | print(type(event.ggparent_comm)) 45 | print('event pkt_size: ', end=" ") 46 | print(event.pkt_size, end=" -> type: ") 47 | print(type(event.pkt_size)) 48 | print('event pkt: ', end=" ") 49 | print(event.pkt, end=" -> type: ") 50 | print(type(event.pkt)) 51 | print('=============================End Debug Info============================= \n') -------------------------------------------------------------------------------- /tests/test_check_v2.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | from src.imds_snoop import check_v2 5 | 6 | 7 | def test_trivial_v1(): 8 | payload = """GET /latest/meta-data/ HTTP/1.1 9 | Host: 169.254.169.254 10 | User-Agent: curl/7.79.1 11 | Accept: */*""" 12 | 13 | assert(check_v2(payload) == False) 14 | 15 | def test_trivial1_v2(): 16 | payload = """PUT /latest/api/token HTTP/1.1 17 | Host: 169.254.169.254 18 | User-Agent: curl/7.79.1 19 | Accept: */* 20 | X-aws-ec2-metadata-token-ttl-seconds: 21600""" 21 | 22 | assert(check_v2(payload) == True) 23 | 24 | def test_trivial2_v2(): 25 | payload = """GET /latest/meta-data/ HTTP/1.1 26 | Host: 169.254.169.254 27 | User-Agent: curl/7.79.1 28 | Accept: */* 29 | X-aws-ec2-metadata-token: AQAEAPyADezUnSXUDdcpky8WuUyjVUDn-f0OKlwRU9bAd60vGWMx5w==""" 30 | 31 | assert(check_v2(payload) == True) -------------------------------------------------------------------------------- /tests/test_gen_log_msg.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | from src.imds_snoop import gen_log_msg 5 | from event_proto import Event 6 | import mock 7 | 8 | #prortype obj for tests 9 | event = Event() 10 | 11 | def my_open(filename): 12 | if filename == '/proc/1/cmdline': 13 | content = 'test1' 14 | elif filename == '/proc/2/cmdline': 15 | content = 'test2' 16 | elif filename == '/proc/3/cmdline': 17 | content = 'test3' 18 | elif filename == '/proc/4/cmdline': 19 | content = 'test4' 20 | else: 21 | raise FileNotFoundError(filename) 22 | file_object = mock.mock_open(read_data=content).return_value 23 | file_object.__iter__.return_value = content.splitlines(True) 24 | return file_object 25 | 26 | 27 | def test_single_file(): 28 | with mock.patch("builtins.open",mock.mock_open(read_data='test')): 29 | assert(gen_log_msg(False,Event()) == 'IMDSv1(!) (pid:1:curl argv:test) called by -> (pid:2:zsh argv:test) -> (pid:3:sshd argv:test) -> (pid:3:sshd argv:test)') 30 | 31 | def test_multiple_files(): 32 | with mock.patch("builtins.open",my_open): 33 | assert(gen_log_msg(False,Event()) == 'IMDSv1(!) (pid:1:curl argv:test1) called by -> (pid:2:zsh argv:test2) -> (pid:3:sshd argv:test3) -> (pid:3:sshd argv:test4)') 34 | 35 | 36 | #Testing erroneaous situations 37 | expected_out = "IMDSv1(!) (pid:1 Unable to get argv information) called by -> (pid:2 Unable to get argv information) -> (pid:3 Unable to get argv information) -> (pid:3 Unable to get argv information)" 38 | 39 | def test_io_error(): 40 | open_mock = mock.mock_open() 41 | with mock.patch("builtins.open",open_mock): 42 | open_mock.side_effect = BlockingIOError 43 | assert(gen_log_msg(False,Event()) == expected_out) 44 | 45 | def test_file_not_found(): 46 | open_mock = mock.mock_open() 47 | with mock.patch("builtins.open",open_mock): 48 | open_mock.side_effect = FileNotFoundError 49 | assert(gen_log_msg(False,Event()) == expected_out) 50 | 51 | def test_permission_error(): 52 | open_mock = mock.mock_open() 53 | with mock.patch("builtins.open",open_mock): 54 | open_mock.side_effect = PermissionError 55 | assert(gen_log_msg(False,Event()) == expected_out) 56 | 57 | def test_unicode_error(): 58 | open_mock = mock.mock_open() 59 | with mock.patch("builtins.open",open_mock): 60 | open_mock.side_effect = UnicodeError 61 | assert(gen_log_msg(False,Event()) == expected_out) 62 | 63 | def test_unicode_decode(): 64 | open_mock = mock.mock_open() 65 | with mock.patch("builtins.open",open_mock): 66 | open_mock.side_effect = UnicodeDecodeError 67 | assert(gen_log_msg(False,Event()) == expected_out) -------------------------------------------------------------------------------- /tests/test_proc_info.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | import mock 5 | from src.imds_snoop import get_proc_info 6 | 7 | def test_trivial1(): 8 | expected_argv = "/bin/bash ./makeCalls.sh" 9 | expected_proc_name = 'curl' 10 | expected_out = ":" + expected_proc_name + " argv:" + expected_argv 11 | 12 | with mock.patch("builtins.open",mock.mock_open(read_data=expected_argv)): 13 | assert(get_proc_info(1234,expected_proc_name) == expected_out) 14 | 15 | 16 | #Testing erroneaous situations 17 | error_message = " Unable to get argv information" 18 | 19 | def test_io_error(): 20 | open_mock = mock.mock_open() 21 | with mock.patch("builtins.open",open_mock): 22 | open_mock.side_effect = BlockingIOError 23 | assert(get_proc_info(123,'arb') == error_message) 24 | 25 | def test_file_not_found(): 26 | open_mock = mock.mock_open() 27 | with mock.patch("builtins.open",open_mock): 28 | open_mock.side_effect = FileNotFoundError 29 | assert(get_proc_info(123,'arb') == error_message) 30 | 31 | def test_permission_error(): 32 | open_mock = mock.mock_open() 33 | with mock.patch("builtins.open",open_mock): 34 | open_mock.side_effect = PermissionError 35 | assert(get_proc_info(123,'arb') == error_message) 36 | 37 | def test_unicode_error(): 38 | open_mock = mock.mock_open() 39 | with mock.patch("builtins.open",open_mock): 40 | open_mock.side_effect = UnicodeError 41 | assert(get_proc_info(123,'arb') == error_message) 42 | 43 | def test_unicode_decode(): 44 | open_mock = mock.mock_open() 45 | with mock.patch("builtins.open",open_mock): 46 | open_mock.side_effect = UnicodeDecodeError 47 | assert(get_proc_info(123,'arb') == error_message) --------------------------------------------------------------------------------