汐白学Pwn-2(SomeBasic)

汐白学Pwn-2(SomeBasic)

三月 20, 2020 (Updated: )

部分理论基础(Linux)

栈的机制

栈是程序运行所使用的一种先进后出后进先出的线性表。大概是这样:
20200330165641
可以看到,就好比向一个单开口的箱子中压书一样(注意:就只是平着压入!!!别在那想为啥不竖着放,这样就可以想拿那个出去就拿那个出去)最先压入的A如果想要拿出来必须从最上面一个个拿出来,最后才能拿出来A,即:先进后出,后进先出。
栈通常用来为程序运行时所调用的各种函数存放其所使用的参数/变量。使用时一般是这样的(每使用一个函数,在调用这个函数时就会用这个模板为函数开辟一块栈帧):

··· ···
ESP 栈顶
Local variable 局部变量
EBP 栈底
retaddr 返回地址
Incoming parameters 传入参数
··· ···

当一个程序运行时需要获取输入时,这个存放输入的变量对于程序而言一般都放在局部变量中(一般是局部变量,不排除别的情况)。这时候看上面的栈帧模板就会发现,如果没有严格控制输入,那么就有可能会产生因为输入过长而导致输入的数据覆盖栈底、返回地址、传入参数······这是输入数据直接存放在栈内局部变量的情况;同理,即使输入数据没有存放在栈帧中,同样也会出现其它的数据覆盖现象,无论是什么数据被覆盖,都有可能会影响到程序的正常执行,从而导致各种可能产生的后果,也正是这样才导致了pwn的出现。
如最简单的栈溢出就是通过覆盖函数返回地址来达成目的。

详细的栈的介绍可以参考ctf-wiki中的栈介绍

这里记录一些常见的可导致溢出的危险函数:

  • 输入
    • gets,直接读取一行,忽略’\x00’
    • scanf
    • vscanf
  • 输出
    • sprintf
  • 字符串
    • strcpy,字符串复制,遇到’\x00’停止
    • strcat,字符串拼接,遇到’\x00’停止
    • bcopy

还未学习,暂空

工具的一些使用记录

pwntools

模块列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
adb:安卓调试桥
args:命令行魔法参数
asm:汇编和反汇编,支持 i386/i686/amd64/thumb 等
constants:对不同架构和操作系统的常量的快速访问
config:配置文件
context:设置运行时变量
dynelf:用于远程函数泄露
encoders:对 shellcode 进行编码
elf:用于操作 ELF 可执行文件和库
flag:提交 flag 到服务器
fmtstr:格式化字符串利用工具
gdb:与 gdb 配合使用
libcdb:libc 数据库
log:日志记录
memleak:用于内存泄露
rop:ROP 利用模块,包括 rop 和 srop
runner:运行 shellcode
shellcraft:shellcode 生成器
term:终端处理
timeout:超时处理
tubes:能与 sockets, processes, ssh 等进行连接
useragents:useragent 字符串数据库
util:一些实用小工具
pwnlib.atexception — Callbacks on unhandled exception
pwnlib.atexit — Replacement for atexit
pwnlib.exception — Pwnlib exceptions
pwnlib.replacements — Replacements for various functions
pwnlib.util.crc — Calculating CRC-sums
pwnlib.util.cyclic — Generation of unique sequences
pwnlib.util.fiddling — Utilities bit fiddling
pwnlib.util.hashes — Hashing functions
pwnlib.util.iters — Extension of standard module itertools
pwnlib.util.lists — Operations on lists
pwnlib.util.misc — We could not fit it any other place
pwnlib.util.net — Networking interfaces
pwnlib.util.packing — Packing and unpacking of strings
pwnlib.util.proc — Working with /proc/
pwnlib.util.safeeval — Safe evaluation of python code
pwnlib.util.web — Utilities for working with the WWW

常用模块和功能

context

设置程序运行时的参数,如程序运行在什么系统什么处理器下。一般只设置三个参数:

context(os = ‘linux’ , arch = ‘i386’ , log_level = ‘debug’)

