跳到主要内容
SUN / 笔记
← 全部文章

CSAPP实验3:Attack

这篇文章旨在帮助同学在做lab找不到思路时可以来这里寻找思路,默认读者已经详细的读过实验平台提供的实验文档

CI

CI意为代码注入,在前三关中我们要在输入的字符串中添加我们自己的代码并想方法让程序执行我们的代码

第一关

函数"getbuf"反汇编代码:

image-20221020171637345

可以看到该函数一开始为栈分配了0x18个字节的空间

//后记补充:这里当时在输入时忘记了0x0a是'\0',所以0x0a开始的数据实际都没输入进去

先尝试输入01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12,然后使用gdb查看调试信息,在进入Gets函数前设置断点查看寄存器中的值:

image-20221020194502138

说实话看到这里一头雾水,那就设置一个断点在Gets函数执行完毕之后,也就是0x40168b处查看寄存器中的值:

image-20221020194744656

没有看到直接有用的值,但是我们知道函数"Gets"做的事情是读入我们的输入并将其拷贝到栈中的某个地方,那就看看栈中到底存了些什么吧,读取rsp寄存器所存的栈地址:

image-20221020195004282

我们能看到输入的0x1-0x9,之后的输入不知道为什么没有在这里;接着往后看会看到一个地址0x55586000,跟前面寄存器的值比对后发现是寄存器rbx的值,暂时意义不明;就当读完了前24(0x18)个字节之后,立马看到了一个地址:0x401829,这个地址在函数"getbuf"所分配的栈空间之外,很有可能是指向一个指令的地址,并且这个地址大概率是getbuf的返回地址,直接在反汇编出来的程序中搜索。果不其然,在函数"test"中找到了这个地址:

image-20221020195632673

很明显,他就是函数"test"在调用完函数"getbuf"后下一个所要执行的指令,那么这一关到这里就很明确了:我们要输入一个字符串,28个字节长,前24个字节与上面在内存中看到的保持一致(控制变量尽可能只修改最关键的那个地址),后4个字节要修改为函数"touch1"的地址,也就是"95 16 40 00"(这样输入经过hex2raw转换后才能正确的进入内存中)

第二关

在第一关中我们已经可以修改函数"getbuf"的返回地址来让他返回到函数"touch1"了;在这一关中我们需要做的更多:不仅要使其返回地址为函数"touch2"的地址,还要修改寄存器的值来传递给它一个cookie

但是该怎么把我们代码注入到程序中呢?把代码作为字符串输入并不会让程序自动去执行一个我们放在内存中的代码,我们需要通过课程中学到的"控制"手段来让程序的运行跳转到我们的代码。其实这样的事情在第一关就已经做过了,只不过在第一关是跳到他提供的函数"touch1",而在这一关是跳转到我们自己输入的代码的地址

所以,我们输入字符串的25-28字节就是栈底,也就是0x55654c98。这样操作,程序在函数"getbuf"返回时会跳转到0x55654c98,而这个地址存储的是我们输入的代码,程序就会执行我们输入的代码

解决了怎么跳转到我们写的代码的问题,还要解决写什么样的代码的问题,该怎样写汇编代码才能让寄存器%rdi中的值变为0x3d9549ca,并且让我们的指令返回到函数"touch2"呢?且看下面的汇编代码:

assembly
# 压入返回地址
pushq   $0x4016c1
# 给寄存器%rdi赋值
movq    $0x3d9549ca,%rdi
# 返回
retq

注意上面的这段代码得到的二进制代码不够24个字节,可以用0补齐到24字节然后再跟上0x55654c98

第三关

与上一关不同,这次我们要传递的参数变成了一个字符串(其实无非就是从cookie的值变成了他的"指针");并且程序调用的函数"hexmatch"和函数"strncmp"可能会破坏栈,那么我们先随便搞点输入看看他会破坏成什么样子把,把第二关的输入前面pushq的地址改为函数"touch3"的地址输入进去试试:image-20221022103246420

