├── .gitignore ├── LICENCE.txt ├── README.md ├── application ├── factory.go ├── reverse │ └── factory.go ├── rfb │ ├── factory.go │ ├── messages.go │ └── stream.go └── stream.go ├── go.mod ├── go.sum ├── output ├── files │ ├── binary │ │ ├── factory.go │ │ └── stream.go │ ├── factory.go │ └── fork │ │ ├── factory.go │ │ └── stream.go └── media │ ├── factory.go │ ├── fork │ ├── factory.go │ └── stream.go │ ├── jpeg │ ├── factory.go │ └── stream.go │ └── mjpeg │ ├── factory.go │ └── stream.go ├── pcapeek.go └── transport └── tcp ├── marshaller.go └── stream.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/jetbrains+all,go 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=jetbrains+all,go 3 | 4 | ### Go ### 5 | # If you prefer the allow list template instead of the deny list, see community template: 6 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 7 | # 8 | # Binaries for programs and plugins 9 | *.exe 10 | *.exe~ 11 | *.dll 12 | *.so 13 | *.dylib 14 | 15 | # Test binary, built with `go test -c` 16 | *.test 17 | 18 | # Output of the go coverage tool, specifically when used with LiteIDE 19 | *.out 20 | 21 | # Dependency directories (remove the comment below to include it) 22 | # vendor/ 23 | 24 | # Go workspace file 25 | go.work 26 | 27 | ### JetBrains+all ### 28 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 29 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 30 | 31 | # User-specific stuff 32 | .idea/**/workspace.xml 33 | .idea/**/tasks.xml 34 | .idea/**/usage.statistics.xml 35 | .idea/**/dictionaries 36 | .idea/**/shelf 37 | 38 | # AWS User-specific 39 | .idea/**/aws.xml 40 | 41 | # Generated files 42 | .idea/**/contentModel.xml 43 | 44 | # Sensitive or high-churn files 45 | .idea/**/dataSources/ 46 | .idea/**/dataSources.ids 47 | .idea/**/dataSources.local.xml 48 | .idea/**/sqlDataSources.xml 49 | .idea/**/dynamic.xml 50 | .idea/**/uiDesigner.xml 51 | .idea/**/dbnavigator.xml 52 | 53 | # Gradle 54 | .idea/**/gradle.xml 55 | .idea/**/libraries 56 | 57 | # Gradle and Maven with auto-import 58 | # When using Gradle or Maven with auto-import, you should exclude module files, 59 | # since they will be recreated, and may cause churn. Uncomment if using 60 | # auto-import. 61 | # .idea/artifacts 62 | # .idea/compiler.xml 63 | # .idea/jarRepositories.xml 64 | # .idea/modules.xml 65 | # .idea/*.iml 66 | # .idea/modules 67 | # *.iml 68 | # *.ipr 69 | 70 | # CMake 71 | cmake-build-*/ 72 | 73 | # Mongo Explorer plugin 74 | .idea/**/mongoSettings.xml 75 | 76 | # File-based project format 77 | *.iws 78 | 79 | # IntelliJ 80 | out/ 81 | 82 | # mpeltonen/sbt-idea plugin 83 | .idea_modules/ 84 | 85 | # JIRA plugin 86 | atlassian-ide-plugin.xml 87 | 88 | # Cursive Clojure plugin 89 | .idea/replstate.xml 90 | 91 | # SonarLint plugin 92 | .idea/sonarlint/ 93 | 94 | # Crashlytics plugin (for Android Studio and IntelliJ) 95 | com_crashlytics_export_strings.xml 96 | crashlytics.properties 97 | crashlytics-build.properties 98 | fabric.properties 99 | 100 | # Editor-based Rest Client 101 | .idea/httpRequests 102 | 103 | # Android studio 3.1+ serialized cache file 104 | .idea/caches/build_file_checksums.ser 105 | 106 | ### JetBrains+all Patch ### 107 | # Ignore everything but code style settings and run configurations 108 | # that are supposed to be shared within teams. 109 | 110 | .idea/* 111 | 112 | !.idea/codeStyles 113 | !.idea/runConfigurations 114 | 115 | # End of https://www.toptal.com/developers/gitignore/api/jetbrains+all,go 116 | -------------------------------------------------------------------------------- /LICENCE.txt: -------------------------------------------------------------------------------- 1 | EUROPEAN UNION PUBLIC LICENCE v. 1.2 2 | EUPL © the European Union 2007, 2016 3 | 4 | This European Union Public Licence (the ‘EUPL’) applies to the Work (as 5 | defined below) which is provided under the terms of this Licence. Any use of 6 | the Work, other than as authorised under this Licence is prohibited (to the 7 | extent such use is covered by a right of the copyright holder of the Work). 8 | 9 | The Work is provided under the terms of this Licence when the Licensor (as 10 | defined below) has placed the following notice immediately following the 11 | copyright notice for the Work: 12 | 13 | Licensed under the EUPL 14 | 15 | or has expressed by any other means his willingness to license under the EUPL. 16 | 17 | 1. Definitions 18 | 19 | In this Licence, the following terms have the following meaning: 20 | 21 | - ‘The Licence’: this Licence. 22 | 23 | - ‘The Original Work’: the work or software distributed or communicated by the 24 | Licensor under this Licence, available as Source Code and also as Executable 25 | Code as the case may be. 26 | 27 | - ‘Derivative Works’: the works or software that could be created by the 28 | Licensee, based upon the Original Work or modifications thereof. This 29 | Licence does not define the extent of modification or dependence on the 30 | Original Work required in order to classify a work as a Derivative Work; 31 | this extent is determined by copyright law applicable in the country 32 | mentioned in Article 15. 33 | 34 | - ‘The Work’: the Original Work or its Derivative Works. 35 | 36 | - ‘The Source Code’: the human-readable form of the Work which is the most 37 | convenient for people to study and modify. 38 | 39 | - ‘The Executable Code’: any code which has generally been compiled and which 40 | is meant to be interpreted by a computer as a program. 41 | 42 | - ‘The Licensor’: the natural or legal person that distributes or communicates 43 | the Work under the Licence. 44 | 45 | - ‘Contributor(s)’: any natural or legal person who modifies the Work under 46 | the Licence, or otherwise contributes to the creation of a Derivative Work. 47 | 48 | - ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of 49 | the Work under the terms of the Licence. 50 | 51 | - ‘Distribution’ or ‘Communication’: any act of selling, giving, lending, 52 | renting, distributing, communicating, transmitting, or otherwise making 53 | available, online or offline, copies of the Work or providing access to its 54 | essential functionalities at the disposal of any other natural or legal 55 | person. 56 | 57 | 2. Scope of the rights granted by the Licence 58 | 59 | The Licensor hereby grants You a worldwide, royalty-free, non-exclusive, 60 | sublicensable licence to do the following, for the duration of copyright 61 | vested in the Original Work: 62 | 63 | - use the Work in any circumstance and for all usage, 64 | - reproduce the Work, 65 | - modify the Work, and make Derivative Works based upon the Work, 66 | - communicate to the public, including the right to make available or display 67 | the Work or copies thereof to the public and perform publicly, as the case 68 | may be, the Work, 69 | - distribute the Work or copies thereof, 70 | - lend and rent the Work or copies thereof, 71 | - sublicense rights in the Work or copies thereof. 72 | 73 | Those rights can be exercised on any media, supports and formats, whether now 74 | known or later invented, as far as the applicable law permits so. 75 | 76 | In the countries where moral rights apply, the Licensor waives his right to 77 | exercise his moral right to the extent allowed by law in order to make 78 | effective the licence of the economic rights here above listed. 79 | 80 | The Licensor grants to the Licensee royalty-free, non-exclusive usage rights 81 | to any patents held by the Licensor, to the extent necessary to make use of 82 | the rights granted on the Work under this Licence. 83 | 84 | 3. Communication of the Source Code 85 | 86 | The Licensor may provide the Work either in its Source Code form, or as 87 | Executable Code. If the Work is provided as Executable Code, the Licensor 88 | provides in addition a machine-readable copy of the Source Code of the Work 89 | along with each copy of the Work that the Licensor distributes or indicates, 90 | in a notice following the copyright notice attached to the Work, a repository 91 | where the Source Code is easily and freely accessible for as long as the 92 | Licensor continues to distribute or communicate the Work. 93 | 94 | 4. Limitations on copyright 95 | 96 | Nothing in this Licence is intended to deprive the Licensee of the benefits 97 | from any exception or limitation to the exclusive rights of the rights owners 98 | in the Work, of the exhaustion of those rights or of other applicable 99 | limitations thereto. 100 | 101 | 5. Obligations of the Licensee 102 | 103 | The grant of the rights mentioned above is subject to some restrictions and 104 | obligations imposed on the Licensee. Those obligations are the following: 105 | 106 | Attribution right: The Licensee shall keep intact all copyright, patent or 107 | trademarks notices and all notices that refer to the Licence and to the 108 | disclaimer of warranties. The Licensee must include a copy of such notices and 109 | a copy of the Licence with every copy of the Work he/she distributes or 110 | communicates. The Licensee must cause any Derivative Work to carry prominent 111 | notices stating that the Work has been modified and the date of modification. 112 | 113 | Copyleft clause: If the Licensee distributes or communicates copies of the 114 | Original Works or Derivative Works, this Distribution or Communication will be 115 | done under the terms of this Licence or of a later version of this Licence 116 | unless the Original Work is expressly distributed only under this version of 117 | the Licence — for example by communicating ‘EUPL v. 1.2 only’. The Licensee 118 | (becoming Licensor) cannot offer or impose any additional terms or conditions 119 | on the Work or Derivative Work that alter or restrict the terms of the 120 | Licence. 121 | 122 | Compatibility clause: If the Licensee Distributes or Communicates Derivative 123 | Works or copies thereof based upon both the Work and another work licensed 124 | under a Compatible Licence, this Distribution or Communication can be done 125 | under the terms of this Compatible Licence. For the sake of this clause, 126 | ‘Compatible Licence’ refers to the licences listed in the appendix attached to 127 | this Licence. Should the Licensee's obligations under the Compatible Licence 128 | conflict with his/her obligations under this Licence, the obligations of the 129 | Compatible Licence shall prevail. 130 | 131 | Provision of Source Code: When distributing or communicating copies of the 132 | Work, the Licensee will provide a machine-readable copy of the Source Code or 133 | indicate a repository where this Source will be easily and freely available 134 | for as long as the Licensee continues to distribute or communicate the Work. 135 | 136 | Legal Protection: This Licence does not grant permission to use the trade 137 | names, trademarks, service marks, or names of the Licensor, except as required 138 | for reasonable and customary use in describing the origin of the Work and 139 | reproducing the content of the copyright notice. 140 | 141 | 6. Chain of Authorship 142 | 143 | The original Licensor warrants that the copyright in the Original Work granted 144 | hereunder is owned by him/her or licensed to him/her and that he/she has the 145 | power and authority to grant the Licence. 146 | 147 | Each Contributor warrants that the copyright in the modifications he/she 148 | brings to the Work are owned by him/her or licensed to him/her and that he/she 149 | has the power and authority to grant the Licence. 150 | 151 | Each time You accept the Licence, the original Licensor and subsequent 152 | Contributors grant You a licence to their contributions to the Work, under the 153 | terms of this Licence. 154 | 155 | 7. Disclaimer of Warranty 156 | 157 | The Work is a work in progress, which is continuously improved by numerous 158 | Contributors. It is not a finished work and may therefore contain defects or 159 | ‘bugs’ inherent to this type of development. 160 | 161 | For the above reason, the Work is provided under the Licence on an ‘as is’ 162 | basis and without warranties of any kind concerning the Work, including 163 | without limitation merchantability, fitness for a particular purpose, absence 164 | of defects or errors, accuracy, non-infringement of intellectual property 165 | rights other than copyright as stated in Article 6 of this Licence. 166 | 167 | This disclaimer of warranty is an essential part of the Licence and a 168 | condition for the grant of any rights to the Work. 169 | 170 | 8. Disclaimer of Liability 171 | 172 | Except in the cases of wilful misconduct or damages directly caused to natural 173 | persons, the Licensor will in no event be liable for any direct or indirect, 174 | material or moral, damages of any kind, arising out of the Licence or of the 175 | use of the Work, including without limitation, damages for loss of goodwill, 176 | work stoppage, computer failure or malfunction, loss of data or any commercial 177 | damage, even if the Licensor has been advised of the possibility of such 178 | damage. However, the Licensor will be liable under statutory product liability 179 | laws as far such laws apply to the Work. 180 | 181 | 9. Additional agreements 182 | 183 | While distributing the Work, You may choose to conclude an additional 184 | agreement, defining obligations or services consistent with this Licence. 185 | However, if accepting obligations, You may act only on your own behalf and on 186 | your sole responsibility, not on behalf of the original Licensor or any other 187 | Contributor, and only if You agree to indemnify, defend, and hold each 188 | Contributor harmless for any liability incurred by, or claims asserted against 189 | such Contributor by the fact You have accepted any warranty or additional 190 | liability. 191 | 192 | 10. Acceptance of the Licence 193 | 194 | The provisions of this Licence can be accepted by clicking on an icon ‘I 195 | agree’ placed under the bottom of a window displaying the text of this Licence 196 | or by affirming consent in any other similar way, in accordance with the rules 197 | of applicable law. Clicking on that icon indicates your clear and irrevocable 198 | acceptance of this Licence and all of its terms and conditions. 199 | 200 | Similarly, you irrevocably accept this Licence and all of its terms and 201 | conditions by exercising any rights granted to You by Article 2 of this 202 | Licence, such as the use of the Work, the creation by You of a Derivative Work 203 | or the Distribution or Communication by You of the Work or copies thereof. 204 | 205 | 11. Information to the public 206 | 207 | In case of any Distribution or Communication of the Work by means of 208 | electronic communication by You (for example, by offering to download the Work 209 | from a remote location) the distribution channel or media (for example, a 210 | website) must at least provide to the public the information requested by the 211 | applicable law regarding the Licensor, the Licence and the way it may be 212 | accessible, concluded, stored and reproduced by the Licensee. 213 | 214 | 12. Termination of the Licence 215 | 216 | The Licence and the rights granted hereunder will terminate automatically upon 217 | any breach by the Licensee of the terms of the Licence. 218 | 219 | Such a termination will not terminate the licences of any person who has 220 | received the Work from the Licensee under the Licence, provided such persons 221 | remain in full compliance with the Licence. 222 | 223 | 13. Miscellaneous 224 | 225 | Without prejudice of Article 9 above, the Licence represents the complete 226 | agreement between the Parties as to the Work. 227 | 228 | If any provision of the Licence is invalid or unenforceable under applicable 229 | law, this will not affect the validity or enforceability of the Licence as a 230 | whole. Such provision will be construed or reformed so as necessary to make it 231 | valid and enforceable. 232 | 233 | The European Commission may publish other linguistic versions or new versions 234 | of this Licence or updated versions of the Appendix, so far this is required 235 | and reasonable, without reducing the scope of the rights granted by the 236 | Licence. New versions of the Licence will be published with a unique version 237 | number. 238 | 239 | All linguistic versions of this Licence, approved by the European Commission, 240 | have identical value. Parties can take advantage of the linguistic version of 241 | their choice. 242 | 243 | 14. Jurisdiction 244 | 245 | Without prejudice to specific agreement between parties, 246 | 247 | - any litigation resulting from the interpretation of this License, arising 248 | between the European Union institutions, bodies, offices or agencies, as a 249 | Licensor, and any Licensee, will be subject to the jurisdiction of the Court 250 | of Justice of the European Union, as laid down in article 272 of the Treaty 251 | on the Functioning of the European Union, 252 | 253 | - any litigation arising between other parties and resulting from the 254 | interpretation of this License, will be subject to the exclusive 255 | jurisdiction of the competent court where the Licensor resides or conducts 256 | its primary business. 257 | 258 | 15. Applicable Law 259 | 260 | Without prejudice to specific agreement between parties, 261 | 262 | - this Licence shall be governed by the law of the European Union Member State 263 | where the Licensor has his seat, resides or has his registered office, 264 | 265 | - this licence shall be governed by Belgian law if the Licensor has no seat, 266 | residence or registered office inside a European Union Member State. 267 | 268 | Appendix 269 | 270 | ‘Compatible Licences’ according to Article 5 EUPL are: 271 | 272 | - GNU General Public License (GPL) v. 2, v. 3 273 | - GNU Affero General Public License (AGPL) v. 3 274 | - Open Software License (OSL) v. 2.1, v. 3.0 275 | - Eclipse Public License (EPL) v. 1.0 276 | - CeCILL v. 2.0, v. 2.1 277 | - Mozilla Public Licence (MPL) v. 2 278 | - GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3 279 | - Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for 280 | works other than software 281 | - European Union Public Licence (EUPL) v. 1.1, v. 1.2 282 | - Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong 283 | Reciprocity (LiLiQ-R+). 284 | 285 | The European Commission may update this Appendix to later versions of the 286 | above licences without producing a new version of the EUPL, as long as they 287 | provide the rights granted in Article 2 of this Licence and protect the 288 | covered Source Code from exclusive appropriation. 289 | 290 | All other changes or additions to this Appendix require the production of a 291 | new EUPL version. 292 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PCAPeek 2 | A proof-of-concept re-assembler for reverse VNC traffic such as [IcedID & Qakbot's VNC Backdoors](https://blog.nviso.eu/2023/03/20/icedids-vnc-backdoors-dark-cat-anubis-keyhole/). 3 | 4 | Do note that as PoC, PCAPeek offers no guarantees on backwards compatibility and might be modified in the future for additional protocols. 5 | 6 | ## Installation 7 | This utility depends on [Npcap](https://npcap.com/#download) for PCAP parsing, which you likely already have installed if you have [WireShark](https://www.wireshark.org/). 8 | 9 | To download and build this utility using the [Go programming language](https://go.dev/), simply... 10 | ```bash 11 | go install github.com/0xThiebaut/PCAPeek@latest 12 | ``` 13 | 14 | ## Usage 15 | To use PCAPeek, use the `--help` flag. 16 | ```bash 17 | PCAPeek --help 18 | ``` 19 | 20 | ``` 21 | PCAPeek is a tool to peek into PCAPs. It doesn't do much besides acting as a proof of concept to reconstruct reverse VNC traffic. 22 | 23 | 24 | Usage: 25 | PCAPeek PCAP [PCAP ...] [flags] 26 | 27 | Flags: 28 | --files Output clipboard files 29 | --files-dir string The output directory for the clipboard files (default "./") 30 | --filter string A BPF filter to apply on the PCAPs 31 | -h, --help help for PCAPeek 32 | --jpeg Output JPEG frames 33 | --jpeg-dir string The output directory for the JPEG frames (default "./") 34 | --jpeg-fps int The number of JPEG frames to output per second (default 0, outputs all frames) 35 | --jpeg-quality int The JPEG frame quality percentage (default 100) 36 | --mjpeg Output MJPEG videos 37 | --mjpeg-dir string The output directory for the MJPEG videos (default "./") 38 | --mjpeg-fps int The number of MJPEG frames to output per second (default 10) 39 | --mjpeg-quality int The MJPEG video quality percentage (default 100) 40 | ``` 41 | 42 | ## Thanks 43 | Thanks to [Brad Duncan (Malware-Traffic-Analysis.net)](https://malware-traffic-analysis.net/) and [Erik Hjelmvik (NETRESEC)](https://www.netresec.com/?page=Blog&month=2022-10&post=IcedID-BackConnect-Protocol) for their extensive research on IcedID and its BackConnect protocol. -------------------------------------------------------------------------------- /application/factory.go: -------------------------------------------------------------------------------- 1 | package application 2 | 3 | import ( 4 | "github.com/0xThiebaut/PCAPeek/transport/tcp" 5 | "github.com/google/gopacket" 6 | "github.com/google/gopacket/tcpassembly" 7 | "sync" 8 | ) 9 | 10 | type Factory interface { 11 | Client(net gopacket.Flow, transport gopacket.Flow, client tcp.PeekStream) bool 12 | Handle(net gopacket.Flow, transport gopacket.Flow, client tcp.Stream, server tcp.Stream) 13 | Server(net gopacket.Flow, transport gopacket.Flow, server tcp.PeekStream) bool 14 | } 15 | 16 | func NewApplicationStreamFactory(strict bool, applications ...Factory) tcpassembly.StreamFactory { 17 | return &factory{ 18 | Applications: applications, 19 | Streams: map[uint64]map[uint64]tcp.Stream{}, 20 | Strict: strict, 21 | } 22 | } 23 | 24 | type factory struct { 25 | Mutex sync.Mutex 26 | Streams map[uint64]map[uint64]tcp.Stream 27 | Applications []Factory 28 | Strict bool 29 | } 30 | 31 | func (f *factory) New(net, transport gopacket.Flow) tcpassembly.Stream { 32 | // Lock the cache for editing 33 | f.Mutex.Lock() 34 | defer f.Mutex.Unlock() 35 | // Ensure the network cache exists 36 | nh := net.FastHash() 37 | if _, ok := f.Streams[nh]; !ok { 38 | f.Streams[nh] = map[uint64]tcp.Stream{} 39 | } 40 | // Ensure the transport cache exists 41 | th := transport.FastHash() 42 | if cached, ok := f.Streams[nh][th]; !ok { 43 | client, server := NewRouterStreams(net, transport, f.Strict, f.Applications...) 44 | f.Streams[nh][th] = server 45 | return client 46 | } else { 47 | delete(f.Streams[nh], th) 48 | if len(f.Streams[nh]) == 0 { 49 | delete(f.Streams, nh) 50 | } 51 | return cached 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /application/reverse/factory.go: -------------------------------------------------------------------------------- 1 | package reverse 2 | 3 | import ( 4 | "github.com/0xThiebaut/PCAPeek/application" 5 | "github.com/0xThiebaut/PCAPeek/transport/tcp" 6 | "github.com/google/gopacket" 7 | ) 8 | 9 | func New(application application.Factory) application.Factory { 10 | return &factory{application: application} 11 | } 12 | 13 | type factory struct { 14 | application application.Factory 15 | } 16 | 17 | func (f *factory) Client(net gopacket.Flow, transport gopacket.Flow, client tcp.PeekStream) bool { 18 | return f.application.Server(net.Reverse(), transport.Reverse(), client) 19 | } 20 | 21 | func (f *factory) Handle(net gopacket.Flow, transport gopacket.Flow, client tcp.Stream, server tcp.Stream) { 22 | f.application.Handle(net.Reverse(), transport.Reverse(), server, client) 23 | } 24 | 25 | func (f *factory) Server(net gopacket.Flow, transport gopacket.Flow, server tcp.PeekStream) bool { 26 | return f.application.Client(net.Reverse(), transport.Reverse(), server) 27 | } 28 | -------------------------------------------------------------------------------- /application/rfb/factory.go: -------------------------------------------------------------------------------- 1 | package rfb 2 | 3 | import ( 4 | "unicode/utf8" 5 | 6 | "github.com/0xThiebaut/PCAPeek/application" 7 | "github.com/0xThiebaut/PCAPeek/output/files" 8 | "github.com/0xThiebaut/PCAPeek/output/media" 9 | "github.com/0xThiebaut/PCAPeek/transport/tcp" 10 | "github.com/google/gopacket" 11 | ) 12 | 13 | func New(mo media.Factory, fo files.Factory) application.Factory { 14 | return &factory{ 15 | Media: mo, 16 | Files: fo, 17 | } 18 | } 19 | 20 | type factory struct { 21 | id uint64 22 | Media media.Factory 23 | Files files.Factory 24 | } 25 | 26 | func (f *factory) Client(net gopacket.Flow, transport gopacket.Flow, client tcp.PeekStream) bool { 27 | // By default, we don't recognize ClientInit messages. 28 | // The protocol only defines these as 1 byte messages which is too small for confident decisions. 29 | // TODO: In the future we may cache these to not lose the byte's meaning. 30 | return false 31 | } 32 | 33 | func (f *factory) Server(net gopacket.Flow, transport gopacket.Flow, server tcp.PeekStream) bool { 34 | init := ServerInit{} 35 | _, err := tcp.UnmarshallPeek(server, &init) 36 | // TODO: Currently mark the stream as supported if the message seems like a ServerInit message 37 | // where the frame buffer has a landscape orientation and the name is a valid UTF-8 string. 38 | // Both of these assertions are NOT required per the protocol and only match a subset of possible values. 39 | return err == nil && init.FramebufferWidth > init.FramebufferHeight && utf8.ValidString(init.Name) 40 | } 41 | 42 | func (f *factory) Handle(net gopacket.Flow, transport gopacket.Flow, client tcp.Stream, server tcp.Stream) { 43 | s := stream{ 44 | id: f.id, 45 | net: net, 46 | transport: transport, 47 | client: client, 48 | server: server, 49 | Media: f.Media, 50 | Files: f.Files, 51 | } 52 | f.id++ 53 | // TODO: In the future we can peek the client and server streams to deduce the connection's stage. 54 | // Currently, we assume IcedID's state which omits handshakes. 55 | client.Notifier(s.ClientInit) 56 | server.Notifier(s.ServerInit) 57 | } 58 | -------------------------------------------------------------------------------- /application/rfb/messages.go: -------------------------------------------------------------------------------- 1 | package rfb 2 | 3 | import ( 4 | "encoding/json" 5 | "strconv" 6 | "unicode" 7 | ) 8 | 9 | type PixelFormat struct { 10 | BitsPerPixel uint8 11 | Depth uint8 12 | BigEndianFlag uint8 13 | TrueColorFlag uint8 14 | RedMax uint16 15 | GreenMax uint16 16 | BlueMax uint16 17 | RedShift uint8 18 | GreenShift uint8 19 | BlueShift uint8 20 | Padding [3]uint8 21 | } 22 | 23 | type ServerInit struct { 24 | FramebufferWidth uint16 25 | FramebufferHeight uint16 26 | ServerPixelFormat PixelFormat 27 | NameLength uint32 `tcp:",Name"` 28 | Name string 29 | } 30 | 31 | type ClientMessageType uint8 32 | 33 | func (t ClientMessageType) MarshalJSON() ([]byte, error) { 34 | if s, ok := map[ClientMessageType]string{ 35 | TypeSetPixelFormat: `SetPixelFormat`, 36 | TypeSetEncodings: `SetEncodings`, 37 | TypeFramebufferUpdateRequest: `FramebufferUpdateRequest`, 38 | TypeKeyEvent: `KeyEvent`, 39 | TypePointerEvent: `PointerEvent`, 40 | TypeClientCutTex: `CutText`, 41 | }[t]; ok { 42 | return json.Marshal(s) 43 | } else { 44 | return json.Marshal(uint8(t)) 45 | } 46 | } 47 | 48 | const ( 49 | TypeSetPixelFormat ClientMessageType = 0 50 | TypeFixColourMapEntries ClientMessageType = 1 51 | TypeSetEncodings ClientMessageType = 2 52 | TypeFramebufferUpdateRequest ClientMessageType = 3 53 | TypeKeyEvent ClientMessageType = 4 54 | TypePointerEvent ClientMessageType = 5 55 | TypeClientCutTex ClientMessageType = 6 56 | ) 57 | 58 | type SetPixelFormat struct { 59 | MessageType ClientMessageType 60 | Padding [3]uint8 61 | PixelFormat PixelFormat 62 | } 63 | 64 | type FixColourMapEntries struct { 65 | MessageType ClientMessageType 66 | Padding uint8 67 | FirstColour uint16 68 | NumberOfColours uint16 `tcp:",RGBIntensities"` 69 | RGBIntensities []RGBIntensity 70 | } 71 | 72 | type RGBIntensity struct { 73 | Red uint16 74 | Green uint16 75 | Blue uint16 76 | } 77 | 78 | type Encoding int32 79 | 80 | func (e Encoding) MarshalJSON() ([]byte, error) { 81 | if s, ok := map[Encoding]string{ 82 | EncodingRaw: `Raw`, 83 | EncodingCopyRect: `CopyRect`, 84 | EncodingRRE: `RRE`, 85 | EncodingHextile: `Hextile`, 86 | EncodingZlib: `ZLIB`, 87 | EncodingTRLE: `TRLE`, 88 | EncodingZRLE: `ZRLE`, 89 | EncodingJPEG: `JPEG`, 90 | EncodingJRLE: `JRLE`, 91 | EncodingZRLE2: `ZRLE2`, 92 | PseudoEncodingDesktopSize: `Desktop Size`, 93 | PseudoEncodingCursor: `Cursor`, 94 | PseudoEncodingCursorWithAlpha: `Cursor with Alpha`, 95 | PseudoEncodingExtendedClipBoard: `Extended Clipboard`, 96 | }[e]; ok { 97 | return json.Marshal(s) 98 | } else { 99 | return json.Marshal(int32(e)) 100 | } 101 | } 102 | 103 | // https://www.iana.org/assignments/rfb/rfb.xml#table-rfb-4 104 | // https://github.com/ultravnc/UltraVNC/blob/ee9954b90ab6b52a2332b349d55f6a98af3f7424/rfb/rfbproto.h#L460-L503 105 | const ( 106 | EncodingRaw Encoding = 0 107 | EncodingCopyRect Encoding = 1 108 | EncodingRRE Encoding = 2 109 | EncodingCoRRE Encoding = 4 110 | EncodingHextile Encoding = 5 111 | EncodingZlib Encoding = 6 112 | EncodingTight Encoding = 7 113 | EncodingZlibHex Encoding = 8 114 | EncodingUltraVNC Encoding = 9 115 | EncodingUltraVNC2 Encoding = 10 116 | EncodingTRLE Encoding = 15 117 | EncodingZRLE Encoding = 16 118 | EncodingXZ Encoding = 18 119 | EncodingXZYW Encoding = 19 120 | EncodingJPEG Encoding = 21 121 | EncodingJRLE Encoding = 22 122 | EncodingZRLE2 Encoding = 24 123 | EncodingZSTD Encoding = 25 124 | EncodingTightZSTD Encoding = 26 125 | EncodingZSTDHex Encoding = 27 126 | EncodingZSTDRLE Encoding = 28 127 | EncodingZSTDYWRLE Encoding = 29 128 | EncodingOpenH264 Encoding = 50 129 | EncodingTightPNG Encoding = -260 130 | PseudoEncodingJPEGQuality10 Encoding = -23 131 | PseudoEncodingJPEGQuality9 Encoding = -24 132 | PseudoEncodingJPEGQuality8 Encoding = -25 133 | PseudoEncodingJPEGQuality7 Encoding = -26 134 | PseudoEncodingJPEGQuality6 Encoding = -27 135 | PseudoEncodingJPEGQuality5 Encoding = -28 136 | PseudoEncodingJPEGQuality4 Encoding = -29 137 | PseudoEncodingJPEGQuality3 Encoding = -20 138 | PseudoEncodingJPEGQuality2 Encoding = -31 139 | PseudoEncodingJPEGQuality1 Encoding = -32 140 | PseudoEncodingDesktopSize Encoding = -223 141 | PseudoEncodingLastRect Encoding = -224 142 | PseudoEncodingTightPointerPosition Encoding = -232 143 | PseudoEncodingCursor Encoding = -239 144 | PseudoEncodingXCursor Encoding = -240 145 | PseudoEncodingCompressionLevel10 Encoding = -247 146 | PseudoEncodingCompressionLevel9 Encoding = -248 147 | PseudoEncodingCompressionLevel8 Encoding = -249 148 | PseudoEncodingCompressionLevel7 Encoding = -250 149 | PseudoEncodingCompressionLevel6 Encoding = -251 150 | PseudoEncodingCompressionLevel5 Encoding = -252 151 | PseudoEncodingCompressionLevel4 Encoding = -253 152 | PseudoEncodingCompressionLevel3 Encoding = -254 153 | PseudoEncodingCompressionLevel2 Encoding = -255 154 | PseudoEncodingCompressionLevel1 Encoding = -256 155 | PseudoEncodingExtendedDesktopSize Encoding = -308 156 | PseudoEncodingCursorWithAlpha Encoding = -314 157 | PseudoEncodingUltraVNCEnableIdleTime Encoding = -32764 158 | PseudoEncodingUltraVNCPseudoSession Encoding = -32765 159 | PseudoEncodingUltraVNCFTProtocolVersion Encoding = -32766 160 | PseudoEncodingUltraVNCEnableKeepAlive Encoding = -32767 161 | PseudoEncodingUltraVNCServerState Encoding = -32768 162 | PseudoEncodingUltraVNCEncodingQueueEnable Encoding = -65525 163 | PseudoEncodingExtendedClipBoard Encoding = -1063131698 164 | ) 165 | 166 | type SetEncodings struct { 167 | MessageType ClientMessageType 168 | Padding uint8 169 | NumberOfEncodings uint16 `tcp:",Encodings"` 170 | Encodings []Encoding 171 | } 172 | 173 | type FramebufferUpdateRequest struct { 174 | MessageType ClientMessageType 175 | Incremental uint8 176 | X uint16 177 | Y uint16 178 | Width uint16 179 | Height uint16 180 | } 181 | 182 | type PointerEvent struct { 183 | MessageType ClientMessageType 184 | ButtonMask uint8 185 | X uint16 186 | Y uint16 187 | } 188 | 189 | type KeyEvent struct { 190 | MessageType ClientMessageType 191 | DownFlag uint8 192 | Padding [2]uint8 193 | Key KeySym 194 | } 195 | 196 | type KeySym uint32 197 | 198 | func (k KeySym) String() string { 199 | if r := rune(k); unicode.IsLetter(r) || unicode.IsNumber(r) || unicode.IsSpace(r) { 200 | return string(rune(k)) 201 | } else { 202 | return `\u` + strconv.FormatInt(int64(k), 16) 203 | } 204 | } 205 | 206 | type CutText struct { 207 | MessageType ClientMessageType 208 | Padding [3]uint8 209 | Length uint32 `tcp:",Text"` 210 | Text []uint8 211 | } 212 | 213 | type ExtendedCutTextHeader struct { 214 | MessageType ClientMessageType 215 | Padding [3]uint8 216 | Length int32 217 | } 218 | 219 | type ExtendedCutText struct { 220 | ExtendedCutTextHeader 221 | Flags uint32 222 | Text []uint8 223 | } 224 | 225 | type ServerMessageType uint8 226 | 227 | func (t ServerMessageType) MarshalJSON() ([]byte, error) { 228 | if s, ok := map[ServerMessageType]string{ 229 | TypeFramebufferUpdate: `FramebufferUpdate`, 230 | TypeSetColourMapEntries: `SetColourMapEntries`, 231 | TypeBell: `Bell`, 232 | TypeServerCutText: `ServerCutText`, 233 | }[t]; ok { 234 | return json.Marshal(s) 235 | } else { 236 | return json.Marshal(uint8(t)) 237 | } 238 | } 239 | 240 | const ( 241 | TypeFramebufferUpdate ServerMessageType = 0 242 | TypeSetColourMapEntries ServerMessageType = 1 243 | TypeBell ServerMessageType = 2 244 | TypeServerCutText ServerMessageType = 3 245 | ) 246 | 247 | type FramebufferUpdate struct { 248 | MessageType ServerMessageType 249 | Padding uint8 250 | NumberOfRectangles uint16 251 | } 252 | 253 | type Rectangle struct { 254 | X uint16 255 | Y uint16 256 | Width uint16 257 | Height uint16 258 | Encoding Encoding 259 | } 260 | 261 | type ZlibRectangle struct { 262 | Rectangle 263 | Length uint32 `tcp:",Data"` 264 | Data []byte 265 | } 266 | 267 | type CursorRectangle struct { 268 | Rectangle 269 | Pixels []byte 270 | Bitmask []uint8 271 | } 272 | 273 | type XCursorRectangle struct { 274 | Rectangle 275 | PrimaryRed uint8 276 | PrimaryGreen uint8 277 | PrimaryBlue uint8 278 | SecondaryRed uint8 279 | SecondaryGreen uint8 280 | SecondaryBlue uint8 281 | Bitmap []uint8 282 | Bitmask []uint8 283 | } 284 | -------------------------------------------------------------------------------- /application/rfb/stream.go: -------------------------------------------------------------------------------- 1 | package rfb 2 | 3 | import ( 4 | "bytes" 5 | "compress/zlib" 6 | "context" 7 | "encoding/binary" 8 | "errors" 9 | "fmt" 10 | "image" 11 | "image/color" 12 | "io" 13 | "math" 14 | "time" 15 | 16 | "github.com/0xThiebaut/PCAPeek/output/files" 17 | "github.com/0xThiebaut/PCAPeek/output/media" 18 | "github.com/0xThiebaut/PCAPeek/transport/tcp" 19 | "github.com/google/gopacket" 20 | ) 21 | 22 | type stream struct { 23 | Negotiate bool 24 | net gopacket.Flow 25 | transport gopacket.Flow 26 | client tcp.Stream 27 | server tcp.Stream 28 | encodings []Encoding 29 | rectangles uint16 30 | date time.Time 31 | format PixelFormat 32 | frame *image.RGBA 33 | zw io.Writer 34 | zr io.Reader 35 | id uint64 36 | Media media.Factory 37 | mstream media.Stream 38 | Files files.Factory 39 | } 40 | 41 | func (s *stream) hasExtendedClipboard() bool { 42 | for _, encoding := range s.encodings { 43 | if encoding == PseudoEncodingExtendedClipBoard { 44 | return true 45 | } 46 | } 47 | return false 48 | } 49 | 50 | func (s *stream) hasZlib() bool { 51 | for _, encoding := range s.encodings { 52 | if encoding == EncodingZlib { 53 | return true 54 | } 55 | } 56 | return false 57 | } 58 | 59 | func (s *stream) ClientInit() { 60 | // Expect the next server message to be a ServerInit 61 | s.server.Notifier(s.ServerInit) 62 | // Discard client data until then 63 | s.client.Notifier(s.client.Clear) 64 | } 65 | 66 | func (s *stream) ServerInit() { 67 | init := ServerInit{} 68 | t, err := tcp.Unmarshall(s.server, &init) 69 | if err != nil { 70 | tcp.Discard(s.server) 71 | return 72 | } 73 | s.format = init.ServerPixelFormat 74 | s.frame = image.NewRGBA(image.Rect(0, 0, int(init.FramebufferWidth), int(init.FramebufferHeight))) 75 | s.mstream = s.Media.New() 76 | go func() { 77 | <-context.Background().Done() 78 | _ = s.mstream.Close() 79 | }() 80 | fmt.Printf("Got ServerInit at %s from %s:%s to %s:%s named %q of ratio %dx%d\n", t.UTC().Format(time.RFC3339Nano), s.net.Dst().String(), s.transport.Dst().String(), s.net.Src().String(), s.transport.Src().String(), init.Name, init.FramebufferWidth, init.FramebufferHeight) 81 | // Expect the next messages to be client-to-server or server-to-client messages 82 | s.server.Notifier(s.Server) 83 | s.client.Notifier(s.Client) 84 | } 85 | 86 | var ( 87 | ErrUnhandledMessage = errors.New(`message not handled`) 88 | ErrUnhandledClientMessage = fmt.Errorf(`client %w`, ErrUnhandledMessage) 89 | ErrUnhandledServerMessage = fmt.Errorf(`server %w`, ErrUnhandledMessage) 90 | ErrUnhandledServerEncoding = fmt.Errorf(`%w due to unknown encoding`, ErrUnhandledServerMessage) 91 | ErrNewStreamNotifier = fmt.Errorf("%w: new stream notifier", io.EOF) 92 | ) 93 | 94 | func (s *stream) Client() { 95 | for { 96 | if err := s.onClient(); err != nil { 97 | if !errors.Is(err, io.EOF) { 98 | tcp.Discard(s.client) 99 | fmt.Println(err) 100 | } 101 | return 102 | } 103 | } 104 | } 105 | 106 | func (s *stream) Server() { 107 | for { 108 | if err := s.onServer(); err != nil { 109 | if !errors.Is(err, io.EOF) { 110 | tcp.Discard(s.server) 111 | fmt.Println(err) 112 | } 113 | return 114 | } 115 | } 116 | } 117 | 118 | func (s *stream) serverRectangle() { 119 | for ; s.rectangles > 0; s.rectangles-- { 120 | if err := s.onServerFramebufferUpdateRectangle(); err != nil { 121 | if !errors.Is(err, io.EOF) { 122 | tcp.Discard(s.server) 123 | fmt.Println(err) 124 | } 125 | return 126 | } 127 | } 128 | if s.rectangles == 0 { 129 | if err := s.mstream.Write(s.frame, s.date); err != nil { 130 | fmt.Println(err) 131 | } 132 | s.server.Notifier(s.Server) 133 | } 134 | } 135 | 136 | func (s *stream) onClient() error { 137 | var t ClientMessageType 138 | if _, err := tcp.UnmarshallPeek(s.client, &t); err != nil { 139 | return err 140 | } 141 | switch t { 142 | case TypeSetPixelFormat: 143 | if err := s.onClientSetPixelFormat(); err != nil { 144 | return err 145 | } 146 | case TypeFixColourMapEntries: 147 | // In some observed cases the ClientInit is observed after a ServerInit.s 148 | if s.client.Length() == 1 { 149 | s.client.Clear() 150 | } else { 151 | return fmt.Errorf(`%w (message type %d)`, ErrUnhandledClientMessage, t) 152 | } 153 | case TypeSetEncodings: 154 | if err := s.onClientSetEncodings(); err != nil { 155 | return err 156 | } 157 | case TypeFramebufferUpdateRequest: 158 | if err := s.onClientFramebufferUpdateRequest(); err != nil { 159 | return err 160 | } 161 | case TypePointerEvent: 162 | if err := s.onClientPointerEvent(); err != nil { 163 | return err 164 | } 165 | case TypeKeyEvent: 166 | if err := s.onClientKeyEvent(); err != nil { 167 | return err 168 | } 169 | case TypeClientCutTex: 170 | if err := s.onClientCutText(); err != nil { 171 | return err 172 | } 173 | default: 174 | return fmt.Errorf(`%w (message type %d)`, ErrUnhandledClientMessage, t) 175 | } 176 | return nil 177 | } 178 | 179 | func (s *stream) onClientSetPixelFormat() error { 180 | var message SetPixelFormat 181 | _, err := tcp.Unmarshall(s.client, &message) 182 | if err != nil { 183 | return err 184 | } 185 | if s.Negotiate { 186 | // Some servers cannot negotiate (typically backdoors) in which case we retain server preferences 187 | s.format = message.PixelFormat 188 | } 189 | return nil 190 | } 191 | 192 | func (s *stream) onClientFixColourMapEntries() error { 193 | var message FixColourMapEntries 194 | _, err := tcp.Unmarshall(s.client, &message) 195 | if err != nil { 196 | return err 197 | } 198 | return nil 199 | } 200 | 201 | func (s *stream) onClientSetEncodings() error { 202 | var message SetEncodings 203 | _, err := tcp.Unmarshall(s.client, &message) 204 | if err != nil { 205 | return err 206 | } 207 | s.encodings = message.Encodings 208 | return nil 209 | } 210 | 211 | func (s *stream) onClientFramebufferUpdateRequest() error { 212 | var message FramebufferUpdateRequest 213 | _, err := tcp.Unmarshall(s.client, &message) 214 | return err 215 | } 216 | 217 | func (s *stream) onClientPointerEvent() error { 218 | var message PointerEvent 219 | _, err := tcp.Unmarshall(s.client, &message) 220 | if err != nil { 221 | return err 222 | } 223 | return nil 224 | } 225 | 226 | func (s *stream) onClientKeyEvent() error { 227 | var message KeyEvent 228 | _, err := tcp.Unmarshall(s.client, &message) 229 | if err != nil { 230 | return err 231 | } 232 | return nil 233 | } 234 | 235 | func (s *stream) onClientCutText() error { 236 | if s.hasExtendedClipboard() { 237 | var header ExtendedCutTextHeader 238 | _, err := tcp.UnmarshallPeek(s.client, &header) 239 | if err != nil { 240 | return err 241 | } 242 | if header.Length < 0 { 243 | var message ExtendedCutText 244 | message.Text = make([]uint8, -header.Length-4) 245 | t, err := tcp.Unmarshall(s.client, &message) 246 | if err == nil { 247 | f := s.Files.New() 248 | defer f.Close() 249 | return f.Write(bytes.NewReader(message.Text), t) 250 | } 251 | return err 252 | } 253 | } 254 | var message CutText 255 | t, err := tcp.Unmarshall(s.client, &message) 256 | if err != nil { 257 | return err 258 | } 259 | f := s.Files.New() 260 | defer f.Close() 261 | return f.Write(bytes.NewReader(message.Text), t) 262 | } 263 | 264 | func (s *stream) onServer() error { 265 | var t ServerMessageType 266 | if _, err := tcp.UnmarshallPeek(s.server, &t); err != nil { 267 | return err 268 | } 269 | switch t { 270 | case TypeFramebufferUpdate: 271 | if err := s.onServerFramebufferUpdate(); err != nil { 272 | return err 273 | } 274 | case TypeServerCutText: 275 | if err := s.onServerCutText(); err != nil { 276 | return err 277 | } 278 | default: 279 | return fmt.Errorf(`%w (message type %d)`, ErrUnhandledServerMessage, t) 280 | } 281 | return nil 282 | } 283 | 284 | func (s *stream) onServerFramebufferUpdate() error { 285 | var message FramebufferUpdate 286 | t, err := tcp.Unmarshall(s.server, &message) 287 | if err != nil { 288 | return err 289 | } 290 | s.date = t 291 | s.rectangles = message.NumberOfRectangles 292 | s.server.Notifier(s.serverRectangle) 293 | return ErrNewStreamNotifier 294 | } 295 | 296 | func (s *stream) onServerCutText() error { 297 | if s.hasExtendedClipboard() { 298 | var header ExtendedCutTextHeader 299 | _, err := tcp.UnmarshallPeek(s.server, &header) 300 | if err != nil { 301 | return err 302 | } 303 | if header.Length < 0 { 304 | var message ExtendedCutText 305 | message.Text = make([]uint8, -header.Length-4) 306 | t, err := tcp.Unmarshall(s.server, &message) 307 | if err == nil { 308 | f := s.Files.New() 309 | defer f.Close() 310 | return f.Write(bytes.NewReader(message.Text), t) 311 | } 312 | return err 313 | } 314 | } 315 | var message CutText 316 | t, err := tcp.Unmarshall(s.server, &message) 317 | if err != nil { 318 | return err 319 | } 320 | f := s.Files.New() 321 | defer f.Close() 322 | return f.Write(bytes.NewReader(message.Text), t) 323 | } 324 | 325 | func (s *stream) onServerFramebufferUpdateRectangle() error { 326 | var header Rectangle 327 | _, err := tcp.UnmarshallPeek(s.server, &header) 328 | if err != nil { 329 | return err 330 | } 331 | switch header.Encoding { 332 | case EncodingZlib: 333 | // Parse the ZLIB rectangle 334 | var rectangle ZlibRectangle 335 | _, err = tcp.Unmarshall(s.server, &rectangle) 336 | if err != nil { 337 | return err 338 | } 339 | // Initiate or populate the ZLIB stream 340 | if s.zw == nil && s.hasZlib() { 341 | b := &bytes.Buffer{} 342 | s.zw = b 343 | if _, err = s.zw.Write(rectangle.Data); err != nil { 344 | return err 345 | } 346 | if s.zr, err = zlib.NewReader(b); err != nil { 347 | return err 348 | } 349 | } else if _, err = s.zw.Write(rectangle.Data); err != nil { 350 | return err 351 | } 352 | 353 | // Define the byte order 354 | var byteorder binary.ByteOrder 355 | if s.format.BigEndianFlag == 0 { 356 | byteorder = binary.LittleEndian 357 | } else { 358 | byteorder = binary.BigEndian 359 | } 360 | 361 | for i := uint16(0); i < rectangle.Height; i++ { 362 | for j := uint16(0); j < rectangle.Width; j++ { 363 | // Read the pixel data 364 | data := make([]byte, s.format.BitsPerPixel/8) 365 | if n, err := s.zr.Read(data); err != nil { 366 | return err 367 | } else if n != len(data) { 368 | return io.EOF 369 | } 370 | 371 | // Format the data 372 | switch s.format.BitsPerPixel { 373 | case 32: 374 | pixel := byteorder.Uint32(data) 375 | s.frame.Set(int(rectangle.X+j), int(rectangle.Y+i), color.RGBA{ 376 | R: uint8((pixel >> s.format.RedShift) & uint32(s.format.RedMax)), 377 | G: uint8((pixel >> s.format.GreenShift) & uint32(s.format.GreenMax)), 378 | B: uint8((pixel >> s.format.BlueShift) & uint32(s.format.BlueMax)), 379 | A: math.MaxUint8, 380 | }) 381 | default: 382 | return errors.New(`unhandled bits per pixel value`) 383 | } 384 | } 385 | } 386 | case PseudoEncodingCursor: 387 | var rectangle CursorRectangle 388 | rectangle.Pixels = make([]byte, uint64(header.Width)*uint64(header.Height)*(uint64(s.format.BitsPerPixel)/8)) 389 | rectangle.Bitmask = make([]uint8, ((uint64(header.Width)+7)/8)*uint64(header.Height)) 390 | _, err = tcp.Unmarshall(s.server, &rectangle) 391 | case PseudoEncodingXCursor: 392 | var rectangle XCursorRectangle 393 | rectangle.Bitmap = make([]uint8, ((uint64(header.Width)+7)/8)*uint64(header.Height)) 394 | rectangle.Bitmask = make([]uint8, ((uint64(header.Width)+7)/8)*uint64(header.Height)) 395 | _, err = tcp.Unmarshall(s.server, &rectangle) 396 | default: 397 | return fmt.Errorf("%w of type %d", ErrUnhandledServerEncoding, header.Encoding) 398 | } 399 | return err 400 | } 401 | -------------------------------------------------------------------------------- /application/stream.go: -------------------------------------------------------------------------------- 1 | package application 2 | 3 | import ( 4 | "github.com/0xThiebaut/PCAPeek/transport/tcp" 5 | "github.com/google/gopacket" 6 | "github.com/google/gopacket/tcpassembly" 7 | ) 8 | 9 | func NewRouterStreams(net gopacket.Flow, transport gopacket.Flow, strict bool, applications ...Factory) (client tcp.Stream, server tcp.Stream) { 10 | // Create a new streams 11 | client = tcp.NewStream() 12 | server = tcp.NewStream() 13 | // Route clients and servers to the first application accepting any of their traffic 14 | client.Notifier(func() { 15 | // Route to the first application accepting the client data 16 | for _, application := range applications { 17 | if application.Client(net, transport, client) { 18 | application.Handle(net, transport, client, server) 19 | // Trigger the client notification 20 | client.Reassembled([]tcpassembly.Reassembly{}) 21 | return 22 | } 23 | } 24 | // If nothing matches either discard the stream (strict mode) or just clear the buffer (loose) and hope 25 | // a later packet might match. 26 | if strict { 27 | tcp.Discard(client) 28 | } else { 29 | client.Clear() 30 | } 31 | }) 32 | // Set server routing logic 33 | server.Notifier(func() { 34 | // Route to the first application accepting the server data 35 | for _, application := range applications { 36 | if application.Server(net.Reverse(), transport.Reverse(), server) { 37 | application.Handle(net, transport, client, server) 38 | // Trigger the server notification 39 | server.Reassembled([]tcpassembly.Reassembly{}) 40 | return 41 | } 42 | } 43 | 44 | // If nothing matches either discard the stream (strict mode) or just clear the buffer (loose) and hope 45 | // a later packet might match. 46 | if strict { 47 | tcp.Discard(server) 48 | } else { 49 | server.Clear() 50 | } 51 | }) 52 | return client, server 53 | } 54 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/0xThiebaut/PCAPeek 2 | 3 | go 1.18 4 | 5 | require github.com/google/gopacket v1.1.19 6 | 7 | require ( 8 | github.com/at-wat/ebml-go v0.16.0 // indirect 9 | github.com/icza/mjpeg v0.0.0-20220812133530-f79265a232f2 // indirect 10 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 11 | github.com/spf13/cobra v1.6.1 // indirect 12 | github.com/spf13/pflag v1.0.5 // indirect 13 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/at-wat/ebml-go v0.16.0 h1:3NPy83uMzVRHWdWlcJYSXWn/+u2GmtPQSL1LZJbjcCw= 2 | github.com/at-wat/ebml-go v0.16.0/go.mod h1:w1cJs7zmGsb5nnSvhWGKLCxvfu4FVx5ERvYDIalj1ww= 3 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 4 | github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= 5 | github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= 6 | github.com/icza/mjpeg v0.0.0-20220812133530-f79265a232f2 h1:BU/CjV8GO8pOeRJXO3sSlOBsjZEV22hE33rG0bn3UMU= 7 | github.com/icza/mjpeg v0.0.0-20220812133530-f79265a232f2/go.mod h1:Eja3x31oRrEOzl6ihhsxY23gXaTYWLP3Gwj5nMAJ7m0= 8 | github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 9 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 10 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 11 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 12 | github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= 13 | github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= 14 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 15 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 16 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 17 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 18 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 19 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 20 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 21 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= 22 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 23 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 24 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 25 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 26 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= 27 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 28 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 29 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 30 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 31 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 32 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 33 | -------------------------------------------------------------------------------- /output/files/binary/factory.go: -------------------------------------------------------------------------------- 1 | package binary 2 | 3 | import ( 4 | "github.com/0xThiebaut/PCAPeek/output/files" 5 | ) 6 | 7 | func New(directory string) files.Factory { 8 | return &factory{ 9 | Directory: directory, 10 | } 11 | } 12 | 13 | type factory struct { 14 | Directory string 15 | id int 16 | } 17 | 18 | func (f *factory) New() files.Stream { 19 | s := &stream{ 20 | Directory: f.Directory, 21 | ID: f.id, 22 | } 23 | f.id++ 24 | return s 25 | } 26 | -------------------------------------------------------------------------------- /output/files/binary/stream.go: -------------------------------------------------------------------------------- 1 | package binary 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "path" 8 | "time" 9 | ) 10 | 11 | type stream struct { 12 | Directory string 13 | ID int 14 | sequence int 15 | } 16 | 17 | func (s *stream) Write(reader io.Reader, t time.Time) error { 18 | name := fmt.Sprintf("%s.%02d.bin", t.UTC().Format("2006-01-02T15-04-05,000"), s.ID) 19 | f, err := os.OpenFile(path.Join(s.Directory, name), os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o666) 20 | if err != nil { 21 | return err 22 | } 23 | defer f.Close() 24 | _, err = io.Copy(f, reader) 25 | return err 26 | } 27 | 28 | func (s *stream) Close() error { 29 | return nil 30 | } 31 | -------------------------------------------------------------------------------- /output/files/factory.go: -------------------------------------------------------------------------------- 1 | package files 2 | 3 | import ( 4 | "io" 5 | "time" 6 | ) 7 | 8 | type Stream interface { 9 | io.Closer 10 | Write(reader io.Reader, t time.Time) error 11 | } 12 | 13 | type Factory interface { 14 | New() Stream 15 | } 16 | -------------------------------------------------------------------------------- /output/files/fork/factory.go: -------------------------------------------------------------------------------- 1 | package fork 2 | 3 | import ( 4 | "github.com/0xThiebaut/PCAPeek/output/files" 5 | ) 6 | 7 | func New(factories ...files.Factory) files.Factory { 8 | return &factory{ 9 | Factories: factories, 10 | } 11 | } 12 | 13 | type factory struct { 14 | Factories []files.Factory 15 | } 16 | 17 | func (f *factory) New() files.Stream { 18 | o := &stream{} 19 | for _, fork := range f.Factories { 20 | o.Streams = append(o.Streams, fork.New()) 21 | } 22 | return o 23 | } 24 | -------------------------------------------------------------------------------- /output/files/fork/stream.go: -------------------------------------------------------------------------------- 1 | package fork 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "time" 7 | 8 | "github.com/0xThiebaut/PCAPeek/output/files" 9 | ) 10 | 11 | type stream struct { 12 | Streams []files.Stream 13 | } 14 | 15 | func (s *stream) Write(reader io.Reader, t time.Time) error { 16 | d, err := io.ReadAll(reader) 17 | if err != nil { 18 | return err 19 | } 20 | for _, forked := range s.Streams { 21 | if err = forked.Write(bytes.NewReader(d), t); err != nil { 22 | return err 23 | } 24 | } 25 | return nil 26 | } 27 | 28 | func (s *stream) Close() (err error) { 29 | for _, forked := range s.Streams { 30 | if ferr := forked.Close(); ferr != nil && err == nil { 31 | err = ferr 32 | } 33 | } 34 | return err 35 | } 36 | -------------------------------------------------------------------------------- /output/media/factory.go: -------------------------------------------------------------------------------- 1 | package media 2 | 3 | import ( 4 | "image" 5 | "io" 6 | "time" 7 | ) 8 | 9 | type Stream interface { 10 | io.Closer 11 | Write(image image.Image, t time.Time) error 12 | } 13 | 14 | type Factory interface { 15 | New() Stream 16 | } 17 | -------------------------------------------------------------------------------- /output/media/fork/factory.go: -------------------------------------------------------------------------------- 1 | package fork 2 | 3 | import ( 4 | "github.com/0xThiebaut/PCAPeek/output/media" 5 | ) 6 | 7 | func New(factories ...media.Factory) media.Factory { 8 | return &factory{ 9 | Factories: factories, 10 | } 11 | } 12 | 13 | type factory struct { 14 | Factories []media.Factory 15 | } 16 | 17 | func (f *factory) New() media.Stream { 18 | o := &stream{} 19 | for _, fork := range f.Factories { 20 | o.Streams = append(o.Streams, fork.New()) 21 | } 22 | return o 23 | } 24 | -------------------------------------------------------------------------------- /output/media/fork/stream.go: -------------------------------------------------------------------------------- 1 | package fork 2 | 3 | import ( 4 | "image" 5 | "time" 6 | 7 | "github.com/0xThiebaut/PCAPeek/output/media" 8 | ) 9 | 10 | type stream struct { 11 | Streams []media.Stream 12 | } 13 | 14 | func (s *stream) Write(image image.Image, t time.Time) error { 15 | for _, forked := range s.Streams { 16 | if err := forked.Write(image, t); err != nil { 17 | return err 18 | } 19 | } 20 | return nil 21 | } 22 | 23 | func (s *stream) Close() (err error) { 24 | for _, forked := range s.Streams { 25 | if ferr := forked.Close(); ferr != nil && err == nil { 26 | err = ferr 27 | } 28 | } 29 | return err 30 | } 31 | -------------------------------------------------------------------------------- /output/media/jpeg/factory.go: -------------------------------------------------------------------------------- 1 | package jpeg 2 | 3 | import ( 4 | "github.com/0xThiebaut/PCAPeek/output/media" 5 | ) 6 | 7 | func New(directory string, fps int, quality int) media.Factory { 8 | return &factory{ 9 | Directory: directory, 10 | FPS: fps, 11 | Quality: quality, 12 | } 13 | } 14 | 15 | type factory struct { 16 | Directory string 17 | id int 18 | FPS int 19 | Quality int 20 | } 21 | 22 | func (f *factory) New() media.Stream { 23 | s := &stream{ 24 | Directory: f.Directory, 25 | ID: f.id, 26 | FPS: f.FPS, 27 | Quality: f.Quality, 28 | } 29 | f.id++ 30 | return s 31 | } 32 | -------------------------------------------------------------------------------- /output/media/jpeg/stream.go: -------------------------------------------------------------------------------- 1 | package jpeg 2 | 3 | import ( 4 | "fmt" 5 | "image" 6 | "image/jpeg" 7 | "os" 8 | "path" 9 | "time" 10 | ) 11 | 12 | type stream struct { 13 | Directory string 14 | ID int 15 | Quality int 16 | FPS int 17 | sequence int 18 | next time.Time 19 | frame image.Image 20 | } 21 | 22 | func (s *stream) Write(image image.Image, t time.Time) error { 23 | if s.next.IsZero() { 24 | name := fmt.Sprintf("%s.%02d", t.UTC().Format("2006-01-02T15-04-05,000"), s.ID) 25 | s.Directory = path.Join(s.Directory, name) 26 | if err := os.MkdirAll(s.Directory, 0o666); err != nil { 27 | return err 28 | } 29 | } 30 | if s.FPS != 0 { 31 | if s.next.IsZero() { 32 | s.next = t.Add(time.Second / time.Duration(s.FPS)) 33 | } 34 | for s.next.Before(t) { 35 | if err := s.write(); err != nil { 36 | return err 37 | } 38 | s.next = s.next.Add(time.Second / time.Duration(s.FPS)) 39 | } 40 | } 41 | s.frame = image 42 | if s.FPS == 0 { 43 | s.next = t 44 | return s.write() 45 | } 46 | return nil 47 | } 48 | 49 | func (s *stream) write() error { 50 | name := fmt.Sprintf("%06d.%s.jpeg", s.sequence, s.next.UTC().Format("2006-01-02T15-04-05,000")) 51 | f, err := os.OpenFile(path.Join(s.Directory, name), os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0o666) 52 | if err != nil { 53 | return err 54 | } 55 | defer f.Close() 56 | s.sequence++ 57 | return jpeg.Encode(f, s.frame, &jpeg.Options{Quality: s.Quality}) 58 | } 59 | 60 | func (s *stream) Close() error { 61 | if s.FPS != 0 { 62 | return s.write() 63 | } 64 | return nil 65 | } 66 | -------------------------------------------------------------------------------- /output/media/mjpeg/factory.go: -------------------------------------------------------------------------------- 1 | package mjpeg 2 | 3 | import ( 4 | "github.com/0xThiebaut/PCAPeek/output/media" 5 | ) 6 | 7 | func New(directory string, fps int, quality int) media.Factory { 8 | return &factory{ 9 | Directory: directory, 10 | FPS: fps, 11 | Quality: quality, 12 | } 13 | } 14 | 15 | type factory struct { 16 | Directory string 17 | id int 18 | FPS int 19 | Quality int 20 | } 21 | 22 | func (f *factory) New() media.Stream { 23 | s := &stream{ 24 | ID: f.id, 25 | FPS: f.FPS, 26 | Quality: f.Quality, 27 | Directory: f.Directory, 28 | } 29 | f.id++ 30 | return s 31 | } 32 | -------------------------------------------------------------------------------- /output/media/mjpeg/stream.go: -------------------------------------------------------------------------------- 1 | package mjpeg 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "image" 7 | "image/jpeg" 8 | "io" 9 | "path" 10 | "time" 11 | 12 | "github.com/icza/mjpeg" 13 | ) 14 | 15 | type stream struct { 16 | frame image.Image 17 | next time.Time 18 | writer mjpeg.AviWriter 19 | Directory string 20 | Quality int 21 | FPS int 22 | ID int 23 | } 24 | 25 | func (s *stream) Write(image image.Image, t time.Time) (err error) { 26 | // Create a writer if needed 27 | if s.writer == nil { 28 | name := fmt.Sprintf("%s.%02d.avi", t.UTC().Format("2006-01-02T15-04-05,000"), s.ID) 29 | if s.writer, err = mjpeg.New(path.Join(s.Directory, name), int32(image.Bounds().Size().X), int32(image.Bounds().Size().Y), int32(s.FPS)); err != nil { 30 | return err 31 | } 32 | } 33 | // Handle first frames 34 | if s.next.IsZero() { 35 | s.next = t.Add(time.Second / time.Duration(s.FPS)) 36 | } 37 | if s.next.Before(t) { 38 | // Encode previous frames only once 39 | b := &bytes.Buffer{} 40 | if err := jpeg.Encode(b, s.frame, &jpeg.Options{Quality: s.Quality}); err != nil { 41 | return err 42 | } 43 | d, err := io.ReadAll(b) 44 | if err != nil { 45 | return err 46 | } 47 | // And keep adding frames until we meet the FPS 48 | for s.next.Before(t) { 49 | if err = s.writer.AddFrame(d); err != nil { 50 | return err 51 | } 52 | s.next = s.next.Add(time.Second / time.Duration(s.FPS)) 53 | } 54 | } 55 | s.frame = image 56 | return nil 57 | } 58 | 59 | func (s *stream) Close() error { 60 | // Make sure to always append the last frame 61 | b := &bytes.Buffer{} 62 | if err := jpeg.Encode(b, s.frame, &jpeg.Options{Quality: s.Quality}); err == nil { 63 | d, err := io.ReadAll(b) 64 | if err == nil { 65 | _ = s.writer.AddFrame(d) 66 | } 67 | } 68 | return s.writer.Close() 69 | } 70 | -------------------------------------------------------------------------------- /pcapeek.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/0xThiebaut/PCAPeek/application" 9 | "github.com/0xThiebaut/PCAPeek/application/reverse" 10 | "github.com/0xThiebaut/PCAPeek/application/rfb" 11 | "github.com/0xThiebaut/PCAPeek/output/files" 12 | "github.com/0xThiebaut/PCAPeek/output/files/binary" 13 | "github.com/0xThiebaut/PCAPeek/output/files/fork" 14 | "github.com/0xThiebaut/PCAPeek/output/media" 15 | mfork "github.com/0xThiebaut/PCAPeek/output/media/fork" 16 | "github.com/0xThiebaut/PCAPeek/output/media/jpeg" 17 | "github.com/0xThiebaut/PCAPeek/output/media/mjpeg" 18 | "github.com/google/gopacket" 19 | "github.com/google/gopacket/layers" 20 | "github.com/google/gopacket/pcap" 21 | "github.com/google/gopacket/tcpassembly" 22 | "github.com/spf13/cobra" 23 | ) 24 | 25 | var ( 26 | // PCAP settings 27 | bpf string 28 | // JPEG output 29 | useJpeg = false 30 | jpegDir = `./` 31 | jpegQuality = 100 32 | jpegFps = 0 33 | // MJPEG 34 | useMjpeg = false 35 | mjpegDir = `./` 36 | mjpegQuality = 100 37 | mjpegFps = 10 38 | // Files 39 | useFiles = false 40 | filesDir = `./` 41 | ) 42 | 43 | var rootCmd = &cobra.Command{ 44 | Use: "PCAPeek PCAP [PCAP ...]", 45 | Short: "PCAPeek peeks into PCAPs", 46 | Long: `PCAPeek is a tool to peek into PCAPs. It doesn't do much besides acting as a proof of concept to reconstruct reverse VNC traffic.`, 47 | Args: cobra.MinimumNArgs(1), 48 | RunE: func(cmd *cobra.Command, args []string) error { 49 | // Trim any BPF spaces 50 | bpf = strings.TrimSpace(bpf) 51 | 52 | // Prepare the media 53 | var mo []media.Factory 54 | if useJpeg { 55 | mo = append(mo, jpeg.New(jpegDir, jpegFps, jpegQuality)) 56 | } 57 | if useMjpeg { 58 | mo = append(mo, mjpeg.New(mjpegDir, mjpegFps, mjpegQuality)) 59 | } 60 | // Prepare the files 61 | var fo []files.Factory 62 | if useFiles { 63 | fo = append(fo, binary.New(filesDir)) 64 | } 65 | // TODO: Abstract VNC extraction 66 | rrfb := reverse.New(rfb.New(mfork.New(mo...), fork.New(fo...))) 67 | pool := tcpassembly.NewStreamPool(application.NewApplicationStreamFactory(false, rrfb)) 68 | assembler := tcpassembly.NewAssembler(pool) 69 | 70 | // Loop the PCAPs 71 | for _, file := range args { 72 | // Open the file 73 | handle, err := pcap.OpenOffline(file) 74 | if err != nil { 75 | return err 76 | } 77 | // Apply any BPF filters 78 | if len(bpf) > 0 { 79 | if err = handle.SetBPFFilter(bpf); err != nil { 80 | return err 81 | } 82 | } 83 | // Create the PCAP source 84 | decoder := gopacket.DecodersByLayerName[handle.LinkType().String()] 85 | source := gopacket.NewPacketSource(handle, decoder) 86 | source.Lazy = true 87 | source.NoCopy = true 88 | // Pipe the PCAPs 89 | for packet := range source.Packets() { 90 | if packet.NetworkLayer() == nil || packet.TransportLayer() == nil || packet.TransportLayer().LayerType() != layers.LayerTypeTCP { 91 | continue 92 | } 93 | tcp, ok := packet.TransportLayer().(*layers.TCP) 94 | if !ok { 95 | continue 96 | } 97 | // Set factory time here for each packet allowing on confirmed read to get the current timestamp or on first read to set the timestamp 98 | assembler.AssembleWithTimestamp(packet.NetworkLayer().NetworkFlow(), tcp, packet.Metadata().Timestamp) 99 | } 100 | } 101 | 102 | return nil 103 | }, 104 | } 105 | 106 | func main() { 107 | rootCmd.PersistentFlags().StringVar(&bpf, `filter`, bpf, `A BPF filter to apply on the PCAPs`) 108 | rootCmd.PersistentFlags().BoolVar(&useJpeg, `jpeg`, useJpeg, `Output JPEG frames`) 109 | rootCmd.PersistentFlags().StringVar(&jpegDir, `jpeg-dir`, jpegDir, `The output directory for the JPEG frames`) 110 | rootCmd.PersistentFlags().IntVar(&jpegQuality, `jpeg-quality`, jpegQuality, `The JPEG frame quality percentage`) 111 | rootCmd.PersistentFlags().IntVar(&jpegFps, `jpeg-fps`, jpegFps, `The number of JPEG frames to output per second (default 0, outputs all frames)`) 112 | rootCmd.PersistentFlags().BoolVar(&useMjpeg, `mjpeg`, useMjpeg, `Output MJPEG videos`) 113 | rootCmd.PersistentFlags().StringVar(&mjpegDir, `mjpeg-dir`, mjpegDir, `The output directory for the MJPEG videos`) 114 | rootCmd.PersistentFlags().IntVar(&mjpegQuality, `mjpeg-quality`, mjpegQuality, `The MJPEG video quality percentage`) 115 | rootCmd.PersistentFlags().IntVar(&mjpegFps, `mjpeg-fps`, mjpegFps, `The number of MJPEG frames to output per second`) 116 | rootCmd.PersistentFlags().BoolVar(&useFiles, `files`, useFiles, `Output clipboard files`) 117 | rootCmd.PersistentFlags().StringVar(&filesDir, `files-dir`, filesDir, `The output directory for the clipboard files`) 118 | 119 | if err := rootCmd.Execute(); err != nil { 120 | _, _ = fmt.Fprintln(os.Stderr, err) 121 | os.Exit(1) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /transport/tcp/marshaller.go: -------------------------------------------------------------------------------- 1 | package tcp 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "io" 7 | "reflect" 8 | "strconv" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | func Unmarshall(s Stream, v any) (time.Time, error) { 14 | if n, t, err := unmarshall(s, reflect.ValueOf(v)); err != nil { 15 | return t, err 16 | } else { 17 | return t, s.Consume(n) 18 | } 19 | } 20 | 21 | func unmarshall(s PeekStream, v reflect.Value) (int, time.Time, error) { 22 | pack := make([]byte, s.Length()) 23 | n, t, _, err := s.Peek(pack) 24 | if err != nil && err != io.EOF { 25 | return n, t, err 26 | } else if n != len(pack) { 27 | pack = pack[:n] 28 | } 29 | n, err = unpack(pack, v) 30 | return n, t, err 31 | } 32 | 33 | func unpack(pack []byte, v reflect.Value) (n int, err error) { 34 | switch k := v.Kind(); k { 35 | case reflect.Pointer, reflect.Interface: 36 | return unpack(pack, v.Elem()) 37 | case reflect.Uint8: 38 | if len(pack) < n+1 { 39 | return n, io.EOF 40 | } 41 | v.SetUint(uint64(pack[n])) 42 | n += 1 43 | case reflect.Uint16: 44 | if len(pack) < n+2 { 45 | return n, io.EOF 46 | } 47 | v.SetUint(uint64(binary.BigEndian.Uint16(pack[n : n+2]))) 48 | n += 2 49 | case reflect.Uint32: 50 | if len(pack) < n+4 { 51 | return n, io.EOF 52 | } 53 | v.SetUint(uint64(binary.BigEndian.Uint32(pack[n : n+4]))) 54 | n += 4 55 | case reflect.Int32: 56 | if len(pack) < n+4 { 57 | return n, io.EOF 58 | } 59 | v.SetInt(int64(int32(binary.BigEndian.Uint32(pack[n : n+4])))) 60 | n += 4 61 | case reflect.Array: 62 | for j := 0; j < v.Type().Len(); j++ { 63 | if len(pack) <= n { 64 | return n, io.EOF 65 | } 66 | var m int 67 | m, err = unpack(pack[n:], v.Index(j)) 68 | n += m 69 | if err != nil { 70 | break 71 | } 72 | } 73 | case reflect.Struct: 74 | return unpackStructure(pack, v) 75 | default: 76 | err = fmt.Errorf(`cannot unmarshal kind %s`, k.String()) 77 | } 78 | return n, err 79 | } 80 | 81 | func unpackStructure(pack []byte, v reflect.Value) (n int, err error) { 82 | sizes := make(map[string]reflect.Value) 83 | for i, t := 0, v.Type(); i < t.NumField(); i++ { 84 | f := v.Field(i) 85 | // Handle any tags 86 | if tag, ok := t.Field(i).Tag.Lookup(`tcp`); ok { 87 | parts := strings.Split(tag, `,`) 88 | // Handle second positions (either a length value or reference) 89 | if len(parts) >= 2 { 90 | ref := strings.TrimSpace(parts[1]) 91 | // Require the reference to be set (avoids ",,..." from evaluating as 0) 92 | if len(ref) > 0 { 93 | if f.CanUint() { 94 | // If the field is an uint, consider it a size reference 95 | sizes[ref] = f 96 | } else if size, err := strconv.ParseUint(ref, 0, 0); err == nil { 97 | // Otherwise consider it a size value 98 | sizes[t.Field(i).Name] = reflect.ValueOf(size) 99 | } 100 | } 101 | } 102 | } 103 | // Assign the value 104 | switch f.Kind() { 105 | case reflect.String: 106 | size := len(pack) - n 107 | if ref, ok := sizes[t.Field(i).Name]; ok && ref.CanUint() { 108 | size = int(ref.Uint()) 109 | } else if ok && ref.CanInt() { 110 | size = int(ref.Int()) 111 | } 112 | if len(pack) < n+size { 113 | return n, io.EOF 114 | } 115 | f.SetString(string(pack[n : n+size])) 116 | n += size 117 | case reflect.Slice: 118 | size := f.Len() 119 | ref, ok := sizes[t.Field(i).Name] 120 | if ok && ref.CanUint() { 121 | size = int(ref.Uint()) 122 | } else if ok && ref.CanInt() { 123 | size = int(ref.Int()) 124 | } 125 | 126 | j := 0 127 | for ; len(pack) > n && j < size; j++ { 128 | e := reflect.New(f.Type().Elem()).Elem() 129 | m, err := unpack(pack[n:], e) 130 | n += m 131 | if err != nil { 132 | return n, err 133 | } 134 | f.Set(reflect.Append(f, e)) 135 | } 136 | 137 | if ok && j < size { 138 | return n, io.EOF 139 | } 140 | 141 | case reflect.Struct: 142 | m, err := unpackStructure(pack[n:], f) 143 | n += m 144 | if err != nil { 145 | return n, err 146 | } 147 | default: 148 | var m int 149 | m, err = unpack(pack[n:], f) 150 | n += m 151 | } 152 | 153 | if err != nil { 154 | break 155 | } 156 | } 157 | return n, err 158 | } 159 | 160 | func UnmarshallPeek(s PeekStream, v any) (time.Time, error) { 161 | _, t, err := unmarshall(s, reflect.ValueOf(v)) 162 | return t, err 163 | } 164 | -------------------------------------------------------------------------------- /transport/tcp/stream.go: -------------------------------------------------------------------------------- 1 | package tcp 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "time" 7 | 8 | "github.com/google/gopacket/tcpassembly" 9 | ) 10 | 11 | func NewStream() Stream { 12 | return &stream{} 13 | } 14 | 15 | type PeekStream interface { 16 | Peek(p []byte) (n int, f time.Time, t time.Time, err error) 17 | Length() int 18 | } 19 | 20 | type Stream interface { 21 | tcpassembly.Stream 22 | PeekStream 23 | Notifier(notifier func()) 24 | Read(p []byte) (n int, f time.Time, t time.Time, err error) 25 | Consume(n int) (err error) 26 | Clear() 27 | } 28 | 29 | type stream struct { 30 | notifier func() 31 | chunks []tcpassembly.Reassembly 32 | } 33 | 34 | func (s *stream) Notifier(notifier func()) { 35 | s.notifier = notifier 36 | } 37 | 38 | func (s *stream) Length() (n int) { 39 | for _, chunk := range s.chunks { 40 | n += len(chunk.Bytes) 41 | } 42 | return n 43 | } 44 | 45 | func (s *stream) Clear() { 46 | s.chunks = s.chunks[:0] 47 | } 48 | 49 | func (s *stream) Reassembled(chunks []tcpassembly.Reassembly) { 50 | s.chunks = append(s.chunks, chunks...) 51 | if s.notifier != nil { 52 | s.notifier() 53 | } 54 | } 55 | 56 | func (s *stream) ReassemblyComplete() { 57 | s.notifier() 58 | } 59 | 60 | func (s *stream) Read(p []byte) (n int, f time.Time, t time.Time, err error) { 61 | n, f, t, err = s.Peek(p) 62 | if eof := s.Consume(n); eof != err { 63 | err = errors.New(`consumption discrepancy`) 64 | } 65 | return n, f, t, err 66 | } 67 | 68 | func (s *stream) Consume(n int) (err error) { 69 | for n > 0 && len(s.chunks) > 0 { 70 | l := len(s.chunks[0].Bytes) 71 | if n < l { 72 | s.chunks[0].Bytes = s.chunks[0].Bytes[n:] 73 | n -= n 74 | } else { 75 | s.chunks = s.chunks[1:] 76 | n -= l 77 | } 78 | } 79 | if n > 0 { 80 | err = io.EOF 81 | } 82 | return err 83 | } 84 | 85 | func (s *stream) Peek(p []byte) (n int, start time.Time, end time.Time, err error) { 86 | for i := 0; i < len(s.chunks) && n < len(p); i++ { 87 | // Copy data 88 | m := copy(p[n:], s.chunks[i].Bytes) 89 | // Set the start time for initial reads 90 | if n == 0 { 91 | start = s.chunks[i].Seen 92 | } 93 | // Set the end time 94 | end = s.chunks[i].Seen 95 | // Increment the read bytes 96 | n += m 97 | } 98 | // Generate an io.EOF if there are no more chunks 99 | if n < len(p) { 100 | err = io.EOF 101 | } 102 | return n, start, end, err 103 | } 104 | 105 | func Discard(stream Stream) { 106 | stream.Notifier(stream.Clear) 107 | stream.Clear() 108 | } 109 | --------------------------------------------------------------------------------