context(os = ‘linux’ , arch = ‘amd64’ , log_level = ‘debug’)

asm

用于生成汇编指令对应的机器码

asm(‘mov eax,0’)
‘\xb8\x00\x00\x00\x00’

也可以查看机器码对应的汇编指令

disasm(“\xb8\x00\x00\x00\x00”)
‘mov eax,0’

不过之前好像看到有人说这东东有缺陷,推荐最好还是用keystone-engine

gdb

一般就用个附加调试

s = process(‘./pwnme’)
context.terminal = [‘gnome-terminal’, ‘-x’, ‘sh’, ‘-c’]
gdb.attach( proc.pidof(s) [0])

可以在attch的时候指定要gdb运行的指令:

gdb.attach(proc.pidof(s) [0], gdbscript=’b *0x400620\nc\n’)

个人喜欢直接终端:gdb -P pid(process之后返回的pid)

shellcraft

生成一些简单的shellcode,推荐先设置好context再用,一般直接输出shellcode内容

print(shellcraft.sh())
不过这里是直接提供的汇编指令,需要将其转为机器码
print(asm(shellcraft.sh()))

packing

用来打包数据或者解包数据

  • 打包——p32/p64(打包为32位或64位的数据)

p32(0x400010,endian = ‘big’) #设置数据为大端存储,默认为小端

  • 解包——u32/u64
tubes

对于一次攻击而言前提就是与目标服务器或者程序进行交互,这里就可以使用remote(address, port)产生一个远程的socket然后就可以读写了

sh = remote(‘ftp.debian.org’,21)
sh.recvline()
‘220 …’
sh.send(‘USER anonymous\r\n’)
sh.recvuntil(‘ ‘, drop=True)
‘331’
sh.recvline()
‘Please specify the password.\r\n’
sh.close()

使用process可以打开一个本地程序并进行交互

sh = process(‘/bin/sh’)
sh.sendline(‘sleep 3; echo hello world;’)
sh.recvline(timeout=1)
‘’
sh.recvline(timeout=5)
‘hello world\n’
sh.close()

使用listen来开启一个本地的监听端口

l = listen()
r = remote(‘localhost’, l.lport)
c = l.wait_for_connection()
r.send(‘hello’)
c.recv()
‘hello’

用于交互时读写的函数

interactive() : 直接进行交互,相当于回到shell的模式,在取得shell之后使用
recv(numb=4096, timeout=default) : 接收指定字节
recvall() : 一直接收直到EOF
recvline(keepends=True) : 接收一行,keepends为是否保留行尾的\n
recvuntil(delims, drop=False) : 一直读到delims的pattern出现为止
recvrepeat(timeout=default) : 持续接受直到EOF或timeout
send(data) : 发送数据
sendline(data) : 发送一行数据,相当于在数据末尾加\n

ELF

elf模块提供了一种便捷的方法能够迅速的得到文件内函数的地址,plt位置以及got表的位置。

e = ELF(‘./libc.so’)
print hex(e.address) # 文件装载的基地址
0x400000
print hex(e.symbols[‘write’]) # 函数地址
0x401680
print hex(e.got[‘write’]) # GOT表的地址
0x60b070
print hex(e.plt[‘write’]) # PLT的地址
0x401680
print hex(e.search(‘/bin/sh’).next())# 字符串/bin/sh的地址

ELF模块下的一些功能

  • asm(address, assembly) : 在指定地址进行汇编
  • bss(offset) : 返回bss段的位置,offset是偏移值
  • checksec() : 对elf进行一些安全保护检查,例如NX, PIE等。
  • disasm(address, n_bytes) : 在指定位置进行n_bytes个字节的反汇编
  • offset_to_vaddr(offset) : 将文件中的偏移offset转换成虚拟地址VMA
  • vaddr_to_offset(address) : 与上面的函数作用相反
  • read(address, count) : 在address(VMA)位置读取count个字节
  • write(address, data) : 在address(VMA)位置写入data
  • section(name) : dump出指定section的数据
ROP

