0%

因为又在这里栽跟头了所以写一篇博客来记录一下orz

题目通过printf泄露地址的情况

例如:

1
2
3
4
5
6
#include<stdio.h>
int main(){
int a;
printf("%p",&a);
return 0;
}

0

可以看到输出的数据是0xe也就是14个字节

我们都知道一个64位程序的地址最多也就8个字节,为什么这个地址是14个字节呢?

原因是printf在输出的时候是将这个6个字节的地址转换成的字符串再加上0x进行输出的

知道原因过后我们的目标就很明确了:将这个14个字节的字符串转换为6个字节的数据

首先是要对获取到的数据进行处理:

我们需要用到python中的decode()方法将字节数据转换为字符串再使用lstrip('0x')去掉字符串左边的‘0x’

再使用bytes.fromhex()方法将十六进制字符串转换为字节数据

但是还没完,我们知道在大部分机器上数据是以小端序的顺序储存的,也就是我们要把我们得到的字节再进行反序操作

即使用[::-1]

完整操作如下:

1
2
3
4
raw_bytes = r(14)#接收数据
hex_str = raw_bytes.decode().lstrip('0x')#.rstrip('\x00') 如果结尾有多的'\x00'还需要.rstrip('\x00')来进行去除
hex_data = bytes.fromhex(hex_str) #转换为字节
reversed_bytes = hex_data[::-1] #反序

然后就可以使用u64(reversed_bytes.ljust(8,b’\x00’))对其处理在写入payload中

puts的情况

构造ROP链来使puts函数泄露libc地址应该是最常见的情况了,puts函数也是输出的字符串,为什么就可以直接通过u64(p.recv(6).ljust(8,b’\x00’))来获得libc地址呢?

在构造payload输出的时候虽然输出的是字符串,但这个字符串本质是直接根据字节数据通过诸如ascii转换而来的,也就是,输出的数据仍是原来的字节,而不是像printf那样输出的是将每四位数据对应的字符将这个字符作为一个单独的字节输出

bins

0

global_max_fast

glibc(GNU C Library)中动态内存分配器(ptmalloc2) 的一个关键全局变量,用于控制 fast bins(快速分配区) 的最大内存块大小。

fast bin

单向链表

32位下fast bin范围为16到64

64位下fast bin范围为32到128

从块分配器的角度来看,fastbin 中的块被视为已分配的块。它们仅在 malloc_consolidate 中批量与相邻对象合并。

double free

1
2
3
4
5
6
7
_int_free
-> if ((size) <= get_max_fast ())
-> unsigned int idx = fastbin_index(size);
-> fb = &fastbin (av, idx);
-> mchunkptr old = *fb, old2;
-> if (old == p)
-> malloc_printerr ("double free or corruption (fasttop)");

这里对double free漏洞的检查:

获取fastbin链表中最后入链的old,然后判断即将释放的fastbin和old的指针是否相同

unsorted bin

unsorted bins的bk和fd指向的是一个指针,而这个指针指向的是top chunk的地址,这个地址又储存在main_arena的0x58偏移处

main_arena - 0x10即为__malloc_hook的,__malloc_hook又是调用malloc时会调用的指针,所以可以通过劫持__malloc_hook来劫持程序控制流

然后通过fastbin attack将下一个堆块改为__malloc_hook - 0x23处,因为只有改为这处,视为堆块的此处的size位置才为正确的数据

binmap

Binmap的底层结构

  1. 物理存储
    binmapmalloc_state结构体中的一个数组(unsigned int binmap[BINMAPSIZE]),每个元素为32位无符号整数(unsigned int)。

    总长度:固定为4个int(即128位,对应63个large bin的索引需求)

    分组逻辑:每个int(32位)称为一个block,每个block负责管理32个连续的large bin状态

计算main_arena

  1. 通过上面的泄露unsorted_bin的fd指针

  2. 通过__malloc_trim函数直接获得

    “比如把 .so 文件放到 IDA 中,找到 malloc_trim 函数,就可以获得偏移了。” -–ctfwiki

tcache

glibc 2.26后

两个结构体

tcache dup

仅限glibc2.26,可以直接对其进行double free

tcache_entry

