Rust语法基础学习

螃蟹好吃捏

[toc]

Rust

0x00. 开始之前

安装

我是WSL2,先开全局代理,然后:

1
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

完成后重启终端,输入以下命令检查是否安装完毕:

1
2
cargo -V
rustc -V

VSCode插件

我使用了:

  • rust
  • rust-analyzer

0x01. 构建一个项目

切换到要创建工作目录的地方,输入以下命令创建一个项目:

1
cargo new <your_project_name>

例如,我们使用如下命令创建一个叫做hello的项目:

1
cargo new hello

使用如下命令构建、运行项目:

1
2
3
cd ./hello
cargo build
cargo run

0x02. 基础语法

变量声明

各种数据类型如下:

1
2
3
4
5
6
let a: i8 = 1;
let b: u32 = 5;
let c: bool = true;
let d: f64 = 1.1;
let e: char = 'z';
let f: &str = "yes";

rust会自动推断变量类型,但rust是强类型语言。

1
let a = 1; // 正确

输出

println!换行输出,而printl!是不换行输出。

如下所示:

1
2
let f: &str= "sunny";
println!("The weather is {} today.", str)

变量性质

rust中,变量被定义后就不可改变:

1
2
let a = 1;
a = 2; // 错误,不可改变其值

若需要改变,可以用mut声明,意味着这是一个可以改变的变量:

1
2
let mut a = 1;
a = 2; // 正确

但我们上面提到了,rust是强类型语言,因此也不能将其赋值给别的变量类型。

重影

我们提到了rust没有使用mut定义的变量的值不可改变,但可以重影:

1
2
3
let a = 1;
let a = a + 2;
println!("a is {}", a);

这段代码的值为3。可以得知,rust在声明变量后可以再次用let声明并重新绑定其值。

数组

数组需要为同类型数据。注意,数组的长度一旦确定,是不能够再改变的。若需要改变,可以参照后文章节vector

如下:

1
2
let c:[i32; 5] = [1, 2, 3, 4, 5];
// 表示c为一个数据类型为i32的长度为5的数组

函数

和其他语言类似:

1
2
3
fn add(a:i32, b:i32) -> i32{
return a + b
}

若没有加上返回值类型,则一定不能有返回值。

函数表达式

也就是如下形式,需要注意的就是大括号内部最后是没有分号的:

1
2
3
4
let x = {
let x = 2;
x + 5
};

条件语句

如下:

1
2
3
4
5
6
7
8
9
let a = 1;
let b = 2;
if a > b {
println!("a is larger.");
}else if a == b{
println!("equal!");
}else{
println!("a is smaller.");
}

注意几点:

  • rust中用于判断的部分不一定需要加小括号
  • rust一定需要大括号

也有三元表达式:

1
2
let a = 1;
let b = if a == 1 {5} else {0};

循环

有如下两种写法,分别是while循环和for-iter迭代器:

1
2
3
4
5
6
// while循环写法
let mut i = 0;
while i <= 10 {
i += 1;
println!("The value of i is {} now.", i);
}

和:

1
2
3
4
5
// for写法
let a: [i32; 6] = [10, 20, 30, 40, 50, 60];
for i in a.iter(){
println!("The value of i is {} now.", i);
}

另外,RUST里面的无限循环可以用loop而不是while True来实现,loop可以外带一个返回值:

1
2
3
4
5
6
7
8
9
10
let mut i: i32 = 0;
let mut x: i32 = 0;
let result: i32 = loop{
x = 2 * i;
if i == 10{
break x;
}
i += 1;
};
println!("The value of result is {}.", result);

(当然不带也是可以的)

迭代器

有三种迭代器:借用迭代器、可变借用迭代器、所有权迭代器。如下:

1
2
3
4
5
6
7
8
9
10
11
// 借用迭代器,不可修改迭代器的值
let vec = vec![1, 2, 3, 4, 5];
let iter = vec.iter();

// 可变借用迭代器,可以修改原数组的值
let mut vec = vec![1, 2, 3, 4, 5];
let iter_mut = vec.iter_mut();