pwntools中的ROP模块可以实现简单的ROP链的操作,就是自动地寻找程序里的gadget,自动在栈上部署对应的参数。

elf = ELF(‘ropasaurusrex’)
rop = ROP(elf)
rop.read(0, elf.bss(0x80))
rop.dump()
# [‘0x0000: 0x80482fc (read)’,
# ‘0x0004: 0xdeadbeef’,
# ‘0x0008: 0x0’,
# ‘0x000c: 0x80496a8’]
str(rop)
# > ‘\xfc\x82\x04\x08\xef\xbe\xad\xde\x00\x00\x00\x00\xa8\x96\x04\x08’

使用ROP(elf)来产生一个rop的对象,这时的ROP链还是空的,需要在其中添加函数。

因为ROP对象实现了__getattr__的功能,可以直接通过func call的形式来添加函数,rop.read(0, elf.bss(0x80))实际相当于rop.call('read', (0, elf.bss(0x80)))。 通过多次添加函数调用,最后使用str将整个rop chain dump出来就可以了。

  • call(resolvable, arguments=()) : 添加一个调用,resolvable可以是一个符号,也可以是一个int型地址,注意后面的参数必须是元组否则会报错,即使只有一个参数也要写成元组的形式(在后面加上一个逗号)
  • chain() : 返回当前的字节序列,即payload
  • dump() : 直观地展示出当前的rop chain
  • raw() : 在rop chain中加上一个整数或字符串
  • search(move=0, regs=None, order=’size’) : 按特定条件搜索gadget,没仔细研究过
  • unresolve(value) : 给出一个地址,反解析出符号

ROP的工作还是推荐通过ROPgadget来进行,pwntools的ROP功能之前貌似看到说是不完善,只能进行相对简单的ROP构造

DynELF 符号泄露

给出一个函数句柄,可以解析任意符号的位置。这个函数的功能是:输入任意一个address,输出这个address中的data(至少1byte)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
p = process('./pwnme')

def leak(address):
payload = 'a' * OverLength + write_addr + p64(0xdeadbeaf) + p64(1) + p64(address) + p64(4)
p.sendline(payload)
data = p.recv(4)
log.debug("%#x => %s" % (address, (data or '').encode('hex')))
return data

d = DynELF(leak, main)
d.lookup(None, 'libc') # libc基址
d.lookup('system', 'libc')

# 指定一份elf的副本可以加速查找过程
d = DynELF(leak, main, elf=ELF('./pwnme'))
d.lookup(None, 'libc')
d.lookup('system', 'libc')

在应用中我们可以在leak函数中布置rop链,使用write函数leak出一个address的地址和数据,然后返回。接着就可以使用d.lookup函数查找符号了,通常我们都是需要找system的符号。

gdb(some)

list/l 命令

可以使用list/l命令查看程序,方便我们添加断点时查看信息。

list+lineNumber(中间有空格)
list 打印函数名称为Function的函数上下文的源程序
list 输出当前行后面的代码
list -显示当前行前面的代码

run/r命令

在gdb中运行程序使用run命令.也可以设置程序运行参数。pwd命令用于显示当前所在目录。

break/b命令

break < function > 在进入指定的函数function时既停止运行,C++中可以使用class::function或function(type, type)格式来指定函数名称
break < lineNumber> 在指定的代码行打断点
break +offset/break -offset 在当前行的前面或后面的offset行打断点,offset为自然数
break filename:lineNumber 在名称为filename的文件中的第lineNumber行打断点
break filename:function 在名称为filename的文件中的function函数入口处打断点
break *address 在程序运行的内存地址处打断点
break 在下一条命令处停止运行
break … if < condition> 在处理某些循环体中可使用此方法进行调试,其中…可以是上述的break lineNumber、break +offset/break -offset中的参数,其中condition表示条件,在条件成立时程序即停止运行,如设置break if i=100表示当i为100时程序停止运行。查看断点时,也可以使用info命令如info breakpoints [n]、info break [n]其中n 表示断点号来查看断点信息。

逐步调试命令

