Rust逆向学习 (2)

news2024/10/7 18:27:12

文章目录

  • Guess a number
    • 0x01. Guess a number .part 1
      • line 1
      • loop
      • line 3~7
      • match
    • 0x02. Reverse for enum
    • 0x03. Reverse for Tuple
    • 0x04. Guess a number .part 2
    • 0x05. 总结

在上一篇文章中,我们比较完美地完成了第一次Rust ELF的逆向工作,但第一次编写的Rust程序毕竟只使用了非常有限的几种Rust特性,Rust还有很多的东西没有涉及,像是流程控制、泛型、Trait等。这些内容我们将在本文以及以后的文章中一一进行学习与探索。

Guess a number

0x01. Guess a number .part 1

本文从一个跳跃不是很大的程序开始,也就是一个真正的猜数字小程序:

use std::cmp::Ordering;
use std::io;    // prelude
use rand::Rng;  // trait

fn main() {
    let secret = rand::thread_rng().gen_range(1, 101);    // ThreadRng: random number generator
    loop {
        println!("Please guess a number between 1 and 100:");
        let mut guess = String::new();
        io::stdin().read_line(&mut guess)
            .expect("Cannot read a line!");
        println!("Your guess is: {}", guess);
        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };
        match guess.cmp(&secret){
            Ordering::Less => println!("Too small."),
            Ordering::Greater => println!("Too large."),
            Ordering::Equal => { 
                println!("You win."); 
                break;
            },
        }
    }
}

这里要注意,使用上一篇文章中的编译工具网站时需要添加库并在代码中通过extern crate rand手动加载rand库,否则会编译失败。

考虑到效率问题,本文对于上述代码的反汇编以IDA的反汇编结果为主,汇编代码分析为辅。

line 1

第一行中thread_rng方法返回ThreadRng实例,也就是使用于单个线程的随机数产生器实例,随后将其作为参数1(即self),参数2和参数3分别为范围的下界和上界。通过汇编代码可以发现,Range这个对象需要两个寄存器传递。通过查看Rust官方库源码也可以发现,Range实际上也就只有开始和结尾这两个属性值:

pub struct Range<Idx> {
    /// The lower bound of the range (inclusive).
    #[stable(feature = "rust1", since = "1.0.0")]
    pub start: Idx,
    /// The upper bound of the range (exclusive).
    #[stable(feature = "rust1", since = "1.0.0")]
    pub end: Idx,
}

gen_range方法以常规的方式使用rax返回了生成的随机数值。

随后,一个drop_in_place直接删除了ThreadRng实例,可见Rust对于生命周期的管理非常严格,后续代码已经没有使用ThreadRng实例的代码,因此Rust直接就将其删除了,尽最大可能减少对象重用与悬垂指针引用的可能。

loop

在Rust的反汇编界面中,continue很少见到,因为对于一个循环而言,其内部很有可能存在生命周期在循环之内的对象,因此即使Rust代码中写continue,Rust也需要首先将循环中创建的对象删除之后再开始新一轮循环。这也就导致IDA的反汇编界面中可能会出现很多goto

line 3~7

println!的特征很好识别,Arguments::new_v1_print一出,就知道肯定又是一次输出,不过输出的具体字符串内容直接查看反汇编界面无法确定,不过在汇编代码中也很好找。随后的String::new等也非常正常。

match

上述代码一共有两个match语句,第一个是将字符串parse的结果进行判断,替换了上一篇文章中的expect。这里parse函数的返回值是一个枚举对象Result<F, F::Err>。我们知道Rust的枚举对象是一个很强大的结构,比C/C++中的枚举对象好用很多,这是因为Rust的枚举对象可以理解成一个Key有限且确定的Map,选择一个Key之后还能够根据Key指定的数据类型自由设置Value。在这里我们不妨研究一下,Rust中的枚举对象是如何组织的。

0x02. Reverse for enum

下面通过一个简单的程序对枚举类型进行逆向分析。

#[derive(Debug)]
pub enum Student {
    Freshman(String),
    Sophomore(String),
    Junior(String),
    Senior(String),
}

