★pwn 手写Shellcode保姆级教程★
题目链接:
百度网盘(提取码yxxx)
🐯前言
本文章为手写Shellcode的教程,本文章将会围绕 NewStarCTF 2023 WEEK2 中的shellcode revenge(此题wp在上个文章)一题展开,深入浅出带你一起编写shellcode。学完本文章,你将会有以下收获:加深对shellcode的理解,提升shellcode的编写能力,提升汇编能力,熟练掌握“异或”操作。
🦁汇编语言
正式开始前,先粗略学习一下一些重要的汇编指令。
mov rax, 0x50
翻译:将0x50这个十六进制数赋值给rax寄存器。若rax原本值为0x40,mov rax, 0x50后则变为0x50
寄存器:一个临时储存数据的东西
add rax, 0x50
翻译:将0x50这个十六进制数加到rax寄存器。若rax原本值为0x40,add rax, 0x50后则变为0x90
push rax
翻译:将rax中的值推进栈中
栈:一个临时储存数据的东西
pop rax
翻译:将栈中此时储存的数据拿出放进rax中
xor al,0x50
翻译:对al寄存器中的数进行“异或”操作,与0x50进行异或。
al寄存器:rax寄存器一共有8个字节:11 22 33 44 55 66 77 88,al就是的最低的1个字节,也就是88的那个字节。ah是77的那个字节,ax是77 88,eax是55 66 77 88。
异或:
0x50是十六进制数,转化为二进制数是0101 0000
假设此时al中的数为0x10,转化为二进制数是0001 0000
0101 0000
0001 0000
上下对每个比特进行比较,如果相同,则为0,不同,则为1。第一个比特,相同,所以结果为0。第二个比特,不同,所以结果为1,如此比较下去,得到结果:
0100 0000
这个值为0x40就是最终的结果。
需要注意的是,异或并不代表是相减,只是这里刚好是相减的值而已,异或有时候不会是相减的值!
一个数,异或同一个数两次,会回到原来的这个数。
例如将刚刚的0x40再异或0x10一次,又会变回0x50
xor eax, dword ptr [rdx + 0x30]
翻译:将rdx寄存器中储存的地址加0x30后,取这个地址中储存的值,将这个值与eax进行异或。
rdx寄存器:寄存器的一种。
dword ptr [ ]:指针。指向某个地址中的值。例如,在0x66660000这个地址中存放了一个值,这个值是0x1234,那么取的就是这个0x1234,而不是0x66660000,如果是xor eax, rdx + 0x30,取的就是这个0x66660000
xor al, byte ptr [rdx + 0x30]
翻译:将rdx寄存器中储存的地址加0x30后,取这个地址中储存的值,将这个值与al进行异或。
与上面的不同点:byte和al。byte和al代表取一个字节,dword和eax代表取四个字节。此外,还有word和ax(两个字节),qword和rax(八个字节)。如果该地址中的值为:11 22 33 44 55 66 77 88,byte取的是88,将0x88与al寄存器异或。word取的是77 88,dword取的是55 66 77 88,qowrd取全部。
以上为看懂下文需要学会的基本汇编指令。如果没有学会请勿继续阅读。
🦁shellcode分析
shellcode实质上是由一堆汇编指令组成的程序。
要编写shellcode,首先要明确需要用shellcode完成什么目标。
基于不同的题目,shellcode需要完成的目标是不一样的,因为题目的限制不同,像shellcode revenge这道题目,限制了只能使用大写字母和数字。
这题作者是利用手写shellcode完成了syscall调用read函数的操作,然后利用read读入不受限制的shellcode,不受限制的shellcode不需要手写,直接利用pwntools生成就可以。
但是shellcode一发入魂才是男人的浪漫嘛,分两次输入shellcode总是有些不爽的,于是我开始考虑手写一个shellcode,能够调用syscall执行execve(“/bin//sh”,0,0),直接开shell!
syscall调用read很容易,在执行syscall这个命令时,只要满足以下条件即可:
RAX = 0
RDI = 0
RSI = 要写入的地址
RDX = 很大的数
相当于执行了read(RDI,RSI,RDX),你就可以往RSI这个地方写很多数据。
其中,RAX是函数的调用号,没有这个,syscall不知道你要调用什么函数。以下是64位系统调用表:
https://blog.csdn.net/SUKI547/article/details/103315487
syscall调用execve就不容易了,需要满足以下条件:
RAX = 0x3b
RDI = "/bin//sh"所在的地址 → “/bin//sh”
RSI = 0
RDX = 0
相当于执行了execve(RDI→"/bin//sh",RSI,RDX)
需要注意的是,RDI中不能直接写入/bin//sh,而是要写地址。例如/bin//sh这串ascii码转换成十六进制是2F 62 69 6E 2F 2F 73 68,存放/bin//sh的地址是0x66660056,那么就需要让RDI的值变成0x66660056,而不是2F 62 69 6E 2F 2F 73 68
还值得一提的是,这里我使用的是/bin//sh,而不是/bin/sh,二者是同等效力。因为要4字节对齐,这样方便之后xor
ascii码转换十六进制网站推荐:
https://www.bchrt.com/tools/ascii-converter/
🦁文件分析
IDA查看。mmap原理在WEEK1中已经讲了。往0x66660000这里最多输入256个字符,然后限制只能是大写字母和数字。随后会执行这里的shellcode
编写python脚本,开始gdb动态调试。
在jumpout那个地方下个断点,然后按c过去
可以看到此时寄存器的状态,即将要执行的汇编指令,栈的状态。
梳理几个有用的寄存器:
RAX = 0
RBX = 0
RDX = 0x66660000
RDI = 0
RSI = 0x66660000
其他寄存器不会被本文使用到。
随后查看一下0x66660000这里保存的数据,我们输入的AAAA被保存在了这里。
🦁shellcode编写
分析完后,开始规划一下怎么利用这点空间完成一系列的参数设置和syscall调用。
推荐边开gdb调试边跟着文章走,否则一次性看完整篇文章可能有些跟不上。
首先给出两个网站:
查看有哪些汇编指令可以被使用:https://nets.ec/Alphanumeric_shellcode
在线编写汇编指令:https://shell-storm.org/online/Online-Assembler-and-Disassembler/
要完成execve,最重要的是/bin//sh,首先要将/bin//sh写入某个地址中,才能调用那个地址。而第一件难事就是/bin//sh的值是2F 62 69 6E 2F 2F 73 68,每一个值都不能直接写入,因为只能写大写字母和数字,只能写30-39,41-5A这个范围的字符。于是,xor就十分重要了,于是开始考虑怎么异或出2F 62 69 6E 2F 2F 73 68
能控制的“十位数”:3-5,“个位数”:如果“十位数”是3,个位数为0-9;如果“十位数”是4,个位数为1-f;如果“十位数”是5,个位数为0-A
将2F 62 69 6E 2F 2F 73 68的“十位数”和“个位数”拆开来看
十位数:2 6 6 6 2 2 7 6
怎样异或出2呢?
0101 ① 5
0011 ② 3
——
0110
0100 ③ 4
——
0010
可以看到,5,3,4这三个数字进行两次异或操作就可以了。
怎样异或出6呢?
0101 ① 5
0011 ② 3
——
0110
可以看到,5,3这两个数字进行一次异或操作就可以了。
但是,这是不想看到的场景,因为最好是需要保证能够“共同”异或出2 6 6 6 2 2 7 6,而不是一个一个地去异或,那样会很麻烦。也就是说,要么是经过两次异或,要么是一次异或。但是不论怎样异或,只要使用的是3,4,5,异或出2必然需要奇数次,异或出6必然需要偶数次。那怎么办呢?这时候就必须要引入其他数字了,而我们又不能输入进去,所以就靠程序原本自带的"\x00"。
还记得之前文件分析看的图吗?
这里有大量的"\x00"可以被利用,而填入shellcode的时候会将这些0覆盖,于是就只能利用在填入的shellcode末尾的0了。
而我们要异或的2 6 6 6 2 2 7 6中,6和7都是偶数次,2是奇数次,如果是想一次性异或这8个数,显然不好做(后面会说做到的方法),那么就一次异或4个数,假设此时shellcode末尾是\x50,后面就全是\x00了,先来看看2 6 6 6
5 0 0 0 ①
3 5 5 5 ②
———
6 5 5 5
4 3 3 3 ③
———
2 6 6 6
这样,就可以实现两次“共同”异或出2 6 6 6。同理,可以用这个方法异或出2 2 7 6
个位数 F 1 8 E F F 3 8
个位数是很好异或出来的。因为四个比特都可以为1。例如异或出F:
0101 ‘5’
1010 ‘A’
这都是可以控制的数字。
综合一下(亿下 ),可以得出以下异或方案(方案不唯一):
50 00 00 00 ①
35 50 50 58 ②
—————
65 50 50 58
4A 32 39 36 ③
—————
2F 62 29 6E / b i n
50 50 00 00 ①
35 35 43 50 ②
—————
65 65 43 50
4A 32 39 36 ③
—————
2F 2F 73 68 / / s h
那么,需要如何完成这些异或操作,并且将/bin//sh放到某个地址中,并且这个地址还是可以控制的呢?(因为后面要改rdi等于这个地址)
先来看看可以控制哪些地址:
绿色的地址可控。为什么这些地址可控呢?因为xor eax, dword ptr [rdx + 0x30]这条指令是可控的,在线汇编就能看得比较明白:
注意要选择x86(64)
\x33代表eax,\x42代表rdx,\x30代表+0x30,由于rdx等于66660000,在shellcode的开头的那个0的位置,+0x30就到了下面绿色的那个0x30位置,那个位置就可控了。
为了文章的阅读,这里不展示我是怎么试错的,以及改正的过程了,直接给最终版的结构。因为刚开始的时候我以为位置很多,所以/bin//sh就随便放在了后面一点的位置,放在了0x50的地方,写到后面发现shellcode严重缺少位置,因此有了如下结构:
为什么5B,5C,5D不可控,却还能放入/sh呢?因为可以使用xor dword ptr [rdx + 0x5A], eax一次性异或4个字节。
为什么这里是倒着放的呢?因为读取的时候是从低地址读到高地址的,从56开始,读/,读b,读i……
有了存放的地址后,就需要考虑怎么异或,这里我的方案是这样的:
总体流程就是执行了上面说过的异或方案
先给al 0x50,eax就成了00 00 00 50,然后与0x56这个地方的50 50 50 35异或,将上面的异或方案 倒着看就可以了
第三行又异或了一下0x50,是将al归零,0x50异或0x50等于0
第四行是从0x4A这个地方拿出4个字节给eax,然后第五行是将这个地方归零,因为这个地方等会要存数据。
随后又与0x56这个地方异或,完成了异或方案 的两次异或,这个地方就变成了n i b /
第七行和第八行是将rax清空,这个是最快清空rax的方法,只需要两个字节,如果不用这个方法,后面位置会不够用。
然后下面的是一样的,将0x5a这个地方变成h s / /
动态调试看一下,0x00和0x10那两行是即将要执行的汇编指令,从0x4A开始是存储的数据。也就是说,我将这个shellcode划分为了两个段:Code段和Data段
Code段的长度不能覆盖Data段,否则在xor的时候就没有数据可以取了。此时Code段的最大长度是0x4A,在最开始的时候,我没有考虑这些,导致Code段最大长度只有0x30,指令根本放不下。
一路按s单步运行下去,执行完毕后:
/bin//sh写入0x66660056完毕。
接下来就是改寄存器的参数
首先单字节修改了0x38,0x42,.0x48,0x49这四个地方的值,这个是后话,暂且不提。
push rsi将0x66660000压入栈,然后rax取回,rax = 0x66660000,随后异或al,rax = 0x66660056,也就是/bin//sh所在的地址。
然后将这个地址放入0x4A。然后清空rax,让rax = 0x56,这个是后面用的。
随后,需要用到xor edi,dword ptr [rdx + 0x4a]来使得rdi = 0x66660056,但是这个地址不能直接写:
所以这时候就需要靠前面al的单字节修改,将7a异或出来。在执行完将/bin//sh异或出这个操作后,rax = 0x38304a4a,al = 0x4a
显然,利用0x30就可以异或出0x7a了。
所以在上面xor al,0x56后面应该跟着\x33\x30\x4a。然后还要考虑输入完后,\x30是否是可控的。在最开始的时候,\x30是不可控的,通过调整了汇编的顺序,使得\x30被推到了0x30的后面,最后推到了0x38这个位置,就变成可控的了。第一个al单字节修改的就是这个地址。
执行完了这个指令后,RDI = 0x66660056 → “/bin//sh”
然后接下来是让RSI = 0,同样地:
esi也是不可控的,需要al单字节修改。
然后在执行这串命令前,需要先将0x4a这个地方的0x66660056改为0x66660000,因为RSI此时的值是0x66660000,异或0x6666000才能等于0
在\x33\x30\x4a之前,已经让al = 0x56了,之后直接改就行了:
这里异或了两次al是为了让RAX = 0x3b,设置好系统调用所需的参数,同时也是为了将下面的指令推到可控的地方。
下面跟上\x33\x38\x4a,这个\x38被推到了0x42的位置,刚好是可控的,之前的(不是上面的这个图)第二个al单字节修改的就是这个地址。
然后执行完这个地址之后,开始让RDX = 0,rdx是可控的,push + pop就可以了。
然后是syscall调用,指令是0f 05,但是这是不可控的。于是可以在最开始写Data段的时候加上,将\x45 \x4F加到0x48,0x49这两个地方,然后分别异或4a,就变成了\x0f \x05。这是之前的第三、四个al单字节修改的地方。
演示总体流程:
走得比较快,因为考虑到可能需要在不懂的地方需要截图观看,然后截图完后想看下个指令要等好久。
我希望每个读者都应该独立写一个属于自己的shellcode,而不是照抄我的shellcode,我的shellcode结构不是最好的,一定还可以被优化。在看完本篇文章后,你可以尝试不看我的文章去自己复现一遍,然后你可能就会知道种种的限制以及空间不足的问题。然后你应该写一个全新的与我这种结构不同的shellcode。
因此我留给读者一个思考:上文提到了“如果是想一次性异或这8个数,显然不好做(后面会说做到的方法)”,事实上我并没有说这个做到的方法,我也没有去尝试能不能做到,我只是有个大概的框架应该是能成功的。推荐读者可以去尝试利用rax一次性将/bin//sh异或出来。
最后推荐一下另一位师傅的文章,推荐一看,也可以提升手写shellcode的能力:
https://bbs.kanxue.com/thread-274652-1.htm