1
2
3
4
5
6
/* We overlay this structure on the user-data portion of a chunk when
the chunk is stored in the per-thread cache. */
typedef struct tcache_entry
{
struct tcache_entry *next;
} tcache_entry;

一个单向链表

tcache_perthread_struct

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* There is one of these for each thread, which contains the
per-thread cache (hence "tcache_perthread_struct"). Keeping
overall size low is mildly important. Note that COUNTS and ENTRIES
are redundant (we could have just counted the linked list each
time), this is for performance reasons. */
typedef struct tcache_perthread_struct
{
char counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;

# define TCACHE_MAX_BINS 64

static __thread tcache_perthread_struct *tcache = NULL;

通过counts记录tcache_entry上空闲的chunk数目

问题如下:

0

解决方法:

1

notepad C:\Windows\System32\drivers\etc\hosts

原因推测

clash系统代理时会更改本地hosts原因

随机数生成原理

rand()

C 库函数 int rand(void) 返回一个范围在 0 到 RAND_MAX 之间的伪随机数。

RAND_MAX 是一个常量,它的默认值在不同的实现中会有所不同,但是值至少是 32767。

srand()

C 库函数 void srand(unsigned int seed) 播种由函数 rand 使用的随机数发生器。

void srand(unsigned int seed)

参数seed是一个整形值,用于伪随机数生成算法播种。

伪随机数绕过方法

修改seed

有时随机数的种子seed会在栈上,可以通过栈溢出等方法对齐进行覆盖为已知值,再通过这个已知值获取对应的随机数序列

利用ctypes撞库

ctypes是Python内建的用于调用动态链接库函数的功能模块,通过调用该模块以及利用与题目相同的libc文件,来达到一个与题目中随机数生成方式相同的效果,实现撞库,这种方式下如果提前覆盖了指定种子,是更容易达成,如果直接设置时间种子撞库,可能存在时间对不上的情况,需要多测试几次。

使用方法:

1
2
3
4
5
from ctype import *
libc=cdll.LoadLibrary("libc.so.6")
a=[]
for i in range(50):
a.append(libc.rand())
1
2
3
#使用相同时间为种子
elf1=ctypes.CDLL("/lib/x86_64-linux-gnu/libc.so.6")
elf1.srand(elf1.time(0))

何为exit_hook

hook即修改地址,exit_hook即修改exit函数内的地址达到控制执行流的目的

hook rtld_lock_(un)lock_recursive

由fini_array这篇文章知道了exit)()在执行时会先调用__run_exit_handlers,而在这个函数中又会调用_dl_fini