这里我们要关注进入函数"touch3"、进入函数"hexmatch"和退出函数"hexmatch"时栈的变化情况

  1. 进入"touch3"时image-20221022103552163可以看到这时栈的情况基本和我们预想的一致

  2. 进入"hexmatch"时

    image-20221022103558498

    到这里我们可以看到0x55654ca8之后的栈已经被改变了,那么此时的栈顶是哪呢?

    image-20221022104040004

    果然就是0x55654ca8,那么从这里即可得知0x55654ca8-0x55654cb0间的栈是会被破坏的,不能用于存储字符串

  3. 退出"hexmatch"时

    image-20221022103604307

    退出该函数时,可以看到0x55654c98之后的内存也被破坏了

从上面这几张图,结合我们知道的信息有:

  • 我们注入的代码/字符串只会被存储在0x55654c98之后
  • 0x55654c98-0x55654cb0之间的内存会被函数"hexmatch"破坏
  • 0x55654cb0-0x55654cb7要存储跳转到我们指令的地址

所以,我们注入的字符串只能在0x55654cb8以后,但是具体位置应该选在哪呢?

为了确定这个不会被用到的位置,我重新启动程序,寻找函数"test"的栈顶地址:

image-20221022104823544

为什么是函数"test"?因为在我们攻击这段程序将指令引向函数"touch3"之后,函数"test"的栈帧按理来说就再也用不到了,那么把cookie的字符串存在函数"test"的栈帧中应该是最安全的选择

所以有汇编代码:

assembly
pushq $0x4017ad
movq  $0x55654cc0,%rdi
ret

注意在设计注入字符串时尽可能少的改变栈帧,比如0x55654cb8处最好让其仍然存储1

ROP

ROP意为面向返回结果的编程,在ROP的这两关中我们无法像之前那样自己编写指令并让程序执行,而是给我们提供了一个"farm",我们可以把程序导向"farm"中的已有的汇编代码

第二关

首先想明白我们要让程序做什么:

  • 使寄存器%rdi的值为我的cookie值
  • 程序要跳转到函数"touch2"

在CI中,我们可以轻松通过注入汇编代码实现以上两点,但是现在要使用ROP的方法,该怎么办呢?

在这一关中,我们能使用的的指令有:movq, popq, ret和nop。有一个很关键的指令——popq。这个指令可以把我们存在栈中的数据压入到某一个寄存器,也就是说我们可以通过使用popq这个指令来让cookie的值进入%rdi寄存器!

查表,得知popq %rdi的指令编码为0x5f,但是很可惜farm中一个0x5f都没有,这样的话就只能先pop到一个寄存器再mov了。经过搜索后发现以%rdi寄存器为目的的指令只有movq %rax,%rdi,那么就要用popq %rax来搭配

现在我们知道怎么给寄存器%rdi赋值cookie了,那么接下来就要想办法解决函数(指令)之间跳转的问题了。当函数返回时,会自动跳到栈顶的那块地址

因为ROP题目的栈地址采取了随机化的方法防止攻击,所以我没法以一个绝对的地址讲解栈,那么下面为了方便讲解,我设在函数"getbuf"中保存我们输入字符串的起始位置为0

assembly
# 字节0x00到0x17只要不输'0x0a'就无所谓,用不到
0x00 - 0x0f: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
# 从0x18开始,8个字节是函数"getbuf"的返回地址,要把它修改成第一个gadget地址
0x10 - 0x1f: 00 00 00 00 00 00 00 00 'popq %rax' 00 00 00 00
# 第一个gadget要pop一个值到%rax,那么0x20 - 0x27就要存储我们的cookie
# pop之后,栈顶变化,返回地址就变成了0x28-0x2f的地址
0x20 - 0x2f: cookie(4字节) 00 00 00 00 'movq %rax,%rdi' 00 00 00 00
0x30 - 0x37: 函数touch2地址(4字节) 00 00 00 00

第三关

与前一关一样,先来梳理我们需要干什么:

  1. 把cookie以字符串的形式放入栈中
  2. 把cookie的地址赋给寄存器%rdi
  3. 程序跳转到函数"touch3"

