Coder Social home page Coder Social logo

rust_tips_and_tricks's Introduction

rust_tips_and_tricks

This repo is just a collection of Rust tips and tricks useful to interact with the Windows API and develop offensive security tools for that specific operative system.

This is not a tutorial, the content in this repo won't teach you how to code in Rust. The only goal of this repo is to share the knowledge that I have obtained during the last years implementing offensive tools in Rust, hoping that this tips and tricks help you to solve some of the annoying issues that I have found at the time of interacting with Windows API from this language. Also, below I add a snippet of how to start a new project of these characteristics using Dinvoke_rs, hoping that this will solve any pending doubts and will allow anyone to start using the project.

I don't consider myself an expert or guru in Rust, which means that they could be better ways of doing things as I show them here. However, I've found very useful to know all these techniques and I hope that they save you all the time that I had to invest in order to make my code work.

Content

DInvoke_rs

To me, the most straighforward way to create a new tool in Rust that requires the interaction with the Windows API is to download the Dinvoke_rs project and use it as a template, adding my code on top of it. Dinvoke_rs offers three main functionalities:

  • DInvoke: It allows to dynamically find and execute unmanaged code. This is perfect since it allows us to call any function of WinAPI without leaving any trace in the final PE IAT, increasing our OPSEC.
  • Manualmap: Manually maps any PE as LoadLibrary (or the operative system) would do, both from disk and memory.
  • Overload: It manually maps a PE in a file-backed memory section of the current process.

In case that you only need the DInvoke functionality, I have created a minimalist branch on the repository that contains the minimum code required in order to use that crate. In case that you want to use the rest of the functionalities described before, just download the code from the main branch.

Once we have the DInvoke_rs project, we can start calling any WinApi function that we need. For that, it is required to follow these simple steps (the steps below show how to call ntdll!NtAllocateVirtualMemory):

  1. Define the function signature in the crate data (check the Structs and Types section to know how to easily obtain these function signatures):
pub type NtWriteVirtualMemory = unsafe extern "system" fn (HANDLE, PVOID, PVOID, usize, *mut usize) -> i32;

In many cases, you will also need to define the required structs and data types used as input/output parameters. The best practice is to define them in the same data crate.

  1. Create a small function in the crate dinvoke that dynamically obtains the base address of ntdll (or any other loaded dll), and then calls the macro dynamic_invoke!():
/// Dynamically calls NtAllocateVirtualMemory.
///
/// It will return the NTSTATUS value returned by the call.
pub fn nt_allocate_virtual_memory (handle: HANDLE, base_address: *mut PVOID, zero_bits: usize, size: *mut usize, allocation_type: u32, protection: u32) -> i32 {

    unsafe 
    {
        let ret;
        let func_ptr: data::NtAllocateVirtualMemory;
        let ntdll = get_module_base_address(&lc!("ntdll.dll"));
        dynamic_invoke!(ntdll,&lc!("NtAllocateVirtualMemory"),func_ptr,ret,handle,base_address,zero_bits,size,allocation_type,protection);

        match ret {
            Some(x) => return x,
            None => return -1,
        }
    }   
}
  1. Define in src::main.rs the required parameters and make the call:
...

let ba = usize::default();
let base_address: *mut PVOID = std::mem::transmute(&ba);
let zero_bits = 0 as usize;
let dwsize = 354 as usize; // Allocate as much memory as you need
let size: *mut usize = std::mem::transmute(&dwsize);
let handle = HANDLE {0 : -1}; // Current process
let ret = dinvoke::nt_allocate_virtual_memory(
          handle, 
          base_address, 
          zero_bits, 
          size, 
          MEM_COMMIT | MEM_RESERVE, 
          PAGE_READWRITE);

if ret == 0
{
    println!("Success!");
}
else
{
    println!("rip");
}

You just need to repeat these steps for any other WinAPI call that you want to use.

If you don't care about or do not need the advantages that DInvoke_rs offers, it may be better for you to directly import the crates windows and/or ntapi instead of loosing your time defining types, structs and function signatures. These crates will act like some sort of "PInvoke" for both Win32 (windows) and NT API (ntapi), allowing you to directly call any WinAPI function at the expense of losing a little bit of stealth and OPSEC.

Structs and Types

Definition of structs