_dl_fini函数源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#ifdef SHARED
int do_audit = 0;
again:
#endif
for (Lmid_t ns = GL(dl_nns) - 1; ns >= 0; --ns)
{
__rtld_lock_lock_recursive (GL(dl_load_lock));
unsigned int nloaded = GL(dl_ns)[ns]._ns_nloaded;
if (nloaded == 0
#ifdef SHARED
|| GL(dl_ns)[ns]._ns_loaded->l_auditing != do_audit
#endif
)
__rtld_lock_unlock_recursive (GL(dl_load_lock));

关键函数

1
2
__rtld_lock_lock_recursive (GL(dl_load_lock));
__rtld_lock_unlock_recursive (GL(dl_load_lock));

__rtld_lock_lock_recursive()的定义:

1
2
# define __rtld_lock_lock_recursive(NAME) \
GL(dl_rtld_lock_recursive) (&(NAME).mutex)

GL的定义:

1
2
3
4
5
# if IS_IN (rtld)
# define GL(name) _rtld_local._##name
# else
# define GL(name) _rtld_global._##name
# endif

_rtld_global是一个结构体,我们可以在pwndbg中用p _rtld_global来查看

结构体太长,这里我们这看结构体中作为上面俩函数的参数的部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
_dl_load_lock = {
mutex = {
__data = {
__lock = 0,
__count = 0,
__owner = 0,
__nusers = 0,
__kind = 1,
__spins = 0,
__elision = 0,
__list = {
__prev = 0x0,
__next = 0x0
}
},
__size = '\000' <repeats 16 times>, "\001", '\000' <repeats 22 times>,
__align = 0
}
},

函数的参数即为 _rtld_global._dl_load_lock.mutex.__size的地址

如何利用

既然我们已经知道了关键函数及其参数的位置,那么我们直接将关键函数改为one_gadget来get shell,也可以将关键函数改为system,再把参数地址改为/bin/sh\x00地址来get shell

如何寻找偏移

_rtld_global结构位于ld.so中 ( ld.sym['_rtld_global'] ),而libc_baseld_base又有固定的差值,所以我们可以通过libc的基址来推出函数及其参数的地址

1
2
3
libc2.23下偏移为0×5f0040,两个hook的偏移为3848和3850

libc2.27下偏移为0×61b060,两个hook的偏移为3840和3848

由于这个函数对于libc_base的偏移是固定的,故可以使用p &_rtld_global._dl_rtld_lock_recursive来查看函数地址,再减去libc_base得到偏移

1
2
pwndbg> p &_rtld_global._dl_rtld_lock_recursive
$4 = (void (**)(void *)) 0x7f1c260a8f60 <_rtld_global+3840>

hook __libc_atexit

exit.c源码如下:

1
2
3
4
5
6
7
__run_exit_handlers (int status, struct exit_function_list **listp,
bool run_list_atexit, bool run_dtors)
{
...
if (run_list_atexit)
RUN_HOOK (__libc_atexit, ());
...

而在我们调用__run_exit_handlers这个函数时,参数run_list_atexit传进去的值就为真:

1
2
3
4
void exit (int status)
{
__run_exit_handlers (status, &__exit_funcs, true, true);
}

因此,可以直接改__libc_atexit的值为one_gadget,在执行exit函数(从main函数退出时也调用了exit())时,就能直接getshell了。
这个__libc_atexit有一个极大的优点,就是它在libc而非ld中,随远程环境的改变,不会有变化。缺点就是,它是无参调用的hook,传不了/bin/sh的参数,one_gadget不一定都能打通。

参考文章

pwn总结

exit hook

宸极实验室——『CTF』从两道题目学习 exit_hook

0

什么是fini_array

fini_array被__libc_csu_fini调用的函数的指针数组.一般可以在ida中用shift+f7找到

一般来讲这个数组有两个数据,但也有特例(只有一个数据)

2

3

fini_array能干嘛

由图0可知,exit在执行时会调用这个指针,具体在exit-->__run_exit_handlers-->_dl_fini-->_dl_cal_fini里(下图是已经将fini_array的数据改为了main函数的地址)

1

4

利用思路

实现多次执行main

先来看静态链接的__libc_csu_fini函数代码(动态链接并没有这些指令)

5

由图可知,__libc_csu_fini在执行fini_array时先执行的fini_array[1],再执行的fini_array[0]

我们可以fini_array改为main函数来实现多次利用程序中漏洞的机会

也可以将fini_array[1]改为函数A,将fini_array[0]改为__libc_csu_fini 以实现死循环

构造栈迁移

通过上图还可以发现在0x968位置将一个地址赋给了ebp,这个地址就是我们的fini_array的地址

利用这一点我们可以先构造死循环利用程序的漏洞在指定位置写入ROP链,然后再通过将fini_array改为leave;ret;实现栈迁移执行布置好的ROP链

参考文章

详解64位静态编译程序的fini_array劫持及ROP攻击

劫持 64 位 fini_array 进行 ROP 攻击

和媳妇一起学Pwn 之 3x17

何为格式化字符串漏洞

1
2
3
4
5
6
7
#include<stdio.h>
int main(){
` char buf[16];
gets(buf,0x400);
printf(buf);
return 0;
}

形如上述代码的程序一般具有格式化字符串漏洞,试想如果往buf里输入%p等格式化字符串时会发生什么

那么细心的师傅们可能就会说了:你这后面也没跟第二个参数啊,这能运行吗?

答案是肯定的,并且打印出的地址就是栈上的地址,这就是格式化字符串最简单的用法:泄露栈地址

那么还有什么别的用途吗,且听我继续分析

格式化字符串函数介绍

在开始之前,我们先来介绍一下格式化字符串函数

格式化字符串函数

常见的有格式化字符串函数有

  • 输入
    • scanf
  • 输出
函数 基本介绍
printf 输出到 stdout
fprintf 输出到指定 FILE 流
vprintf 根据参数列表格式化输出到 stdout
vfprintf 根据参数列表格式化输出到指定 FILE 流
sprintf 输出到字符串
snprintf 输出指定字节数到字符串
vsprintf 根据参数列表格式化输出到字符串
vsnprintf 根据参数列表格式化输出指定字节到字符串
setproctitle 设置 argv
syslog 输出日志
err, verr, warn, vwarn 等 。。。

格式化字符串

其基本格式如下

1
%[parameter][flags][field width][.precision][length]type

每一种 pattern 的含义请具体参考维基百科的格式化字符串 。以下几个 pattern 中的对应选择需要重点关注

  • parameter
    • n$,获取格式化字符串中的指定参数
  • flag
  • field width
    • 输出的最小宽度
  • precision
    • 输出的最大长度
  • length,输出的长度
    • hh,输出一个字节
    • h,输出一个双字节
    • ll, 输出一个八字节
  • type
    • d/i,有符号整数
    • u,无符号整数
    • x/X,16 进制 unsigned int 。x 使用小写字母;X 使用大写字母。如果指定了精度,则输出的数字不足时在左侧补 0。默认精度为 1。精度为 0 且值为 0,则输出为空。
    • o,8 进制 unsigned int 。如果指定了精度,则输出的数字不足时在左侧补 0。默认精度为 1。精度为 0 且值为 0,则输出为空。
    • s,如果没有用 l 标志,输出 null 结尾字符串直到精度规定的上限;如果没有指定精度,则输出所有字节。如果用了 l 标志,则对应函数参数指向 wchar_t 型的数组,输出时把每个宽字符转化为多字节字符,相当于调用 wcrtomb 函数。
    • c,如果没有用 l 标志,把 int 参数转为 unsigned char 型输出;如果用了 l 标志,把 wint_t 参数转为包含两个元素的 wchart_t 数组,其中第一个元素包含要输出的字符,第二个元素为 null 宽字符。
    • p, void * 型,输出对应变量的值。printf(“%p”,a) 用地址的格式打印变量 a 的值,printf(“%p”, &a) 打印变量 a 所在的地址。
    • n,不输出字符,但是把已经成功输出的字符个数写入对应的整型指针参数所指的变量。
    • %, ‘%‘字面值,不接受任何 flags, width。

参数

即对应要输出的变量

如何利用

那么基础打牢,开始实践吧

泄露内存

上文已经介绍过用%p泄露出栈的地址,那么如何泄露栈变量的值和对应字符串呢

要做到这些只需要将%p换成%x和%s即可

我们可以用%k$s这种形式的格式化字符串来获取偏移量为k的地址的数据,所以只要知道指定内存地址相对于参数位置的偏移就可以打印任意地址的数据了

覆盖内存

这里需要用到一个非常特别的字符串%n

1
%n,不输出字符,但是把已经成功输出的字符个数写入对应的整型指针参数所指的变量。

这使得我们可以通过精妙的构造来向指定地址写入任意的值

可以使用pwntools中fmtstr中的神器fmtstr_payload

使用方法入下

1
fmtstr_payload(offset,{原始地址:目标地址})

关于偏移

这里主要说一下64位与32位程序的差异,64位程序是用寄存器传参,所以在计算偏移的时候要把寄存器数量考虑上,而32位程序就没这个顾虑了

大小数字覆盖问题

覆盖小数字

针对要写入的值小于偏移的值的情况,我们可以换一种方法来构造

我们可以将格式化字符串放在我们构造的数据的中间,将地址放在后面

1
payload = 'aa%8$naa' + p32(a_addr)

就像这样

覆盖大数字

我们需要用到一下两个标志

1
2
hh 对于整数类型,printf期待一个从char提升的int尺寸的整型参数。
h 对于整数类型,printf期待一个从short提升的int尺寸的整型参数。

我们可以将一个大数字进行分解,就像这样

1
2
3
4
0x0804A028 \x78
0x0804A029 \x56
0x0804A02a \x34
0x0804A02b \x12

然后通过如下这种方法构造payload进行覆盖

1
p32(0x0804A028)+p32(0x0804A029)+p32(0x0804A02a)+p32(0x0804A02b)+pad1+'%6$n'+pad2+'%7$n'+pad3+'%8$n'+pad4+'%9$n'

参考文章:

ctf-wiki

何为延迟绑定

延迟绑定指的是在程序运行过程中将动态链接库中的符号与程序中的代码进行绑定的过程,这样做的目的可以减少程序启动时间以及避免因静态链接带来的内存和磁盘空间浪费问题

延迟绑定涉及到plt表和got表这两个概念,下文对此加以说明

plt和got的狗血恋情

plt表和got表是什么

got表和plt表都是程序调用外部函数时,定位该函数需要使用到的表。

got

got表(Global Offset Table)是全局偏移量表,这是链接器在执行链接时需要填充的部分,保存了所有外部符号的地址信息。

got表分为以下四个部分

  • GOT[0] 是.dynamic段的装载地址,.dynamic段包含了动态链接器用来绑定过程地址的信息,比如符号的位置和重定位信息;
  • GOT[1] 是动态链接器的标识link_map的地址;
  • GOT[2] 包含动态链接器的延迟绑定代码_dl_runtime_resolve的入口点,用于得到真正的函数地址,回写到对应的got表中;
  • GOT[3] 从此开始就是函数的地址。

6

plt

plt表(Procedure Linkage Table)是程序链接表.位于代码段.plt节的PLT是一个数组,每个条目占16个字节。

  • **PLT[0]**用于跳转到动态链接器
  • **PLT[1]**用于调用系统启动函数__libc_start_main(),我们熟悉的main()函数就是在这里面调用的
  • **PLT[2]**开始就是被调用的各个函数条目。

延迟绑定过程

7

这里先放一张流程图作为参考↑

现在以printf函数为例探究plt和got这俩小情侣是如何操作的

这里直接查看到printf第一次被调用时plt表里的内容

0

可以看到跳进plt表中后又跳进了0x404000地址,那我们继续跟进

1

这个地址中储存的地址其实就是我们plt表上的地址,所以这里又跳回来了

8

由图这里调回来后执行prepare resolver,推测是和将真实地址解析到got表相关的函数

那么程序接着执行跳转到0x401020位置,我们继续跟进看看这对小情侣到底要干嘛

2

这里执行了一系列代码

3

看不懂,但根据流程图应该是与解析地址有关的函数

那么执行完这一段代码后再来看看我们printf的got表地址

5

A,您猜怎么着,确实got表的内容变了,根据实践,这就是printf的真实地址

那么延迟绑定就到此结束了

9

之后函数再call printf函数时就可以直接通过got表跳转到真实地址了

参考文章:

延迟绑定过程分析

elf文件的延迟绑定机制和plt,got表之间的调用

使用反汇编理解动态库函数调用方式GOT/PLT

何为沙箱

沙箱即限制程序只能调用指定函数来保证程序的安全性

何为orw

orw即open,read,write函数的缩写,一般情况下程序会限制只能调用这三个函数的其中几种

那么由于程序只允许我们调用这三个函数,那么我们无法通过系统调用execve来getshell,只能通过orw这三个函数来获取关键信息(即flag)

如何判断

可以通过seccomp-tools来判断是否开启沙箱

seccomp-tools dump ./pwn

0

如何绕过

shellcode绕过

原理

通过向栈上注入orw的代码让程序执行(需要关闭nx保护)

例如:

1
2
3
4
5
6
7
8
#fd = open('/home/orw/flag',0) 
s = ''' xor edx,edx; mov ecx,0; mov ebx,0x804a094; mov eax,5; int 0x80; '''

#read(fd,0x804a094,0x20)
s = ''' mov edx,0x40; mov ecx,ebx; mov ebx,eax; mov eax,3; int 0x80; '''

#write(1,0x804a094,0x20)
s = ''' mov edx,0x40; mov ebx,1; mov eax,4 int 0x80; '''

ROP绕过

适用于开启了nx保护的情况

原理

通过栈迁移等方法向.bss段写入代码然后构造ROP链到bss段执行相关代码

参考文章