pub fn get_student(grade: i32, name: String) -> Option<Student> {
    match grade {
        1 => Some(Student::Freshman(name)),
        2 => Some(Student::Sophomore(name)),
        3 => Some(Student::Junior(name)),
        4 => Some(Student::Senior(name)),
        _ => None
    }
}

pub fn main() {
    let x = get_student(4, "CoLin".to_string()).unwrap();
    println!("{:?}", x);
}

上述代码定义了一个枚举类型。首先来看get_student方法:

可以看到,在反汇编界面中,IDA将match语句识别为switch语句,通过汇编代码的分析也能够很容易地发现跳表的存在。

通过查看main函数的方法调用,可以获得get_student方法的参数分别为:Student对象指针、grade参数、name参数。在switch语句中,我们发现每一个分支都有大量的值传送指令,含义未知,但我们可以通过函数调用前后获取到枚举类型的大小与内容。


经过分析,获取到了枚举对象的内容如上图所示。从函数内容等处可以推断出,枚举对象的第一个值3表示的是枚举对象grade的关键字索引,这里由于返回的是Student::Senior,索引为3,也即枚举对象中的4个索引值对应了0、1、2、3这4个索引值。后面还有3个值,其中有字符串指针和字符串长度,经过测试发现,String对象占0x18大小内存,偏移0x8为字符串指针,偏移0和0x10均为字符串长度。

之后,笔者修改了Student枚举类型的定义,在每一项后面加上了一个i32,经过调试发现枚举类型的属性偏移如下:

0x0         枚举索引
0x4         i32
0x8~0x20    String

位于后面的i32类型反而在内存中更加靠前了。笔者推测这可能与Rust对tuple的内存排布有关,考虑到枚举索引很少有超过1个字节(不然就意味着有超过255个分支),使用后面4个字节能节省一定的内存空间。不过无论tuple是如何排布的,Rust的枚举类型在内存中的布局现在已经很清楚了,就是索引值+内容

不过既然都已经看到了tuple的不寻常,接下来不妨也对其进行一番研究。

0x03. Reverse for Tuple

下面将尝试通过数个Tuple的反编译结果分析Tuple的内存布局。众所周知,Tuple就是若干个数据的集合,这些数据之间没有什么明确的关联,只有一个Tuple将它们约束在一个集合中。

pub fn main() {
    let x = (2, 3, 5, 7, 11, String::new());
}

对于上述代码逆向的结果如下:

example::main:
        sub     rsp, 72
        lea     rdi, [rsp + 48]
        call    alloc::string::String::new
        mov     dword ptr [rsp], 2
        mov     dword ptr [rsp + 4], 3
        mov     dword ptr [rsp + 8], 5
        mov     dword ptr [rsp + 12], 7
        mov     dword ptr [rsp + 16], 11
        mov     rax, qword ptr [rsp + 48]
        mov     qword ptr [rsp + 24], rax
        mov     rax, qword ptr [rsp + 56]
        mov     qword ptr [rsp + 32], rax
        mov     rax, qword ptr [rsp + 64]
        mov     qword ptr [rsp + 40], rax
        mov     rdi, rsp
        call    qword ptr [rip + core::ptr::drop_in_place<(i32,i32,i32,i32,i32,alloc::string::String)>@GOTPCREL]
        add     rsp, 72
        ret

从相对于rsp的偏移量可以看出Tuple的排布情况,上述Tuple的内存排布顺序与数据的定义顺序相同。

但对于下面一个Tuple而言就不同了:

pub fn main() {
    let x = (2, 3, 5, 7, 11, String::new(), "CoLin");
}

逆向的结果为:

example::main:
        sub     rsp, 88
        lea     rdi, [rsp + 64]
        call    alloc::string::String::new
        mov     dword ptr [rsp + 24], 2
        mov     dword ptr [rsp + 28], 3
        mov     dword ptr [rsp + 32], 5
        mov     dword ptr [rsp + 36], 7
        mov     dword ptr [rsp + 40], 11
        mov     rax, qword ptr [rsp + 64]
        mov     qword ptr [rsp], rax
        mov     rax, qword ptr [rsp + 72]
        mov     qword ptr [rsp + 8], rax
        mov     rax, qword ptr [rsp + 80]
        mov     qword ptr [rsp + 16], rax
        lea     rax, [rip + .L__unnamed_1]
        mov     qword ptr [rsp + 48], rax
        mov     qword ptr [rsp + 56], 5
        mov     rdi, rsp
        call    qword ptr [rip + core::ptr::drop_in_place<(i32,i32,i32,i32,i32,alloc::string::String,&str)>@GOTPCREL]
        add     rsp, 88
        ret