In many situations you will need to use several structs in order to interact with WinAPI. The easiest way to use these structs is by import them directly from the official crates windows and ntapi. By doing so, you won't need to define them manually.

Although this is very convenient, I have noticed that not all the structs in those crates are well defined. The vast majority of cases where the struct definition is wrong is due to an incorrect number of fields which prevents to use the struct efficiently (you can't access directly to the fields you are interested on...), but in some cases even the size of the struct was wrong.

Either the struct is poorly defined or it is not defined at all, you can define your own structs very easily (preferably in the crate data):

#[repr(C)]
pub struct SYSTEM_HANDLE_INFORMATION {
    pub number_of_handles: u32,
    pub handles: Vec<SYSTEM_HANDLE_TABLE_ENTRY_INFO>,
}

By default, you will need to add the #[repr(C)] attribute to keep the order of the fields, otherwise Rust may change that order randomly at compilation time.

On the other hand, some structs have fields that are arrays of an undetermined size and those fields are commonly defined in Rust as an array of one single element. For example:

#[repr(C)]
pub struct SYSTEM_HANDLE_INFORMATION {
    pub NumberOfHandles: ULONG,
    pub Handles: [SYSTEM_HANDLE_TABLE_ENTRY_INFO; 1],
}

In this situation, I have noticed that is way better to manually define the struct in your code instead of directly import it as it is from the official crates, which will allow you to replace the one element array with a dynamic size vector:

#[repr(C)]
pub struct SYSTEM_HANDLE_INFORMATION {
    pub number_of_handles: u32,
    pub handles: Vec<SYSTEM_HANDLE_TABLE_ENTRY_INFO>,
}

Finally, I recommend to add the trait Default for any manually defined struct in case that you need to instantiate it somewhere else in your code. If the struct is solely composed of basic type fields, you will be able to automatically derive this trait by using the #[derive(Copy, Clone, Default)] attribute; otherwise, you will need to manually implement it:

// Example of how to automatically derive the trait Default
#[repr(C)]
#[derive(Copy, Clone, Default, PartialEq, Debug, Eq)]
pub struct ApiSetNamespace {
    pub unused: [u8;12],
    pub count: i32, 
    pub entry_offset: i32,
}

// Example of how to manually implement the trait Default
#[repr(C)]
#[derive(Clone)]
pub struct PeMetadata {
    pub pe: u32,
    pub is_32_bit: bool,
    pub image_file_header: IMAGE_FILE_HEADER,
    pub opt_header_32: IMAGE_OPTIONAL_HEADER32,
    pub opt_header_64: IMAGE_OPTIONAL_HEADER64,
    pub sections: Vec<IMAGE_SECTION_HEADER> 
}

impl Default for PeMetadata {
    fn default() -> PeMetadata {
        PeMetadata {
            pe: u32::default(),
            is_32_bit: false,
            image_file_header: IMAGE_FILE_HEADER::default(),
            opt_header_32: IMAGE_OPTIONAL_HEADER32::default(),
            opt_header_64: IMAGE_OPTIONAL_HEADER64::default(),
            sections: Vec::default(),  
        }
    }
}

Instantiation of structs

The best way of intantiating a struct is to use the method default() in case that the trait is defined:

let handle: HANDLE = HANDLE::default();

Another two ways of instantiating a struct, specially if you are going to use it only as an ouput parameter (and therefore you just need to reserve the corresponding memory) are these two:

let create_info: PS_CREATE_INFO = std::mem::zeroed(); // Good if you can't use the method default
let unused: Vec<u8> = vec![0;size_of::<HANDLE>()];
let handle: *mut HANDLE = std::mem::transmute(unused.as_ptr());

Obviously, this very last option is only good when you need to create a pointer to the struct. In any other case, it is better to use the other two alternatives.

NTSTATUS

NTSTATUS is a struct heavily used in the NT API, and in Rust you can define it as a i32. There is not much mistery on this topic, just know that you can obtain the hex value of a NTSTATUS printing it like this:

let ret: i32 = dinvoke::nt_allocate_virtual_memory(
          handle, 
          base_address, 
          zero_bits, 
          size, 
          MEM_COMMIT | MEM_RESERVE, 
          PAGE_READWRITE);

println!("NTSTATUS returned by NtAllocateVirtualMemory: {:x}", ret);

Then you can search for this hex value in the official documentation and get a little bit of info about why is your code failing (WARNING: You may end up loosing your mind after receiving the tenth "Invalid Parameter" NTSTATUS in a row).

Function signatures

If you are using DInvoke to call WinAPI, you will need to define the signature for every function that you are dynamically calling. This is something similar to what is done in C#, where in order to create a Delegate you need to define the input and output parameters of the function. Defining a WinAPI function's signature is very easy:

  • If this call is contained in what we know as Win32 (documented Windows API), then look for the signature in the crate windows.
  • If the call belongs to the undocumented part of the WinAPI, get the signature from the crate ntapi.
  • If it is not defined in any of those crates, you will need to manually create the signature. Take a look at the existing examples in DInvoke in order to success in this task.

Once you know which parameters are expected and returned, go to the data crate and just define the function as a new data type:

pub type NewWinApiFunction = unsafe extern "system" fn (HANDLE, *mut PVOID, usize, *mut usize, u32, u32) -> i32; 

Very often you will see that the type of some parameters are not directly defined:

pub unsafe extern "system" fn NtWriteVirtualMemory(
    ProcessHandle: HANDLE,
    BaseAddress: PVOID,
    Buffer: PVOID,
    BufferSize: SIZE_T,
    NumberOfBytesWritten: PSIZE_T
) -> NTSTATUS

Here for example, the parameter BufferSize is defined as a SIZE_T. If you follow the link, you will see that the SIZE_T is defined this other way:

type SIZE_T = ULONG_PTR;

Then, you need to follow another link to obtain the real basic type behind that parameter:

type ULONG_PTR = usize;

Here you have several options in order to deal with this situation:

  1. You can import the types required directly from the official crates, but you will add that dependencies to your project.
  2. You can manually define the SIZE_T data type. Very tedious if you have a huge amount of new types.
  3. Or you can do what I usually do. You can define the parameter BufferSize as a usize which is the underlaying basic type, and everything will work perfectly.

The same way, sometimes you will find that some parameters are defined as structs of one single field. For example, you could have certain WinAPI function that expects as an input parameter a struct defined this way:

#[repr(C)]
pub struct Struct {
    pub 0: i32
}

Here you have almost the same situation than before. If you want, you can import the struct from the corresponding crate, or you can define the struct manually in your code, but for me the simplest way of dealing with this is to consider that the WinAPI function expects an i32 directly, getting rid of the struct and making it easier to implement the code.

I think that the only struct like this that I keep in my projects is HANDLE (which has a single field, an isize), and I do so because it is a very commonly used struct and I feel like its presence makes the final code easier to understand for other people.

Pointers

Casting

Usually you will need to cast between different types of pointers when working with the WinAPI. The most common case is when you have a struct pointer and you have to cast it to a PVOID (which in Rust is defined as *mut c_void) before passing it to any WinAPI call.

When you are dealing with basic type pointers, you can cast between them using the keyword as:

let a: *mut i32 = get_i32_mut();
let b: *mut u64 = a as *mut u64;

However, most of the time you will be dealing with WinAPI structs and types pointers. In that case, you can use the function std::mem::transmute() or use once again the keyword as:

// Example 1
let a: *mut ComplexStruct = get_complexstruct_pointer();
let b: *mut HANDLE = std::mem::transmute(a);

// Example 2
let a: *mut ComplexStruct = get_complexstruct_pointer();
let b: *mut HANDLE = a as *mut HANDLE;

// Example 3
let a: *mut ComplexStruct = get_complexstruct_pointer();
let b: *mut HANDLE = a as *mut _;

You can also use transmute() to get a pointer to a struct or any other data type using the special character &:

let a: i32 = 238i32;
let b: PVOID = std::mem::transmute(&a);
let c: *mut PVOID = std::mem::transmute(&b);
let a: ComplexStruct = ComplexStruct::default();
let b: *mut ComplexStruct = std::mem::transmute(&a);

Memory addresses

Since memory addresses have different size depending on the system architecture, the best way to deal with them is using the type usize or isize. These data types have a 4/8 bytes size depending on whether the operative system is x86 or x64, which makes them perfect for the task. Also, they will allow you to perform arithmetic operations over any given memory address as we will see in the next section.

You can directly convert any pointer into an usize using the keyword as. Also, memory addresses can be printed using the hex format placeholder {:x}:

let handle: *mut HANDLE = get_pointer();
let handle_addr = handle as usize;
println!("The memory address that the variable handle is pointing to is {:x}", handle_addr);

You can also obtain the memory address of a function or a basic type variable this way:

fn main() 
{
    unsafe
    {
        let addr: usize = (main as *const()) as usize;
        println!("main()'s base address 0x{:x}", addr);
        let number = 15i32;
        let number_addr = (number as *const i32) as usize;
        println!("Memory address of the variable number: 0x{:x}", number_addr);
    }
}

To obtain the memory address of a varible that is not of a basic data type, you need to use once again the method transmute to cast it into a usize:

let handle: HANDLE = HANDLE::default();
let handle_addr: usize = std::mem::transmute(&handle);
println!("The memory address where the variable handle is located is 0x{:x}", handle_addr);

The last code can be simplified in case that you only want to print the memory address:

let handle: HANDLE = HANDLE::default();
println!("The memory address where the variable handle is located is 0x{:p}", &handle);

Arithmetic operations

There are several ways to increment/decrement a pointer in Rust, and this is required in many situations that involve the WinAPI. To me, the best way to increment or decrement a pointer is by using the functions add() and sub(). These functions expect one single input parameter, which is the offset to calculate from the starting pointer.

Take into account that the final offset is different depending on the type of the pointer. A add(1) will increment by 8 bits a *mut i8, by 32 bits a *mut i32 and by size_of::<T>() a *mut T pointer.

let mut ptr: *mut u8 = get_pointer_to_buffer();
println!("{}", *ptr); // First u8 in the buffer
ptr = ptr.add(1); // Now ptr points to start_of_buffer + 8 cause u8 has a size of 8 bites;
println!("{}", *ptr); // Second u8 in the buffer
ptr = ptr.add(2); // ptr points to start_of_buffer + (8 * 3);
println!("{}", *ptr); // Fourth u8 in the buffer

You can also cast the pointer into a usize and then add or sub any desired offset (in bits):

let ptr: usize = get_pointer_to_buffer() as usize;
let ptr2: *mut u32 = (ptr + 2) as *mut u32;
println!("{}", *ptr2); // Here we would be printing the 32 bits unsigned number located at start_of_buffer + 2;

Compile

Reducing PE size

By default, rust compiler optimizes for execution speed, compilation speed and ease of debugging. This leads to bigger binaries size, which can be inappropiate for offensive tools. There are several compiling options you can use to reduce the final binary size. For that, you just need to add the following to Cargo.toml:

[profile.release]
opt-level = 'z'     # Optimize for size
lto = true          # Enable link-time optimization
codegen-units = 1   # Reduce number of codegen units to increase optimizations
panic = 'abort'     # Abort on panic
strip = true        # Strip symbols from binary*

Then, all you have to do is to compile in release mode using cargo build --release. This info has been obtained from this answer where you can also find additional tips on this topic.

Take into account that you might not want to use some of those flags in your project by default (for example, panic = 'abort' will reduce de binary size by removing unwind data, making it impossible to recover from an unexpected exception). In my experience, the only flags that you can use without worrying at all are opt-level = 'z' and strip = true; for the others, make sure you test the resulting binary before using it on a production environment or a client.

Compile to dll

Rust projects can be compiled to different artifacts: .lib, .exe, .dll, .so, etc. By default, in Windows your code will be compiled to .exe format, but I have found very useful to be able to compile my code into a native dll as I would do from other languages like C or C++.

To do so, you have to add the following lines to Cargo.toml:

[lib]
crate-type = ["cdylib"]

Then, you just need to rename the default main.rs file to lib.rs. After that, simply compile your code as you would normally do to get a C style dll.

These two steps won't be required if at the time of creating the project with cargo, you specify the tag --lib (you would be creating a library), although that would make it harder to debug your code and I do not recommend it at first.

If you want that the final dll exports a certain function of your code, you can do so by changing the function's signature like this:

#[no_mangle]
pub extern fn run()
{
    ...      
}

The final dll will export a function named run that can be called as usual (for example, with LoadLibrary + GetProcAddress or through DInvoke).

Define target architecture

If you want to compile to a different system architecture (for example, compile a 32 bits binary from x64 machine) you can create a .cargo folder in the root of your project, and place a config file inside of it. In this config file you can define the toolchain that you want to use:

[build]
target = "x86_64-pc-windows-msvc" 

By default, the two toolchains (from the stable channel) that I normally use are:

  • x86_64-pc-windows-msvc to target a x64 architecture.
  • i686-pc-windows-msvc to create a 32 bits binary.

You can list the toolchains installed on your system with the command rustup toolchain list. You can install any additional toolchain with rustup install <toolchain>.

Issues resolution

I wasn't sure how to name this section, but here I will add both some extra tricks that do not have their own section and also troubleshooting tips.

You will see that I don't really know the origin/cause of some of the issues I will comment below, but at the end the important thing is to show you how you can solve them.

transmute

Let's have a look at the following code:

let handle = GetCurrentProcess();
let base_address: *mut PVOID = std::mem::transmute(&usize::default());
let zero_bits = 0 as usize;
let size: *mut usize = std::mem::transmute(&dwsize);
let ret = dinvoke::nt_allocate_virtual_memory(handle, base_address, zero_bits, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);

Here I'm just calling NtAllocateVirtualMemory to allocate a certain amount of memory. Well, this code will work 99% of the times, but the remaining 1% will fail "for no reason" leading to every sort of unexpected behavior.

Don't ask me why this happens because I don't really know it, but this situation arises when you pass the output of the method default() as a reference directly into the method transmute(). So the best way to remove that 1% chance of unexpected failure is to rewrite the previous code like this:

let handle = GetCurrentProcess();
let a = usize::default(); // This is the key line
let base_address: *mut PVOID = std::mem::transmute(&a);
let zero_bits = 0 as usize;
let size: *mut usize = std::mem::transmute(&dwsize);
let ret = dinvoke::nt_allocate_virtual_memory(handle, base_address, zero_bits, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);

Here you can see that first I store the output from the trait default() in a temporary variable a, and then I pass that variable's reference to the method transmute() in order to get a PVOID. This code will never experience the same erratic behavior commented before, so I recommend you to always add that extra line to your code.

VCRuntime

Many times you will receive the error message "The code execution cannot proceed because VCRUNTIME140.dll was not found" at the time of running your binaries on remote machines.

To fix this, you need to statically link that dll. First, add the following line to Cargo.toml:

[build-dependencies]
static_vcruntime = "2.0"

Then, add a file named build.rs in the root of your project with this content:

 fn main() {
    static_vcruntime::metabuild();
}

Then recompile and the issue will be gone.

Another way of doing this same thing is to create a .cargo folder in the root of the project, and place a Cargo.toml file inside of it with the following content:

[target.'cfg(all(windows, target_env = "msvc"))']
rustflags = ["-C", "target-feature=+crt-static"]

Nightly

Some experimental features are only available using the nightly channel. For example, in Unwinder I used the intrinsic llvm.addressofreturnaddress in order to get the memory address where the next return address was located in the stack. This intrinsic was only available in the nightly channel, but it was pretty useful and made my life way easier.

If you want to use some cool feature only available in nightly, you just need to install the corresponding toolchain:

rustup install nightly

Once you have the toolchain installed, you can set it up for a specific project (you could also set it up globally, but I do not recommend it):

cd ~/yourproject/
rustup override add nightly

This way, you will be able to use any nightly feature on that specific project.

ASM

Rust allows to insert assembly code in your projects as well. To do so, I use the crate cc-rs. To use this crate, first add the dependency in Cargo.toml:

[build-dependencies]
cc = "*"

Next, you have to create a build.rs file in the root of the project where you will indicate the .asm files that you want to compile together with the Rust code:

fn main()
{
    // Use the `cc` crate to build a C file and statically link it.
    cc::Build::new()
        .file("src/stub.asm")
        .compile("stub");
}

Now, you can create a src::stub.asm file and insert any desired code on it:

.code

     FancyFunction PROC FRAME
        push rbp
        .pushreg rbp
        mov rbp, rsp
        .setframe rbp, 0 
        .endprolog

        ...

        mov rsp, rbp
        pop rbp
        ret
    FancyFunction ENDP

end

Finally, you can call from Rust any of the functions defined in the .asm file by adding the corresponding extern signatures:

extern "C"
{
    fn FancyFunction(address: *const c_void, size: usize, protection: u32, old: *mut u32, virtual_protect: *mut c_void) -> bool;
}

...

pub fn main()
{
  let ret = FancyFunction(param1, param2...);
  if ret == true
  {
    println!("Alright!");
  }
}

Wide char strings

In rust, strings (both &str and String) are utf8 encoded. However, in the Windows API are widely used the so called wide char strings, which are utf16 encoded (2 bytes for each char). This kind of strings can be found, for instance, in the well know UNICODE_STRING struct.

So, you can convert any Rust string to an utf16 encoded string this way:

let mut module_path_utf16: Vec<u16> = "any text".encode_utf16().collect();
module_path_utf16.push(0);

Okay, I know what you are going to say: this is not a String, it is a Vector. But at the end it's almost the same, just a memory buffer with some random content which now will be utf16 encoded. And from this, you can easily obtain a UNICODE_STRING which is probably what you are trying to achieve at this point:

let unicode = UNICODE_STRING::default();
let object_name: *mut UNICODE_STRING = std::mem::transmute(&unicode);
dinvoke::rtl_init_unicode_string(object_name, module_path_utf16.as_ptr());
let unicode_object = *object_name; // Completely unnecessary

Encrypt string literals

Good OPSEC demands string literals encryption to avoid giving away certain information that can be used to detect the malicious behaviour of your payload.

I personally like to use the crate litcrypt to hide the strings literals of my code, specially when I am using DInvoke_rs. I find it very easy to use and it seems very reliable, never had any issue using it.

If you want to do the same, just add the dependency in Cargo.toml:

[dependencies]
litcrypt = "0.3"

Then you just need to initialize the macro by adding this code in your crate:

#[macro_use]
extern crate litcrypt;
use_litcrypt!();

From there, you can call the macro lc!() which will encrypt your string literals at compilation time and will unencrypt them at runtime:

/// Dynamically calls NtWriteVirtualMemory.
///
/// It will return the NTSTATUS value returned by the call.
pub fn nt_write_virtual_memory (handle: HANDLE, base_address: PVOID, buffer: PVOID, size: usize, bytes_written: *mut usize) -> i32 {

    unsafe 
    {
        let ret;
        let func_ptr: data::NtWriteVirtualMemory;
        let ntdll = get_module_base_address(&lc!("ntdll.dll"));
        // What would be the point of using DInvoke if I publish all the WinAPI functions that I am using
        // through the string literals on my code? :) 
        dynamic_invoke!(ntdll,&lc!("NtWriteVirtualMemory"),func_ptr,ret,handle,base_address,buffer,size,bytes_written);

        match ret {
            Some(x) => return x,
            None => return -1,
        }
    }

}

This is just an example, but you can use it almost everywhere you have a sensitive string literal.

Just remember to set the environment variable LITCRYPT_ENCRYPT_KEY before compiling your code:

 set LITCRYPT_ENCRYPT_KEY="yoursupersecretkey"

Remove absolute paths

Most of the times, rust binaries will contain undesired absolute paths on them as a result of the compilation process. These strings could potentially leak the OS username that was used to compile the project, which may impact on your operation's OPSEC. The best way to remove these absolute paths is by using the compilation flag --remap-path-prefix. You can pass this flag directly to rustc or, as I prefer, add it to the .cargo\config file of your project and compile as usual using cargo build --release.

[build]
rustflags = ["--remap-path-prefix", "C:\\Users\\YourUser="] # This will remove any occurrence of C:\Users\YourUser in the resulting binary.

The only absolute path not affected by this flag is the .pdb path. To remove this whole string, remember to add the following line to your cargo.toml file:

[profile.release]
strip = true 
...

Resources

  • windows and ntapi crates.
  • At the end of this post you can find a primitives comparison between C# and Rust data types, very useful to carry out a port of code between the two languages.
  • Rust also has macros that are a very powerful feature for offensive tools development.
  • Check out this post if you want to reate PIC shellcode.
  • Again, a very interesting discussion about Rust executable's size and how to minimize them.
  • More about minimizing Rust binaries.
  • A little bit of extra info about nightly channel.

Contribution

I will try to keep this repo updated and add other valuable tips and tricks in the near future. Feel free to make a pull request if you think you have some interesting tips to share with the community, but keep in mind that your contribution should be Windows related and that this repo is more like a cheatsheet and not a Rust tutorial.

rust_tips_and_tricks's People

Contributors

kudaes avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.