Skip to main content

windows_bridge/file_transfer/
bridge.rs

1use 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
22/// Chunk size shared with the guest.
23const CHUNK_SIZE: u64 = 64 * 1024;
24
25/// Number of low bits reserved for the transfer handle in a begin response.
26const TRANSFER_HANDLE_BITS: u32 = 12;
27
28/// Largest transfer handle that fits in a begin response.
29const TRANSFER_HANDLE_MAX: u32 = (1 << TRANSFER_HANDLE_BITS) - 1;
30
31/// Stage reported by the `file-transfer` shellcode.
32#[derive(Clone, Copy, PartialEq, Eq)]
33pub struct FileTransferStage(u8);
34
35impl_stage!(FileTransferStage);
36
37impl FileTransferStage {
38    /// No transfer operation was started.
39    pub const NONE: Self = Self(0x00);
40
41    /// File name query stage.
42    pub const FILE_NAME: Self = Self(0x01);
43
44    /// File size query stage.
45    pub const FILE_SIZE: Self = Self(0x02);
46
47    /// File mapping stage.
48    pub const MAPPING: Self = Self(0x03);
49
50    /// Transfer buffer setup stage.
51    pub const BUFFER: Self = Self(0x04);
52
53    /// File transfer stage.
54    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
72/// Status reported by the `file-transfer` shellcode.
73pub type FileTransferStatus = Status<FileTransferStage>;
74
75/// State associated with a host-side file for an active transfer.
76struct HostFile {
77    /// Host-side file corresponding to the active transfer.
78    file: File,
79
80    /// Host-side path to the file.
81    path: PathBuf,
82
83    /// Expected size of the file being transferred.
84    expected_size: u64,
85
86    /// Number of bytes received so far.
87    received: u64,
88
89    /// Guest virtual address of the shared transfer buffer.
90    buffer: Option<Va>,
91}
92
93impl HostFile {
94    /// Creates the host output file, truncating any existing content.
95    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    /// Sets the guest buffer address shared for this transfer.
116    fn set_buffer(&mut self, buffer: Va) {
117        self.buffer = Some(buffer);
118    }
119
120    /// Appends a chunk to the host output file.
121    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    /// Flushes the host output.
137    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
149/// State associated with a transfer handle.
150struct TransferSession {
151    /// Guest-side path of the file being transferred.
152    path: String,
153
154    /// Host-side state of the file being transferred.
155    host_file: HostFile,
156
157    /// Reusable buffer for transferring chunks.
158    chunk_buffer: Vec<u8>,
159}
160
161impl TransferSession {
162    /// Creates a transfer session with a buffer large enough for one chunk.
163    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
172/// Host-side bridge handler for the `file-transfer` shellcode.
173pub struct FileTransferBridge {
174    /// Host-side output directory for transferred files.
175    output_directory: PathBuf,
176
177    /// Next candidate transfer handle.
178    next_transfer_handle: u32,
179
180    /// Next numeric prefix for output file names.
181    next_output_id: u64,
182
183    /// Active transfer sessions indexed by their transfer handle.
184    transfers: HashMap<u32, TransferSession>,
185}
186
187impl_bridge_contract!(FileTransferBridge);
188
189impl FileTransferBridge {
190    /// Method used to begin a transfer.
191    const METHOD_BEGIN: u16 = 0x0001;
192
193    /// Method used to register the shared guest buffer.
194    const METHOD_SET_BUFFER: u16 = 0x0002;
195
196    /// Method used to transfer a guest buffer.
197    const METHOD_CHUNK: u16 = 0x0003;
198
199    /// Method used to close a transfer.
200    const METHOD_CLOSE: u16 = 0x0004;
201
202    /// Method used to report the final status.
203    const METHOD_EXIT: u16 = 0xffff;
204
205    /// Allows the shellcode to continue its current stage.
206    const RESPONSE_CONTINUE: u64 = 0x0000_0000;
207
208    /// Aborts the shellcode's current stage.
209    const RESPONSE_ABORT: u64 = 0xffff_ffff;
210
211    /// Creates a handler that writes transferred files to `output_directory`.
212    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    /// Allocates the next transfer handle.
222    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    /// Handles a `file-transfer` bridge packet.
233    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    /// Handles the [`METHOD_BEGIN`] bridge method.
254    ///
255    /// Starts a transfer and returns its newly allocated handle.
256    ///
257    /// [`METHOD_BEGIN`]: Self::METHOD_BEGIN
258    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        // Windows file sizes are LONGLONG.
280        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        // REVIEW: avoid clone()
297        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    /// Handles the [`METHOD_SET_BUFFER`] bridge method.
327    ///
328    /// [`METHOD_SET_BUFFER`]: Self::METHOD_SET_BUFFER
329    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    /// Handles the [`METHOD_CHUNK`] bridge method.
351    ///
352    /// [`METHOD_CHUNK`]: Self::METHOD_CHUNK
353    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    /// Handles the [`METHOD_CLOSE`] bridge method.
393    ///
394    /// [`METHOD_CLOSE`]: Self::METHOD_CLOSE
395    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    /// Handles the [`METHOD_EXIT`] bridge method.
430    ///
431    /// [`METHOD_EXIT`]: Self::METHOD_EXIT
432    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    /// Handles a bridge packet with an unknown method.
456    ///
457    /// Logs the packet details and returns no response.
458    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    /// Request code for the `file-transfer` shellcode.
487    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
504/// Returns a filesystem-safe name for a transferred file.
505fn 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}