可以看到,这里是将String::new()产生的String实例放在了开头,随后才是5个i32,最后是&str。至于为什么要这样排列,询问了一个Rust大手子之后,给到的答案是:Rust数据结构和内存排布没有必然关联,Rust编译器可能根据不同的架构进行相应的内存结构调整,说人话就是——不能预判,不是必然顺序排列。不过考虑到对于Tuple的遍历、索引等操作在代码中都是固定的,编译器在编译的时候完全可以将地址偏移与索引值一一对应,不影响正常的索引,但对于反编译则是一个巨大的噩梦,因为你不确定某个索引值的数据到底有多少偏移。另外,如何通过汇编代码对栈空间的布局判断是否存在一个tuple也是一个问题。在定义变量时,一个tuple完全可以拆分为多个变量进行定义,反正在汇编代码中也不会保存临时变量的变量名。这在内存中会表现出来不同吗?

我们还是通过实际验证来解答我们的问题。

pub fn main() {
    let x = (2, 3, 5, 7, 11, 13);
    println!("{}", x.0);
}
pub fn main() {
    let x = 2;
    let y = 3;
    let z = 5;
    let a = 7;
    let b = 11;
    let c = 13;
    println!("{}{}{}{}{}{}", x, y, z, a, b, c);
}

给出上面的两个Rust函数,通过查看6个整数值在内存中的排布可以发现,两者对于6个整数值都是按相同顺序进行排列,从低地址到高地址依次为2、3、5、7、11、13。不过在编译过程中发现,只有当变量被使用时,Rust编译器才会将这个变量编译到ELF中,否则这个变量将不会出现在ELF中。也就是说,我们不能仅仅通过栈内存排布判断源代码中是否定义了Tuple。不过转念一想,这样其实是合理的。Tuple实际上就相当于是一个匿名的结构体实例,想一想C语言中的结构体,实际上也就是将一堆各种类型的数据集合在一起,使用相邻的内存空间保存各个属性而已。定义一个具有两个int类型的C语言结构体,将其在栈内存中分配一个实例空间,与在栈内存中分配两个int类型的变量,在本质上是完全相同的。

因此,我们在对Rust ELF进行逆向分析时,不必纠结源码的编写者是否定义了元组,全部将其看做独立的变量就可以了。

0x04. Guess a number .part 2

好不容易说完了对Rust枚举类型和元组的逆向,接下来让我们回到最开始的那个程序,说到两个match语句。

对于第一个match语句,match的对象是一个枚举类型,在match语句体之内实际上是按照枚举类型进行分支。在汇编语句中,Rust是这样完成分支的:

注意0xCEAC处的指令:mov al, byte ptr [rsp+1D8h+var_C0],第二个操作数是parse方法的返回值,也就是Result<F, F::Err>。考虑到这里的Fu32类型,整个枚举类型占用的空间大小为8字节,因此rax返回的直接就是对象本身的内容(0x??_0000_0000)。第1个字节为枚举索引值,后4个字节为转换后的值。在0xCEAC地址的这条指令将第1个字节赋值给al后进行了比较(cmp rax, 0),这也就是分支的具体实现方法——提取出枚举类型的索引值,根据索引值进行分支。

对于后面cmp方法返回值的match与之类似,本质上使用的也是if-else结构,主要是因为分支数量较少,没有必要使用跳转表,分支逻辑如上图所示。不过不同的是,第一个分支是判断枚举对象索引值是否等于0xFF,即-1。经过调试发现,Ordering::Less对应的枚举索引为-1,Ordering::Greater对应1,Ordering::Equal对应0。而对于每个分支,都只是一个简单的输出语句,这里就不再分析了。

0x05. 总结

