windows_bridge/file_transfer/
bridge.rs1use std::{
2 collections::HashMap,
3 fs::{File, OpenOptions},
4 io::{Error, ErrorKind, Write as _},
5 path::PathBuf,
6};
7
8use vmi::{
9 Va, VmiContext,
10 arch::amd64::Amd64,
11 driver::VmiRead,
12 os::windows::WindowsOs,
13 trace::Hex,
14 utils::{
15 bridge::{BridgeHandler, BridgePacket, BridgeResponse},
16 shellcode::{Status, impl_bridge_contract, impl_stage},
17 },
18};
19
20use crate::bridge::BridgeOutput;
21
22const CHUNK_SIZE: u64 = 64 * 1024;
24
25const TRANSFER_HANDLE_BITS: u32 = 12;
27
28const TRANSFER_HANDLE_MAX: u32 = (1 << TRANSFER_HANDLE_BITS) - 1;
30
31#[derive(Clone, Copy, PartialEq, Eq)]
33pub struct FileTransferStage(u8);
34
35impl_stage!(FileTransferStage);
36
37impl FileTransferStage {
38 pub const NONE: Self = Self(0x00);
40
41 pub const FILE_NAME: Self = Self(0x01);
43
44 pub const FILE_SIZE: Self = Self(0x02);
46
47 pub const MAPPING: Self = Self(0x03);
49
50 pub const BUFFER: Self = Self(0x04);
52
53 pub const TRANSFER: Self = Self(0x05);
55}
56
57impl std::fmt::Debug for FileTransferStage {
58 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
59 let name = match *self {
60 Self::NONE => "None",
61 Self::FILE_NAME => "FileName",
62 Self::FILE_SIZE => "FileSize",
63 Self::MAPPING => "Mapping",
64 Self::BUFFER => "Buffer",
65 Self::TRANSFER => "Transfer",
66 _ => return self.0.fmt(f),
67 };
68 f.write_str(name)
69 }
70}
71
72pub type FileTransferStatus = Status<FileTransferStage>;
74
75struct HostFile {
77 file: File,
79
80 path: PathBuf,
82
83 expected_size: u64,
85
86 received: u64,
88
89 buffer: Option<Va>,
91}
92
93impl HostFile {
94 fn create(path: PathBuf, expected_size: u64) -> Result<Self, Error> {
96 if let Some(parent) = path.parent() {
97 std::fs::create_dir_all(parent)?;
98 }
99
100 let file = OpenOptions::new()
101 .create(true)
102 .truncate(true)
103 .write(true)
104 .open(&path)?;
105
106 Ok(Self {
107 file,
108 path,
109 expected_size,
110 received: 0,
111 buffer: None,
112 })
113 }
114
115 fn set_buffer(&mut self, buffer: Va) {
117 self.buffer = Some(buffer);
118 }
119
120 fn append(&mut self, bytes: &[u8]) -> Result<(), Error> {
122 let length = bytes.len() as u64;
123 if length > CHUNK_SIZE || self.received + length > self.expected_size {
124 return Err(Error::new(
125 ErrorKind::InvalidData,
126 "file-transfer chunk exceeds declared size",
127 ));
128 }
129
130 self.file.write_all(bytes)?;
131 self.received += length;
132
133 Ok(())
134 }
135
136 fn flush(&mut self) -> Result<(), Error> {
138 if self.received != self.expected_size {
139 return Err(Error::new(
140 ErrorKind::UnexpectedEof,
141 "file-transfer byte count does not match declared size",
142 ));
143 }
144
145 self.file.flush()
146 }
147}
148
149struct TransferSession {
151 path: String,
153
154 host_file: HostFile,
156
157 chunk_buffer: Vec<u8>,
159}
160
161impl TransferSession {
162 fn new(path: String, host_file: HostFile) -> Self {
164 Self {
165 path,
166 host_file,
167 chunk_buffer: vec![0; CHUNK_SIZE as usize],
168 }
169 }
170}
171
172pub struct FileTransferBridge {
174 output_directory: PathBuf,
176
177 next_transfer_handle: u32,
179
180 next_output_id: u64,
182
183 transfers: HashMap<u32, TransferSession>,
185}
186
187impl_bridge_contract!(FileTransferBridge);
188
189impl FileTransferBridge {
190 const METHOD_BEGIN: u16 = 0x0001;
192
193 const METHOD_SET_BUFFER: u16 = 0x0002;
195
196 const METHOD_CHUNK: u16 = 0x0003;
198
199 const METHOD_CLOSE: u16 = 0x0004;
201
202 const METHOD_EXIT: u16 = 0xffff;
204
205 const RESPONSE_CONTINUE: u64 = 0x0000_0000;
207
208 const RESPONSE_ABORT: u64 = 0xffff_ffff;
210
211 pub fn new(output_directory: PathBuf) -> Self {
213 Self {
214 output_directory,
215 next_transfer_handle: 1,
216 next_output_id: 0,
217 transfers: HashMap::new(),
218 }
219 }
220
221 fn allocate_transfer_handle(&mut self) -> Option<u32> {
223 if self.next_transfer_handle > TRANSFER_HANDLE_MAX {
224 return None;
225 }
226
227 let handle = self.next_transfer_handle;
228 self.next_transfer_handle += 1;
229 Some(handle)
230 }
231
232 fn handle_packet<Driver>(
234 &mut self,
235 vmi: &VmiContext<'_, WindowsOs<Driver>>,
236 packet: BridgePacket,
237 ) -> Option<BridgeResponse<BridgeOutput>>
238 where
239 Driver: VmiRead<Architecture = Amd64>,
240 {
241 let method = packet.method();
242
243 match method {
244 Self::METHOD_BEGIN => Some(self.handle_begin(vmi, packet)),
245 Self::METHOD_SET_BUFFER => Some(self.handle_set_buffer(vmi, packet)),
246 Self::METHOD_CHUNK => Some(self.handle_chunk(vmi, packet)),
247 Self::METHOD_CLOSE => Some(self.handle_close(vmi, packet)),
248 Self::METHOD_EXIT => Some(self.handle_exit(vmi, packet)),
249 _ => self.handle_unknown(vmi, packet),
250 }
251 }
252
253 fn handle_begin<Driver>(
259 &mut self,
260 vmi: &VmiContext<'_, WindowsOs<Driver>>,
261 packet: BridgePacket,
262 ) -> BridgeResponse<BridgeOutput>
263 where
264 Driver: VmiRead<Architecture = Amd64>,
265 {
266 let file_handle = packet.value1();
267 let file_size = packet.value2();
268 let file_name_buffer = packet.value3();
269 let file_name_length = packet.value4() as usize;
270
271 let path = match vmi.read_string_utf16_limited(Va(file_name_buffer), file_name_length) {
272 Ok(path) => path,
273 Err(err) => {
274 tracing::error!(%err, "cannot read filename");
275 return BridgeResponse::new(0);
276 }
277 };
278
279 if file_size > i64::MAX as u64 {
281 tracing::error!(path, size = file_size, "rejected size");
282 return BridgeResponse::new(0);
283 }
284
285 let transfer_handle = match self.allocate_transfer_handle() {
286 Some(transfer_handle) => transfer_handle,
287 None => {
288 tracing::error!(path, "handles exhausted");
289 return BridgeResponse::new(0);
290 }
291 };
292
293 let output_id = self.next_output_id;
294 self.next_output_id += 1;
295
296 let output_path = self
298 .output_directory
299 .join(output_filename(output_id, &path));
300
301 let host_file = match HostFile::create(output_path.clone(), file_size) {
302 Ok(host_file) => host_file,
303 Err(err) => {
304 tracing::error!(%err, path = ?output_path.display(), "cannot create file");
305 return BridgeResponse::new(0);
306 }
307 };
308
309 self.transfers.insert(
310 transfer_handle,
311 TransferSession::new(path.clone(), host_file),
312 );
313
314 tracing::info!(
315 transfer_handle,
316 file_handle = %Hex(file_handle),
317 path,
318 size = file_size,
319 output = ?output_path.display(),
320 "started"
321 );
322
323 BridgeResponse::new((CHUNK_SIZE << TRANSFER_HANDLE_BITS) | u64::from(transfer_handle))
324 }
325
326 fn handle_set_buffer<Driver>(
330 &mut self,
331 _vmi: &VmiContext<'_, WindowsOs<Driver>>,
332 packet: BridgePacket,
333 ) -> BridgeResponse<BridgeOutput>
334 where
335 Driver: VmiRead<Architecture = Amd64>,
336 {
337 let transfer_handle = packet.value1() as u32;
338 let buffer = packet.value2();
339
340 let transfer = match self.transfers.get_mut(&transfer_handle) {
341 Some(transfer) => transfer,
342 None => return BridgeResponse::new(Self::RESPONSE_ABORT),
343 };
344
345 transfer.host_file.set_buffer(Va(buffer));
346
347 BridgeResponse::new(Self::RESPONSE_CONTINUE)
348 }
349
350 fn handle_chunk<Driver>(
354 &mut self,
355 vmi: &VmiContext<'_, WindowsOs<Driver>>,
356 packet: BridgePacket,
357 ) -> BridgeResponse<BridgeOutput>
358 where
359 Driver: VmiRead<Architecture = Amd64>,
360 {
361 let transfer_handle = packet.value1() as u32;
362 let length = packet.value2() as usize;
363
364 let transfer = match self.transfers.get_mut(&transfer_handle) {
365 Some(transfer) => transfer,
366 None => return BridgeResponse::new(Self::RESPONSE_ABORT),
367 };
368
369 let buffer = match transfer.host_file.buffer {
370 Some(buffer) => buffer,
371 None => return BridgeResponse::new(Self::RESPONSE_ABORT),
372 };
373
374 if length > CHUNK_SIZE as usize {
375 return BridgeResponse::new(Self::RESPONSE_ABORT);
376 }
377
378 let bytes = &mut transfer.chunk_buffer[..length];
379 if let Err(err) = vmi.read(buffer, bytes) {
380 tracing::error!(%err, "cannot read chunk");
381 return BridgeResponse::new(Self::RESPONSE_ABORT);
382 }
383
384 if let Err(err) = transfer.host_file.append(bytes) {
385 tracing::error!(%err, "cannot write chunk");
386 return BridgeResponse::new(Self::RESPONSE_ABORT);
387 }
388
389 BridgeResponse::new(Self::RESPONSE_CONTINUE)
390 }
391
392 fn handle_close<Driver>(
396 &mut self,
397 _vmi: &VmiContext<'_, WindowsOs<Driver>>,
398 packet: BridgePacket,
399 ) -> BridgeResponse<BridgeOutput>
400 where
401 Driver: VmiRead<Architecture = Amd64>,
402 {
403 let transfer_handle = packet.value1() as u32;
404 let transfer_status = packet.value2();
405
406 const TRANSFER_SUCCESS: u64 = 0;
407
408 let mut transfer = match self.transfers.remove(&transfer_handle) {
409 Some(transfer) => transfer,
410 None => return BridgeResponse::new(Self::RESPONSE_ABORT),
411 };
412
413 if transfer_status == TRANSFER_SUCCESS {
414 if let Err(err) = transfer.host_file.flush() {
415 tracing::error!(%err, path = transfer.path, "cannot flush file");
416 return BridgeResponse::new(Self::RESPONSE_ABORT);
417 }
418
419 tracing::info!(
420 path = ?transfer.host_file.path.display(),
421 size = transfer.host_file.received,
422 "closed"
423 );
424 }
425
426 BridgeResponse::new(Self::RESPONSE_CONTINUE)
427 }
428
429 fn handle_exit<Driver>(
433 &mut self,
434 _vmi: &VmiContext<'_, WindowsOs<Driver>>,
435 packet: BridgePacket,
436 ) -> BridgeResponse<BridgeOutput>
437 where
438 Driver: VmiRead<Architecture = Amd64>,
439 {
440 let encoded_status = packet.value1();
441
442 let status = FileTransferStatus::decode(encoded_status);
443
444 tracing::debug!(
445 stage = ?status.stage(),
446 kind = ?status.kind(),
447 code = status.code(),
448 native_code = status.native_code(),
449 "shellcode completed"
450 );
451
452 BridgeResponse::default()
453 }
454
455 fn handle_unknown<Driver>(
459 &self,
460 _vmi: &VmiContext<'_, WindowsOs<Driver>>,
461 packet: BridgePacket,
462 ) -> Option<BridgeResponse<BridgeOutput>>
463 where
464 Driver: VmiRead<Architecture = Amd64>,
465 {
466 tracing::warn!(
467 request = %Hex(packet.request()),
468 method = %Hex(packet.method()),
469 value1 = %Hex(packet.value1()),
470 value2 = %Hex(packet.value2()),
471 value3 = %Hex(packet.value3()),
472 value4 = %Hex(packet.value4()),
473 "unknown bridge method"
474 );
475
476 None
477 }
478}
479
480impl<Driver> BridgeHandler<WindowsOs<Driver>> for FileTransferBridge
481where
482 Driver: VmiRead<Architecture = Amd64>,
483{
484 type Output = BridgeOutput;
485
486 const REQUEST: u16 = 0x0012;
488
489 #[tracing::instrument(name = "file_transfer", skip_all)]
490 fn handle(
491 &mut self,
492 vmi: &VmiContext<'_, WindowsOs<Driver>>,
493 packet: BridgePacket,
494 ) -> Option<BridgeResponse<BridgeOutput>> {
495 debug_assert_eq!(
496 packet.request(),
497 <Self as BridgeHandler<WindowsOs<Driver>>>::REQUEST
498 );
499
500 self.handle_packet(vmi, packet)
501 }
502}
503
504fn output_filename(output_id: u64, path: &str) -> String {
506 let basename = path
507 .rsplit(['\\', '/'])
508 .find(|component| !component.is_empty())
509 .unwrap_or("file");
510
511 let basename = basename
512 .chars()
513 .map(|character| {
514 if character.is_ascii_alphanumeric() {
515 character
516 }
517 else {
518 '_'
519 }
520 })
521 .collect::<String>();
522
523 format!("{output_id:04}-{basename}")
524}