├── Dockerfile ├── LICENSE.txt ├── README.md ├── doc └── notes.md ├── peetch ├── __init__.py ├── __main__.py ├── c_utils │ └── libssl.c ├── ebpf_programs │ ├── peetch_kprobes.c │ ├── peetch_proxy.c │ └── peetch_uprobes.c ├── globals.py ├── proxy.py └── utils.py ├── requirements.txt └── setup.py /Dockerfile: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0+ 2 | # Guillaume Valadon 3 | 4 | FROM ubuntu:22.04 5 | 6 | ENV TZ=Europe/Paris DEBIAN_FRONTEND=noninteractive 7 | RUN echo 'PS1="quarkslab/peetch:\w# "' >> /root/.bashrc 8 | 9 | # Install dependencies 10 | RUN set -x && \ 11 | PACKAGES="bison build-essential cmake flex git \ 12 | libedit-dev libllvm11 llvm-11-dev libclang-11-dev python3 zlib1g-dev \ 13 | libelf-dev libfl-dev python3-distutils python3-pip linux-headers-$(uname -r) \ 14 | libssl-dev iproute2 tmux curl bc libelf-dev zip tshark curl" && \ 15 | apt-get update && apt-get install -y $PACKAGES && \ 16 | apt-get autoclean && apt-get --purge -y autoremove && \ 17 | rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 18 | 19 | # Compile bcc 20 | RUN git clone https://github.com/iovisor/bcc && cd bcc/ && mkdir build && \ 21 | cd build && cmake .. && make install && cd src && make install && rm -rf /bcc/ 22 | 23 | # Install peetch 24 | COPY . /peetch 25 | RUN cd /peetch && pip install --upgrade pip && pip install -r requirements.txt && \ 26 | pip install . && rm -rf /peetch -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # peetch 2 | 3 | `peetch` is a collection of tools aimed at experimenting with different aspects of eBPF to bypass TLS protocol protections. 4 | 5 | Currently, peetch includes three subcommands. The first called `dump` aims to sniff network traffic by associating information about the source process with each packet. The second called `tls` allows identifying processes using OpenSSL to extract cryptographic keys. The third one `proxy` automatically intercepts TLS traffic from processes using OpenSSL and decrypts messages on the fly. 6 | 7 | Combined, the first two commands make it possible to decrypt TLS exchanges recorded in the PCAPng format. 8 | 9 | ## Installation 10 | 11 | `peetch` relies on several dependencies including [bcc](https://github.com/iovisor/bcc) and [Scapy](https://github.com/secdev/scapy). A Docker image can be easily built in order to easily test `peetch` using the following command: 12 | 13 | ```shell 14 | docker build -t quarkslab/peetch . 15 | ``` 16 | 17 | ## Commands Walk Through 18 | 19 | The following examples assume that you used the following command to enter the Docker image and launch examples within it: 20 | 21 | ```shell 22 | docker run --privileged --network host --mount type=bind,source=/sys,target=/sys --mount type=bind,source=/proc,target=/proc --pid host --rm -it quarkslab/peetch 23 | ``` 24 | 25 | ### `dump` 26 | 27 | This sub-command gives you the ability to sniff packets using an eBPF TC classifier and to retrieve the corresponding PID and process names with: 28 | 29 | ```shell 30 | peetch dump 31 | curl/1289291 - Ether / IP / TCP 10.211.55.10:53052 > 208.97.177.124:https S / Padding 32 | curl/1289291 - Ether / IP / TCP 208.97.177.124:https > 10.211.55.10:53052 SA / Padding 33 | curl/1289291 - Ether / IP / TCP 10.211.55.10:53052 > 208.97.177.124:https A / Padding 34 | curl/1289291 - Ether / IP / TCP 10.211.55.10:53052 > 208.97.177.124:https PA / Raw / Padding 35 | curl/1289291 - Ether / IP / TCP 208.97.177.124:https > 10.211.55.10:53052 A / Padding 36 | ``` 37 | 38 | Note that as of today, `dump` will only capture IPv4 based TCP segments. 39 | 40 | For convenience, the captured packets can be store to PCAPng along with process information using `--write`: 41 | 42 | ```shell 43 | peetch dump --write peetch.pcapng 44 | ^C 45 | ``` 46 | 47 | This PCAPng can easily be manipulated with Wireshark or Scapy: 48 | 49 | ```python 50 | scapy 51 | >>> l = rdpcap("peetch.pcapng") 52 | >>> l[0] 53 | >>> 54 | >>> l[0].comment 55 | b'curl/1289909' 56 | ``` 57 | 58 | ### `tls` 59 | 60 | This sub-command aims to identify processes that uses OpenSSL and makes it easy to dump several things like plaintext and secrets. 61 | 62 | By default, `peetch tls` will only display one line per process, the `--directions` argument makes it possible to display the exchanged messages: 63 | 64 | ```shell 65 | peetch tls --directions 66 | <- curl (1291078) 208.97.177.124/443 TLS1.2 ECDHE-RSA-AES128-GCM-SHA256 67 | -> curl (1291078) 208.97.177.124/443 TLS1.2 ECDHE-RSA-AES128-GCM-SHA256 68 | ``` 69 | 70 | Displaying OpenSSL buffer content is achieved with `--content`. 71 | 72 | ```shell 73 | peetch tls --content 74 | <- curl (1290608) 208.97.177.124/443 TLS1.2 ECDHE-RSA-AES128-GCM-SHA256 75 | 76 | 0000 47 45 54 20 2F 20 48 54 54 50 2F 31 2E 31 0D 0A GET / HTTP/1.1.. 77 | 0010 48 6F 73 74 3A 20 77 77 77 2E 70 65 72 64 75 2E Host: www.perdu. 78 | 0020 63 6F 6D 0D 0A 55 73 65 72 2D 41 67 65 6E 74 3A com..User-Agent: 79 | 0030 20 63 75 72 6C 2F 37 2E 36 38 2E 30 0D 0A 41 63 curl/7.68.0..Ac 80 | 81 | -> curl (1290608) 208.97.177.124/443 TLS1.2 ECDHE-RSA-AES128-GCM-SHA256 82 | 83 | 0000 48 54 54 50 2F 31 2E 31 20 32 30 30 20 4F 4B 0D HTTP/1.1 200 OK. 84 | 0010 0A 44 61 74 65 3A 20 54 68 75 2C 20 31 39 20 4D .Date: Thu, 19 M 85 | 0020 61 79 20 32 30 32 32 20 31 38 3A 31 36 3A 30 31 ay 2022 18:16:01 86 | 0030 20 47 4D 54 0D 0A 53 65 72 76 65 72 3A 20 41 70 GMT..Server: Ap 87 | ``` 88 | 89 | The `--secrets` arguments will display TLS Master Secrets extracted from memory. The following example leverages `--write` to write master secrets to simplify decrypting TLS messages with Scapy: 90 | 91 | ```shell 92 | (sleep 5; curl https://www.perdu.com/?name=highly%20secret%20information --tls-max 1.2 --http1.1 --tlsv1.2) & 93 | 94 | peetch tls --write & 95 | curl (1293232) 208.97.177.124/443 TLS1.2 ECDHE-RSA-AES128-GCM-SHA256 96 | 97 | peetch dump --write traffic.pcapng 98 | ^C 99 | 100 | # Add the master secret to a PCAPng file 101 | editcap --inject-secrets tls,1293232-master_secret.log traffic.pcapng traffic-ms.pcapng 102 | 103 | scapy 104 | >>> load_layer("tls") 105 | >>> conf.tls_session_enable = True 106 | >>> l = rdpcap("traffic-ms.pcapng") 107 | >>> l[13][TLS].msg 108 | [] 109 | ``` 110 | 111 | ### `proxy` 112 | 113 | This sub-command uses eBPF programs to automatically intercept TLS traffic from processes using OpenSSL and decrypt messages on the fly using Scapy. 114 | 115 | In the following example, `peetch proxy` display a secret value sent to a server by the `openssl` process. 116 | 117 | ```shell 118 | peetch proxy 119 | [-] Proxying OpenSSL traffic 120 | [+] Intercepting traffic from openssl/451255 to 172.67.133.176/443 via 127.0.0.1/58039 121 | --> 127.0.0.1:58039 > 172.67.133.176:https tcp 122 | <-- 172.67.133.176:https > 127.0.0.1:58039 tcp 123 | --> 127.0.0.1:58039 > 172.67.133.176:https tcp 124 | <-- 172.67.133.176:https > 127.0.0.1:58039 tcp 125 | --> 127.0.0.1:58039 > 172.67.133.176:https tcp 126 | --> 127.0.0.1:58039 > 172.67.133.176:https tcp 127 | 128 | ###[ TLS Application Data ]### 129 | data = b'GET /?secret=9590 HTTP/1.1\r\nHost: www.perdu.com\r\n\r\n\n' 130 | ``` 131 | 132 | ## Limitations 133 | 134 | By design, `peetch` only supports OpenSSL, IPv4 and TLS 1.2. 135 | -------------------------------------------------------------------------------- /doc/notes.md: -------------------------------------------------------------------------------- 1 | # Notes 2 | 3 | This document contains various information used to debug and test `peetch` features. 4 | 5 | # Tests 6 | 7 | * `openssl s_client` 8 | ``` 9 | rm secrets.txt ; (sleep 1 ;echo -e "GET /?secret=$RANDOM HTTP/1.1\r\nHost: www.perdu.com\r\n\r\n") |openssl s_client -connect www.perdu.com:443 -keylogfile secrets.txt -no_tls1_3 -4; cat secrets.txt 10 | ``` 11 | 12 | * test the connectin to the proxy 13 | ``` 14 | curl --connect-to www.perdu.com:443:127.0.0.1:2807 https://www.perdu.com 15 | ``` 16 | 17 | * libcurl binaries 18 | ``` 19 | # TLS1.2 & HTTP/1.1 20 | curl https://echo.free.beeceptor.com/?secret_number=$RANDOM --tls-max 1.2 --http1.1 --tlsv1.2 --libcurl secret_binary.c 21 | cc -o secret_binary secret_binary.c -lssl -lcurl && ./secret_binary 22 | 23 | # TLS1.2 & HTTP/2.0 24 | curl https://echo.free.beeceptor.com/?secret_number=$RANDOM --tls-max 1.2 --http2 --tlsv1.2 --libcurl secret_binary2.c 25 | cc -o secret_binary2 secret_binary2.c -lssl -lcurl && ./secret_binary2 26 | ``` 27 | 28 | # OpenSSL Offsets 29 | 30 | The following radare2 commands could be used to validate the OpenSSL offsets dynamically generated by `peetch`: 31 | 32 | ``` 33 | cat curl.rr2 34 | #!/usr/bin/rarun2 35 | program=/usr/bin/curl 36 | arg0=5 37 | arg1=--tls-max 38 | arg2=1.2 39 | arg3=--tlsv1.2 40 | arg4=--http1.1 41 | arg5=https://www.perdu.com 42 | 43 | radare2 -e dbg.profile=curl.rr2 -A -d /usr/bin/curl 44 | [...]> dcu sym.imp.curl_globa_init 45 | [...]> dmi libssl SSL_read 46 | [...]> dmi libssl SSL_write 47 | [...]> db 0xffffa479eeb4 # address of SSL_read 48 | [...]> db 0xffff9086f504 # address of SSL_write 49 | [...]> dc 50 | 51 | [...]> px 48 @ `pv @ x0 + 0x918` + 0x50 # offset off ssl_session and master_secret 52 | - offset - 6061 6263 6465 6667 6869 6A6B 6C6D 6E6F 0123456789ABCDEF 53 | 0xaaab0a47aa60 ce6a 7675 5e95 dfd4 e5ed 10c1 c0b7 9bd9 .jvu^........... 54 | 0xaaab0a47aa70 808f 416a 13ef cb29 6c8e 323a 40cb e5ea ..Aj...)l.2:@... 55 | 0xaaab0a47aa80 ccdb 09d2 7f46 7fb9 7115 6082 f4dc 2725 .....F..q.`...'% 56 | ``` 57 | ``` -------------------------------------------------------------------------------- /peetch/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0+ 2 | # Guillaume Valadon 3 | 4 | import argparse 5 | import asyncio 6 | import atexit 7 | import ctypes as ct 8 | import binascii 9 | import os 10 | import socket 11 | import struct 12 | import sys 13 | import time 14 | 15 | from bcc import BPF, BPFProgType, BPFAttachType 16 | import pyroute2 17 | from scapy.all import Ether, wrpcapng, hexdump, load_layer, conf 18 | 19 | import peetch.globals 20 | from peetch.proxy import all_tasks 21 | import peetch.utils 22 | 23 | 24 | load_layer("tls") 25 | conf.tls_session_enable = True 26 | 27 | global BPF_HANDLER, BPF_TLS_HANDLER 28 | 29 | EBPF_PROGRAMS_DIRNAME = os.path.join(os.path.dirname(__file__), 30 | "ebpf_programs/") 31 | BPF_DUMP_PROGRAM_FILENAME = "%s/peetch_kprobes.c" % EBPF_PROGRAMS_DIRNAME 32 | BPF_DUMP_PROGRAM_SOURCE = open(BPF_DUMP_PROGRAM_FILENAME).read() 33 | BPF_TLS_PROGRAM_FILENAME = "%s/peetch_uprobes.c" % EBPF_PROGRAMS_DIRNAME 34 | BPF_TLS_PROGRAM_SOURCE = open(BPF_TLS_PROGRAM_FILENAME).read() 35 | BPF_PROXY_PROGRAM_FILENAME = "%s/peetch_proxy.c" % EBPF_PROGRAMS_DIRNAME 36 | BPF_PROXY_PROGRAM_SOURCE = open(BPF_PROXY_PROGRAM_FILENAME).read() 37 | PACKETS_CAPTURED = [] 38 | TLS_INFORMATION = {} 39 | 40 | 41 | def load_classifier(interface, ebpf_function): 42 | """ 43 | Load an eBPF TC Classifier 44 | """ 45 | iproute_handler = pyroute2.IPRoute() 46 | 47 | ip_link = iproute_handler.link_lookup(ifname=interface) 48 | if not ip_link: 49 | sys.exit() 50 | 51 | ip_link = ip_link[0] 52 | iproute_handler.tc("add", "clsact", ip_link) 53 | 54 | # add ingress clsact 55 | iproute_handler.tc("add-filter", "bpf", ip_link, ":1", 56 | fd=ebpf_function.fd, name=ebpf_function.name, 57 | parent="ffff:fff2") 58 | 59 | # add egress clsact 60 | iproute_handler.tc("add-filter", "bpf", ip_link, ":1", 61 | fd=ebpf_function.fd, name=ebpf_function.name, 62 | parent="ffff:fff3") 63 | 64 | 65 | def unload_classifier(interface): 66 | """ 67 | Unload an eBPF TC Classifier 68 | """ 69 | os.system("tc qdisc del dev %s clsact" % args.interface) 70 | 71 | 72 | def exit_handler_command(interface, filename, bpf_handler): 73 | """ 74 | Exit command nicely 75 | """ 76 | time.sleep(0.01) 77 | bpf_handler.detach_kprobe(event="security_sk_classify_flow", 78 | fn_name="kprobe_security_sk_classify_flow") 79 | unload_classifier(interface) 80 | if filename: 81 | wrpcapng(filename, PACKETS_CAPTURED) 82 | 83 | 84 | def exit_handler_proxy(bpf_handler, connect_function, cgroup_fd): 85 | """ 86 | Exit proxy nicely 87 | """ 88 | time.sleep(0.01) 89 | bpf_handler.detach_func(connect_function, cgroup_fd, 90 | BPFAttachType.CGROUP_INET4_CONNECT) 91 | if cgroup_fd > 0: 92 | os.close(cgroup_fd) 93 | 94 | 95 | def handle_skb_event(cpu, data, size): 96 | """ 97 | Handle SKB events from the kernel 98 | """ 99 | 100 | # Structure retrieved from the kernel 101 | class SkbEvent(ct.Structure): 102 | _fields_ = [("pid", ct.c_uint32), 103 | ("raw", ct.c_ubyte * (size - ct.sizeof(ct.c_uint32)))] 104 | 105 | # Map the data from kernel to the structure 106 | skb_event = ct.cast(data, ct.POINTER(SkbEvent)).contents 107 | data = bytes(skb_event.raw) 108 | 109 | # Extract the process name 110 | for i in range(len(data)): 111 | if data[i] == 0: 112 | break 113 | process_name = data[:i].decode("utf-8", "replace") 114 | data = data[16:] 115 | 116 | process_information = "%s/%d" % (process_name, skb_event.pid) 117 | 118 | # Parse the packet with Scapy 119 | pkt = Ether(data) 120 | 121 | if args.write: 122 | pkt.comment = str(process_information) 123 | PACKETS_CAPTURED.append(pkt) 124 | else: 125 | if not args.raw: 126 | print(process_information, end=" - ") 127 | print(pkt.summary()) 128 | 129 | 130 | def dump_command(args): 131 | # Compile eBPF programs 132 | bpf_handler = BPF(text=BPF_DUMP_PROGRAM_SOURCE) 133 | 134 | # Attach the kprobe 135 | bpf_handler.attach_kprobe(event="security_sk_classify_flow", 136 | fn_name="kprobe_security_sk_classify_flow") 137 | 138 | # Setup the exit handler 139 | atexit.register(exit_handler_command, args.interface, args.write, 140 | bpf_handler) 141 | 142 | # Load eBPF TC Classifier 143 | classifier_function = bpf_handler.load_func("process_frame", BPF.SCHED_CLS) 144 | load_classifier(args.interface, classifier_function) 145 | 146 | # Handle incoming skb events 147 | bpf_handler["skb_events"].open_perf_buffer(handle_skb_event) 148 | try: 149 | while True: 150 | bpf_handler.perf_buffer_poll() 151 | except KeyboardInterrupt: 152 | pass 153 | 154 | 155 | def handle_tls_event(cpu, data, size): 156 | class TLSEvent(ct.Structure): 157 | _fields_ = [("address", ct.c_uint32), 158 | ("port", ct.c_uint16), 159 | ("tls_version", ct.c_uint16), 160 | ("comm", ct.c_char * 64), 161 | ("message", ct.c_uint8 * 64), 162 | ("message_length", ct.c_uint32), 163 | ("pid", ct.c_uint32), 164 | ("is_read", ct.c_uint32)] 165 | 166 | # Map the data from kernel to the structure 167 | tls_event = ct.cast(data, ct.POINTER(TLSEvent)).contents 168 | 169 | # Get TLS information 170 | pid_to_delete = None 171 | master_secret = None 172 | ciphersuite = None 173 | bpf_map_tls_information = BPF_TLS_HANDLER["tls_information_cache"] 174 | for pid, tls_info in bpf_map_tls_information.items_lookup_batch(): 175 | if pid.value == tls_event.pid: 176 | ciphersuite = tls_info.ciphersuite.decode("ascii", "ignore") 177 | master_secret = binascii.hexlify(tls_info.master_secret) 178 | master_secret = master_secret.decode("ascii", "ignore") 179 | pid_to_delete = [pid] 180 | break 181 | 182 | # Delete pid from the eBPF map 183 | if not pid_to_delete: 184 | bpf_map_tls_information.items_delete_batch(pid_to_delete) 185 | 186 | # Unpack the IPv4 destination address 187 | addr = struct.pack("I", tls_event.address) 188 | 189 | # Discard empty content 190 | if args.content and tls_event.message_length == 0: 191 | return 192 | 193 | # Display the TLS event 194 | if args.directions: 195 | if tls_event.is_read: 196 | print("->", end=" ") 197 | else: 198 | print("<-", end=" ") 199 | print("%s (%d)" % (tls_event.comm.decode("ascii", "replace"), 200 | tls_event.pid), end=" ") 201 | print("%s/%d" % (socket.inet_ntop(socket.AF_INET, addr), 202 | socket.ntohs(tls_event.port)), end=" ") 203 | 204 | version = (tls_event.tls_version & 0xF) - 1 205 | print("TLS1.%d %s" % (version, ciphersuite)) 206 | 207 | # Display TLS secrets 208 | if (args.secrets or args.write) and tls_event.tls_version == 0x303: 209 | key_log = "CLIENT_RANDOM 28071980 %s\n" % master_secret 210 | if args.secrets: 211 | print("\n %s\n" % key_log) 212 | if args.write: 213 | fd = open("%d-master_secret.log" % pid.value, "w") 214 | fd.write(key_log) 215 | fd.close() 216 | 217 | # Display the message content in hexadecimal 218 | if args.content and tls_event.message_length: 219 | hex_message = hexdump(tls_event.message[:tls_event.message_length], 220 | dump=True) 221 | print("\n ", end="") 222 | print(hex_message.replace("\n", "\n ")) 223 | print() 224 | 225 | 226 | def _tls_ebpf_programs(directions_bool, args_ssl_session_offset, 227 | args_ssl_cipher_offset, args_master_secret_offset, 228 | args_client_hello_offset, args_client_random_offset): 229 | # Get SSL structures offsets 230 | offsets = [str(offset) for offset in peetch.utils.get_offsets()] 231 | ssl_session_offset, ssl_cipher_offset, master_secret_offset, \ 232 | client_hello_offset, client_random_offset = offsets 233 | 234 | if ssl_session_offset == ssl_cipher_offset and \ 235 | ssl_cipher_offset == master_secret_offset and master_secret_offset == '0': # noqa: E501 236 | return None 237 | 238 | if args_ssl_session_offset is not None: 239 | ssl_session_offset = str(args_ssl_session_offset) 240 | 241 | if args_ssl_cipher_offset is not None: 242 | ssl_cipher_offset = str(args_ssl_cipher_offset) 243 | 244 | if args_master_secret_offset is not None: 245 | master_secret_offset = str(args_master_secret_offset) 246 | 247 | if args_client_hello_offset is not None: 248 | client_hello_offset = str(args.client_hello_offset) 249 | 250 | if args_client_random_offset is not None: 251 | client_random_offset = str(args.client_random_offset) 252 | 253 | # Compile eBPF programs 254 | ebpf_programs = BPF_TLS_PROGRAM_SOURCE.replace("DIRECTIONS", 255 | directions_bool) 256 | ebpf_programs = ebpf_programs.replace("SSL_SESSION_OFFSET", 257 | ssl_session_offset) 258 | ebpf_programs = ebpf_programs.replace("MASTER_SECRET_OFFSET", 259 | master_secret_offset) 260 | ebpf_programs = ebpf_programs.replace("SSL_CIPHER_OFFSET", 261 | ssl_cipher_offset) 262 | ebpf_programs = ebpf_programs.replace("CLIENT_HELLO_OFFSET", 263 | client_hello_offset) 264 | ebpf_programs = ebpf_programs.replace("CLIENT_RANDOM_OFFSET", 265 | client_random_offset) 266 | 267 | return ebpf_programs 268 | 269 | 270 | def tls_command(args): 271 | global BPF_TLS_HANDLER 272 | 273 | # Process arguments 274 | if args.content: 275 | args.directions = True 276 | 277 | directions_bool = "1" 278 | if args.directions: 279 | directions_bool = "0" 280 | 281 | ebpf_programs = _tls_ebpf_programs(directions_bool, 282 | args.ssl_session_offset, 283 | args.ssl_cipher_offset, 284 | args.master_secret_offset, 285 | args.client_hello_offset, 286 | args.client_random_offset) 287 | if ebpf_programs is None: 288 | print("ERROR: cannot guess SSL offsets!", file=sys.stderr) 289 | sys.exit(1) 290 | bpf_handler = BPF(text=ebpf_programs) 291 | BPF_TLS_HANDLER = bpf_handler 292 | 293 | # Attach the probes 294 | try: 295 | bpf_handler.attach_uprobe(name="ssl", 296 | sym="SSL_write", fn_name="SSL_write") 297 | bpf_handler.attach_uprobe(name="ssl", 298 | sym="SSL_read", fn_name="SSL_read") 299 | bpf_handler.attach_uretprobe(name="ssl", 300 | sym="SSL_read", fn_name="SSL_read_ret") 301 | except Exception: 302 | print("tls - cannot attach to eBPF probes!") 303 | sys.exit() 304 | 305 | def handle_tls_event(cpu, data, size): 306 | class TLSEvent(ct.Structure): 307 | _fields_ = [("address", ct.c_uint32), 308 | ("port", ct.c_uint16), 309 | ("tls_version", ct.c_uint16), 310 | ("comm", ct.c_char * 64), 311 | ("message", ct.c_uint8 * 64), 312 | ("message_length", ct.c_uint32), 313 | ("pid", ct.c_uint32), 314 | ("is_read", ct.c_uint32)] 315 | 316 | # Map the data from kernel to the structure 317 | tls_event = ct.cast(data, ct.POINTER(TLSEvent)).contents 318 | 319 | # Get TLS information 320 | pid_to_delete = None 321 | master_secret = None 322 | ciphersuite = None 323 | client_random = None 324 | bpf_map_tls_information = bpf_handler["tls_information_cache"] 325 | for pid, tls_info in bpf_map_tls_information.items_lookup_batch(): 326 | if pid.value == tls_event.pid: 327 | ciphersuite = tls_info.ciphersuite.decode("ascii", "ignore") 328 | master_secret = binascii.hexlify(tls_info.master_secret) 329 | master_secret = master_secret.decode("ascii", "ignore") 330 | client_random = binascii.hexlify(tls_info.client_random) 331 | client_random = client_random.decode("ascii", "ignore") 332 | pid_to_delete = [pid] 333 | break 334 | 335 | # Delete pid from the eBPF map 336 | if not pid_to_delete: 337 | bpf_map_tls_information.items_delete_batch(pid_to_delete) 338 | 339 | # Unpack the IPv4 destination address 340 | addr = struct.pack("I", tls_event.address) 341 | 342 | # Discard empty content 343 | if args.content and tls_event.message_length == 0: 344 | return 345 | 346 | # Display the TLS event 347 | if args.directions: 348 | if tls_event.is_read: 349 | print("->", end=" ") 350 | else: 351 | print("<-", end=" ") 352 | print("%s (%d)" % (tls_event.comm.decode("ascii", "replace"), 353 | tls_event.pid), end=" ") 354 | print("%s/%d" % (socket.inet_ntop(socket.AF_INET, addr), 355 | socket.ntohs(tls_event.port)), end=" ") 356 | 357 | version = (tls_event.tls_version & 0xF) - 1 358 | print("TLS1.%d %s" % (version, ciphersuite)) 359 | 360 | # Display TLS secrets 361 | if (args.secrets or args.write) and tls_event.tls_version == 0x303: 362 | key_log = "CLIENT_RANDOM %s %s\n" % (client_random, master_secret) 363 | if args.secrets: 364 | print("\n %s\n" % key_log) 365 | if args.write: 366 | fd = open("%d-master_secret.log" % pid.value, "w") 367 | fd.write(key_log) 368 | fd.close() 369 | 370 | # Display the message content in hexadecimal 371 | if args.content and tls_event.message_length: 372 | hex_message = hexdump(tls_event.message[:tls_event.message_length], 373 | dump=True) 374 | print("\n ", end="") 375 | print(hex_message.replace("\n", "\n ")) 376 | print() 377 | 378 | bpf_handler["tls_events"].open_perf_buffer(handle_tls_event) 379 | while True: 380 | try: 381 | bpf_handler.perf_buffer_poll() 382 | except KeyboardInterrupt: 383 | sys.exit() 384 | 385 | 386 | def proxy_command(args): 387 | # Compile eBPF programs 388 | ebpf_programs = BPF_PROXY_PROGRAM_SOURCE.replace("PEETCH_PROXY_PID", 389 | str(os.getpid())) 390 | bpf_handler = BPF(text=ebpf_programs) 391 | peetch.globals.BPF_HANDLER = bpf_handler 392 | 393 | # Load the eBPF function 394 | connect_function = bpf_handler.load_func("connect_v4_prog", 395 | prog_type=BPFProgType.CGROUP_SOCK_ADDR, # noqa: E501 396 | attach_type=BPFAttachType.CGROUP_INET4_CONNECT) # noqa: E501 397 | 398 | # Attach the eBPF function to the default cgroup 399 | cgroup_fd = os.open("/sys/fs/cgroup", os.O_RDONLY) 400 | bpf_handler.attach_func(connect_function, cgroup_fd, 401 | BPFAttachType.CGROUP_INET4_CONNECT) 402 | 403 | # Attach the SSL_* uprobes 404 | ebpf_programs = _tls_ebpf_programs("1", None, None, None, None, None) 405 | if ebpf_programs is None: 406 | print("ERROR: cannot guess SSL offsets!", file=sys.stderr) 407 | sys.exit(1) 408 | 409 | bpf_tls_handler = BPF(text=ebpf_programs) 410 | peetch.globals.BPF_TLS_HANDLER = bpf_tls_handler 411 | try: 412 | bpf_tls_handler.attach_uprobe(name="ssl", 413 | sym="SSL_write", fn_name="SSL_write") 414 | bpf_tls_handler.attach_uprobe(name="ssl", 415 | sym="SSL_read", fn_name="SSL_read") 416 | bpf_tls_handler.attach_uretprobe(name="ssl", 417 | sym="SSL_read", fn_name="SSL_read_ret") # noqa: E501 418 | except Exception: 419 | print("proxy - cannot attach to eBPF SSL_* uprobes!") 420 | sys.exit() 421 | 422 | print("[-] Proxying OpenSSL traffic") 423 | 424 | # Setup the exit handler 425 | atexit.register(exit_handler_proxy, bpf_handler, 426 | connect_function, cgroup_fd) 427 | 428 | # Start the proxy 429 | try: 430 | asyncio.run(all_tasks(args.debug)) 431 | except KeyboardInterrupt: 432 | pass 433 | 434 | 435 | def main(): 436 | global args 437 | argv = sys.argv[1:] 438 | 439 | # Parsing arguments 440 | parser = argparse.ArgumentParser(description="peetch - an eBPF playground") 441 | subparser = parser.add_subparsers() 442 | parser.set_defaults(func=lambda args: parser.print_help()) 443 | 444 | # Prepare the 'dump' subcommand 445 | dump_parser = subparser.add_parser("dump", 446 | help="Sniff packets with eBPF") 447 | dump_parser.add_argument("--raw", action="store_true", 448 | help="display packets only") 449 | dump_parser.add_argument("--write", type=str, 450 | help="pcapng filename") 451 | dump_parser.add_argument("--interface", type=str, 452 | help="interface name", default=str(conf.iface)) 453 | dump_parser.set_defaults(func=dump_command) 454 | 455 | # Prepare the 'tls' subcommand 456 | tls_parser = subparser.add_parser("tls", 457 | help="Identify processes that uses TLS") 458 | tls_parser.add_argument("--directions", action="store_true", 459 | help="display read & write calls") 460 | tls_parser.add_argument("--content", action="store_true", 461 | help="display buffers content") 462 | tls_parser.add_argument("--secrets", action="store_true", 463 | help="display TLS secrets") 464 | tls_parser.add_argument("--write", action="store_true", 465 | help="write TLS secrets to files") 466 | tls_parser.add_argument("--ssl_session_offset", 467 | help="offset to the ssl_session_t structure") 468 | tls_parser.add_argument("--master_secret_offset", 469 | help="offset to the master secret in an ssl_session_t structure") # noqa: E501 470 | tls_parser.add_argument("--ssl_cipher_offset", 471 | help="offset to the ssl_cipher structure in an ssl_session_t structure") # noqa: E501 472 | tls_parser.add_argument("--client_hello_offset", 473 | help="offset to the CLIENTHELLO_MSG structure in an ssl structure") # noqa: E501 474 | tls_parser.add_argument("--client_random_offset", 475 | help="offset to the client random in an CLIENTHELLO_MSG structure") # noqa: E501 476 | tls_parser.set_defaults(func=tls_command) 477 | 478 | # Prepare the 'proxy' subcommand 479 | proxy_parser = subparser.add_parser("proxy", 480 | help="Automatically intercept TLS connections") # noqa: E501 481 | proxy_parser.add_argument("--debug", action="store_true", 482 | help="display debug information") 483 | proxy_parser.set_defaults(func=proxy_command) 484 | 485 | # Print the Help message when no arguments are provided 486 | if not argv: 487 | parser.print_help(sys.stderr) 488 | sys.exit(1) 489 | 490 | # Call the sub-command 491 | args = parser.parse_args(argv) 492 | args.func(args) 493 | -------------------------------------------------------------------------------- /peetch/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # SPDX-License-Identifier: GPL-2.0+ 3 | # Guillaume Valadon 4 | 5 | from . import main 6 | 7 | main() 8 | -------------------------------------------------------------------------------- /peetch/c_utils/libssl.c: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0+ 2 | // Guillaume Valadon 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | #define STRUCTURE_SIZE 8192 11 | 12 | struct libssl_offsets_t { 13 | uint64_t ssl_session; 14 | uint64_t ssl_cipher; 15 | uint64_t master_secret; 16 | uint64_t client_hello; 17 | uint64_t client_random; 18 | }; 19 | 20 | 21 | struct libssl_offsets_t libssl_offsets(char *ip4_address, uint16_t port) { 22 | struct libssl_offsets_t offsets = { .ssl_session = 0, 23 | .ssl_cipher = 0, .master_secret = 0}; 24 | 25 | // Create the SSL context and set the TLS version 26 | const SSL_METHOD *method = TLS_client_method(); 27 | SSL_CTX *ctx = SSL_CTX_new(method); 28 | 29 | int ret = SSL_CTX_set_max_proto_version(ctx, TLS1_2_VERSION); 30 | if (ret < 0) { 31 | printf("SSL_CTX_set_max_proto_version() error - %d\n", ret); 32 | return offsets; 33 | } 34 | 35 | // Set up a TLS connection to get the SSL session structure 36 | SSL *ssl = SSL_new(ctx); 37 | 38 | int fd = socket(AF_INET, SOCK_STREAM, 0); 39 | if (fd < 0) { 40 | printf("socket() error - %d\n", fd); 41 | return offsets; 42 | } 43 | 44 | struct sockaddr_in addr; 45 | addr.sin_family = AF_INET; 46 | addr.sin_port = htons(port); 47 | ret = inet_aton(ip4_address, (struct in_addr*) &addr.sin_addr.s_addr); 48 | if (ret < 0) { 49 | printf("inet_aton() error - %d\n", fd); 50 | return offsets; 51 | } 52 | 53 | ret = connect(fd, (struct sockaddr*) &addr, sizeof(addr)); 54 | if (ret < 0) { 55 | printf("connect() error - %d\n", fd); 56 | return offsets; 57 | } 58 | 59 | ret = SSL_set_fd(ssl, fd); 60 | if (ret < 0) { 61 | printf("SSL_set_fd() error - %d\n", fd); 62 | return offsets; 63 | } 64 | 65 | ret = SSL_connect(ssl); 66 | if (ret < 0) { 67 | printf("SSL_connect() error - %d\n", fd); 68 | return offsets; 69 | } 70 | 71 | // SSL_SESSION* offset 72 | SSL_SESSION *session = SSL_get_session(ssl); 73 | for (uint64_t i = 0x000; i < STRUCTURE_SIZE; i++) { 74 | uint64_t value = (uint64_t) ssl + i; 75 | uint64_t *ptr = (uint64_t*) value; 76 | if ((uint64_t) *ptr == (uint64_t) session) { 77 | offsets.ssl_session = i; 78 | break; 79 | } 80 | } 81 | 82 | // SSL_CIPHER* offset 83 | const SSL_CIPHER *cipher = SSL_get_current_cipher(ssl); 84 | for (uint64_t i = 0x000; i < STRUCTURE_SIZE; i++) { 85 | uint64_t value = (uint64_t) session + i; 86 | uint64_t *ptr = (uint64_t*) value; 87 | if ((uint64_t) *ptr == (uint64_t) cipher) { 88 | offsets.ssl_cipher = i; 89 | break; 90 | } 91 | } 92 | 93 | // TLS 1.2 MASTER_SECRET offset 94 | uint8_t master_secret[48]; 95 | ret = SSL_SESSION_get_master_key(session, 96 | (unsigned char*)&master_secret, sizeof(master_secret)); 97 | if (ret != 48) { 98 | printf("SSL_SESSION_get_master_key() error - %d\n", fd); 99 | return offsets; 100 | } 101 | for (uint64_t i=0x000; i < STRUCTURE_SIZE; i++) { 102 | uint64_t value = (uint64_t) session + i; 103 | uint64_t *ptr = (uint64_t*) value; 104 | value = (uint64_t) session + i + 47; 105 | uint64_t *ptr_end = (uint64_t*) value; 106 | if ((*ptr & 0xFF) == master_secret[0] && \ 107 | (*ptr_end & 0xFF) == master_secret[47]) { 108 | offsets.master_secret = i; 109 | break; 110 | } 111 | } 112 | 113 | // Client Random offset 114 | uint8_t client_random[32]; 115 | offsets.client_hello = 0; 116 | ret = SSL_get_client_random(ssl, (unsigned char*)&client_random, sizeof(client_random)); 117 | if (ret != 32) { 118 | printf("SSL_get_client_random() error - %d\n", fd); 119 | return offsets; 120 | } 121 | for (uint64_t i=0; i < STRUCTURE_SIZE; i++) { 122 | if (offsets.client_hello != 0) 123 | break; 124 | 125 | uint64_t value = (uint64_t) ssl + i; 126 | uint64_t *ptr = (uint64_t*) value; 127 | 128 | if (*ptr) { 129 | for (uint8_t j=0; j < sizeof(STRUCTURE_SIZE) ; j++) { 130 | uint8_t *new_ptr = (uint8_t*) ptr + j; 131 | if ((new_ptr[0] & 0xFF) == client_random[0] && \ 132 | (new_ptr[31] & 0xFF) == client_random[31]) { 133 | offsets.client_hello = i; 134 | offsets.client_random = j; 135 | break; 136 | } 137 | } 138 | } 139 | 140 | } 141 | 142 | return offsets; 143 | } 144 | 145 | 146 | int main() { 147 | /* 148 | Compile it with: 149 | cc -o libssl_offsets libssl.c -lssl 150 | */ 151 | struct libssl_offsets_t offsets = libssl_offsets("1.1.1.1", 443); 152 | printf("--ssl_session_offset=0x%lx\n", offsets.ssl_session); 153 | printf("--ssl_cipher_offset=0x%lx\n", offsets.ssl_cipher); 154 | printf("--master_secret_offset=0x%lx\n", offsets.master_secret); 155 | printf("--client_hello=0x%lx\n", offsets.client_hello); 156 | printf("--client_random=0x%lx\n", offsets.client_random); 157 | 158 | return EXIT_SUCCESS; 159 | } 160 | -------------------------------------------------------------------------------- /peetch/ebpf_programs/peetch_kprobes.c: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0+ 2 | // Guillaume Valadon 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | 15 | struct key_t { 16 | u32 dst; 17 | u32 src; 18 | }; 19 | 20 | struct data_t { 21 | u32 pid; 22 | char name[TASK_COMM_LEN]; 23 | }; 24 | 25 | BPF_HASH(pid_cache, struct key_t, struct data_t); 26 | 27 | BPF_PERF_OUTPUT(skb_events); 28 | 29 | int process_frame(struct __sk_buff *skb) { 30 | // Data accessors 31 | unsigned char *data = (void *)(long)skb->data; 32 | unsigned char *data_end = (void *)(long)skb->data_end; 33 | 34 | // Mapping data to the Ethernet and IP headers 35 | struct ethhdr *eth = (struct ethhdr *)data; 36 | struct iphdr *iph = (struct iphdr*) (data + sizeof(struct ethhdr)); 37 | 38 | // Simple length check 39 | if ((data + sizeof(struct ethhdr) + sizeof(struct iphdr)) > data_end) 40 | return TC_ACT_OK; 41 | 42 | // Discard everything but IPv4 43 | if (ntohs(eth->h_proto) != ETH_P_IP) 44 | return TC_ACT_OK; 45 | 46 | // Discard everything but TCP 47 | if (iph->protocol != IPPROTO_TCP) 48 | return TC_ACT_OK; 49 | 50 | // Retrieve the PID and the process name from the IP addresses 51 | struct key_t key = { .dst = iph->daddr, .src = iph->saddr }; 52 | struct data_t *value = (struct data_t *) pid_cache.lookup(&key); 53 | if (value == NULL) { 54 | key.dst = iph->saddr; 55 | key.src = iph->daddr; 56 | value = (struct data_t *) pid_cache.lookup(&key); 57 | if (value == NULL) 58 | return TC_ACT_OK; 59 | } 60 | 61 | // Check the PID 62 | if (value->pid == 0) 63 | return TC_ACT_OK; 64 | 65 | struct data_t tmp; 66 | __builtin_memset(&tmp, 0, sizeof(tmp)); // it makes the eBPF verifier happy! 67 | tmp.pid = value->pid; 68 | for (u8 i=0; i < TASK_COMM_LEN; i++) 69 | tmp.name[i] = value->name[i]; 70 | 71 | skb_events.perf_submit_skb(skb, skb->len, &tmp, sizeof(tmp)); 72 | 73 | return TC_ACT_OK; 74 | } 75 | 76 | int kprobe_security_sk_classify_flow(struct pt_regs *ctx, struct sock *sk, struct flowi *fl) { 77 | // Discard everything but IPv4 78 | if (sk->sk_family != AF_INET) 79 | return 0; 80 | 81 | // Extract IPv4 related structures 82 | union flowi_uli uli; 83 | struct flowi4 ip4; 84 | bpf_probe_read(&ip4, sizeof(ip4), &fl->u.ip4); 85 | bpf_probe_read(&uli, sizeof(uli), &ip4.uli); 86 | 87 | // Get IP addresses and ports 88 | struct key_t key; 89 | struct data_t data; 90 | 91 | // it makes the eBPF verifier happy! 92 | __builtin_memset(&key, 0, sizeof(key)); 93 | __builtin_memset(&data, 0, sizeof(data)); 94 | 95 | bpf_probe_read(&key.src, 96 | sizeof(sk->__sk_common.skc_daddr), 97 | &sk->__sk_common.skc_daddr); 98 | bpf_probe_read(&key.dst, 99 | sizeof(sk->__sk_common.skc_rcv_saddr), 100 | &sk->__sk_common.skc_rcv_saddr); 101 | 102 | // Get and store the PID 103 | u64 id = bpf_get_current_pid_tgid(); 104 | data.pid = id >> 32; 105 | 106 | // Get and store the process name 107 | bpf_get_current_comm(data.name, sizeof(data.name)); 108 | 109 | // Store data 110 | pid_cache.update(&key, &data); 111 | 112 | return 0; 113 | } 114 | -------------------------------------------------------------------------------- /peetch/ebpf_programs/peetch_proxy.c: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0+ 2 | // Guillaume Valadon 3 | 4 | #include 5 | #include 6 | 7 | struct data_t { 8 | u32 pid; 9 | char name[64]; 10 | u32 ip; 11 | u32 port; 12 | }; 13 | 14 | BPF_HASH(pid_cache, u64); 15 | 16 | BPF_HASH(destination_cache, u16, struct data_t); 17 | 18 | int connect_v4_prog(struct bpf_sock_addr *ctx) { 19 | struct data_t data; 20 | 21 | // Get and store the PID 22 | u64 id = bpf_get_current_pid_tgid(); 23 | data.pid = id >> 32; 24 | 25 | // Do not intercept connection from peetch itself 26 | if (PEETCH_PROXY_PID == data.pid) 27 | return 1; 28 | 29 | // Do not intercept well known TCP services 30 | if (ctx->user_port == bpf_htons(53)) // DNS 31 | return 1; 32 | 33 | // Check if the PID is in the cache 34 | u64 *tmp_id = pid_cache.lookup((u64*) &id); 35 | if (tmp_id == NULL) 36 | return 1; 37 | if (*tmp_id != id) 38 | return 1; 39 | 40 | // Get and store the process name 41 | bpf_get_current_comm(data.name, 64); 42 | 43 | // Rewrite the source port 44 | struct sockaddr_in sa = {}; 45 | sa.sin_family = AF_INET; 46 | u16 new_port = bpf_htons(data.pid & 0xFFFF); // lower part of the PID will be used as the TCP source port 47 | sa.sin_port = new_port; 48 | sa.sin_addr.s_addr = bpf_htonl(0x7f000001); // 127.0.0.1 49 | 50 | if (bpf_bind(ctx, (struct sockaddr *) &sa, sizeof(sa)) != 0) 51 | return 0; 52 | 53 | // Get and store the real destination IPv4 address and port 54 | data.ip = ctx->user_ip4; 55 | data.port = ctx->user_port; 56 | 57 | // Store the connection data into the cache 58 | destination_cache.update(&new_port, &data); 59 | 60 | // Divert the connection to peetch proxy 61 | ctx->user_ip4 = 0x0100007f; 62 | ctx->user_port = bpf_htons(2807); 63 | 64 | /* 65 | Note: 66 | - 1: accept 67 | - 0: discard 68 | */ 69 | return 1; 70 | } 71 | 72 | TRACEPOINT_PROBE(syscalls, sys_enter_openat) { 73 | #define _PATH_MAX 128 74 | char filename[_PATH_MAX + 1]; 75 | 76 | // Retrieve the filename 77 | long ret = bpf_probe_read((void*)&filename, _PATH_MAX, (void*)args->filename); 78 | if (ret != 0) { 79 | //bpf_trace_printk("sys_enter_openat() - bpf_probe_read() failed\n"); 80 | return 0; 81 | } 82 | 83 | // Check if the filename contains "libssl.so" 84 | for (u16 i=8; i < _PATH_MAX; i++) { 85 | if (filename[i-8] == 'l' && filename[i-7] == 'i' && filename[i-6] == 'b' && filename[i-5] == 's' && 86 | filename[i-4] == 's' && filename[i-3] == 'l' && filename[i-2] == '.' && filename[i-1] == 's') { 87 | 88 | // Get and store the PID 89 | u64 id = bpf_get_current_pid_tgid(); 90 | u64 pid = id >> 32; 91 | pid_cache.update(&id, &id); 92 | break; 93 | } 94 | } 95 | 96 | return 0; 97 | } 98 | -------------------------------------------------------------------------------- /peetch/ebpf_programs/peetch_uprobes.c: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0+ 2 | // Guillaume Valadon 3 | 4 | #include 5 | #include 6 | 7 | 8 | // Data structure sent to userland 9 | struct tls_event_t { 10 | u32 addr; 11 | u16 port; 12 | u16 tls_version; 13 | #define COMM_MAX_LEN 64 14 | char comm[COMM_MAX_LEN]; 15 | #define MESSAGE_MAX_LEN 64 16 | u8 message[MESSAGE_MAX_LEN]; 17 | u32 message_length; 18 | u32 pid; 19 | u32 is_read; 20 | }; 21 | BPF_PERF_OUTPUT(tls_events); 22 | 23 | 24 | // Store SSL_* buffer information 25 | struct SSL_buffer_t { 26 | u64 ptr; 27 | u32 length; 28 | u32 tls_version; 29 | u32 is_read; 30 | }; 31 | BPF_HASH(SSL_read_buffers, u32, struct SSL_buffer_t); 32 | 33 | 34 | // Store connect information indexed by PID 35 | BPF_HASH(pid_cache, u32); 36 | 37 | 38 | // TLS information 39 | struct TLS_information_t { 40 | u16 tls_version; 41 | #define CIPHERSUITE_MAX_LEN 32 42 | char ciphersuite[CIPHERSUITE_MAX_LEN]; 43 | #define MASTER_SECRET_MAX_LEN 48 44 | u8 master_secret[MASTER_SECRET_MAX_LEN]; 45 | #define CLIENT_RANDOM_MAX_LEN 32 46 | u8 client_random[CLIENT_RANDOM_MAX_LEN]; 47 | }; 48 | BPF_HASH(tls_information_cache, u32, struct TLS_information_t); 49 | 50 | 51 | TRACEPOINT_PROBE(syscalls, sys_enter_connect) { 52 | // Retrieve the sockaddr_in structure 53 | struct sockaddr_in addr_in; 54 | long ret = bpf_probe_read((void*)&addr_in, sizeof(addr_in), args->uservaddr); 55 | if (ret != 0) { 56 | bpf_trace_printk("sys_enter_connect() - bpf_probe_read() failed\n"); 57 | return 0; 58 | } 59 | 60 | // Discard everything but IPv4 61 | if (addr_in.sin_family != AF_INET) 62 | return 0; 63 | 64 | // Retrieve the PID 65 | u32 pid = bpf_get_current_pid_tgid() & 0xFFFFFFFF; 66 | 67 | // Store a TLS event in the pid_cache 68 | struct tls_event_t event = { .port = addr_in.sin_port, 69 | .addr = addr_in.sin_addr.s_addr}; 70 | pid_cache.update(&pid, (u64*)&event); 71 | 72 | return 0; 73 | } 74 | 75 | 76 | // Dummy openssl ssl_st structure 77 | struct ssl_st { 78 | int version; 79 | }; 80 | 81 | 82 | static u16 get_tls_version(void *ssl_st_ptr) { 83 | // Extract the TLS version from a struct ssl_str pointer 84 | struct ssl_st ssl; 85 | 86 | long ret = bpf_probe_read(&ssl, sizeof(ssl), ssl_st_ptr); 87 | if (ret != 0) { 88 | bpf_trace_printk("get_tls_version() - bpf_probe_read() failed\n"); 89 | return -1; 90 | } 91 | 92 | return ssl.version; 93 | } 94 | 95 | 96 | static void parse_session(struct pt_regs *ctx, u16 tls_version) { 97 | // Parse a struct sl_session_st pointer and send 98 | // data to userspace 99 | 100 | // TLS information sent to userspace 101 | struct TLS_information_t tls_information; 102 | __builtin_memset(&tls_information, 0, sizeof(tls_information)); // it makes the eBPF verifier happy! 103 | tls_information.tls_version = tls_version; 104 | 105 | // Get a ssl_st pointer 106 | void *ssl_st_ptr = (void *) PT_REGS_PARM1(ctx); 107 | 108 | // Get a ssl_session_st pointer 109 | u64 *ssl_session_st_ptr = (u64 *) (ssl_st_ptr + SSL_SESSION_OFFSET); 110 | 111 | u64 address; 112 | long ret = bpf_probe_read(&address, sizeof(address), ssl_session_st_ptr); 113 | if (ret != 0) 114 | bpf_trace_printk("parse_session() #1 - bpf_probe_read() failed\n"); 115 | 116 | // Access the TLS 1.2 master secret 117 | void *ms_ptr = (void *) (address + MASTER_SECRET_OFFSET); 118 | ret = bpf_probe_read(&tls_information.master_secret, 119 | sizeof(tls_information.master_secret), ms_ptr); 120 | if (ret != 0) 121 | bpf_trace_printk("parse_session() #2 - bpf_probe_read() failed\n"); 122 | 123 | // Get a ssl_cipher_st pointer 124 | void *ssl_cipher_st_ptr = (void *) (address + SSL_CIPHER_OFFSET); 125 | ret = bpf_probe_read(&address, sizeof(address), ssl_cipher_st_ptr); 126 | if (ret != 0) 127 | bpf_trace_printk("parse_session() #3 - bpf_probe_read() failed\n"); 128 | 129 | // Get the SSL_cipher_st point to the name member 130 | ssl_cipher_st_ptr = (void *) (address + 8); 131 | ret = bpf_probe_read(&address, sizeof(address), ssl_cipher_st_ptr); 132 | if (ret != 0) 133 | bpf_trace_printk("parse_session() #4 - bpf_probe_read() failed\n"); 134 | 135 | // Access the TLS ciphersuite 136 | void *cs_ptr = (void *) address; 137 | ret = bpf_probe_read(&tls_information.ciphersuite, 138 | sizeof(tls_information.ciphersuite), cs_ptr); 139 | if (ret != 0) 140 | bpf_trace_printk("parse_session() #5 - bpf_probe_read() failed\n"); 141 | 142 | // Retrieve the Client Random 143 | void* client_hello_ptr = (void *) (ssl_st_ptr + CLIENT_HELLO_OFFSET); 144 | ret = bpf_probe_read(&address, sizeof(address), client_hello_ptr); 145 | if (ret != 0) 146 | bpf_trace_printk("parse_session() #6 - bpf_probe_read() failed\n"); 147 | 148 | u8 client_random[CLIENT_RANDOM_MAX_LEN + CLIENT_RANDOM_OFFSET]; 149 | ret = bpf_probe_read(client_random, sizeof(client_random), client_hello_ptr); 150 | if (ret != 0) 151 | bpf_trace_printk("parse_session() #7 - bpf_probe_read() failed\n"); 152 | 153 | ret = bpf_probe_read(&tls_information.client_random, sizeof(tls_information.client_random), client_random + CLIENT_RANDOM_OFFSET); 154 | if (ret != 0) 155 | bpf_trace_printk("parse_session() #8 - bpf_probe_read() failed\n"); 156 | 157 | u32 pid = bpf_get_current_pid_tgid() & 0xFFFFFFFF; 158 | tls_information_cache.update(&pid, &tls_information); 159 | } 160 | 161 | 162 | static int SSL_read_write(struct pt_regs *ctx, u16 tls_version, struct SSL_buffer_t *buffer) { 163 | // A buffer is needed 164 | if (buffer == NULL) 165 | return 0; 166 | 167 | // Retrieve connect() information for PID 168 | u32 pid = bpf_get_current_pid_tgid() & 0xFFFFFFFF; 169 | struct tls_event_t *event = (struct tls_event_t*) pid_cache.lookup(&pid); 170 | if (event == NULL) { 171 | 172 | //get ppid to check if it is a child of a known process 173 | struct task_struct *task; 174 | task = (struct task_struct *)bpf_get_current_task(); 175 | pid = task->real_parent->tgid; 176 | 177 | event = (struct tls_event_t*) pid_cache.lookup(&pid); 178 | if(event == NULL) 179 | return 0; 180 | } 181 | 182 | // Build a new TLS event and fill it 183 | struct tls_event_t new_event; 184 | 185 | bpf_get_current_comm(&new_event.comm, COMM_MAX_LEN); 186 | new_event.pid = pid; 187 | new_event.port = event->port; 188 | new_event.addr = event->addr; 189 | new_event.is_read = buffer->is_read; 190 | new_event.tls_version = tls_version; 191 | 192 | long ret = bpf_probe_read(&new_event.message, 193 | sizeof(new_event.message), (void*) buffer->ptr); 194 | if (ret != 0) { 195 | bpf_trace_printk("SSL_read_write() - bpf_probe_read() failed\n"); 196 | return 0; 197 | } 198 | new_event.message_length = buffer->length; 199 | 200 | // Send the event to userland 201 | tls_events.perf_submit(ctx, &new_event, sizeof(new_event)); 202 | 203 | // Flush the PID cache 204 | if (DIRECTIONS) // this will be replaced by a boolean in Python 205 | pid_cache.delete(&pid); 206 | 207 | return 0; 208 | } 209 | 210 | 211 | int SSL_read(struct pt_regs *ctx) { 212 | // Retrieve the PID 213 | u32 pid = bpf_get_current_pid_tgid() & 0xFFFFFFFF; 214 | 215 | // Store a SSL read buffer information in the cache 216 | struct SSL_buffer_t buffer; 217 | __builtin_memset(&buffer, 0, sizeof(buffer)); 218 | buffer.ptr = PT_REGS_PARM2(ctx); 219 | 220 | // Get TLS version 221 | void *ssl_st_ptr = (void *) PT_REGS_PARM1(ctx); 222 | buffer.tls_version = get_tls_version(ssl_st_ptr); 223 | 224 | SSL_read_buffers.update(&pid, &buffer); 225 | 226 | return 0; 227 | } 228 | 229 | 230 | int SSL_read_ret(struct pt_regs *ctx) { 231 | // Discard if nothing was received 232 | int buffer_length = PT_REGS_RC(ctx); 233 | if (buffer_length == -1) 234 | return 0; 235 | 236 | // Retrieve SSL read buffers information for PID 237 | u32 pid = bpf_get_current_pid_tgid() & 0xFFFFFFFF; 238 | struct SSL_buffer_t *buffer = (struct SSL_buffer_t*) SSL_read_buffers.lookup(&pid); 239 | if (buffer == NULL) 240 | return 0; 241 | 242 | // Add buffer information 243 | buffer->length = buffer_length; 244 | buffer->is_read = 1; 245 | 246 | long ret = SSL_read_write(ctx, buffer->tls_version, buffer); 247 | SSL_read_buffers.delete(&pid); 248 | 249 | return ret; 250 | } 251 | 252 | 253 | int SSL_write(struct pt_regs *ctx) { 254 | // Retrieve the buffer information 255 | struct SSL_buffer_t buffer; 256 | 257 | buffer.ptr = PT_REGS_PARM2(ctx); 258 | buffer.length = PT_REGS_PARM3(ctx); 259 | buffer.is_read = 0; 260 | 261 | // Get TLS version 262 | void *ssl_st_ptr = (void *) PT_REGS_PARM1(ctx); 263 | u16 tls_version = get_tls_version(ssl_st_ptr); 264 | 265 | parse_session(ctx, tls_version); 266 | 267 | return SSL_read_write(ctx, tls_version, &buffer); 268 | } 269 | -------------------------------------------------------------------------------- /peetch/globals.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0+ 2 | # Guillaume Valadon 3 | 4 | BPF_HANDLER = None 5 | BPF_TLS_HANDLER = None 6 | PACKETS_CAPTURED = [] 7 | TLS_INFORMATION = {} 8 | -------------------------------------------------------------------------------- /peetch/proxy.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0+ 2 | # Guillaume Valadon 3 | 4 | import asyncio 5 | import binascii 6 | import ctypes as ct 7 | import socket 8 | import struct 9 | import sys 10 | import time 11 | 12 | from scapy.all import sniff, IP, TCP, conf 13 | 14 | import peetch.globals 15 | 16 | 17 | def retrieve_client_information(bpf_handler, port_src): 18 | """ 19 | Get client information from eBPF maps 20 | """ 21 | 22 | process_name, process_pid, ip_dst, port_dst = [None] * 4 23 | bpf_map_destination_cache = bpf_handler["destination_cache"] 24 | 25 | destination_key_to_delete = None 26 | 27 | for destination_key, destination_data in bpf_map_destination_cache.items_lookup_batch(): # noqa: E501 28 | process_pid = destination_data.pid 29 | process_name = destination_data.name.decode("ascii", "replace") 30 | 31 | # Retrieve destination IP and port 32 | address_packed = struct.pack("I", destination_data.ip) 33 | ip_dst = socket.inet_ntop(socket.AF_INET, address_packed) 34 | port_dst = socket.ntohs(destination_data.port) 35 | 36 | if socket.ntohs(destination_key.value) == port_src: 37 | ct_array = ct.c_uint16 * 1 38 | destination_key_to_delete = ct_array(destination_key) 39 | break 40 | 41 | if destination_key_to_delete: 42 | bpf_map_destination_cache.items_delete_batch(destination_key_to_delete) 43 | 44 | return process_name, process_pid, ip_dst, port_dst 45 | 46 | 47 | def retrieve_tls_information(bpf_handler, process_pid): 48 | """ 49 | Get TLS information from eBPF maps 50 | """ 51 | tls_version, ciphersuite, client_random, master_secret = [None] * 4 52 | 53 | retries = 5 54 | pid_to_delete = None 55 | bpf_map_tls_information = bpf_handler["tls_information_cache"] 56 | while pid_to_delete is None and retries: 57 | retries -= 1 58 | for pid, tls_info in bpf_map_tls_information.items_lookup_batch(): 59 | if pid.value == process_pid: 60 | ciphersuite = tls_info.ciphersuite.decode("ascii", "ignore") 61 | master_secret = binascii.hexlify(tls_info.master_secret) 62 | master_secret = master_secret.decode("ascii", "ignore") 63 | client_random = binascii.hexlify(tls_info.client_random) 64 | client_random = client_random.decode("ascii", "ignore") 65 | tls_version = (tls_info.tls_version & 0xF) - 1 66 | if len(ciphersuite): 67 | ct_array = ct.c_uint * 1 68 | pid_to_delete = ct_array(pid) 69 | break 70 | #time.sleep(0.005) 71 | 72 | # Delete pid from the eBPF map 73 | if pid_to_delete: 74 | bpf_map_tls_information.items_delete_batch(pid_to_delete) 75 | 76 | return tls_version, ciphersuite, client_random, master_secret 77 | 78 | 79 | async def dots(): 80 | """ 81 | print dots 82 | """ 83 | while True: 84 | await asyncio.sleep(0.5) 85 | print(".", end="", flush=True) 86 | 87 | 88 | def decrypt_messages(tls_information, packets): 89 | """ 90 | Decrypt and display messages 91 | """ 92 | 93 | tls_version = tls_information.get("version", sys.maxsize) 94 | if tls_version < 3: 95 | for p in sniff(offline=packets): 96 | if TLSApplicationData in p: # noqa: F821 97 | print() 98 | p[TLSApplicationData].show() # noqa: F821 99 | 100 | 101 | async def handle_client(local_reader, local_writer): 102 | """ 103 | Proxy a new client connection 104 | """ 105 | 106 | # Retrieve source IP and port used to connect to the proxy 107 | ip_src, port_src = local_reader._transport.get_extra_info("peername") 108 | 109 | # Retrieve process information, and destination IP and port 110 | tmp = retrieve_client_information(peetch.globals.BPF_HANDLER, port_src) 111 | process_name, process_pid, ip_dst, port_dst = tmp 112 | 113 | if process_name is None: 114 | print("[!] Did not find the real destination") 115 | local_writer.close() 116 | return 117 | 118 | print("", flush=True) 119 | print(f"\r[+] Intercepting traffic from {process_name}/{process_pid}", end="") # noqa: E501 120 | print(f" to {ip_dst}/{port_dst} via {ip_src}/{port_src}") 121 | 122 | try: 123 | # Connect to the real destination and copy data between sockets 124 | tmp = await asyncio.open_connection(ip_dst, port_dst) 125 | remote_reader, remote_writer = tmp 126 | pipe1 = pipe(local_reader, remote_writer, process_pid, 127 | "-->", ip_src, ip_dst, port_src, port_dst) 128 | pipe2 = pipe(remote_reader, local_writer, process_pid, 129 | "<--", ip_dst, ip_src, port_dst, port_src) 130 | await asyncio.gather(pipe1, pipe2) 131 | 132 | # Decrypt and display TLS messages 133 | decrypt_messages(peetch.globals.TLS_INFORMATION, 134 | peetch.globals.PACKETS_CAPTURED) 135 | 136 | # Reset global variables 137 | conf.tls_nss_keys = {} 138 | peetch.globals.TLS_INFORMATION = {} 139 | peetch.globals.PACKETS_CAPTURED = [] 140 | except ConnectionResetError as e: 141 | print(f" {e}") 142 | finally: 143 | local_writer.close() 144 | 145 | 146 | async def pipe(reader, writer, pid, direction, ip_src, ip_dst, port_src, port_dst): # noqa: E501 147 | """" 148 | Copy data from one socket to another and retrieve TLS information 149 | Inspired by https://stackoverflow.com/a/46422554 150 | """ 151 | 152 | try: 153 | while not reader.at_eof(): 154 | data = await reader.read(8192) 155 | if not len(data): 156 | continue 157 | 158 | # Rebuild the IP packet 159 | tls_record = IP(dst=ip_dst, src=ip_src) 160 | tls_record /= TCP(dport=port_dst, sport=port_src) 161 | tls_record /= TLS(data) # noqa: F821 162 | peetch.globals.PACKETS_CAPTURED += [tls_record] 163 | 164 | # Display a short summary 165 | sprintf_fmt = "%IP.src%:%TCP.sport% > %IP.dst%:%TCP.dport% %IP.proto%" # noqa: E501 166 | summary = tls_record.sprintf(sprintf_fmt) 167 | print(f" {direction} {summary}") 168 | 169 | # Copy data to the other socket 170 | writer.write(data) 171 | 172 | # Get and store TLS information 173 | tls_version, ciphersuite, client_random, master_secret = retrieve_tls_information(peetch.globals.BPF_TLS_HANDLER, pid) # noqa: E501 174 | if ciphersuite: 175 | if tls_version < 3: 176 | client_random_bytes = binascii.unhexlify(client_random) 177 | master_secret_bytes = binascii.unhexlify(master_secret) 178 | conf.tls_nss_keys = {"CLIENT_RANDOM": {client_random_bytes: master_secret_bytes}} # noqa: E501 179 | peetch.globals.TLS_INFORMATION = {"version": tls_version, 180 | "ciphersuite": ciphersuite} # noqa: E501 181 | finally: 182 | writer.close() 183 | 184 | 185 | async def tcp_proxy(): 186 | """ 187 | async TCP proxy 188 | """ 189 | server = await asyncio.start_server(handle_client, "127.0.0.1", 2807) 190 | await server.serve_forever() 191 | 192 | 193 | async def all_tasks(debug): 194 | """ 195 | all proxy tasks 196 | """ 197 | tasks = [tcp_proxy()] 198 | if debug: 199 | tasks += [dots()] 200 | await asyncio.gather(*tasks) 201 | -------------------------------------------------------------------------------- /peetch/utils.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0+ 2 | # Guillaume Valadon 3 | 4 | import ctypes 5 | import os 6 | import os.path 7 | import sys 8 | 9 | # Identify the library path 10 | dirname = os.path.dirname(__file__) 11 | UTILS_LIB_PATH = None 12 | for root, dirs, files in os.walk(dirname): 13 | for file in files: 14 | if file.startswith("utils_lib") and file.endswith(".so"): 15 | UTILS_LIB_PATH = os.path.join(root, file) 16 | break 17 | 18 | 19 | class LIBSSLOffsets(ctypes.Structure): 20 | _fields_ = [("ssl_session", ctypes.c_uint64), 21 | ("ssl_cipher", ctypes.c_uint64), 22 | ("master_secret", ctypes.c_uint64), 23 | ("client_hello", ctypes.c_uint64), 24 | ("client_random", ctypes.c_uint64)] 25 | 26 | 27 | if UTILS_LIB_PATH: 28 | libssl_offset = ctypes.CDLL(UTILS_LIB_PATH) 29 | libssl_offset.libssl_offsets.argstypes = [ctypes.c_char_p, ctypes.c_uint16] 30 | libssl_offset.libssl_offsets.restype = LIBSSLOffsets 31 | 32 | def get_offsets(address_ipv4=b"1.1.1.1", port=443): 33 | # Retrieve offsets inside SSL structures 34 | raw_offsets = libssl_offset.libssl_offsets(address_ipv4, port) 35 | return (raw_offsets.ssl_session, 36 | raw_offsets.ssl_cipher, 37 | raw_offsets.master_secret, 38 | raw_offsets.client_hello, 39 | raw_offsets.client_random) 40 | else: 41 | print("ERROR: cannot find the utils_lib dynamic library!", file=sys.stderr) 42 | sys.exit(1) 43 | 44 | if __name__ == "__main__": 45 | print(["0x%x" % offset for offset in get_offsets()]) 46 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | scapy @ git+https://github.com/secdev/scapy@master 2 | cryptography 3 | pyroute2 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0+ 2 | # Guillaume Valadon 3 | 4 | from distutils.core import setup, Extension 5 | 6 | setup(name="peetch", 7 | description="An eBPF playground", 8 | author="Guillaume Valadon", 9 | author_email="gvaladon@quarkslab.com", 10 | version="0.2.0", 11 | packages=["peetch"], 12 | package_data={"peetch": ["ebpf_programs/peetch_*.c"]}, 13 | entry_points={"console_scripts": ["peetch=peetch:main"]}, 14 | ext_modules=[Extension("peetch.utils_lib", sources=["peetch/c_utils/libssl.c"], libraries=["ssl"])], 15 | ) 16 | --------------------------------------------------------------------------------