在本文中,我们学习了:

  1. Rust的枚举类型在汇编代码层的数据结构实现。
  2. Rust的元组Tuple类型在汇编代码层无法被有效识别,但可将其看做多个独立变量进行分析。
  3. 三个Ordering枚举对象的索引值为-1、0、1,与一般枚举对象索引值从0开始不同。
  4. Rust倾向于当变量不再使用时就删除变量对象,以尽可能地提高安全性。
  5. Rust的元组类型在汇编代码层栈空间的数据排列顺序与元组类型中数据的定义顺序不一定相同。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1121294.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

JVM(Java Virtual Machine)垃圾收集器篇

前言 本文参考《深入理解Java虚拟机》一书&#xff0c;本文主要介绍几个经典的垃圾收集器&#xff1a;Serial、ParNew、parallelScavenge、CMS、Serial Old、Parallel Old、G1 本系列其他文章链接&#xff1a; JVM&#xff08;Java Virtual Machine&#xff09;内存模型篇 JV…

2434: 【区赛】[慈溪2013]统计方格

题目描述 给出一张 n 行 m 列仅由黑白方格组成的黑白图片&#xff08;行从上到下 1 到 n 编号&#xff0c;列从左到右 1 到 m 编号&#xff09;。如下图是一张由 17 行 18 列方格构成的黑白图片&#xff0c;图片中的任意一个方格要么是白色&#xff0c;要么是黑色。 仔细观察这…

介绍Sigmoid函数的平移、平滑和翻转【基于Python可视化分析】

文章目录 简介Sigmoid函数Sigmoid函数曲线调控参数设置python可视化参考 简介 本篇博客介绍了具有S型曲线的Sigmoid函数&#xff0c;以及如何设置、调整Sigmoid函数的参数实现S曲线的平滑、平移和翻转操作。博客给出了Python代码示例&#xff0c;更加深刻形象。&#x1f606;&…

hdlbits系列verilog解答(两输入与门)-06

文章目录 wire线网类型介绍一、问题描述二、verilog源码三、仿真结果 wire线网类型介绍 wire线网类型是verilog的一种数据类型&#xff0c;它是一种单向的物理连线。它可以是输入也可以是输出&#xff0c;它与reg寄存器数据类型不同&#xff0c;它不能存储数据&#xff0c;只能…

数据结构与算法 | 第二章:线性表

本文参考网课为 数据结构与算法 1 第二章线性表&#xff0c;主讲人 张铭 、王腾蛟 、赵海燕 、宋国杰 、邹磊 、黄群。 本文使用IDE为 Clion&#xff0c;开发环境 C14。 更新&#xff1a;2023 / 10 / 22 数据结构与算法 | 第二章&#xff1a;线性表 线性表总览线性结构概念特…

大数据技术学习笔记(三)—— Hadoop 的运行模式

目录 1 本地模式2 伪分布式模式3 完全分布式模式3.1 准备3台客户机3.2 同步分发内容3.2.1 分发命令3.2.2 执行分发操作 3.3 集群配置3.3.1 集群部署规划3.3.2 配置文件说明3.3.3 修改配置文件3.3.4 分发配置信息 3.4 SSH无密登录配置3.4.1 配置ssh3.4.2 无密钥配置 3.5 单点启动…

人工智能(6):机器学习基础环境安装与使用

1 库的安装 整个机器学习基础阶段会用到Matplotlib、Numpy、Pandas等库&#xff0c;为了统一版本号在环境中使用&#xff0c;将所有的库及其版本放到了文件requirements.txt当中&#xff0c;然后统一安装 新建一个用于人工智能环境的虚拟环境 mkvirtualenv ai matplotlib3.8…

Mybatis应用场景之动态传参、两字段查询、用户存在性的判断

目录 一、动态传参 1、场景描述 2、实现过程 3、代码测试 二、两字段查询 1、场景描述 2、实现过程 3、代码测试 4、注意点 三、用户存在性的判断 1、场景描述 2、实现过程 3、代码测试 一、动态传参 1、场景描述 在进行数据库查询的时候&#xff0c;需要动态传入…

【源码解析】Spring源码解读-bean的加载

Spring的整体流程其实就是通过配置 xml、注解将自定义bean类信息进行配置&#xff0c;然后通过BeanDefinitionReader读取配置信息&#xff0c;由Dom转换成xml解析成Docment。在通过加载的配置信息进行初始化Bean对象&#xff0c;然后在对象的前后进行处理&#xff0c;也就是不同…