next < count>。单步跟踪,如果有函数调用不会进入函数,如果后面不加count表示一条一条的执行,加count表示执行后面的count条指令,
s/step < count>。单步跟踪,如果有函数调用则进入该函数(进入该函数前提是此函数编译有Debug信息),与next类似,其不加count表示一条一条执行,加上count表示自当前行开始执行count条代码指令
set step-mode.set step-mode on用于打开step-mode模式,这样在进行单步跟踪时,程序不会因为没有debug信息而不停止运行,这很有利于查看机器码,可以通过set step-mode off关闭step-mode模式
finish。运行程序直到当前函数完成并打印函数返回时的堆栈地址和返回值及参数值等信息。
until。运行程序直到退出循环体
stepi(缩写si)和nexti(缩写ni)。stepi和nexti用于单步跟踪一条及其指令,一条程序代码有可能由数条机器指令完成,stepi和nexi可以单步执行机器指令。

continue/c命令

当程序遇到断点停止运行后可以使用continue命令恢复程序的运行到下一个断点或直到程序结束。

print命令

请查看:https://blog.csdn.net/linuxheik/article/details/17380767

watch命令

watch命令一般来观察某个表达式(变量也可视为一种表达式)的值是否发生了变化,如果由变化则程序立即停止运行,其具体用法如下:

watch < expr> 为表达式(变量)expr设置一个观察点一旦其数值由变化,程序立即停止运行
rwatch < expr> 当表达式expr被读时,程序立即停止运行
awatch < expr> 当表达式expr的值被读或被写时程序立即停止运行
info watchpoints 列出当前所设置的所有观察点

return命令

如果在函数中设置了调试断点,在断点后还有语句没有执行完,这个时候我们可以使用return命令强制函数忽略还没有执行的语句并返回。可以直接使用return命令用于取消当前函数的执行并立即返回函数值,也可以指定表达式如 return < expression>那么该表达式的值会被作为函数的返回值。

info命令

info命令可以用来在调试时查看寄存器、断点、观察点和信号等信息。其用法如下:

info registers:查看除了浮点寄存器以外的寄存器
info all-registers: 查看所有的寄存器包括浮点寄存器
info registers < registersName>:查看指定寄存器
info break: 查看所有断点信息
info watchpoints: 查看当前设置的所有观察点
info signals info handle: 查看有哪些信号正在被gdb检测
info line: 查看源代码在内存中的地址
info threads: 可以查看多线程

finish命令

执行完当前的函数。

run(缩写r)和quit(缩写q)分别可以开始运行程序和退出gdb调试

whatis或ptype显示变量的类型

bt显示函数调用路径

x命令

x/< n/f/u > < addr >

n、f、u是可选的参数。

n是一个正整数,表示显示内存的长度,也就是说从当前地址向后显示几个地址的内容。
f 表示显示的格式。如果地址所指的是字符串,那么格式可以是s,如果 地址是指令地址,那么格式可以是i。
u 表示从当前地址往后请求的字节数,如果不指定的话,GDB默认是4个bytes。u参数可以用下面的字符来代替,b表示单字节,h表示双字节,w表示四字 节,g表示八字节。当我们指定了字节长度后,GDB会从指内存定的内存地址开始,读写指定字节,并把其当作一个值取出来。

< addr > 表示一个内存地址。
n/f/u三个参数可以一起使用。例如:

x/3uh 0x54320 表示,从内存地址0x54320读取内容,h表示以双字节为一个单位,3表示三个单位,u表示按十六进制显示。

输出格式:
一般来说,GDB会根据变量的类型输出变量的值。但你也可以自定义GDB的输出的格式。例如,你想输出一个整数的十六进制,或是二进制来查看这个整型变量的中的位的情况。要做到这样,你可以使用GDB的数据显示格式:

x 按十六进制格式显示变量。
d 按十进制格式显示变量。
u 按十六进制格式显示无符号整型。
o 按八进制格式显示变量。
t 按二进制格式显示变量。
a 按十六进制格式显示变量。
c 按字符格式显示变量。
f 按浮点数格式显示变量。