// 所有权迭代器,会获取所有权,使用结束后vec不再可使用
let vec = vec![1, 2, 3, 4, 5];
let into_iter = vec.into_iter();

切片

使用..即可对字符串切片,同样是左闭右开。

1
2
3
4
5
fn main() {
let s1 = String::from("hello, world!");
let slice = &s1[0..3];
println!("The slice of string is {}.", slice);
}

结构体

先看一个完整的结构体写法,以及其输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 若要对结构体进行格式化输出,才需要导入这个包
#[derive(Debug)]

// 定义结构体
struct Book{
title: String,
content: String,
price: i32,
}


fn main() {
// 实例化一个结构体
let b1 = Book{
title: "Harry Potter".to_string(),
content: String::from("VoldMort and Harry"),
price: 60
};

// 通过格式化输出结构体,需要导入包
println!("rect is {:#?}", b1);

}

输出结构体需要#[derive(Debug)],此时可以通过如下两种方式来输出结构体:

  • {:?} 结构体输出在一行内,适合属性较少
  • {:#?} 结构体输出为多行,适合属性多一点

枚举

看下面这个例子,我们展示了如何编写一个枚举,并使用match来判断枚举类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
enum Fruit{
Apple,
Orange,
Banana
}

fn describe_fruit(fruit:Fruit){
match fruit{
Fruit::Apple => { println!("This is an apple."); },
Fruit::Orange => { println!("This is an orange."); },
Fruit::Banana => { println!("This is a banana."); },
}
}

fn main() {
let my_fruit = Fruit::Orange;
describe_fruit(my_fruit);
}

文件与IO

我们可以用std::fs::read_to_string函数来读取文件内容为字符串,但该函数的返回值并不是字符串类型,需要加上.unwrap()来转换其类型为字符串类型。

可以用如下方式来读取文件内容为字符串:

1
2
3
4
fn main() {
let contents:String = std::fs::read_to_string("src/flag").unwrap();
println!("File contents: {}.", contents);
}

也可以通过std::fs::read函数来以u8的数组类型读取:

1
2
3
4
fn main() {
let contents: Vec<u8> = std::fs::read("src/flag").unwrap();
println!("File contents: {:?}.", contents);
}

写入文件,使用std::fs::write函数即可,第一个参数为文件路径,第二个参数为文件内容:

1
2
3
fn main() {
std::fs::write("src/flag2", "flag{You_write_flag!}").unwrap();
}

从命令行读入,我们使用std::io::read_line()函数即可。

在如下的例子中,我们通过String::new()来创建了一个可变的空字符串,并从标准输入读入数据,随后输出到标准输出。

1
2
3
4
5
6
fn main() {
let mut str_buf = String::new();

std::io::stdin().read_line(&mut str_buf).expect("Failed to read line.");
println!("Your input is {}", str_buf);
}

向量

和数组很像,但是是可变的,这与c++等很多语言类似。

1
2
3
4
5
6
7
8
9
10
11
fn main() {
let mut vector: Vec<i32> = Vec::new();
let mut vector = vec![1, 2, 3, 4];

println!("initial vec: {:?}", vector);

vector.push(5);
vector.push(6);

println!("final vec: {:?}", vector)
}

在上面这段代码中,我们使用Vec::new()来创建了一个空向量,并使用宏vec![1, 2, 3, 4]来初始化了vector

也可以使用append来将一个向量拼接到另一个向量:

1
2
3
4
5
6
7
8
fn main() {
let mut vector: Vec<i32> = Vec::new();
let mut vector = vec![1, 2, 3, 4];

let mut vector2 = vec![5, 6, 7, 8];
vector.append(&mut vector2);
println!("The vec is {:?}", vector);
}

字符串

字符串是String类型。

创建一个空的字符串:

1
let a: String = String::new();

需要注意的是,常量字符串并不是String类型,而是字符串切片类型&str

因此,可以使用如下方式来从常量字符串创建一个字符串:

1
let b: String = String::from("hello");

可以使用to_string()方法来将基础变量类型转换为字符串类型:

1
2
3
let c: String = 1.to_string();
let d: String = 1.1.to_string();
let e: String = "haha".to_string();

可以通过push方法来追加字符,或者push_str方法来追加字符串:

1
2
3
let mut e: String = "haha".to_string();
e.push('u');
e.push_str("what");

字符串可以通过+号进行拼接,但是要注意所有权:

1
2
3
4
5
let mut a = String::from("hello");
let mut b = String::from("world");

a = a + ", " + &b;
println!("{}", a);

注意,我们并不想改变b的所有权,因此使用了a,但b是借用&b

使用.len()方法获取字符串长度:

1
2
3
4
5
6
7
fn main() {
let mut a = String::from("hello");
let mut b = String::from("world");

a = a + ", " + &b;
println!("The string is {}, and the length of it is {}.", a, a.len());
}

0x03. 内存管理语法

所有权

Rust里面最有意思的部分(当然是个人认为)

一个值的所有权只能为一个变量所拥有。因此,考虑如下情况:

1
2
3
4
5
fn main() {
let s1 = String::from("Hello"); // 返回值是一个指向heap中的chunk的指针
let s2 = s1; // 把s1指针的值赋值给s2
println!("{}, world!", s1); // 已经赋值给s2,所有权转移,s1已经不存在了!所以报错
}

克隆

和上面的所有权对应。

由于Rust的所有权机制,因此难以直接将指针的值赋值给另外一个变量,而有时候我们需要这个机制。

此时可以采用克隆,克隆会直接复制整份数据,而不是传递指针。

1
2
3
4
5
fn main() {
let s1 = String::from("Hello");
let s2 = s1.clone();
println!("{}, world!", s1);
}

函数的所有权

对于pwner其实不难理解。看一个例子如下:

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
fn main() {
let s = String::from("hello");
// s 被声明有效

takes_ownership(s);
// s 的值被当作参数传入函数
// 所以可以当作 s 已经被移动,从这里开始已经无效

let x = 5;
// x 被声明有效

makes_copy(x);
// x 的值被当作参数传入函数
// 但 x 是基本类型,依然有效
// 在这里依然可以使用 x 却不能使用 s

} // 函数结束, x 无效, 然后是 s. 但 s 已被移动, 所以不用被释放


fn takes_ownership(some_string: String) {
// 一个 String 参数 some_string 传入,有效
println!("{}", some_string);
} // 函数结束, 参数 some_string 在这里释放

fn makes_copy(some_integer: i32) {
// 一个 i32 参数 some_integer 传入,有效
println!("{}", some_integer);
} // 函数结束, 参数 some_integer 是基本类型, 无需释放

总结,对于一个指针:

  • 在函数A中传递给函数B,在函数A中不再可以使用;
  • 在函数B结尾时其指向的chunk自动释放。

而对于一个基本变量类型例如i32

  • 在函数A中传递给函数B,在函数A中仍然可以使用。

这很好理解,因为Rust的安全管理机制,传递指针给函数相当于改变了所有权,因此无法再使用该堆块(指针)。

而对于基本变量类型,函数传参时基本相当于直接将值赋值给了寄存器,自然谈不上释放和所有权转移。

引用

C++的引用较为类似。我们刚刚提到了所有权的概念,而引用只会”租借“,获得使用权,而不会改变所有权。

因此下面这段代码正确:

1
2
3
4
5
fn main() {
let s1 = String::from("hello");
let s2 = &s1; // 把s1借给s2,没有改变使用权
println!("{}, world!", s1); // 仍然可以访问s1
}

因此,同样也可以将引用传递给函数:

1
2
3
4
5
6
7
8
9
fn main() {
let s1 = String::from("hello");
stdout(&s1); // 传递的也是租借的变量
println!("{}, world2!", s1);
}

fn stdout(a: &String){ // a的变量类型是字符串的引用
println!("{}, world!", a);
}

可能难以理解,尤其是我们刚刚还学了所有权这个概念后。

实际上,引用是指向原有指针的一个指针,而不是指向原指针指向的数据的指针,偷了个图:

img

因此,我们不难想到,一个引用在原变量的所有权已经丢失的时候,引用也失效了:

1
2
3
4
5
6
7
8
9
10
fn main() {
let s1 = String::from("hello");
let s2: &String = &s1; // s2是s1的一个引用
stdout(s1); // s1的所有权丢失了
println!("{}, world2!", s2); // 报错,因为s2已经寄了
}

fn stdout(a: String){
println!("{}, world!", a);
}

此外,租借的变量也无法修改其变量内容,除非变量所有者通过&mut允许你修改其值:

1
2
3
4
5
6
fn main() {
let mut s1 = String::from("hello");
let s2:&mut String = &mut s1; // s2是s1的一个可变引用
s2.push_str(" world!");
println!("{}", s2);
}

0x04. Rust Pwn

插件安装

首先需要在Windows安装Rust,选择默认即可。

随后安装依赖。下载该文件夹内容后,通过cargo进行编译:

1
cargo build --release

编译完成后,可以在target/release找到rs-dml.exe可执行文件,将其复制一份放到环境变量中。

随后安装插件IDARustDemangler,下载后将py文件放置到IDAplugins文件夹下即可,IDA PRO8.3是支持的。

随后启动IDA后,只需要点击左上角edit-plugins-IDA Rust Demangler即可。

虽然但是,即使有了这个插件,可读性也没有变得很高(甚至说是持平),因此还是愉快地读汇编吧。

函数调用

这一点与C语言和C++就持平,而不是像golang一样甚至改变了函数参数寄存器。

Rust中,函数的几个参数仍然是rdi rsi rdx rcx r8 r9

函数名

实际上,IDA反编译后的函数名看着虽然十分冗长,但是遵循name mangling命名规则。

我们看一个例子,我有一个rust项目,名为hello,那么在IDA第一层的main函数中,可以看到其调用了如下函数:

image-20240529143115907

可以看到其中调用的函数名称为_ZN11hello__mainE1639c47d804b494029。剖析这个函数名,实际上组成如下:

  • 固定开头_ZN
  • 长度11,例如这里表示hello__main的长度
  • 项目名称hello
  • 函数名main
  • 哈希值E1639c47d804b494029

是不是会觉得简单多了?有了这个小知识后,观察如下函数名称(随便截的图):

image-20240529143423592

通过hello这个字符串定位,我们便可以方便地找到哪些是该Rust项目中编写的函数,而哪些是库函数。

常见函数参数

很遗憾,ida并不能正确识别Rust的函数参数,这让我们在阅读反编译的Rust代码时带来了麻烦。

如图所示:

image-20240529153718117

这里有一个read_line函数,乍一看似乎是对变量v7进行输入,那这样就掉入陷阱了。

实际上,read_line函数有三个参数,第一个参数为self,即core::result:Result<usize,std::io::error::Error>,第二个参数是io::stdin(),第三个参数rdx,才是要输入的字符串的地址。

根据以上信息,我们按y,将函数改为三个参数,如下:

image-20240529155733893

可以看到,改为三个参数后,我们看到输入的字符串刚好是通过String::new()来创建的字符串。

基于此,现总结在IDA反汇编中的常见函数的参数,供在做Rust时能够快速识别函数,理清逻辑。

read_line()

由于在rustread_line()的写法通常为如下形式:

1
2
3
io::stdin()
.read_line(&mut choice)
.expect("failed to read input.");

因此在汇编中其会先call一个std::io::stdio::stdin形式的函数,随后才会调用read_line

三个参数如下:

  • 第一个参数self,即core::result:Result<usize, std::io::error::Error>
  • 第二个参数io::stdin()对象示例,同时也是返回值
  • 第三个参数为要输入的字符串的地址

Rust语法基础学习
http://example.com/2024/04/01/system/multiarch/RUST/
作者
Ltfall
发布于
2024年4月1日
许可协议