2023-10-22

一、总线通信协议简介 总线是计算机系统中负责连接各个硬件的通信线路&#xff0c;它可以传输数据、地址和控制信号。通信协议是指双方实体完成通信所遵循的规则。总线通信协议是一种规定总线设备之间数据通信方式和方法的规则&#xff0c;它包括数据的通信方式、速率、格式、…

python爬虫之js逆向入门:常用加密算法的逆向和实践

一、强大的Chrome DevTools Chrome DevTools是一组内置于Google Chrome浏览器中的开发者工具&#xff0c;用于帮助开发人员调试、分析和优化Web应用程序。它提供了一系列功能强大的工具&#xff0c;用于检查和编辑HTML、CSS和JavaScript代码&#xff0c;监视网络请求、性能分析…

redis怎么设计一个高性能hash表

问题 redis 怎么解决的hash冲突问题 &#xff1f;redis 对于扩容rehash有什么优秀的设计&#xff1f; hash 目标是解决hash冲突&#xff0c;那什么是hash冲突呢&#xff1f; 实际上&#xff0c;一个最简单的 Hash 表就是一个数组&#xff0c;数组里的每个元素是一个哈希桶&…

ida81输入密码验证算法分析以及破解思路

本文分析了ida81对输入密码的验证流程&#xff0c;分别对输入密码到生成解密密钥、密码素材的生成过程以及文件数据的加密过程这三个流程进行分析&#xff0c;并尝试找一些可利用的破绽。很遗憾&#xff0c;由于水平有限&#xff0c;目前也只是有个思路未能完全实现&#xff0c…

查看当前cmake版本支持哪些版本的Visual Studio

不同版本的的cmake对Visual Studio的版本支持不同&#xff0c;以下图示展示了如何查看当前安装的cmake支持哪些版本的Visual Studio。 1.打开cmake-gui 2.查看cmake支持哪些版本的Visual Studio

28. 使用 k8e 玩转 kube-vip with Cilium‘s Egress Gateway 特性

因为在私有云环境下,我们需要保障集群服务 APIServer地址的高可用,所以提供的方案就是使用一个 VIP 让 API Server 的流量可以负载均衡的流入集群。另外,kube-vip 还支持 Service LB,方便SVC 服务的负载均衡,结合 cilium Egress Gateway 特性可以做到集群内的容器对外访问…

canvas绘制刮涂层抽奖效果

实现的效果&#xff1a;主要用到画布设置图层覆盖效果globalCompositeOperation属性 实现的源代码&#xff1a; <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta name"viewport" content"…

canvas常用的几种重叠绘图设置globalCompositeOperation

globalCompositeOperation描述了2个图形交叉的时候是什么样子&#xff0c;它的值有很多&#xff0c;这里就盗一张很经典的图&#xff1a; 我们来看一个source-in的例子吧&#xff1a; <!DOCTYPE html> <html lang"en"><head><meta charset&q…

论文-分布式-并发控制-Lamport逻辑时钟

目录 前言 逻辑时钟讲解 算法类比为面包店内取号 Lamport算法的时间戳原理 Lamport算法的5个原则 举例说明 算法实现 参考文献 前言 在并发系统中&#xff0c;同步与互斥是实现资源共享的关键Lamport面包店算法作为一种经典的解决并发问题的算法&#xff0c;它的实现原…

VTM/VVC 编译与测试-- YUV与RGB空间转换

环境配置:ubuntu 18.04 一、VVC测试 软件下载: 官网下载 VVC:http://jvet.hhi.fraunhofer.de/ 这里可以选择版本。 编译工具: 在开始编译前,需要电脑的环境中中有gcc、g++、cmake、make这四个工具。 sudo apt-get install gcc g++ sudo apt-get install cmake sudo …

Android微信逆向--实现发朋友圈动态

Android微信逆向--实现发朋友圈动态 0x0 前言# 最近一直在研究Windows逆向的东西&#xff0c;想着快要把Android给遗忘了。所以就想利用工作之余来研究Android相关的技术&#xff0c;来保持对Android热情。调用微信代码来发送朋友圈动态一直是自己想实现的东西&#xff0c;研…