简单的汇总

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
start                       #开始调试,停在第一行代码处,(gdb)start
l                        #list的缩写查看源代码,(gdb) l [number/function]
b <lines>            #b: Breakpoint的简写,设置断点。(gdb) b 10
b <func>             #b: Breakpoint的简写,设置断点。(gdb) b main
b filename:[line/function]  #b:在文件filename的某行或某个函数处设置断点
i breakpoints   #i:info 的简写。(gdb)i breakpoints
d [bpNO]        #d: Delete breakpoint的简写,删除指定编号的某个断点,或删除所有断点。断点编号从1开始递增。 (gdb)d 1
s                     #s: step执行一行源程序代码,如果此行代码中有函数调用,则进入该函数;(gdb) s
n                      #n: next执行一行源程序代码,此行代码中的函数调用也一并执行。(gdb) n
r                       #Run的简写,运行被调试的程序。如果此前没有下过断点,则执行完整个程序;如果有断点,则程序暂停在第一个可用断点处。(gdb) r
c                       #Continue的简写,继续执行被调试程序,直至下一个断点或程序结束。(gdb) c
finish                #函数结束
p [var]              #Print的简写,显示指定变量(临时变量或全局变量 例如 int a)的值。(gdb) p a
display [var]               #display,设置想要跟踪的变量(例如 int a)。(gdb) display a
undisplay [varnum]      #undisplay取消对变量的跟踪,被跟踪变量用整型数标识。(gdb) undisplay 1
set args                #可指定运行时参数。(gdb)set args 10 20 args可以是内存中某个地址
show args           #查看运行时参数。
x/<n/f/u> addr #查看内存中的值,n为数量,f为输出格式,u为值类型
q                          #Quit的简写,退出GDB调试环境。(gdb) q
help [cmd]            #GDB帮助命令,提供对GDB名种命令的解释说明。如果指定了“命令名称”参数,则显示该命令的详细说明;如果没有指定参数,则分类显示所有GDB命令,供用户进一步浏览和查询。(gdb)help
回车                     #重复前面的命令,(gdb)回车

程序的保护机制

Canary

原理

该保护开启的程序运行时会在开辟的栈帧上设置一个可当作令牌一样的随机值,程序在运行过程中会对该值进行校验,一旦校验时发现该值被改变则立即停止程序运行。

实现

开启Canary保护的程序在开辟栈帧时的结构如下:

   High
   Address |                 |
           +-----------------+
           | args            |
           +-----------------+
           | return address  |
           +-----------------+
   rbp =>  | old ebp         |
           +-----------------+
 rbp-8 =>  | canary value    |
           +-----------------+
           | 局部变量        |
   Low     |                 |
   Address

当程序启用 Canary 编译后,在函数序言部分会取 fs 寄存器 0x28 处的值,存放在栈中 %ebp-0x8 的位置。 这个操作即为向栈中插入 Canary 值,代码如下:

1
2
mov    rax, qword ptr fs:[0x28]
mov qword ptr [rbp - 8], rax

在函数返回之前,会将该值取出,并与 fs:0x28 的值进行异或。如果异或的结果为 0,说明 Canary 未被修改,函数会正常返回,这个操作即为检测是否发生栈溢出。

1
2
3
4
mov    rdx,QWORD PTR [rbp-0x8]
xor rdx,QWORD PTR fs:0x28
je 0x4005d7 <main+65>
call 0x400460 <__stack_chk_fail@plt>

如果 Canary 已经被非法修改,此时程序流程会走到 stack_chk_fail。stack_chk_fail 也是位于 glibc 中的函数,默认情况下经过 ELF 的延迟绑定,定义如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
eglibc-2.19/debug/stack_chk_fail.c

void __attribute__ ((noreturn)) __stack_chk_fail (void)
{
__fortify_fail ("stack smashing detected");
}

void __attribute__ ((noreturn)) internal_function __fortify_fail (const char *msg)
{
/* The loop is added only to keep gcc happy. */
while (1)
__libc_message (2, "*** %s ***: %s terminated\n",
msg, __libc_argv[0] ?: "<unknown>");
}

