forked from realmadsci/rrod-proxy
-
Notifications
You must be signed in to change notification settings - Fork 0
/
rrod_proxy.py
291 lines (231 loc) · 10.4 KB
/
rrod_proxy.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
# Required packages:
# `python -m pip install trio colorama termcolor`
import argparse
import os
from colorama import init
from termcolor import cprint
import trio
# Set up terminal coloring for Windows
init()
class RrodMessageSplitter:
def __init__(self):
self.data = b""
def _compute_length(self, data):
i = 0
shift = 0
length = 0
while len(data) > i:
d = data[i]
length |= (d & 0x7F) << shift
shift += 7
i += 1
if not (d & 0x80):
return (i, length)
return (0, 0)
def process(self, data):
messages = []
# Append the new data
self.data = self.data + data
(hdr_len, length) = self._compute_length(self.data)
while hdr_len and len(self.data) >= hdr_len + length:
# If we have enough for a full message, strip off
# the "length" header and return the message:
messages.append(self.data[hdr_len : hdr_len + length])
self.data = self.data[hdr_len + length :]
(hdr_len, length) = self._compute_length(self.data)
return messages
def add_header(data):
"""
Add header "length field" to outgoing RRoD messages.
"""
# Get the length
l = len(data)
# Split it into groups of 7 bits per group, in little endian order:
h = bytearray()
# Grab 7 bits and shift down:
h.append(l & 0x7F)
l >>= 7
while l:
h.append(l & 0x7F)
l >>= 7
# Set the MSb of all except the last byte:
for i in range(len(h) - 1):
h[i] |= 0x80
return h + data
async def handle_keypress(key, client, server):
"""
Handle keypresses
NOTE: This is only called when the proxy is up and connected, so the client and server sockets should
always be valid capable of sending data inside this function.
NOTE: The Esc key b"\x1b" and Ctrl-C b"\x03" are already taken to mean "exit" so you'll never see them in here!
"""
# TODO: INSERT SKETCHY BUSINESS HERE TO ADD SOME TRAFFIC?
cprint(f"Got unused key: {key}", "yellow")
client_splitter = RrodMessageSplitter()
async def handle_client_to_server_data(data, client, server):
"""
Handle data that has arrived from the client and is heading to the server.
You can either do the simple "send it along" thing like so:
`await server.send(data)`
or you can do fancier things, even including spoofing data back to the client if you wish.
"""
msgs = client_splitter.process(data)
for m in msgs:
cprint("Client ---> Server\n" + m.hex(" "), "green")
# TODO: INSERT SKETCHY BUSINESS HERE TO ANALYZE AND/OR MODIFY TRAFFIC!
# Send the message to the server
await server.send(add_header(m))
server_splitter = RrodMessageSplitter()
async def handle_server_to_client_data(data, client, server):
"""
Handle data that has arrived from the server and is heading to the client.
You can either do the simple "send it along" thing like so:
`await client.send(data)`
or you can do fancier things, even including spoofing data back to the server if you wish.
"""
msgs = server_splitter.process(data)
for m in msgs:
cprint("Client <--- Server\n" + m.hex(" "), "blue")
# TODO: INSERT SKETCHY BUSINESS HERE TO ANALYZE AND/OR MODIFY TRAFFIC!
# Send the message to the client
await client.send(add_header(m))
class TcpProxy:
def __init__(self, client_port, server_ip, server_port):
self.client_port = client_port
self.server_ip = server_ip
self.server_port = server_port
self.client_conn = None
self.server_conn = None
async def run(self):
# Create a TCP socket to listen locally
with trio.socket.socket(
family=trio.socket.AF_INET, # IPv4
type=trio.socket.SOCK_STREAM, # TCP
) as client_sock:
# NOTE: This binds to ONLY the localhost interface, so nothing outside of this local host can connect to it!
await client_sock.bind(("127.0.0.1", self.client_port))
client_sock.listen()
# This loop will reconnect things whenever the client socket drops, so we don't have to keep restarting this proxy for every connection:
while True:
(self.client_conn, addr) = await client_sock.accept()
# NOTE: We *could* spawn off multiple child tasks here and open multiple parallel connections with accept() but we _don't_ for
# simplicity and because the goal of this proxy is to study/manipulate the connection (rather than actually be a proxy):
with self.client_conn:
print(
f"Connected by {addr}, connecting to {self.server_ip}:{self.server_port}"
)
with trio.socket.socket(
family=trio.socket.AF_INET, # IPv4
type=trio.socket.SOCK_STREAM, # TCP
) as self.server_conn:
await self.server_conn.connect(
(self.server_ip, self.server_port)
)
async def server_recv_loop(cancel_scope):
# Loop forever, sending received traffic to the client
try:
while True:
data = await self.server_conn.recv(4096)
if not data:
cprint(f"Server disconnected.", "red")
break
await handle_server_to_client_data(
data, self.client_conn, self.server_conn
)
except Exception as e:
cprint(f"Client <--- Server exception: {e}", "red")
# Close down all tasks
cancel_scope.cancel()
async def client_recv_loop(cancel_scope):
# Loop forever, sending received traffic to the server
try:
while True:
data = await self.client_conn.recv(4096)
if not data:
cprint(f"Client disconnected.", "red")
break
await handle_client_to_server_data(
data, self.client_conn, self.server_conn
)
except Exception as e:
cprint(f"Client ---> Server exception: {e}", "red")
# Close down all tasks
cancel_scope.cancel()
async with trio.open_nursery() as nursery:
nursery.start_soon(server_recv_loop, nursery.cancel_scope)
nursery.start_soon(client_recv_loop, nursery.cancel_scope)
if os.name == "nt":
# Windows keypress handling
import msvcrt
async def keyboard():
"""Return an interator of keypresses from getch"""
while True:
ch = await trio.to_thread.run_sync(msvcrt.getch, cancellable=True)
# Handle special chars by sending a byte string prefixed with 0xE0
if ch[0] == 0 or ch[0] == 0xE0:
ch2 = await trio.to_thread.run_sync(msvcrt.getch, cancellable=True)
# Prefix 0xE0 to the front.
ch = b"\xE0" + ch2
yield ch
else:
# Linux keypress handling
import termios
import sys
import tty
async def keyboard():
#"""Return an iterator of characters from stdin."""
# In Linux, we can use non-blocking keyboard input as a file-type stream rather
# than needing janky background threads!
keystream = trio.lowlevel.FdStream(os.dup(sys.stdin.fileno()))
# Set up the terminal to return keys immediately
stashed_term = termios.tcgetattr(sys.stdin)
try:
tty.setcbreak(sys.stdin, termios.TCSANOW)
while True:
try:
ch = await keystream.receive_some(1)
# Detect and group the escape sequences
# NOTE: Linux and Windows will have different sequences for special keys!
if (ch == b"\x1b"):
# The escape codes don't seem to have a standard "end delimiter"
# so just keep accepting chars until a gap occurs. :shrug:
while True:
with trio.move_on_after(0.01) as cancel_scope:
ch += await keystream.receive_some(1)
if (cancel_scope.cancelled_caught):
break
yield ch
except KeyboardInterrupt:
yield b"\x03"
finally:
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, stashed_term)
async def main():
parser = argparse.ArgumentParser()
parser.add_argument(
"client_port", help="Local machine port for client to connect to"
)
parser.add_argument(
"remote_ip", help="Remote (server) IP address for proxy to connect to"
)
parser.add_argument(
"remote_port", help="Remote (server) port for proxy to connect to"
)
args = parser.parse_args()
tcp_proxy = TcpProxy(args.client_port, args.remote_ip, args.remote_port)
async with trio.open_nursery() as root_nursery:
# Run the TCP proxy:
root_nursery.start_soon(tcp_proxy.run)
# Handle keypresses
async for key in keyboard():
if key == b"\x1b" or key == b"\x03":
cprint(f"Received escape key. Exiting...", "red")
root_nursery.cancel_scope.cancel()
# Snooping into `_sock._closed` private variable is sketch, but :shrug:
elif (
tcp_proxy.client_conn is not None
and not tcp_proxy.client_conn._sock._closed
and tcp_proxy.server_conn is not None
and not tcp_proxy.server_conn._sock._closed
):
await handle_keypress(key, tcp_proxy.client_conn, tcp_proxy.server_conn)
trio.run(main)