第1步和第3步没什么难的,这样的事我们已经做过好多次了;关键在于第2步,由于程序采用了栈随机化,我们根本不知道放进去的字符串的地址,只有可能知道它与寄存器%rsp中存储的栈顶地址的相对距离。其实基本可以确定:我们要用寄存器%rsp中的值去计算得出我们的字符串存储的位置

那这样的话我们的指令大抵要完成三个内容:

  1. 记录某个时刻程序寄存器%rsp的值(某时刻的栈顶地址)
  2. 对那个栈顶地址进行某些计算得到cookie地址
  3. 将cookie地址赋给寄存器%rdi

第一步

如果我们能直接movq %rsp,%rdi那肯定是最方便的,但是farm中以%rsp为源寄存器的指令只有movq %rsp,%rax,那么我们就只能先把寄存器%rsp的值存储在寄存器%rax中

注意,这时因为已经进入了第一个gadget,寄存器%rsp的值与在函数"getbuf"时已有不同;若仍以在函数"getbuf"中保存我们输入字符串的起始位置为0的话,第一条指令执行完毕后保存的寄存器%rax的值为0x20(相对地址)

第二步

我们要想办法计算得到cookie字符串的值,就要解决一个问题:加法。题目并没有给出add指令的二进制码,那我们该怎么实现呢?我想了好久,就在我漫无目的的翻着farm中函数时,找到了惊喜:

这不就是rax = rdi + rsi么?那接下来我们要做的就很明确了:把cookie相对0x20的偏移量和0x20对应的绝对地址放进寄存器%rdi和%rsi中

先讨论偏移量怎么算吧:

到目前为止,我们还没有确定cookie到底放在我们输入的哪里也就是栈的哪个位置,按照CI时cookie存放的经验,应该是放在我们输入的字符串的结尾。那就先设cookie相对0x20的偏移量为n吧

假设我们一共会用a个gadget,其中有b个gadget有pop相关操作,那么应该有

n=8∗(n−1)+8∗b+8=8∗(n+b) n = 8*(n-1)+8*b+8 = 8*(n+b)

其中8∗n8*n是第一个gadget之后所有gadget的返回地址要占用的空间;8∗b8*b是pop出去的数据所要占的空间;88是函数touch3地址所要占的空间

具体计算步骤

  1. movq %rax,%rdi

    将第一步中保存的栈的地址赋给寄存器%rdi

  2. popq %rax(提供的pop指令只能pop到%rax)

    把cookie的偏移量赋给寄存器%rax

  3. movl %eax,%ecx

    这时我应该movq %rax,%rsi,但是farm中未给出相关操作,所以3-5步就是曲线将寄存器%rax中的值转移到%rsi中

  4. movl %ecx,%edx

  5. movl %edx,esi

  6. lea (%rdi,%rsi,1),%rax

    计算出了cookie地址并存储在寄存器%rax中

第三步

把cookie地址赋给寄存器%rdi

因为在前一步cookie地址已经被存储在寄存器%rax中了,所以这一步要实现的就是简单的movq %rax,%rdi,在farm中就有,直接用就行

这三步结束之后,我们得知一共要用8个gadget,其中有1个是pop操作,那么n的值就是8∗(8+1)=72=0x488*(8+1)=72=0x48

最后的字节序列:

assembly
# 字节0x00到0x17只要不输'0x0a'就无所谓,用不到
0x00 - 0x0f: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10 - 0x1f: 00 00 00 00 00 00 00 00 'movq %rsp,%rax' 00 00 00 00
0x20 - 0x2f: 'movq %rax,rdi' 00 00 00 00 'popq %rax' 00 00 00 00 00 
0x30 - 0x3f: 48 00 00 00 00 00 00 'movl %eax,%ecx' 00 00 00 00 
0x40 - 0x4f: 'movl %ecx,%edx' 00 00 00 00 'movl %edx,esi' 00 00 00 00 
0x50 - 0x5f: 'lea (%rdi,%rsi,1),%rax' 00 00 00 00 'movq %rax,%rdi' 00 00 00 00
0x60 - 0x6f: 函数touch3地址 cookie字符串

相关文章

lab说明 →Chapter 2 程序结构和执行 →CSAPP实验五:Cache →