这意味可以通过劫持 __stack_chk_failgot 值劫持流程或者利用 __stack_chk_fail 泄漏内容 (参见 stack smash)。

进一步,对于 Linux 来说,fs 寄存器实际指向的是当前栈的 TLS 结构,fs:0x28 指向的正是 stack_guard。

1
2
3
4
5
6
7
8
9
10
11
typedef struct
{
void *tcb; /* Pointer to the TCB. Not necessarily the
thread descriptor used by libpthread. */
dtv_t *dtv;
void *self; /* Pointer to the thread descriptor. */
int multiple_threads;
uintptr_t sysinfo;
uintptr_t stack_guard;
...
} tcbhead_t;

如果存在溢出可以覆盖位于 TLS 中保存的 Canary 值那么就可以实现绕过保护机制。
事实上,TLS 中的值由函数 security_init 进行初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static void
security_init (void)
{
// _dl_random的值在进入这个函数的时候就已经由kernel写入.
// glibc直接使用了_dl_random的值并没有给赋值
// 如果不采用这种模式, glibc也可以自己产生随机数

//将_dl_random的最后一个字节设置为0x0
uintptr_t stack_chk_guard = _dl_setup_stack_chk_guard (_dl_random);

// 设置Canary的值到TLS中
THREAD_SET_STACK_GUARD (stack_chk_guard);

_dl_random = NULL;
}

//THREAD_SET_STACK_GUARD宏用于设置TLS
#define THREAD_SET_STACK_GUARD(value) \
THREAD_SETMEM (THREAD_SELF, header.stack_guard, value)

NX

NX即No-eXecute(不可执行)的意思,NX(DEP)的基本原理是将数据所在内存页标识为不可执行,当程序溢出成功转入shellcode时,程序会尝试在数据页面上执行指令,此时CPU就会抛出异常,而不是去执行恶意指令。

PIE(ASLR)

PIE机制,在windows中被称作ASLR,即地址随机化。PIE在linux中作为内核参数存在,可在/proc/sys/kernel/randomize_va_space中找到其具体的值,0、1、2三个值代表不同的工作强度,具体如下:

  • 0 - 表示关闭进程地址空间随机化。
  • 1 - 表示将mmap的基址,stack和vdso页面随机化。
  • 2 - 表示在1的基础上增加栈(heap)的随机化。

另外,地址随机化保护有“两个开关”,一个是系统环境下的地址随机化设置,一个是gcc编译时的地址随机化设置。

只有当系统环境下的随机化保护开启时,程序的随机化保护才会生效。

注:gcc的随机化设置不影响程序运行时的堆栈段地址,仅影响程序本身的bss、data、text静态段地址

RELRO

在Linux系统安全领域数据可以写的存储区就会是攻击的目标,尤其是存储函数指针的区域。 所以在安全防护的角度来说尽量减少可写的存储区域对安全会有极大的好处.

GCC, GNU linker以及Glibc-dynamic linker一起配合实现了一种叫做relro的技术: read only relocation。大概实现就是由linker指定binary的一块经过dynamic linker处理过 relocation之后的区域为只读.

设置符号重定向表格为只读或在程序启动时就解析并绑定所有动态符号,从而减少对GOT(Global Offset Table)攻击。RELRO为” Partial RELRO”,说明我们对GOT表具有写权限。

设置命令

  • NX:-z execstack / -z noexecstack (关闭 / 开启) 不让执行栈上的数据,于是JMP ESP就不能用了
  • Canary:-fno-stack-protector /-fstack-protector / -fstack-protector-all (关闭 / 开启 / 全开启) 栈里插入cookie信息
  • PIE:-no-pie / -pie (关闭 / 开启) 地址随机化,另外打开后会有get_pc_thunk
  • RELRO:-z norelro / -z lazy / -z now (关闭 / 部分开启 / 完全开启) 对GOT表具有写权限

上篇-汐白学Pwn-1(准备)/)

下篇-汐白学pwn-3.1(ROP-Basic)/)

隐藏